/* Design system ported from _artifacts/hmm-course-portal-ui.html (the
   verified, contrast-measured mockup). Tokens, type scale, and component
   classes below are a direct lift, not a reinterpretation -- see that file
   for the contrast measurements and reasoning behind each decision.
   Self-hosted fonts (both Perfectly Nineties and Rethink Sans) rather than
   the mockup's base64-embedded/Google-Fonts-linked versions -- those exist
   only so the mockup renders standalone; this is a live product. */

/* The browser's own [hidden] { display: none } is a UA-origin rule, so ANY
   author rule that sets display beats it -- including .btn's own
   display:inline-flex and .call-recording-indicator's display:flex below.
   Without this, an element the JS marks hidden stays visible. That is not
   cosmetic: public/session.js renders the "Stop recording" button and the
   recording indicator with the `hidden` attribute and only unhides them on
   'recording-active'. With no rule here, both showed from the moment a call
   opened, the indicator claimed a call was being recorded when it was not,
   and clicking Stop called withdraw_session_recording() -- which sets
   recording_state to 'stopped' PERMANENTLY, since start_session_recording()
   refuses any session already stopped. Hit live 2026-09-13 on session
   ffe1f81b: recording killed before it ever began. Do not remove. */
[hidden] { display: none !important; }

@font-face {
  font-family: "Perfectly Nineties";
  src: url("fonts/PerfectlyNineties-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Perfectly Nineties";
  src: url("fonts/PerfectlyNineties-Italic.ttf") format("truetype");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Rethink Sans";
  src: url("fonts/RethinkSans_400Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Rethink Sans";
  src: url("fonts/RethinkSans_500Medium.ttf") format("truetype");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Rethink Sans";
  src: url("fonts/RethinkSans_600SemiBold.ttf") format("truetype");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Rethink Sans";
  src: url("fonts/RethinkSans_700Bold.ttf") format("truetype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ==================================================================
   RESET
   ========================================================================= */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
button { font: inherit; color: inherit; background: none; border: none; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
img, svg { display: block; max-width: 100%; }
input { font: inherit; color: inherit; }

/* =========================================================================
   TOKENS -- values copied verbatim from the mockup, which copied them
   verbatim from lib/theme.ts. Do not introduce new colors here.
   ========================================================================= */
:root {
  --dark-brown: #593904;
  --mid-brown: #af9983;
  --light-brown: #c8b19b;
  --beige: #efdece;
  --additional-brown: #eadfd4;
  --off-white: #f9f1e9;
  --site-white: #fffcf8;
  --green: #848241;
  /* Olive fill under light text (buttons, selected chips): 4.62:1 with --site-white,
     AA. --green stays for borders, icons and data fills. */
  --green-action: #787636;
  --warn: #b85c00;
  /* Count bubbles only (.unread-badge, .notif-badge): 11-12px white text on --warn is 4.49:1,
     under AA. --warn itself stays, it is used widely; this darker orange is 5.4:1. */
  --badge-bg: #a35200;
  --overlay: rgba(89, 57, 4, 0.55);

  /* Theme-invariant: painted on video or on a data fill, so they must NOT
     swap in dark. Never redefined in the dark block. */
  --fixed-dark: #593904;
  --fixed-light: #fffcf8;
  --fixed-mid: #c8b19b;
  --on-period: #fffcf8;
  --cover-mark: rgba(249, 241, 233, 0.08);

  /* Literals that used to bypass the tokens; each needs a dark value. */
  --note-red: #c81e1e;
  --note-red-bg: #fbe4e4;
  --ok-text: #3b6a2c;
  --wrap-bg: #fbe9d3;
  --focus-ring: rgba(89, 57, 4, 0.35);
  --focus-glow: rgba(89, 57, 4, 0.15);
  --select-tint: rgba(132, 130, 65, 0.16);
  --bubble-mine: #e4e3c4;
  --card-clear: rgba(255, 255, 255, 0);
  --phase-menstrual: rgba(114, 47, 55, 0.16);
  --phase-follicular: rgba(132, 130, 65, 0.16);
  --phase-ovulatory: rgba(175, 153, 131, 0.22);
  --phase-luteal: rgba(89, 57, 4, 0.12);
  --phase-menstrual-swatch: rgba(114, 47, 55, 0.9);
  --phase-follicular-swatch: rgba(132, 130, 65, 0.9);
  --phase-ovulatory-swatch: rgba(175, 153, 131, 0.9);
  --phase-luteal-swatch: rgba(89, 57, 4, 0.9);

  /* Shawn's 2026-09-16 visual refinement: a slight border and drop shadow
     on cards, pills, buttons and inputs, so they read as distinct surfaces
     against the page rather than flat color blocks. rgba(89, 57, 4, ...) is
     --dark-brown's own channels (#593904) at low alpha, so the translucent
     edge reads correctly against every ground this file uses (beige, white,
     olive/tan pills) without introducing a new color. */
  --edge: 1px solid rgba(89, 57, 4, 0.16);
  /* Second pass, 2026-09-28: 0 1px 3px at 0.10 was close to invisible on
     this palette's beige grounds, so the same idea a notch up. Deliberately
     still much tighter than the one-off shadows this file already uses for
     things that genuinely float (0 6px 18px on card hover, 0 10px 24px on
     the acct menu) -- the target is "not flat," not "floating." */
  --lift: 0 2px 6px rgba(89, 57, 4, 0.14);
  /* Third pass, 2026-09-28: the same shadow at two more distances, for the
     interaction feedback in the REDUCED MOTION section below. Hovered is
     --lift a notch further off the page; pressed is a notch TIGHTER than
     resting, because a control that rises when you push it feels wrong.
     Same rgba(89, 57, 4, ...) channels as --edge/--lift, so still no new
     colour. Both stay well inside the range --lift established: the target
     is "this responds to me," not "this floats." */
  --lift-hover: 0 4px 10px rgba(89, 57, 4, 0.18);
  --lift-press: 0 1px 3px rgba(89, 57, 4, 0.16);

  /* --edge/--lift alone still read flat (Shawn, 2026-09-28), so every
     surface also gets a barely-there vertical sheen: white above,
     --dark-brown's own channels below, ~5% of swing top to bottom, so a
     surface reads as lit from above instead of as a flat color block.
     Applied as `background-image`, never as `background`, so each surface
     keeps whatever ground color it already sets. Note that every fill in
     this file uses the `background` SHORTHAND, which resets
     background-image to none -- so a variant rule that sets its own ground
     later (.btn-primary, .mood-chip.is-selected, ...) drops the sheen
     unless it re-declares it. That is load-bearing below, not an oversight.

     Two rules govern where this may be applied:
     1. Never over a color that encodes data -- --sev-*, --period, the
        phase/severity dots, calendar day cells, the status/progress bars.
        A sheen shifts a swatch's perceived value, and the severity ramp is
        read off the screen, not decorated. Interactive severity selectors
        (.severity-btn, selected) are controls, not displays: the gradient
        is allowed there; count tiles and other data displays stay flat.
     2. Only where the surface's OWN text clears WCAG AA at both ends of
        the gradient. The worst point is the bottom for dark text on a
        light ground, but the TOP for light text on a dark ground -- the
        white stop lightens the fill out from under the text. That measured
        exclusion is every --green and --warn fill carrying light text
        (site-white on --green is 3.910:1 bare, already under AA, and the
        white top takes it to 3.289:1) and the --mid-brown lab badges
        (2.664:1 bare). Those keep --edge and --lift and no sheen. */
  --sheen: linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(89, 57, 4, 0.05));

  /* The same depth for --green and --warn fills, which --sheen cannot have.
     THE ASYMMETRY IS THE WHOLE POINT, and it is the thing most likely to be
     "tidied" back into one token by someone who reads only the first one:
     on a LIGHT ground the risk is the dark bottom stop, because darkening
     reduces contrast under dark text. On a MID-TONE ground carrying LIGHT
     text it is the exact opposite -- the white TOP stop lightens the fill
     out from under white text, and that is the end that fails. Measured on
     --green: 3.910:1 bare (already under AA, pre-existing and tracked
     separately), 3.289:1 under --sheen's white top. So these fills get a
     darkening-only ramp: no top stop at all, which leaves the top of the
     surface untouched and makes the bottom MORE readable, not less. Every
     surface below is contrast-neutral at the top and contrast-positive at
     the bottom.

     Alpha is 0.12 rather than --sheen's 0.05 so the two read as the same
     strength rather than the same number: measured top-to-bottom luminance
     ratio is 1.118 on --green and 1.095 on --warn, against --sheen's 1.111
     on --beige and 1.089 on --site-white. A mid-tone ground simply has less
     room to darken per unit of alpha than a near-white one.

     rgba(89, 57, 4, 0) is deliberate and is not the same as `transparent`,
     which is rgba(0, 0, 0, 0) -- interpolating from transparent BLACK to
     opaque brown drags the midpoint toward grey. Zero-alpha in the ramp's
     own hue keeps it clean. */
  --sheen-midtone: linear-gradient(180deg, rgba(89, 57, 4, 0), rgba(89, 57, 4, 0.12));

  /* symptom-mapping-portal-spec.md §4.2 -- the one new color token this
     build adds. Period-bleeding marker. The one color in this portal
     that means "a body event happened on this day" -- never a
     symptom-severity state, never a phase state. Used for: the period
     day cell fill (calendar/day-detail, symptom-history.html, a later
     commit) and the cycle-status phase dot when the current phase is
     menstrual (symptom-mapping.js). Ported verbatim from the app's own
     PERIOD_BURGUNDY (her-mood-mentor/app/(tabs)/symptom-mapping/
     cycle.tsx:33) -- not invented here, promoted to a real token. */
  --period: #722f37;

  /* Reversed 2026-09-23 by Shawn: severity now matches the app's own
     4-step scale exactly, hex-for-hex, rather than this portal's former
     4-step brown ramp (chosen so --warn orange never appeared for a data
     state). Source of truth is the app's severityColor() --
     her-mood-mentor/lib/symptomMapping.ts:428-439 -- and these two must
     stay in step: change one, change the other. */
  --sev-none: #efdece;
  --sev-mild: #848241;
  --sev-moderate: #eab308;
  --sev-severe: #8b1a1a;
  /* Severity pills (.severity-btn): text on the unselected tint, 4.5:1+ on
     the tint over --beige. Light shades are darker, dark shades lighter. */
  --on-mild: #fffcf8;
  --on-moderate: #593904;
  --sev-mild-text: #4b4a1f;
  --sev-moderate-text: #6b4a00;
  --sev-severe-text: #8b1a1a;

  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-xxl: 48px;

  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 20px;
  --radius-xxl: 32px;
  --radius-xxxl: 40px;
  --radius-pill: 999px;

  --font-heading: 'Perfectly Nineties', Georgia, 'Times New Roman', serif;
  --font-body: 'Rethink Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

/* =========================================================================
   DARK THEME -- warm brown. A redefinition of the tokens above, set by
   theme-init.js as <html data-theme="dark">. Token names are hues but used
   by role, so in dark --dark-brown is the cream TEXT colour and --site-white
   is the CARD colour. Things painted on video or on a data fill use the
   --fixed-* tokens instead and never swap. Inside @media screen so print
   stays light. scripts/check-contrast.mjs guards this block: every colour
   token in :root must be redefined here or listed there as constant.
   ========================================================================= */
:root { color-scheme: light; }
@media screen {
  :root[data-theme="dark"] {
    color-scheme: dark;
    /* Brand-brown ground: the page is the light theme's --dark-brown
       (#593904) itself; cards are a darker step of the same hue, secondary
       surfaces a lighter one. Cards are darker, not lighter, because the
       data fills (severity, period, revenue blue) need 3:1 against the card
       and a lighter brown card cannot give it. */
    --off-white: #593904;
    --site-white: #3b2704;
    --beige: #644511;
    --additional-brown: #6e4c1a;
    --light-brown: #74531f;
    --mid-brown: #ecdcc0;
    --dark-brown: #fbf1e2;
    --green: #d0cd7c;
    --green-action: #d0cd7c;
    --warn: #ffbe82;
    --badge-bg: #ffbe82;
    --overlay: rgba(20, 12, 2, 0.72);
    --edge: 1px solid rgba(251, 241, 226, 0.16);
    --lift: 0 2px 6px rgba(0, 0, 0, 0.40);
    --lift-hover: 0 4px 10px rgba(0, 0, 0, 0.45);
    --lift-press: 0 1px 3px rgba(0, 0, 0, 0.40);
    --sheen: linear-gradient(180deg, rgba(255, 240, 220, 0.05), rgba(20, 12, 4, 0.14));
    --sheen-midtone: linear-gradient(180deg, rgba(255, 240, 220, 0.12), rgba(255, 240, 220, 0));
    /* Lifted in dark only (#8b1a1a and #722f37 vanish on brown). Deliberately
       NOT hex-for-hex with the app's severityColor() / PERIOD_BURGUNDY in
       dark; light still is. Text on a period fill is dark here (--on-period),
       because white on this lifted rose is only 3:1. */
    --sev-severe: #ff6b55;
    --on-mild: #2b1a03;
    --on-moderate: #2b1a03;
    --sev-mild-text: #e3e09a;
    --sev-moderate-text: #fae08a;
    --sev-severe-text: #ffd2c8;
    --period: #e0708a;
    --on-period: #2b1a03;
    --note-red: #ffc4b8;
    --note-red-bg: #5e2c20;
    --ok-text: #c6e8b0;
    --wrap-bg: #6a4318;
    --focus-ring: rgba(251, 241, 226, 0.55);
    --focus-glow: rgba(251, 241, 226, 0.18);
    --select-tint: rgba(208, 205, 124, 0.22);
    --bubble-mine: #565318;
    --card-clear: rgba(59, 39, 4, 0);
    --phase-menstrual: rgba(224, 112, 138, 0.34);
    --phase-follicular: rgba(206, 203, 116, 0.26);
    --phase-ovulatory: rgba(251, 241, 226, 0.16);
    --phase-luteal: rgba(200, 140, 70, 0.34);
    --phase-menstrual-swatch: #e0708a;
    --phase-follicular-swatch: #cecb74;
    --phase-ovulatory-swatch: #e6d6bc;
    --phase-luteal-swatch: #c88c46;
    /* Dark-only green accent for bold text, card headings and the active
       rail item. Brand green lifted: 6.32:1 on the page, 7.93:1 on a card,
       5.29:1 on beige (see check-contrast.mjs). Not for body text, buttons
       or data fills. */
    --accent-green: #d4d184;
  }
  :root[data-theme="dark"] main strong,
  :root[data-theme="dark"] main b,
  :root[data-theme="dark"] .pcard h2,
  :root[data-theme="dark"] .pcard h3,
  :root[data-theme="dark"] .practitioner-rail .header-tab.is-active { color: var(--accent-green); }
}

/* =========================================================================
   CONTRAST DECISIONS (measured against this palette, not eyeballed --
   see the mockup file for the full ratio table). The consequences that
   matter while editing this file:
   1. midBrown is NEVER a text color. Dividers, disabled fills, and resting
      placeholder text only (placeholders are contrast-exempt under WCAG).
      That rule had drifted badly: a 2026-09-28 sweep of every text/ground
      pair in this file found 40 midBrown and lightBrown text declarations,
      every one of them failing AA (2.08:1 on beige, 2.44:1 on off-white,
      2.67:1 on site-white for midBrown; 1.85:1 to 2.01:1 for lightBrown).
      All 40 now render darkBrown, which clears AA on every light ground
      this palette has -- worst case 5.08:1 on lightBrown, 7.36:1 at the
      dark end of a sheened beige, 9.38:1 on sheened site-white -- so a new
      rule needs no per-ground check, it just must not use the light browns
      for text. Three deliberate survivors, each commented at its own rule:
      .text-input::placeholder, .calendar-cell.is-outside .calendar-cell-num,
      and .cover-fallback-eyebrow (lightBrown on the darkBrown cover is
      5.08:1 and already passes).
   2. Input borders use darkBrown, not lightBrown/midBrown -- the lighter
      browns fail the 3:1 non-text minimum against both the page and the
      input's own near-white fill.
   3. Buttons keep the app's real green fill/text (matches Button.tsx).
   4. Italic emphasis renders in darkBrown, never green, so it stays
      legible at body-copy sizes.
   5. Two palette-level failures survive this file and cannot be fixed
      inside it, because no existing token beats them and the token block
      forbids new colors. Both are Shawn's call and both are cross-repo,
      since these hexes come from the app's lib/theme.ts:
        --green  #848241: siteWhite on it is 3.910:1, and it as TEXT on
          off-white is 3.578:1. It is the fill of every primary button and
          the accent of 26 text rules. It is also --sev-mild, hex-for-hex
          in sync with the app's severityColor().
        --warn   #b85c00: siteWhite on it is 4.494:1 (.btn-danger,
          .history-action-btn--danger, .retiring-tag, .unread-badge), and
          it as TEXT on off-white is 4.112:1 (11 error/alert rules). Both
          directions fail, so no text-token swap rescues it -- siteWhite is
          already the lightest token and darkBrown on warn is worse
          (2.27:1). Only darkening --warn itself fixes it.
      Do not "fix" either one by reaching for --period or --sev-severe as a
      substitute ground. Those two hexes mean one specific thing each.
   ========================================================================= */

body {
  background: var(--off-white);
  color: var(--dark-brown);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* =========================================================================
   TYPOGRAPHY
   ========================================================================= */
.pn-72, .pn-56, .pn-36, .pn-24, .pn-18, .pn-16 {
  font-family: var(--font-heading);
  font-weight: 400;
  font-style: normal;
  line-height: 1.1;
  color: var(--dark-brown);
}
/* Tracking, 2026-09-28. Perfectly Nineties is drawn with the generous
   sidebearings a text face needs at 16px, and at 42-72px those same
   sidebearings read as gaps -- the word stops being a shape and becomes a
   row of letters. -0.015em pulls it back to where a display serif is
   normally set. It is a ratio, not a pixel value, so it stays correct all
   the way down each clamp.
   The tightening stops at .pn-24 on purpose: .pn-18 and .pn-16 are card
   and section titles at near-body size, where negative tracking costs
   legibility instead of buying presence, and this palette's users are
   often reading on a phone on a bad day. No font SIZE changes here. */
.pn-72 { font-size: clamp(42px, 5vw + 24px, 72px); letter-spacing: -0.015em; }
.pn-56 { font-size: clamp(34px, 4vw + 20px, 56px); letter-spacing: -0.015em; }
.pn-36 { font-size: clamp(27px, 3vw + 16px, 36px); letter-spacing: -0.015em; }
.pn-24 { font-size: clamp(21px, 1.5vw + 15px, 24px); letter-spacing: -0.01em; }
.pn-18 { font-size: clamp(17px, 1vw + 12px, 18px); }
/* Fixed, not clamped -- portal-shell-redesign-spec.md §10.4: a card title
   must not grow with the viewport the way the rest of this ladder does.
   One new step in this existing named ladder, added once here in Phase 1;
   Phase 3 reuses it for the client library's product card titles. */
.pn-16 { font-size: 16px; }

/* =========================================================================
   VERTICAL RHYTHM DEFAULTS -- root-caused 2026-09-14: this file defined
   spacing TOKENS but no element-level defaults, so a heading's own spacing
   only existed if someone wrote a bespoke container rule (or an inline
   style) for that exact spot. Forgetting produced exactly the "new
   section collides with the next one" bug Shawn kept flagging. These
   rules are that default; a new heading/paragraph/table/list needs no
   bespoke rule to sit correctly rather than colliding with what follows.

   margin-top/margin-bottom only, never a `margin` shorthand -- headings'
   horizontal margin was already 0 (the .pn-* rule above used to assert it
   via `margin: 0`; browsers default heading inline-margin to 0 on their
   own, so removing that line and asserting only block margins here changes
   nothing horizontally).

   Values are a generalization of this file's own existing one-offs, not a
   new opinion: h2/h3's margin-top/bottom (32px above, 16px below) is the
   exact pattern practitioner.js and sessions.js already hand-wrote inline
   (`style="margin-top:32px"`, `style="margin:var(--space-xl) 0
   var(--space-md)"`) a dozen-plus times. h1's margin-bottom used to be
   --space-sm to match 3 of the 4 existing `.xxx-main h1` one-offs; it now
   takes the shared --space-md, see the note on the h1 rule below.

   Adjacent-margin handling: ordinary CSS margin collapsing (two touching
   block siblings resolve to the larger of their two margins, not the sum)
   already keeps heading-after-heading and heading-after-paragraph from
   doubling, with no extra rule needed. The one case collapsing can't
   handle is a heading that opens its own container -- there the "collapsed"
   margin would bubble out through the container's own edge and pad
   whatever sits above the *container*, not space the heading from its own
   content. :first-child below zeroes exactly that case, so a section's own
   margin/padding (already set throughout this file) keeps doing that job. */
h1, h2, h3, h4 { margin-bottom: var(--space-md); }
/* 2026-09-28: h1's margin-bottom was --space-sm (8px), which at a 42-72px
   serif is less than a fifth of the cap height -- the page title and the
   first line under it read as one block, and the screen opens with no
   moment of arrival. --space-md (16px) is the next step on the existing
   scale and is what h2/h3/h4 already use for the same job. Still well
   under h1's own 24px margin-top, so the title stays bound to what follows
   it rather than floating between the two. That is exactly what the shared
   h1/h2/h3/h4 rule above already sets, so h1 no longer overrides it and
   this rule is down to the margin-top it always had. */
h1 { margin-top: var(--space-lg); }
h2, h3, h4 { margin-top: var(--space-xl); }
h1:first-child, h2:first-child, h3:first-child, h4:first-child { margin-top: 0; }

/* Same reasoning, for the other elements that carry vertical rhythm --
   matches the margin-bottom-only, :last-child-zeroed pattern
   .lesson-body p and .exhibit-notes p already used twice over before this
   existed as a shared rule. */
p, table, ul, ol { margin-top: 0; margin-bottom: var(--space-md); }
p:last-child, table:last-child, ul:last-child, ol:last-child { margin-bottom: 0; }

/* Emphasis: Perfectly Nineties italic, never bold, always darkBrown. */
.emph {
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 400;
  color: var(--dark-brown);
}

.rs-24 { font-family: var(--font-body); font-size: 24px; font-weight: 400; }
.rs-20 { font-family: var(--font-body); font-size: 20px; font-weight: 400; }
.rs-18 { font-family: var(--font-body); font-size: 18px; font-weight: 400; }
.rs-16 { font-family: var(--font-body); font-size: 16px; font-weight: 400; }
.rs-14 { font-family: var(--font-body); font-size: 14px; font-weight: 400; }
.rs-semibold { font-weight: 600; }
.rs-bold { font-weight: 700; }

/* READING MEASURE, 2026-09-28. These two carried max-width: 46em, which at
   their own sizes is 828px and 736px -- both WIDER than .shell's 720px, so
   neither cap had ever bitten and running prose was set to the full column
   (640px inside the shell's padding). 65ch is the long-standing
   typographic measure. Measured in real Chrome against the self-hosted
   Rethink Sans: 1ch is 9.594px at 16px, so .body-copy goes 640px -> 624px,
   which is 66.7 characters -> 65. A small move, and the honest size of the
   problem: this portal's prose was already close. .lede is 18px, where
   65ch is 702px, so its cap still does not bite inside .shell and only
   ever will on a wider container.

   `ch` not `em` because ch is the width of the font's own "0" and so
   tracks the actual characters per line, which is the thing that makes a
   long line tiring. A max-width can only ever make an element narrower
   than its container, so at 375px this resolves to the available width and
   cannot introduce a horizontal scroll.

   line-height 1.5 -> 1.6 on prose only. NOT on `body`, deliberately: body's
   1.5 is inherited by every button, badge, table cell and calendar cell in
   the portal, several of which are sized off it, and loosening the leading
   of a 10px badge is not what "easier to read" means. Paragraph spacing is
   already --space-md from the VERTICAL RHYTHM DEFAULTS above and needs no
   new value. */
.lede { font-family: var(--font-body); font-size: 18px; line-height: 1.6; color: var(--dark-brown); max-width: 65ch; margin: 0; }
/* margin: 0 here used to beat the element-level `p` rhythm default above (a
   class outranks an element selector), so every <p class="body-copy"> sat
   flush against whatever followed it -- the "Write notes" pill under "No
   notes have been written" among others. Now it takes the same token gap;
   :last-child zeroes it for non-<p> uses. */
.body-copy { font-family: var(--font-body); font-size: 16px; line-height: 1.6; color: var(--dark-brown); max-width: 65ch; margin: 0 0 var(--space-md); }
.body-copy:last-child { margin-bottom: 0; }
/* Agreement and release lists (agreementBody.js): items were 4.6px apart. */
.agreement-list li + li { margin-top: var(--space-sm); }

.field-label {
  display: block;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--dark-brown);
  margin-bottom: var(--space-sm);
}

/* =========================================================================
   ICONS
   ========================================================================= */
.icon { width: 22px; height: 22px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
.icon-sm { width: 16px; height: 16px; }
.icon-fill { fill: currentColor; stroke: none; }

/* =========================================================================
   LAYOUT SHELL
   ========================================================================= */
.shell {
  max-width: 720px;
  margin-inline: auto;
  padding-inline: clamp(20px, 5vw, 40px);
}
.view { min-height: 100vh; }

/* =========================================================================
   SHARED PORTAL HEADER
   Sign-in intentionally does not use this header -- an unauthenticated
   visitor has nowhere it could take her yet.
   ========================================================================= */
.wordmark {
  font-family: var(--font-heading);
  font-size: 20px;
  color: var(--dark-brown);
}
.header-links { display: flex; align-items: center; gap: var(--space-md); }

/* Practitioner tab links. These used to sit in a tab bar under the header,
   which needed three wrapper rules (.portal-header--tabs, .header-top-row,
   .header-tabs) to stack a second row beneath the wordmark without redoing
   .portal-header's base rule, which app.js's renderHeader() also uses. The
   shell redesign moved the links into .practitioner-rail, so all three
   wrappers were deleted rather than left as debris -- the rail renders
   .header-tab anchors as direct children, restyled for the rail in the
   min-width:1041px block below. .header-tab-badge still carries the team
   chat unread count. */
.header-tab {
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 600;
  color: var(--dark-brown);
  padding: var(--space-sm) 0;
  border-bottom: 2px solid transparent;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.header-tab.is-active { color: var(--green); font-weight: 700; border-bottom-color: var(--green); }
.header-tab-badge {
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  /* Olive, not --warn. Orange means "this is blocked and you cannot proceed"
     (an unsigned consent form stopping a booking). Unread team messages are
     actionable, not blocked, and this badge sat in the rail as the loudest
     thing on the screen while meaning the mildest. Olive is the colour this
     design uses for active and actionable; --warn is kept scarce so it still
     registers when something really is stopped. */
  background: var(--green);
  color: var(--site-white);
  font-size: 11px;
  font-weight: 700;
  display: none;
  align-items: center;
  justify-content: center;
}
.header-tab-badge.has-count { display: inline-flex; }
/* =========================================================================
   PRACTITIONER SHELL (portal-shell-redesign-spec.md Part A, Phase 1) --
   full-width top bar + account menu and a 248px labelled navigation rail.
   The 56px client avatar ("pins") strip that used to hold column 1 is gone
   (Shawn, 2026-10-02). #practitioner-view is the ONLY id this grid rule
   touches -- .shell itself (LAYOUT SHELL, top of this file)
   is untouched and still centers .practitioner-main-col's own content
   inside the grid's second column, exactly as it does on every other page
   that uses it (index/lesson/messages/protocol/product.html main elements,
   all read and confirmed to have no #practitioner-view anywhere in them --
   this rule cannot reach them). Same "new selector, not a shared rule"
   discipline the deleted .portal-header--tabs used to establish, for the
   identical reason: .portal-header's own base rule (app.js's
   renderHeader(), read and confirmed) is used as-is by EIGHTEEN
   member-facing pages, not five -- measured by grep 2026-10-02, the
   figure this comment used to give was wrong and is corrected here
   because client-resources-page-spec.md makes it actively misleading.
   practitioner.js has its own top bar and never calls it. .shell is used
   more broadly still: all of those plus this file, practitioner.html,
   confirmed the same way.
   One shell breakpoint (1040px), matching spec §6.2/§17 Q3's reasoning --
   Labs still leaves the shell entirely (a separate `?view=` page load), so
   nothing narrower than this needs its own state.
   ========================================================================= */
/* .client-shell is the client pages' version of the same grid (the parent of
   app.js renderHeader()'s slot: <body>, or #library-view / #resources-view).
   Same rule, one selector list -- the client rail is this rail, not a copy.
   Whatever else sits in that parent (a banner, the page's <main>) goes in
   column 2; the rail spans the rows beside it. */
#practitioner-view[data-view="practitioner"],
.client-shell {
  display: grid;
  grid-template-columns: 248px minmax(0, 1fr);
  grid-template-rows: auto 1fr;
}
#practitioner-header,
.client-shell > .client-header-slot { grid-column: 1 / -1; grid-row: 1; }
#practitioner-rail { grid-column: 1; grid-row: 2; }
.client-shell > .practitioner-rail { grid-column: 1; grid-row: 2 / span 4; }
.practitioner-main-col { grid-column: 2; grid-row: 2; min-width: 0; } /* grid item -- keeps .shell's own content from overflowing its column */
/* width: 100% on the page's own blocks only: .shell's auto side margins would
   otherwise shrink a grid item to its content, but fixed-position children
   (dialogs, toasts, the bell panel) must keep their own width. */
.client-shell > :not(.client-header-slot):not(.practitioner-rail) { grid-column: 2; min-width: 0; }
.client-shell > main, .client-shell > .shell { width: 100%; }

@media (max-width: 1040px) {
  #practitioner-view[data-view="practitioner"],
  .client-shell { display: block; }
}

#practitioner-view[data-view="practitioner"] .practitioner-main-col > .shell { max-width: 1240px; }
.shell--wide { max-width: 1240px; }

/* =========================================================================
   PAGE CARDS (portal-card-layout-spec.md) -- the one box and the one grid
   every page uses. .pgrid sets the columns by itself (3 at 1440 with the
   rail, 2 at ~1280, 1 on a phone) -- no breakpoints, no column-count
   classes. min(100%, ...) is what stops a 340px track overflowing a phone.
   .pcard is a surface, not a control: no hover, no click target. Anything
   inside it whose colour encodes data stays flat (no sheen).
   Cards in one row are the same height (grid items stretch); a card much
   taller than its row-mates balances itself with .pcols inside.
   ========================================================================= */
.pgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: var(--space-lg); align-items: stretch; }
.pcard { background: var(--site-white); background-image: var(--sheen); border: var(--edge); border-radius: var(--radius-md); box-shadow: var(--lift); padding: var(--space-lg); min-width: 0; }
.pcard > :first-child { margin-top: 0; }
.pcard > :last-child { margin-bottom: 0; }
.pcard--wide { grid-column: 1 / -1; }
.pcard-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-md); }
.pcard-head h2, .pcard-head h3 { flex: 1; margin: 0; }
/* A list grouped by person/month/status, one column per group. */
.pcols { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); gap: var(--space-lg); align-items: stretch; }
.pcols > * > :first-child { margin-top: 0; }
/* Hairline between columns (same weight and colour as the row dividers). Only
   where the page is wide enough that columns sit side by side; below that they
   stack and the gap does the work. A group that wraps onto a second row can
   show a line at its left edge, which is why this stops at wide screens. */
@media (min-width: 1100px) {
  .pcols > * + * { border-left: 1px solid var(--light-brown); padding-left: var(--space-lg); }
}
/* Tasks card: on wide screens the columns scroll inside the card (header and
   Done toggle stay put), with a soft shadow on whichever edge has more. Phone
   and tablet stack the columns and the page scrolls instead: no nested scroll
   traps on touch. */
@media (min-width: 1100px) {
  #team-tasks-body .pcols {
    max-height: 60vh;
    overflow-y: auto;
    background:
      linear-gradient(var(--site-white) 30%, var(--card-clear)) 0 0 / 100% 24px no-repeat local,
      linear-gradient(var(--card-clear), var(--site-white) 70%) 0 100% / 100% 24px no-repeat local,
      linear-gradient(rgba(89, 57, 4, 0.14), rgba(89, 57, 4, 0)) 0 0 / 100% 8px no-repeat scroll,
      linear-gradient(rgba(89, 57, 4, 0), rgba(89, 57, 4, 0.14)) 0 100% / 100% 8px no-repeat scroll;
  }
}
@media (max-width: 640px) {
  .pgrid { gap: var(--space-md); }
  .pcard { padding: var(--space-md); }
}

/* Skip link: first tab stop on every client page, visible only while focused. */
.skip-link { position: absolute; left: var(--space-md); top: -100px; z-index: 60; padding: var(--space-sm) var(--space-md); background: var(--site-white); color: var(--dark-brown); border: 2px solid var(--dark-brown); border-radius: var(--radius-md); font-size: 14px; font-weight: 600; }
.skip-link:focus { top: var(--space-sm); }

/* ---------- top bar ---------- */
.practitioner-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-md);
  padding: 13px var(--space-lg);
  background: var(--site-white);
  border-bottom: 1px solid var(--beige);
}
.topbar-sub { color: var(--green); }

/* Account menu -- native <details>, no JS open/close state to manage.
   The headshot circle (.acct-shot) and header chip open the photo dialog
   (.acct-photo-btn, defined with the other profile photo rules below). */
.acct { position: relative; }
.acct > summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  cursor: pointer;
  padding: 3px var(--space-sm) 3px 3px;
  border-radius: var(--radius-md);
  transition: background 0.12s ease;
}
.acct > summary::-webkit-details-marker { display: none; }
.acct > summary:hover { background: var(--beige); }
.acct-chip {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--beige);
  background-image: var(--sheen);
  display: grid;
  place-items: center;
  font-size: 10.5px;
  font-weight: 700;
  color: var(--dark-brown);
  flex-shrink: 0;
}
.acct-name { font-size: 13px; color: var(--dark-brown); font-weight: 500; }
.acct-caret { font-size: 10px; color: var(--dark-brown); }
.acct-menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-sm));
  width: 268px;
  background: var(--site-white);
  border: 1px solid var(--beige);
  border-radius: var(--radius-md);
  padding: 14px;
  z-index: 40;
}
.acct-who {
  display: flex;
  gap: 11px;
  align-items: center;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--beige);
  margin-bottom: var(--space-sm);
}
.acct-shot {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background: var(--beige);
  display: grid;
  place-items: center;
  font-size: 14px;
  font-weight: 700;
  color: var(--dark-brown);
  flex-shrink: 0;
}
.acct-who-name { font-size: 14px; font-weight: 600; color: var(--dark-brown); }
.acct-who-email { font-size: 12px; color: var(--dark-brown); }
.acct-who-role {
  margin-top: 3px;
  font-size: 9.5px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--dark-brown);
}
.acct-item {
  display: block;
  padding: var(--space-sm) 9px;
  border-radius: var(--radius-md);
  font-size: 13px;
  color: var(--dark-brown);
  transition: background 0.1s ease;
}
.acct-item:hover { background: var(--additional-brown); }
.acct-theme { border-top: 1px solid var(--beige); margin-top: var(--space-sm); padding: var(--space-sm) 9px 0; }
.theme-control { display: flex; align-items: center; justify-content: space-between; gap: var(--space-sm); }
.theme-control-label { font-size: 13px; color: var(--dark-brown); }
.theme-seg { display: inline-flex; gap: 2px; padding: 2px; border: 1.5px solid var(--dark-brown); border-radius: var(--radius-pill); background: var(--additional-brown); }
.theme-seg button { width: 40px; height: 36px; display: grid; place-items: center; border-radius: var(--radius-pill); color: var(--dark-brown); }
.theme-seg button[aria-checked="true"] { background: var(--dark-brown); color: var(--site-white); }
.theme-seg button:focus-visible { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
.auth-theme { margin-bottom: var(--space-md); }

/* Web push control (pushClient.js): one row in the staff account menu and one
   above the client's message thread. Tokens only, so both themes follow. Empty
   (unsupported device, no key configured) takes no room at all. */
.acct-push { border-top: 1px solid var(--beige); margin-top: var(--space-sm); padding: var(--space-sm) 9px 0; }
.acct-push:empty { display: none; }
.push-note, .push-on, .push-recommend { margin: 0; font-size: 13px; line-height: 1.45; color: var(--dark-brown); }
.push-recommend { font-weight: 600; margin-bottom: var(--space-sm); }
.acct-push .push-note, .acct-push .push-on, .acct-push .push-recommend { margin-bottom: var(--space-sm); font-size: 12px; }
.push-btn { flex-shrink: 0; }
.push-link { background: none; border: 0; min-height: 44px; padding: 0 var(--space-sm); font: inherit; font-size: 13px; color: var(--dark-brown); text-decoration: underline; cursor: pointer; }
.push-link:focus-visible { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
.push-error { margin: 0; padding-left: var(--space-sm); border-left: 3px solid var(--warn); font-size: 13px; color: var(--dark-brown); }
.acct-push .push-error { margin-top: var(--space-sm); font-size: 12px; }

/* ---------- navigation rail (#practitioner-rail, 248px) ---------- */
.practitioner-rail {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  background: var(--site-white);
  border-right: 1px solid var(--beige);
  padding: var(--space-md) 0;
}
.rail-group-label {
  margin: 0 0 7px;
  padding: 0 18px;
  font-family: var(--font-body);
  font-size: 10.5px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  font-weight: 700;
  /* Section headings read as headings, not links: brand green, bold. */
  color: var(--green-action);
}
/* The client rail stacks several groups, so each heading after the first
   needs air above it (the practitioner rail has one heading at the top). */
.client-shell > .practitioner-rail .rail-group-label { margin-top: 14px; }
/* Collapsible group heading (railCollapse.js). Everything after a collapsed
   heading is hidden; the heading box is the same height either way, so it
   never jumps. The unread count only shows while collapsed. */
.rail-group-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-height: 24px;
  text-align: left;
  letter-spacing: inherit;
  text-transform: inherit;
  border-radius: var(--radius-sm);
}
.rail-group-toggle:focus-visible { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
.rail-group-chevron { flex-shrink: 0; transition: transform 0.1s ease; }
.rail-group-toggle[aria-expanded="false"] .rail-group-chevron { transform: rotate(-90deg); }
.rail-group-toggle[aria-expanded="true"] .rail-group-count { display: none; }
.rail-group-label:has(> .rail-group-toggle[aria-expanded="false"]) ~ * { display: none; }
@media (prefers-reduced-motion: reduce) { .rail-group-chevron { transition: none; } }
.rail-sep { height: 1px; background: var(--beige); margin: 14px 18px; }
.rail-count { margin-left: auto; font-size: 11px; color: var(--dark-brown); }
@media (min-width: 1041px) {
  /* Restyled from the underlined phone-nav look (kept as the base
     .header-tab rule below, for the <=1040px row) into the rail's block
     nav-link presentation -- spec §6.6/§11. */
  .practitioner-rail .header-tab {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 8px;
    width: calc(100% - 16px);
    padding: 7px 10px;
    /* Squared on the left only -- .is-active's own inset 2px 0 0 --green
       accent bar sits flush on that edge, and a fully-rounded corner
       curved the bar into a "(" shape. Applied here on the base rule,
       not just .is-active, so an item's corners don't visibly change
       shape the moment it becomes active. */
    border-radius: 0 var(--radius-md) var(--radius-md) 0;
    border-bottom: none;
    font-size: 13.5px;
    font-weight: 400;
    color: var(--dark-brown);
    transition: background 0.1s ease;
  }
  .practitioner-rail .header-tab:hover { background: var(--additional-brown); }
  .practitioner-rail .header-tab.is-active {
    background: var(--beige);
    color: var(--dark-brown);
    font-weight: 600;
    border-bottom-color: transparent;
    box-shadow: inset 2px 0 0 var(--green);
  }
  .practitioner-rail .header-tab-badge { margin-left: auto; }
  /* The phone-only link into the chat list; desktop has the TEAM CHAT rows. */
  .practitioner-rail .header-tab.rail-chat-phone { display: none; }
}
@media (max-width: 1040px) {
  .practitioner-rail {
    position: static;
    height: auto;
    overflow-x: auto;
    overflow-y: visible;
    display: flex;
    align-items: center;
    /* Restores the spacing the deleted .header-tabs wrapper used to
       supply (git show HEAD:public/style.css, pre-redesign .header-tabs
       rule) -- without it these run together edge to edge. */
    gap: var(--space-lg);
    padding: 0 var(--space-md);
    border-right: none;
    border-bottom: 1px solid var(--beige);
  }
  /* Only the four Practice links survive at this width (spec §6.7) --
     everything else this rail carries is the Your clients tab in another
     form, and stacking both would put two rows of chrome above the fold
     on a phone. */
  .practitioner-rail .rail-group-label,
  .practitioner-rail .rail-sep,
  #rail-needs-you,
  #rail-active-clients,
  #rail-team-chat {
    display: none;
  }
}

.rail-client {
  display: flex;
  align-items: center;
  gap: 10px;
  width: calc(100% - 16px);
  margin: 0 8px;
  padding: 7px 10px;
  /* Squared on the left only -- same reasoning as .practitioner-rail
     .header-tab above: .is-active's inset 2px 0 0 --green accent bar
     sits flush on that edge, and applying this on the base rule (not
     just .is-active) keeps the corners from changing shape on activation. */
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  font-size: 13.5px;
  color: var(--dark-brown);
  text-align: left;
  transition: background 0.1s ease;
}
.rail-client .header-tab-badge { margin-left: auto; }
.rail-chat-err { margin-left: auto; font-size: 11px; font-weight: 600; color: var(--warn); }
.rail-client:hover { background: var(--additional-brown); }
.rail-client.is-active { background: var(--beige); font-weight: 600; box-shadow: inset 2px 0 0 var(--green); }
.rail-client-av {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--beige);
  color: var(--dark-brown);
  display: grid;
  place-items: center;
  font-size: 10px;
  font-weight: 700;
  flex-shrink: 0;
}
.rail-client.is-active .rail-client-av { background: var(--green); background-image: var(--sheen-midtone); color: var(--off-white); }

/* Load failure inside one of the rail's own groups ("Needs you", "Active
   clients"). Deliberately not a full .banner error -- too heavy for a
   248px column (spec §6). A single muted circle with a retry affordance
   instead. */
.rail-retry {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: none;
  border: 1.5px solid var(--warn);
  color: var(--warn);
  font-family: var(--font-body);
  font-weight: 700;
  cursor: pointer;
}

/* =========================================================================
   BUTTONS -- mirrors components/ui/Button.tsx (padding, radius, fill,
   weight). min-height asserted explicitly for the 44-52px touch target.
   ========================================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  min-height: 52px;
  padding: 16px 32px;
  border: var(--edge);
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 16px;
  width: 100%;
  text-align: center;
  box-shadow: var(--lift);
}
.btn-primary { background: var(--green-action); background-image: var(--sheen-midtone); color: var(--site-white); }
.btn-secondary { background: var(--dark-brown); background-image: var(--sheen); color: var(--site-white); }
.btn-outline { background: transparent; border: 1.5px solid var(--green); color: var(--green); }
/* Aug 2026, practitioner.js's delete-document confirmation -- the first
   genuinely destructive action in this app's UI, warrants its own real
   variant rather than an inline color override in a JS template string. */
.btn-danger { background: var(--warn); background-image: var(--sheen-midtone); color: var(--site-white); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
.btn-inline { white-space: nowrap; width: auto; padding: 12px 20px; min-height: 44px; font-size: 14px; }

/* ADJACENT-BUTTON DEFAULT -- root-caused 2026-09-14, same shape as the
   VERTICAL RHYTHM DEFAULTS above but for buttons: this file had no rule
   for two actions sitting next to each other, so it only ever happened
   correctly where somebody wrote a one-off container rule (.call-controls)
   or a hand-written `style="margin-left:8px"` (practitioner.js's Look up/
   Cancel, Activate/Cancel, etc. -- 8px is exactly --space-sm). Generalized
   here so a new pair needs neither: two .btn-inline elements that are
   literal DOM siblings read as a pair with no class or inline style at
   the call site.

   Measures ~12px in practice, not a clean 8px, when there's whitespace
   between the two tags in the source (there almost always is, in a
   multi-line template literal) -- the collapsed whitespace text node
   between two inline-level boxes renders as ~4px on top of this margin,
   the two don't cancel out. This is not new: practitioner.js's own
   existing `style="margin-left:8px"` pairs measure the same ~12px today,
   whitespace included, same as this rule -- generalizing them changes
   nothing about how wide the gap actually renders, only removes the need
   to write it by hand.

   This can't help a pair that ISN'T DOM-adjacent -- rendered by different
   functions, or with an empty slot/panel div between them (approved-
   systems' Print this list / View history was exactly this). .btn-row
   below is the named wrapper for that case.

   Flex/grid containers that already space their own children with `gap`
   (.btn-row, .call-controls, .schedule-row, .lab-job-row-status,
   .owner-edit-row .field) reset this back to 0 on their own children --
   margin and gap both apply to a flex child, they don't collapse the way
   two ordinary block siblings' margins do above, so leaving both in place
   would double the space. A future flex button-row should use .btn-row
   rather than write a new bespoke gap rule, so it inherits this reset
   instead of needing its own. */
.btn-inline + .btn-inline { margin-left: var(--space-sm); }

/* Named row for pairs/trios .btn-inline's own adjacency rule can't reach.
   approved-systems' Print this list / Edit list / View history row
   (practitioner.js) is the first use. .call-controls (VIDEO SESSIONS,
   further down) is this same flex+gap declaration under its own name --
   kept separate only for its fullscreen/idle-fade rules, which have
   nothing to do with spacing, so it shares this instead of repeating it. */
.btn-row, .call-controls { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.btn-row { margin: var(--space-lg) 0; }
.btn-row .btn-inline, .call-controls .btn-inline { flex: 0 0 auto; }
.btn-row > .btn-inline + .btn-inline,
.call-controls > .btn-inline + .btn-inline,
.schedule-row > .btn-inline + .btn-inline,
.lab-job-row-status > .btn-inline + .btn-inline,
.owner-edit-row .field > .btn-inline + .btn-inline,
.reco-actions > .btn-inline + .btn-inline {
  margin-left: 0;
}

a:focus-visible, button:focus-visible, input:focus-visible, [tabindex]:focus-visible {
  outline: 3px solid var(--dark-brown);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* =========================================================================
   FORM FIELDS
   ========================================================================= */
.field { margin-bottom: var(--space-lg); }
.text-input {
  width: 100%;
  min-height: 52px;
  padding: 0 var(--space-md);
  background: var(--site-white);
  background-image: var(--sheen);
  border: 1.5px solid var(--dark-brown);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: 16px;
  color: var(--dark-brown);
  box-shadow: var(--lift);
}
/* The one midBrown text that SURVIVED the 2026-09-28 contrast sweep, and
   deliberately: placeholders are exempt from WCAG's contrast minimum, and
   darkening this one would make an empty field look like a filled one --
   which is a worse failure for the person reading it than a faint hint. */
.text-input::placeholder { color: var(--mid-brown); }
.text-input:focus { outline: none; border-width: 2px; box-shadow: 0 0 0 3px var(--focus-glow); }

/* form-display-redesign-spec.md §5.1. Sentence-case question label, used
   everywhere a question's own label is rendered on the client form
   (fill/submitted/preview) -- replaces .field-label's ALL-CAPS treatment
   for question/static-block text specifically; .field-label itself is
   unchanged and still used for short captions elsewhere. */
.question-label { display: block; font-family: var(--font-body); font-weight: 600; font-size: 17px; line-height: 1.4; color: var(--dark-brown); margin-bottom: var(--space-sm); }

/* §5.2. Binds a static block's description to the single question it
   immediately precedes (e.g. "Nutrition" intro + its own satisfaction
   scale) -- a lighter, whiter box than .card so the two levels of section/
   group nesting read as distinct. A group of one (an ordinary question, or
   a static block with nothing to pair with) renders as a plain .field div,
   unchanged from today -- no .field-group wrapper. */
.field-group { background: var(--site-white); background-image: var(--sheen); border: 1px solid var(--light-brown); border-radius: var(--radius-md); padding: var(--space-md); margin-bottom: var(--space-md); }

/* §5.4/AC14. single_select/multi_select options as tappable rows
   (>=44px) with a visible native control -- :has() drives the checked
   state directly off the real input, so no JS class-toggle is needed and
   the input's own keyboard/AT semantics are untouched. */
.option-row { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 10px 14px; margin-bottom: 8px; border: 1.5px solid var(--light-brown); border-radius: var(--radius-md); background: var(--site-white); background-image: var(--sheen); cursor: pointer; }
.option-row:has(input:checked) { border-color: var(--green); background: var(--off-white); background-image: var(--sheen); font-weight: 600; }
.option-row:has(input:focus-visible) { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
.option-row input[type="radio"], .option-row input[type="checkbox"] { accent-color: var(--green); width: 18px; height: 18px; flex-shrink: 0; }

/* §5.4. Segmented scale buttons. The real <input type="radio"> stays in
   the DOM with its existing name/data-qid/value attributes, positioned
   to cover its own button (absolute, opacity 0) -- click/tap and keyboard
   both land on the real control, and :has(input:checked) drives the
   visible state, so syncFormStateFromDom()'s existing selectors and
   native radio-group keyboard behavior are unchanged. */
.scale-buttons { display: flex; gap: 6px; }
.scale-btn {
  position: relative; flex: 1; min-height: 48px;
  display: flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--light-brown); border-radius: var(--radius-md);
  background: var(--site-white); background-image: var(--sheen); font-weight: 700; font-size: 16px; color: var(--dark-brown);
}
.scale-btn input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
.scale-btn:has(input:checked) { border-color: var(--green); background: var(--green-action); background-image: var(--sheen-midtone); color: var(--site-white); }
.scale-btn:has(input:focus-visible) { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
.scale-endcaps { display: flex; justify-content: space-between; margin-top: 6px; }
.scale-endcap { font-size: 13px; color: var(--dark-brown); }
.scale-endcap--max { text-align: right; }

/* =========================================================================
   CARDS
   ========================================================================= */
.card {
  display: block;
  background: var(--beige);
  background-image: var(--sheen);
  border: var(--edge);
  border-radius: var(--radius-lg);
  /* calc(20px - 1px): the new border above didn't exist when 20px was
     set, so it cost 1px of inner width per side that wasn't there before
     -- tight titles ("Video Session Recording Consent") wrapped and the
     card grew. This keeps the content box identical to pre-border. */
  padding: calc(20px - 1px);
  box-shadow: var(--lift);
}

/* =========================================================================
   COVER IMAGERY -- shared by the library card grid and the product hero
   (product.js). A real Kajabi cover photo/screenshot renders in .cover-img;
   .cover-fallback sits underneath it at all times, so a product with no
   mapped cover, or an <img> that fails to load, always resolves to a
   deliberate typographic panel -- never a broken image, never a grey box.
   ========================================================================= */
.cover-media {
  position: relative;
  overflow: hidden;
  background: var(--beige);
}
.cover-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.cover-fallback {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--space-xs);
  padding: var(--space-md);
  background: var(--fixed-dark);
  overflow: hidden;
}
.cover-fallback-mark {
  position: absolute;
  top: -0.2em;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-heading);
  font-style: italic;
  font-size: 200px;
  line-height: 1;
  color: var(--cover-mark);
  pointer-events: none;
  user-select: none;
}
.cover-fallback-eyebrow {
  position: relative;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--fixed-mid);
}
.cover-fallback-title {
  position: relative;
  font-family: var(--font-heading);
  font-style: italic;
  font-size: clamp(19px, 2vw + 14px, 24px);
  line-height: 1.25;
  color: var(--fixed-light);
  max-width: 22ch;
}
.card-cover { aspect-ratio: 16 / 9; }
/* client-portal-information-design-spec.md §5.6, phone only. At 375px a
   product card is a single 335px-wide column, so a 16/9 cover is 188px of
   mostly decorative art before a word of the card is read, and a member with
   four products gets most of a phone screen of it on the page she lands on.
   3/1 takes that to ~112px, which still reads as a cover and still fits the
   typographic .cover-fallback's eyebrow + title (measured at 375px, not
   estimated). Desktop is untouched -- there the cover is a 3-column grid
   item, not the whole screen's width. Scoped to #library-view: product.js's
   own hero passes .hero-cover, never .card-cover, so this is belt and braces
   matching this file's discipline rather than a required guard. */
@media (max-width: 480px) {
  #library-view .card-cover { aspect-ratio: 3 / 1; }
}
.hero-cover {
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-lg);
}

/* Icon chips -- ported from the app's numbered lesson-row circle (ModuleCard/
   LessonRow in app/courses/). Video rows fill solid olive (the "real
   accent" this pass is meant to bring in); file/download rows stay on the
   neutral additionalBrown so the colour carries actual meaning (playable
   vs. not), not decoration for its own sake. */
.icon-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}
.icon-chip .icon { width: 18px; height: 18px; opacity: 1; }
.icon-chip--video { background: var(--green); }
.icon-chip--video .icon { color: var(--site-white); }
.icon-chip--file { background: var(--additional-brown); }
.icon-chip--file .icon { color: var(--dark-brown); }
/* A tool's own line drawing (tool-art-*.png, alpha only) in place of the generic
   file icon: tinted with currentColor so it follows light and dark. Same 44px
   box for every chip in a tile grid, so tiles with and without art line up. */
.icon-chip--art { background-color: var(--dark-brown); -webkit-mask: var(--art) center / contain no-repeat; mask: var(--art) center / contain no-repeat; }
.icon-chip--art[data-art="symptoms"] { --art: url("tool-art-symptoms.png"); }
.icon-chip--art[data-art="supplements"] { --art: url("tool-art-supplements.png"); }
.icon-chip--art[data-art="edc"] { --art: url("tool-art-edc.png"); }
.card-grid .resource-card .icon-chip { width: 44px; height: 44px; }

/* =========================================================================
   REDUCED MOTION -- motion is opted IN only here, so a user who prefers
   reduced motion (or whose browser doesn't support the query) gets instant
   state changes by default.
   ========================================================================= */
@media (prefers-reduced-motion: no-preference) {
  .module-panel { transition: grid-template-rows 0.25s ease; }
  .icon-chevron { transition: transform 0.2s ease; }
  .auth-card, .library-state { transition: opacity 0.2s ease; }

  /* Timings mirror components/motion/FadeInOnScroll.tsx and
     useScrollReveal.ts (600ms, Easing.out(Easing.cubic)) -- there is no
     scroll-linked measurement on a static page like this one, so the reveal
     runs once on load rather than on viewport entry, but the feel matches. */
  .card-grid .product-card {
    animation: cover-reveal 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) both;
  }
  .card-grid .product-card:nth-child(2) { animation-delay: 0.06s; }
  .card-grid .product-card:nth-child(3) { animation-delay: 0.12s; }
  .card-grid .product-card:nth-child(4) { animation-delay: 0.18s; }
  .card-grid .product-card:nth-child(n+5) { animation-delay: 0.24s; }
  @keyframes cover-reveal {
    from { opacity: 0; transform: translateY(12px); }
    to { opacity: 1; transform: translateY(0); }
  }

  .product-card {
    transition: transform 0.25s cubic-bezier(0.215, 0.61, 0.355, 1), box-shadow 0.25s ease;
  }
  a.product-card:hover, a.product-card:focus-visible {
    transform: translateY(-3px);
    box-shadow: 0 10px 24px rgba(89, 57, 4, 0.16);
  }

  /* -----------------------------------------------------------------------
     INTERACTION FEEDBACK, 2026-09-28. The depth pass gave surfaces height
     but nothing answered back when you touched it. Two groups, and the
     split is about what the thing IS, not how it looks:

     GROUP A, things you press -- buttons, pills, chips, segmented options,
     cards that are links. These lift and grow a little under the cursor and
     settle DOWN under the finger.

     GROUP B, things you travel through -- tabs and the rows of a list.
     These get the press settle and their existing colour change, and
     deliberately NO scale: a row that grows inside a scrolling list shoves
     its neighbours around in the reader's peripheral vision, and the whole
     point of a list is that the eye can run down it undisturbed.

     Deliberately absent from both:
     - Anything that encodes data. .severity-btn (as a display; the
       interactive selected pill is a control and carries the sheen), .calendar-cell,
       .phase-legend-chip, .lab-badge, .tag--*, .hub-bar-fill and friends
       are VALUES, not controls. The flat-means-data distinction the depth
       pass established is the thing a practitioner reads a severity off,
       and it has to survive this too.
     - .text-input, which draws its focus ring with box-shadow rather than
       outline. A box-shadow transition on it would fade the ring in, and a
       focus ring that arrives late is a focus ring that failed. Every
       other element here focuses through the global
       `outline: 3px solid var(--dark-brown)` rule, which no transition in
       this block can reach.
     - .product-card, hence a.card:not(.product-card) -- it has owned a
       larger, slower hover of its own since the mockup and does not need
       two.

     Timing: 140ms on the same cubic-bezier(0.215, 0.61, 0.355, 1) the
     product card and the app's Easing.out(Easing.cubic) already use. Fast
     enough to feel attached to the cursor, slow enough not to snap.

     `background` rather than `background-image` alone, because .acct-item /
     .rail-client / .header-tab each carried their own
     `transition: background 0.1s ease` and a `transition` declared later at
     equal specificity replaces it wholesale -- naming only the longhands
     the sheen needs would have silently deleted their colour fade. */
  .btn,
  a.card:not(.product-card),
  .hub-pill, .mood-chip, .binaural-preset, .round-selector-option,
  .history-toggle-btn, .history-action-btn, .revenue-range-btn,
  .header-tab, .hub-tab,
  .acct-item, .rail-client, a.lesson-row, .file-row-dl,
  .roster-row--button, .roster-row--link, .thread-row {
    transition: box-shadow 140ms cubic-bezier(0.215, 0.61, 0.355, 1),
                background 140ms cubic-bezier(0.215, 0.61, 0.355, 1),
                transform 140ms cubic-bezier(0.215, 0.61, 0.355, 1);
  }

  /* Hover is gated on a real pointer. Without this a tap on a phone leaves
     the card sitting at scale(1.01) until you tap something else, because
     touch browsers emulate :hover as sticky -- so on touch the press state
     below is the entire feedback, which is the correct answer anyway. */
  @media (hover: hover) {
    .btn:not(:disabled):not([aria-disabled="true"]):hover,
    a.card:not(.product-card):hover,
    .hub-pill:hover, .mood-chip:hover,
    .binaural-preset:not([aria-disabled="true"]):hover,
    .round-selector-option:not([aria-disabled="true"]):hover,
    .history-toggle-btn:hover, .history-action-btn:hover,
    .revenue-range-btn:hover {
      /* 1%, not 2%. Measured at 2x on a hub card's own 11px body copy,
         variance-of-Laplacian over the glyph edges normalised by the
         region's contrast: 2.932 at rest, 2.892 at scale(1.01), 2.809 at
         scale(1.02). 1.01 costs 1.4% of edge sharpness and 1.02 costs
         4.2%, so the top of the range buys three times the softening for
         one extra percent of movement nobody asked for. Chrome
         re-rasterises a settled transform, so those are best-case numbers
         and the mid-transition frame can be softer still -- another
         reason to sit at the bottom of the range.

         No will-change anywhere. A transform transition is promoted on
         demand; pinning a thirty-card grid to its own GPU textures is
         tens of MB of video memory bought for nothing. */
      transform: translateY(-1px) scale(1.01);
      box-shadow: var(--lift-hover);
    }
  }

  /* Pressed: down and back to true size, with a shadow tighter than rest.
     :active fires on touch as well as mouse and clears the moment the
     finger lifts, so this is what carries the feedback on a phone. */
  .btn:not(:disabled):not([aria-disabled="true"]):active,
  a.card:not(.product-card):active,
  .hub-pill:active, .mood-chip:active,
  .binaural-preset:not([aria-disabled="true"]):active,
  .round-selector-option:not([aria-disabled="true"]):active,
  .history-toggle-btn:active, .history-action-btn:active,
  .revenue-range-btn:active {
    transform: translateY(1px) scale(1);
    box-shadow: var(--lift-press);
  }

  /* Group B: the settle, without the grow. */
  .header-tab:active, .hub-tab:active,
  .acct-item:active, .rail-client:active, a.lesson-row:active,
  .file-row-dl:active, .roster-row--button:active,
  .roster-row--link:active, .thread-row:active {
    transform: translateY(1px);
  }

  /* -----------------------------------------------------------------------
     SCROLL REVEAL, 2026-09-29. A content block fades up 10px ONCE as it
     enters the viewport and then stays put. Reveal-on-enter only: nothing
     in here is a function of scroll offset, because parallax and
     scroll-linked motion are the effects that make a migraine-prone reader
     ill. Sitting inside this no-preference block is what makes
     `reduce` structural rather than an override -- under `reduce` the two
     rules below do not exist, so the JS can set every attribute it likes
     and the content is simply visible.

     NEITHER RULE CAN FIRE UNTIL JAVASCRIPT HAS RUN. public/scrollReveal.js
     puts data-reveal-enabled on <html> before it hides anything, and puts
     data-reveal="hidden" on each block itself. Script blocked, script
     throws, no IntersectionObserver: no attribute, no selector match, every
     block renders as authored. Folding `opacity: 0` into a plain
     `.card { }` rule and stripping it from JS is the "simplification" that
     turns all three of those into a blank page for a client -- see the
     header of scrollReveal.js. Do not.

     `transition: none` on the hidden state is load-bearing, not tidiness.
     a.card:not(.product-card) is in the interaction-feedback transition
     list above, so without it the act of HIDING a card would animate the
     card 10px downward over 140ms. Going the other way needs no such
     guard: the "in" rule's animation starts at exactly the translateY(10px)
     the hidden rule already declared, so the transform's computed value
     does not change across that flip and no transition is triggered.

     `backwards`, never `both`. The fill has to end with the block on its
     own styles again -- a forwards fill would pin transform to translateY(0)
     and permanently outrank the hover lift above.

     --reveal-delay is set inline per block by scrollReveal.js: a small
     stagger down a grid or list, capped so the last card of a long one is
     not left waiting. No will-change anywhere, deliberately. Promoting a
     long list to one GPU texture per row is tens of MB of video memory for
     motion that runs once. */
  html[data-reveal-enabled] [data-reveal="hidden"] {
    opacity: 0;
    transform: translateY(10px);
    transition: none;
  }
  html[data-reveal-enabled] [data-reveal="in"] {
    animation: scroll-reveal 450ms cubic-bezier(0.215, 0.61, 0.355, 1) var(--reveal-delay, 0ms) backwards;
  }
  @keyframes scroll-reveal {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
  }
}

/* Shadow feedback for everyone, including under reduced motion -- it sits
   OUTSIDE the no-preference query on purpose. Light and motion sensitivity
   are common in this population and the reduce query has to kill every
   transform and every transition, but it should not leave a user with no
   answer at all when she hovers a button. With no transition to animate it
   the shadow simply appears, which is exactly the instant, non-moving
   feedback that setting asks for. Listed after the block above so the
   transitioned version wins for everyone else. */
@media (prefers-reduced-motion: reduce) and (hover: hover) {
  .btn:not(:disabled):not([aria-disabled="true"]):hover,
  a.card:not(.product-card):hover,
  .hub-pill:hover, .mood-chip:hover,
  .binaural-preset:not([aria-disabled="true"]):hover,
  .round-selector-option:not([aria-disabled="true"]):hover,
  .history-toggle-btn:hover, .history-action-btn:hover,
  .revenue-range-btn:hover {
    box-shadow: var(--lift-hover);
  }
}

/* =========================================================================
   BANNERS -- not in the mockup (it has no error states to design), kept
   from the working build. Styled with the ported tokens.
   ========================================================================= */
.banner {
  padding: var(--space-md);
  border-radius: var(--radius-md);
  margin: 0 0 var(--space-lg);
  font-family: var(--font-body);
  font-size: 15px;
  color: var(--dark-brown);
  background: var(--additional-brown);
  background-image: var(--sheen);
  border: 1.5px solid var(--light-brown);
  box-shadow: var(--lift);
}
.banner.error { border-color: var(--warn); }
/* The consent card's two checkbox lines: the whole line is the tap target. */
.consent-check { display: flex; align-items: flex-start; gap: 12px; min-height: 44px; padding: 6px 0; margin-bottom: var(--space-sm); line-height: 1.5; cursor: pointer; }
.consent-check a { color: inherit; font-weight: 600; text-decoration: underline; }
.consent-check input[type="checkbox"] { width: 22px; height: 22px; margin-top: 1px; flex-shrink: 0; accent-color: var(--green); }

/* Loading skeletons, not in the mockup either -- neutral fill only. */
.skeleton { background: var(--beige); border-radius: var(--radius-lg); height: 88px; opacity: 0.6; }

/* Coordinator, round 3 item 7: standard visually-hidden technique -- for
   an accessible name (e.g. an overlay heading) a loading/error state
   needs but the visual design doesn't want printed twice. */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

/* =========================================================================
   SIGN IN
   ========================================================================= */
.auth-shell {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-xl) var(--space-md);
  background: var(--beige);
}
/* The single flex child of .auth-shell -- see the comment on this element
   in index.html for why it must stay the only one. Banner and both cards
   stack inside it in normal block flow. */
.auth-card-wrap { width: 100%; max-width: 420px; }
/* A real card surface (the mockup left this bare) -- floats the sign-in
   flow above the shell's beige ground instead of the two tones reading as
   one flat field. */
.auth-card {
  width: 100%;
  max-width: 420px;
  background: var(--site-white);
  background-image: var(--sheen);
  border-radius: var(--radius-xl);
  padding: var(--space-xl) clamp(20px, 5vw, 32px);
  box-shadow: 0 4px 20px rgba(89, 57, 4, 0.1);
}
.auth-wordmark { font-family: var(--font-heading); font-size: 22px; text-align: center; margin-bottom: var(--space-xl); }
.auth-heading { margin-bottom: var(--space-sm); }
.auth-lede { margin-bottom: var(--space-xl); }

/* Real Turnstile widget mounts inside .turnstile-box (interaction-only
   appearance -- invisible unless Cloudflare's risk engine forces a
   challenge). The box's border/padding gives it a defined, findable home
   on the page even when the widget itself renders nothing. */
.turnstile-box {
  display: flex;
  align-items: center;
  min-height: 24px;
  margin-bottom: var(--space-lg);
}

.help-row { text-align: center; margin-top: var(--space-lg); font-size: 14px; }
.help-row a { text-decoration: underline; font-weight: 600; }

.sent-icon {
  width: 56px; height: 56px;
  border-radius: 50%;
  background: var(--green);
  background-image: var(--sheen-midtone);
  color: var(--site-white);
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto var(--space-lg);
}
.resend-row { margin-top: var(--space-lg); }
.resend-note { text-align: center; font-size: 14px; margin-top: var(--space-sm); }

/* Cross-device sign-in (see index.html for the two screens this serves).
   No monospace webfont added for one small feature -- a system stack
   renders a UUID-shaped code perfectly well. */
.code-paste-row {
  margin-top: var(--space-xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--light-brown);
}
.code-display {
  background: var(--site-white);
  background-image: var(--sheen);
  border: 1.5px solid var(--dark-brown);
  border-radius: var(--radius-md);
  padding: var(--space-md);
  text-align: center;
}
.code-display span {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 18px;
  letter-spacing: 0.5px;
  color: var(--dark-brown);
  word-break: break-all;
  user-select: all;
}

/* =========================================================================
   LIBRARY
   ========================================================================= */
.library-main { padding-top: var(--space-xl); padding-bottom: var(--space-xxl); }
.library-main .lede { margin-bottom: var(--space-xl); }

/* Balanced tile grid, shared by every grid of tiles (.card-grid, the growth
   tiles, the revenue stat tiles). Columns come from the grid's OWN width (a
   container query, not the screen), so a tile grid inside a narrow card still
   behaves: 1 column under 540px, 2 up to 820px, 3 above. Every tile is the
   same width, and a short last row is CENTERED (5 tiles -> 3 + 2, 7 -> 3 + 3
   + 1) instead of hanging off the left. Two and four tiles use 2 across / 2 + 2 rather
   than 3 + 1. A single tile is capped at 420px and centered. Tiles in a row are
   the same height because flex items stretch. */
.card-grid, .growth-grid, .revenue-stat-row {
  --tile-gap: var(--space-md);
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--tile-gap);
}
.card-grid > *, .growth-grid > *, .revenue-stat-row > * { flex: 0 0 100%; min-width: 0; }
@container (min-width: 540px) {
  .card-grid > *, .growth-grid > *, .revenue-stat-row > * { flex-basis: calc((100% - var(--tile-gap)) / 2); }
}
@container (min-width: 820px) {
  .card-grid > *, .growth-grid > *, .revenue-stat-row > * { flex-basis: calc((100% - 2 * var(--tile-gap)) / 3); }
  :is(.card-grid, .growth-grid, .revenue-stat-row):has(> :nth-child(4):last-child, > :nth-child(2):last-child) > * { flex-basis: calc((100% - var(--tile-gap)) / 2); }
}
.card-grid > :only-child, .growth-grid > :only-child, .revenue-stat-row > :only-child { flex-basis: min(100%, 420px); }
.growth-grid, .revenue-stat-row { --tile-gap: var(--space-lg); }

/* The card itself is now just the outer frame -- .cover-media bleeds to its
   edges (top corners follow from overflow:hidden on the card), .product-card-body
   holds the padding the flat version used to put directly on .card. */
.product-card {
  padding: 0;
  overflow: hidden;
  background: var(--site-white);
  background-image: var(--sheen);
  box-shadow: var(--lift);
}
.product-card-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: 18px 20px 20px;
}
.tag {
  display: inline-flex;
  align-items: center;
  padding: 3px 12px;
  border: var(--edge);
  border-radius: var(--radius-pill);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  box-shadow: var(--lift);
}
/* Course/Workbook is real, derived data (productTag() in app.js), not a
   decorative label -- the fill says something true about the product, so it
   gets the app's real green rather than a hairline outline. */
.tag--course { background: var(--green); color: var(--site-white); }
.tag--workbook { background: var(--light-brown); color: var(--dark-brown); }
/* margin-bottom: 0 -- this h3 is a flex item in .product-card-body's own
   column (gap: --space-sm already spaces it from the meta line below); the
   new default's margin-bottom would add on top of that gap, not replace
   it, since flex items never collapse margins with `gap` or with each
   other. margin-top keeps its own smaller, pre-existing value for the
   same reason. Was h2 before portal-shell-redesign-spec.md P3.4: with the
   library's h1 now living at the top of the page, a product card title
   sharing a level with the section heading above it read as two headings
   of equal weight -- h3.pn-16 (added once, in Phase 1) fixes that. */
.product-card-body h3 { margin-top: var(--space-xs); margin-bottom: 0; }
.product-card-meta { font-size: 15px; color: var(--dark-brown); margin: 0; }
.card-cta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-sm);
  font-size: 15px;
  font-weight: 700;
  color: var(--dark-brown);
}
.card-cta .icon { width: 16px; height: 16px; }

.empty-state {
  text-align: center;
  padding: var(--space-xxl) var(--space-md);
  max-width: 440px;
  margin: 0 auto;
}
.empty-state-icon-wrap {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  background: var(--beige);
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto var(--space-lg);
}
.empty-state .body-copy { margin: 0 auto var(--space-lg); }
.empty-state .help-text { font-size: 14px; }
.empty-state .help-text a { text-decoration: underline; font-weight: 600; }

/* Plain external links to publicly hosted PDFs -- no entitlement gate, no
   /api/document proxy. Shown identically below the product grid (or below
   the empty-state) to every signed-in member; see freeResourcesHtml() in
   index.js. Same .card base and .card-cta "Open" pattern as the product
   cards above it, laid out as a row instead of a stack since there's no
   cover image here. */
/* portal-home-and-hub-infodesign-spec.md §5.2: this class now also wraps
   the sibling grids that used to be the "More resources" section --
   Free resources plus the Research Library (one grid), and, when
   applicable, Your coaching resources. Reused deliberately, not left over -- see the
   .up-next/.your-coach comment below.

   client-portal-information-design-spec.md §5.5: the section's own h2 and
   lede rules are gone with the lede itself. client-resources-page-spec.md
   §4 R3: so is the <details> that briefly held the h2, and with it the
   heading -- the whole block moved to resources.html, where the page's own
   h1 names it.

   The margin-top only makes sense where this block FOLLOWS another
   section, which it did on index.html. On resources.html it is the first
   thing under the h1, so it takes the ordinary h1-to-content rhythm (the
   h1's own --space-md margin-bottom) instead of opening with a 48px gap,
   which is what calm-tools.html and research-library.html do under their
   own h1s. One scoped rule, no new token. */
.resource-card {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}
.resource-card-title { flex: 1; font-size: 17px; font-weight: 700; color: var(--dark-brown); }
.resource-card .card-cta { margin-top: 0; }
/* Symptom tracker card only (Commit C) -- every other .resource-card
   above is a bare title with no description, so .resource-card-title's
   own flex:1 (it's normally a direct flex child of .resource-card
   itself) stays untouched and unused here; this wrapper takes flex:1
   instead, stacking the title over a one-line description. */
.resource-card-text { display: flex; flex-direction: column; gap: 2px; flex: 1; }
.resource-card-desc { font-family: var(--font-body); font-size: 14px; font-weight: 400; color: var(--dark-brown); margin: 0; }
a.resource-card:hover, a.resource-card:focus-visible { box-shadow: 0 6px 18px rgba(89, 57, 4, 0.14); }

/* Inside a .card-grid (freeResourcesHtml(), formsCardHtml(),
   coachingResourcesHtml()'s lesson card), a .resource-card is a
   tile (1-3 across, see the balanced tile grid above), not the full-width
   row .symptom-tracker-card/.calm-tools etc. use above -- a long title
   had less room before it wrapped to three lines while the status text
   (formsCardHtml()'s "Not started") and CTA rode along beside it in the
   same flex row. Grid instead of flex scoped to this context only: the
   title spans the full row on its own, the status (when present) and CTA
   share the bottom row, so the CTA still lands flush against the card's
   right edge. */
.card-grid .resource-card {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-rows: auto 1fr;
  align-items: start;
  gap: var(--space-xs) var(--space-md);
}
/* Icon and title share row 1 and the Open line sits on the bottom row, so a
   tile with a description and one without line up identically; a taller
   neighbour only adds space between title block and Open line. */
.card-grid .resource-card .icon-chip { grid-row: 1; align-self: start; }
.card-grid .resource-card-title { grid-column: 2 / -1; }
.card-grid .resource-card .rs-14 { grid-column: 2; align-self: end; }
.card-grid .resource-card .card-cta { grid-column: 3; align-self: end; }
/* portal-home-and-hub-infodesign-spec.md §5.2: the "Your coach"/"Up next"
   groupings (index.js) put retakeNaqCardHtml()'s card -- title+description
   stacked in .resource-card-text, previously only ever rendered as a
   standalone full-width row -- inside a .card-grid for the first time.
   Without this, .resource-card-text auto-placed into a single grid cell
   next to the icon, pushing "Open" up beside the title instead of below
   it. Same fix as .resource-card-title above: span the full row so the
   status/cta row below lines up exactly like every other card here. */
.card-grid .resource-card-text { grid-column: 2 / -1; }

/* =========================================================================
   CLIENT LIBRARY -- PHASE 3 (portal-shell-redesign-spec.md Part B).
   Scoped to #library-view only. .shell, .card-grid, .card, .resource-card,
   .product-card, .icon-chip and .banner all have callers outside the
   library (lesson.html, product.html, protocol.html, messages.html, the
   practitioner view), so their base rules stay untouched -- new selector,
   never a shared rule, same discipline as §6.3/§11 of the spec.

   FIVE of these rules now also name #resources-view, the client resources
   page (client-resources-page-spec.md §6.5). They are the ones its reused
   markup depends on, and two of them are load-bearing rather than
   cosmetic -- see .file-row's own comment at the bottom of this block and
   the partial-failure notice's, which is a contrast floor, not a tint. A
   comma list, deliberately, rather than either of the two cheaper
   options: duplicated rules drift, and giving resources.html an
   id="library-view" wrapper to inherit the whole block for free would
   mean every future #library-view rule written for the library page
   silently landing on the resources page too.
   ========================================================================= */
#library-view .shell, #resources-view .shell, #onboarding-view .shell { max-width: 1160px; }
/* Home: one two-column grid on wide screens. My care, My tools and Learn run the
   full width; the cards above them (photo, Up next, Program Review) pair up as
   halves, and an odd one out takes the full row, so no row leaves an empty third. */
@media (min-width: 900px) {
  #library-view .your-tools { grid-column: 1 / -1; }
  #library-view .pgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  #library-view .pgrid > .pcard:not(.pcard--wide, .your-tools):nth-child(odd):is(:last-child, :has(+ :is(.pcard--wide, .your-tools))) { grid-column: 1 / -1; }
}
@media (max-width: 899px) {
  #library-view .pgrid { grid-template-columns: minmax(0, 1fr); }
}

/* P3.7 partial-failure notice -- coaching clients only. Rendered by
   loadLibrary() on the library page and by resources.js on the resources
   page, from the one failureNoticeHtml() both call, hence both ids.
   Background and border already match the mockup (client.html artboard
   3) via the base .banner rule (--additional-brown fill, 1.5px
   --light-brown border) -- no override needed. .btn-outline's default
   green text measures 3.05:1 against that fill in Chrome, below the
   4.5:1 text needs, so the button gets --dark-brown text here (7.96:1),
   scoped to this one notice; the border and the transparent fill (the
   "outline" shape) are untouched.

   #resources-view was MISSED when the notice moved to the second page,
   which left the only control on that page that appears after something
   has already gone wrong rendering at 3.05:1. */
#library-view .library-partial-failure .btn-outline, #resources-view .library-partial-failure .btn-outline { color: var(--dark-brown); }

/* Coaching-resource rows inside a grid card. .file-row is a no-wrap flex row
   and .file-row-dl can't shrink, so on a card narrower than ~300px (3 columns
   at ~890-1010px windows, 2 columns at ~630-700px) the Download button
   overflowed the card edge. Let it drop below the title instead, which holds
   at any width. The 140px basis keeps the title from being squeezed into a
   one-word column before the button wraps. Library only; lesson.html's
   full-width .file-row is untouched. */
#library-view .file-row, #resources-view .file-row { flex-wrap: wrap; }
#library-view .file-row-name, #resources-view .file-row-name { flex: 1 1 140px; }

/* =========================================================================
   PRODUCT
   ========================================================================= */
.product-main { padding-top: var(--space-lg); padding-bottom: var(--space-xxl); }
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 15px;
  font-weight: 600;
  margin-bottom: var(--space-lg);
  padding: var(--space-xs) 0;
}
.back-link .icon { width: 18px; height: 18px; }
.product-main .tag { margin-bottom: var(--space-sm); }
#course-meta { color: var(--dark-brown); font-size: 15px; margin-bottom: var(--space-md); }

.module {
  border-top: 1px solid var(--light-brown);
}
.module:last-child { border-bottom: 1px solid var(--light-brown); }
.module-header {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) 4px;
  min-height: 56px;
  text-align: left;
  border-radius: var(--radius-md);
}
.module-header-text { display: flex; flex-direction: column; gap: 2px; }
.module-title { font-family: var(--font-body); font-weight: 700; font-size: 17px; color: var(--dark-brown); }
.module-meta { font-size: 14px; color: var(--dark-brown); opacity: 0.75; }
.module-header[aria-expanded="true"] .icon-chevron { transform: rotate(180deg); }
/* An open module gets its own ground, the same tan the lesson-row hover
   uses -- one visual language for "this is where you are" across the page. */
.module-header[aria-expanded="true"] { background: var(--additional-brown); }

.module-panel {
  display: grid;
  grid-template-rows: 0fr;
}
.module-panel.is-open { grid-template-rows: 1fr; }
.module-panel-inner { overflow: hidden; min-height: 0; }
/* Padding only when open: on a collapsed (0fr) panel it can't shrink away,
   so it left a sliver showing the top of the hidden text. */
.module-panel.is-open .module-panel-inner { padding-bottom: var(--space-sm); }

/* Single-module products (all 3 flat products) skip the accordion header
   entirely and render lesson rows directly -- see product.js. */
.no-module-heading { padding-top: var(--space-sm); }

.lesson-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-height: 56px;
  padding: var(--space-sm) 4px;
  border-radius: var(--radius-md);
  color: var(--dark-brown);
}
a.lesson-row:hover, a.lesson-row:focus-visible { background: var(--additional-brown); }
.lesson-row-title { flex: 1; font-size: 16px; }
.lesson-row-files {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 13px;
  color: var(--dark-brown);
  opacity: 0.75;
  flex-shrink: 0;
}
.lesson-row-duration { font-size: 14px; color: var(--dark-brown); opacity: 0.75; flex-shrink: 0; }
.lesson-row--download { cursor: default; }
.lesson-row-dl {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  color: var(--dark-brown);
  padding: 6px 12px;
  border: 1.5px solid var(--dark-brown);
  border-radius: var(--radius-pill);
  flex-shrink: 0;
}

/* =========================================================================
   LESSON
   ========================================================================= */
.lesson-main { padding-top: var(--space-lg); padding-bottom: var(--space-xxl); max-width: 760px; }
/* Matches the app's sectionLabel treatment (rs14Bold, textAccent, 1.5px
   tracking) instead of a dimmed neutral line -- olive as a real accent. */
.lesson-eyebrow { font-size: 13px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: var(--green); margin-bottom: var(--space-xs); }
/* The eyebrow's own 4px margin-bottom is the whole gap to the title below
   it -- a deliberate tight pairing, the two read as one unit. Without this,
   the h1 default's own margin-top (24px) would collapse against that 4px
   and win (collapsing takes the larger of the two), pushing the title
   away from the label it's naming. */
.lesson-eyebrow + h1 { margin-top: 0; }
/* Differs from the h1 default (now --space-md): this heading sits directly
   above the video frame, a large visual block that reads better with more
   air above it than a heading before a line of body text needs. */
.lesson-main h1 { margin-bottom: var(--space-lg); }

/* Real Cloudflare Stream iframe, not the mockup's decorative placeholder
   transport bar -- the iframe embed already provides its own native
   controls (play/pause, scrubber, captions, fullscreen), so there is
   nothing for a second, non-functional control bar to do. The frame
   itself keeps the mockup's exact container treatment. */
.video-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--fixed-dark);
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-bottom: var(--space-xl);
}
.video-frame iframe { width: 100%; height: 100%; border: 0; }
.video-placeholder-text {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--fixed-light);
  font-size: 14px;
  text-align: center;
  padding: var(--space-md);
}

/* Sanitized lesson body text (see lesson.js -- DOMPurify strips everything
   but plain structural/inline tags before this ever reaches the DOM).
   Typography matches body-copy; headings inside the source content are
   downgraded so they read as in-body emphasis, not a second page title
   competing with the real <h1> above. */
.lesson-body {
  margin-bottom: var(--space-xl);
  padding: var(--space-lg);
  background: var(--site-white);
  border-radius: var(--radius-lg);
  box-shadow: 0 1px 3px rgba(89, 57, 4, 0.08);
  font-family: var(--font-body);
  font-size: 16px;
  /* Same reading-measure pass as .body-copy above, and this is the page it
     matters most on: a lesson is the one screen in the portal that is
     nothing but running prose. .lesson-main is .shell widened to 760px, so
     inside the shell's padding and this block's own --space-lg the text
     column was 632px; 46em (736px) never reached it. 65ch measures 624px
     against Rethink Sans, so the cap is real but small. The leading is the
     larger half of this change on a long lesson. */
  line-height: 1.6;
  color: var(--dark-brown);
  max-width: 65ch;
}
/* The page card is the box; the document inside it is just text. */
.pcard .lesson-body { margin-bottom: 0; padding: 0; background: none; box-shadow: none; border-radius: 0; }
.lesson-body p { margin: 0 0 var(--space-md); }
.lesson-body p:last-child { margin-bottom: 0; }
.lesson-body ul, .lesson-body ol { margin: 0 0 var(--space-md); padding-left: 1.4em; list-style: disc; }
.lesson-body ol { list-style: decimal; }
.lesson-body li { margin-bottom: var(--space-xs); }
.lesson-body a { text-decoration: underline; font-weight: 600; }
.lesson-body blockquote {
  margin: 0 0 var(--space-md);
  padding-left: var(--space-md);
  border-left: 3px solid var(--light-brown);
  color: var(--dark-brown);
}
.lesson-body h1, .lesson-body h2, .lesson-body h3,
.lesson-body h4, .lesson-body h5, .lesson-body h6 {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 16px;
  color: var(--dark-brown);
  margin: 0 0 var(--space-sm);
}
.lesson-body hr { border: none; border-top: 1px solid var(--light-brown); margin: var(--space-lg) 0; }

/* A distinct, neutral surface from .lesson-body's white card -- files read
   as a separate block of the page, not a continuation of the body copy. */
.files-section {
  background: var(--additional-brown);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
}
.file-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-height: 56px;
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--light-brown);
}
.file-row:last-child { border-bottom: none; }
.file-row-name { flex: 1; font-size: 16px; }
.file-row-dl {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  color: var(--dark-brown);
  padding: 8px 14px;
  border: 1.5px solid var(--dark-brown);
  border-radius: var(--radius-pill);
  flex-shrink: 0;
  box-shadow: var(--lift);
}
.file-row-dl:hover, .file-row-dl:focus-visible { background: var(--site-white); }

/* =========================================================================
   PRACTITIONER -- roster list (practitioner.html, Phase One: read-only,
   no per-row action yet). Same row/divider shape as .file-row above, not a
   card grid -- a name+email pair has no cover image or "Open" affordance
   to justify one.
   ========================================================================= */
.roster-list { list-style: none; margin: 0; padding: 0; }
.roster-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
  min-height: 56px;
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--light-brown);
}
.roster-row:last-child { border-bottom: none; }
.roster-row-name { font-size: 16px; font-weight: 700; color: var(--dark-brown); }
.roster-row-email { font-size: 14px; color: var(--dark-brown); }
/* Same look as .roster-row-email -- a generic secondary metadata line
   (Aug 2026: reused for a document's "Published <date>" row, not just an
   email), named for what it means rather than the one context it started in. */
.roster-row-meta { font-size: 14px; color: var(--dark-brown); }
/* In a page card (narrow phone column) the meta drops under the name instead of squeezing it. */
.pcard .roster-row--link { flex-wrap: wrap; row-gap: var(--space-sm); }

/* Roster rows are buttons in the publish flow (Phase Two) -- reset button
   chrome on top of the existing flex/spacing rules above rather than
   duplicating them. */
.roster-row--button {
  width: 100%;
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
  font: inherit;
}
.roster-row--button:hover .roster-row-name, .roster-row--button:focus-visible .roster-row-name { text-decoration: underline; }

/* Roster rows as real links (Aug 2026: client-facing document list,
   protocol.js) -- same flex/spacing rules, no button chrome to reset,
   just the underline-on-hover an <a> doesn't get by default here. */
.roster-row--link { display: flex; text-decoration: none; color: inherit; }
.roster-row--link:hover .roster-row-name, .roster-row--link:focus-visible .roster-row-name { text-decoration: underline; }

/* Practitioner-facing "what this client already has" list (Aug 2026,
   practitioner.js's publish panel) -- title+date stacked on the left,
   Delete pinned right. A new class rather than reusing .roster-row: that
   one puts name/email side by side with no justify-content, the wrong
   shape for a row that also needs a button pinned to the far right. */
.doc-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  min-height: 56px;
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--light-brown);
}
.doc-row:last-child { border-bottom: none; }
.doc-row-title { display: block; font-size: 16px; font-weight: 700; color: var(--dark-brown); }
.doc-row-date { display: block; margin-top: var(--space-sm); font-size: 14px; color: var(--dark-brown); }

/* Delete affordance on each row above. Small, quiet, red only on hover --
   not the primary action on this row; opening the document isn't offered
   here at all (there is nothing to preview after publish; she already
   saw it before confirming). */
.doc-row-delete {
  background: none;
  border: none;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--dark-brown);
  cursor: pointer;
  padding: 6px 10px;
  flex-shrink: 0;
}
.doc-row-delete:hover, .doc-row-delete:focus-visible { color: var(--warn); }

/* =========================================================================
   PRACTITIONER -- publish panel (practitioner.html, Phase Two)
   ========================================================================= */
.publish-section { max-width: 640px; }
.publish-section .field { margin-top: var(--space-lg); }
#publish-preview-wrap { margin-top: var(--space-lg); }
#publish-open-confirm { margin-top: var(--space-xl); }

.publish-confirm {
  margin-top: var(--space-xl);
  padding: var(--space-lg);
  background: var(--additional-brown);
  background-image: var(--sheen);
  border-radius: var(--radius-lg);
}
.publish-confirm-name { font-size: 20px; font-weight: 700; color: var(--dark-brown); margin: var(--space-xs) 0 0; }
.publish-confirm-email { font-size: 15px; color: var(--dark-brown); margin: 2px 0 var(--space-lg); }

.banner.success { border-color: var(--green); }

/* =========================================================================
   CLIENT MESSAGING (messages.html, client-facing; the "Messages" hub
   section in practitioner.js, staff-facing) -- _artifacts/
   client-messaging-spec.md.
   ========================================================================= */

/* The always-visible SLA/crisis block (S4). Never a dismissible banner --
   no close affordance anywhere in this rule, on purpose. A left accent
   bar on the crisis paragraph specifically (not the whole block, and not
   .banner.error's full-border treatment) draws the eye to it without
   reading as "something is broken," which it isn't. */
.safety-note {
  background: var(--additional-brown);
  background-image: var(--sheen);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
  margin-bottom: var(--space-lg);
}
.safety-note p { margin: 0; }
.safety-note p + p { margin-top: var(--space-sm); }
.safety-note-crisis {
  border-left: 3px solid var(--warn);
  padding-left: var(--space-md);
}

/* Fine print below the message cards, on the page background: the same words as the
   first-visit dialog, small but readable (13px), links underlined. */
.safety-note--fine {
  background: none;
  background-image: none;
  padding: 0 var(--space-md);
  margin: var(--space-lg) 0 var(--space-md);
}
.safety-note--fine p { font-size: 13px; line-height: 1.5; }
.safety-note--fine a { color: inherit; text-decoration: underline; }

/* First-visit crisis dialog: bottom sheet on a phone, centred on desktop. */
.msg-crisis-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  background: var(--overlay);
}
.msg-crisis-sheet {
  box-sizing: border-box;
  width: 100%;
  max-width: 480px;
  max-height: 90dvh;
  overflow-y: auto;
  padding: var(--space-xl) var(--space-lg);
  background: var(--site-white);
  border: var(--edge);
  border-radius: var(--radius-xxl);
  box-shadow: var(--lift);
}
.msg-crisis-title { margin: 0 0 var(--space-md); }
.msg-crisis-sheet .body-copy { margin: 0 0 var(--space-md); }
.msg-crisis-sheet a { color: inherit; text-decoration: underline; }
.msg-crisis-sheet .btn { width: 100%; margin-top: var(--space-sm); }
@media (max-width: 640px), (pointer: coarse) and (max-height: 500px) {
  .msg-crisis-backdrop { align-items: flex-end; padding: 0; }
  .msg-crisis-sheet {
    max-width: none;
    border-radius: var(--radius-xxl) var(--radius-xxl) 0 0;
    padding-bottom: calc(var(--space-xl) + env(safe-area-inset-bottom));
  }
}

/* Thread history -- chronological, oldest at top, ordinary conversation
   reading order (S9). "Not live" is carried by the absence of any
   presence/typing/read-receipt chrome here, not by the layout. */
.msg-thread { margin-bottom: var(--space-lg); }
.msg-thread-empty { padding: var(--space-lg) 0; }
.msg-row { display: flex; margin-bottom: var(--space-sm); }
.msg-row--mine { justify-content: flex-end; }
.msg-row--theirs { justify-content: flex-start; }
/* Measured 2026-09-28 during the reading-measure pass and deliberately left
   alone: .messages-main is .shell, so the thread is 640px and this 80% is
   512px, which at .msg-bubble-body's 15px is 57 characters -- already
   inside the 65-character target. A ch cap here would have been a rule
   that never fires at any width the portal actually renders at. The
   leading on .msg-bubble-body did move, below. */
.msg-bubble {
  max-width: 75%;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-sm);
  background: var(--site-white);
  color: var(--dark-brown);
  border: var(--edge);
}
.msg-row--mine .msg-bubble { background: var(--bubble-mine); color: var(--dark-brown); border-radius: var(--radius-lg) var(--radius-lg) var(--radius-sm) var(--radius-lg); }
@media (max-width: 480px) { .msg-bubble { max-width: 90%; } }
.msg-bubble-body { margin: 0; font-size: 15px; color: var(--dark-brown); line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
.msg-bubble-time { margin: var(--space-sm) 0 0; font-size: 12px; color: var(--dark-brown); }
/* Team chat only (teamChat.js) -- a group/Team conversation can have more
   than one "theirs" sender, unlike a client thread's own two-side layout,
   so a non-mine bubble names who sent it. */
.msg-bubble-sender { margin: 0 0 2px; font-size: 12px; font-weight: 700; color: var(--dark-brown); }

/* Message photo attachments (message-attachments-spec.md S13/S14/S16) --
   shared shape across messages.js, practitioner.js's client-hub thread,
   and teamChat.js. .msg-attachment-wrap is a block so the image always
   sits on its own line below any body text, matching .msg-bubble-body's
   own block layout. */
.msg-attachment-wrap { display: block; margin-top: var(--space-sm); }
.msg-attachment {
  display: block;
  max-width: 240px;
  max-height: 240px;
  width: auto;
  height: auto;
  border-radius: var(--radius-md);
  cursor: pointer;
}
/* Fixed footprint while the fetch is in flight -- without it the bubble
   collapses to zero height and then jumps once the real image loads. */
.msg-attachment--loading { width: 160px; height: 120px; background: var(--beige); opacity: 0.6; cursor: default; }
.msg-attachment-broken {
  width: 160px;
  height: 120px;
  margin-top: var(--space-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-sm);
  background: var(--beige);
  border-radius: var(--radius-md);
  font-size: 13px;
  color: var(--dark-brown);
  text-align: center;
}
.msg-attachment-failed { margin: var(--space-sm) 0 0; font-size: 13px; color: var(--warn); }
/* Shown on the sender's own bubble while an attachment upload is still in
   flight (messages.js/teamChat.js/practitioner.js), replaced in place by
   the real attachment or .msg-attachment-failed above once it settles.
   Same shape as .msg-attachment-failed, neutral color instead of --warn --
   this is progress, not an error. No progress bar: fetch() can't report
   real upload progress without switching to XHR, not worth it here. */
.msg-attachment-pending { margin: var(--space-sm) 0 0; font-size: 13px; color: var(--dark-brown); }

/* PDF message attachments (pdf-attachment-messaging-spec.md) -- a
   distinct, non-thumbnail chip, never a preview image (no client-side PDF
   rendering anywhere in this app). The container also carries
   .msg-attachment-wrap (see markup) for the same block-level spacing the
   image path gets; display:flex here intentionally overrides that rule's
   display:block, later in source order, same specificity. The Download
   button reuses .btn/.btn-outline/.btn-inline -- no new button styling. */
.msg-attachment-pdf {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  max-width: 280px;
  padding: var(--space-sm);
  background: var(--beige);
  border: 1.5px solid var(--light-brown);
  border-radius: var(--radius-md);
}
.msg-attachment-pdf-icon {
  flex: 0 0 auto;
  width: 32px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--dark-brown);
  color: var(--site-white);
  font-size: 10px;
  font-weight: 700;
  border-radius: var(--radius-sm);
}
.msg-attachment-pdf-meta { flex: 1 1 auto; min-width: 0; }
.msg-attachment-pdf-name {
  display: block;
  font-size: 14px;
  font-weight: 600;
  color: var(--dark-brown);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.msg-attachment-pdf-size { display: block; font-size: 12px; color: var(--dark-brown); }

/* Composer -- pinned at the bottom of the thread on both surfaces. No
   typing indicator, no send-state beyond the button's own disabled/label
   lifecycle -- deliberately nothing that implies a watching human. */
.composer { margin-top: var(--space-md); }
.composer-textarea {
  width: 100%;
  min-height: 96px;
  padding: var(--space-sm) var(--space-md);
  background: var(--site-white);
  border: 1.5px solid var(--dark-brown);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: 16px;
  color: var(--dark-brown);
  resize: vertical;
  margin-bottom: var(--space-sm);
}

/* Photo picker preview (S13) -- a small thumbnail with an overlaid
   remove (x) affordance, above the Add a photo / Send row. */
.composer-photo-preview { position: relative; display: inline-block; margin-bottom: var(--space-sm); }
.composer-photo-thumb { display: block; max-width: 120px; max-height: 120px; border-radius: var(--radius-md); }
.composer-photo-remove {
  position: absolute;
  top: -8px;
  right: -8px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: none;
  background: var(--dark-brown);
  color: var(--site-white);
  font-size: 16px;
  line-height: 24px;
  cursor: pointer;
}
.composer-photo-warning { margin: var(--space-xs) 0 0; font-size: 13px; color: var(--warn); }

/* Voice notes (voice-notes-spec.html) -- one recorder and one player shared by
   client messages, the staff client-hub thread and team chat. Existing tokens
   only, so both themes work; the recording dot is --note-red, which the dark
   theme already lifts. Every control is at least 44px. */
.voice-recorder { flex: 0 0 auto; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); }
/* Recording, preview or an inline note: the host takes the whole row above the buttons. */
.voice-recorder--open { flex: 1 0 100%; order: -1; }
.voice-mic { display: inline-flex; align-items: center; gap: var(--space-sm); min-height: 44px; }
.voice-panel { flex: 1 1 auto; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); padding: var(--space-sm); background: var(--beige); border: 1.5px solid var(--light-brown); border-radius: var(--radius-md); }
.voice-panel--staged { position: relative; max-width: 360px; flex-grow: 0; }
.voice-panel--recording .btn { min-height: 44px; }
.voice-dot { flex: 0 0 auto; width: 12px; height: 12px; border-radius: 50%; background: var(--note-red); }
.voice-clock { flex: 1 1 auto; white-space: nowrap; font-size: 15px; font-weight: 600; color: var(--dark-brown); font-variant-numeric: tabular-nums; }
.voice-remove { position: absolute; top: -10px; right: -10px; width: 32px; height: 32px; border-radius: 50%; border: none; background: var(--dark-brown); color: var(--site-white); font-size: 18px; line-height: 32px; cursor: pointer; }
.voice-remove::after { content: ""; position: absolute; inset: -6px; }
.voice-player { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); max-width: 320px; }
.voice-player.msg-attachment-wrap { display: flex; padding: var(--space-sm); background: var(--beige); border: 1.5px solid var(--light-brown); border-radius: var(--radius-md); }
.voice-player--staged { flex: 1 1 auto; }
.voice-play { flex: 0 0 auto; width: 44px; height: 44px; display: grid; place-items: center; border: none; border-radius: 50%; background: var(--dark-brown); color: var(--site-white); font-size: 18px; cursor: pointer; }
.voice-meter { flex: 1 1 120px; min-width: 0; display: flex; flex-direction: column; gap: var(--space-xs); }
.voice-bar { display: block; height: 4px; border-radius: var(--radius-pill); background: var(--light-brown); overflow: hidden; }
.voice-bar-fill { display: block; width: 0; height: 100%; background: var(--dark-brown); }
.voice-time { white-space: nowrap; font-size: 12px; color: var(--dark-brown); font-variant-numeric: tabular-nums; }
.voice-note-msg { flex: 1 0 100%; margin: 0; font-size: 13px; color: var(--warn); }
.voice-note-msg--composer { flex: 1 0 100%; }
.chat-composer .voice-mic span { display: none; }
.chat-composer .voice-mic { width: 44px; padding: 0; justify-content: center; }
.chat-composer .voice-panel { max-width: 52rem; }
/* Lets a recording or preview panel take the row above the input; the input's
   zero flex-basis keeps everything else on one line. */
.chat-composer-row { flex-wrap: wrap; }
.chat-composer-row .chat-composer-input { flex-basis: 0; }
.chat-msg .voice-player.msg-attachment-wrap { max-width: 360px; }
/* Voice note transcript: its own spacing under the player; the muted pending/failed/expired lines reuse .msg-attachment-pending.
   .voice-note-block is a plain block so the children's top margin collapses through it (the gap above the note is unchanged).
   The min-width keeps a bubble holding only "Voice note expired" from shrinking to a skinny column. */
.voice-note-block { min-width: 12rem; }
.voice-transcript { margin: var(--space-sm) 0 0; max-width: 360px; overflow-wrap: anywhere; }
.voice-transcript--text { font-size: 14px; line-height: 1.5; white-space: pre-wrap; color: inherit; }


/* Frozen (completed/withdrawn) thread -- expected, not broken. Reuses
   .banner's shape with a neutral border, never .banner.error's warn
   color -- same tone the practitioner spec's video-expired copy uses. */
.banner.frozen-note { border-color: var(--light-brown); background: var(--additional-brown); background-image: var(--sheen); }

/* Staff-facing thread list row (practitioner.js) -- same shape as
   .roster-row, plus an unread badge pinned right. */
.thread-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  min-height: 56px;
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--light-brown);
  width: 100%;
  background: none;
  border-top: none;
  border-left: none;
  border-right: none;
  text-align: left;
  cursor: pointer;
  font: inherit;
}
.thread-row:last-child { border-bottom: none; }
/* Needs-you rows: client messages, bookings and team-meeting notes hold one or
   two lines, and min-height 56px centred the one-line ones so they sat in more
   space than the two-line ones. Same padding, no minimum, for every row. */
#needs-you-section .thread-row { min-height: 0; padding: var(--space-md) 0; }
.thread-row:hover .thread-row-name, .thread-row:focus-visible .thread-row-name { text-decoration: underline; }
.thread-row-name { font-size: 16px; font-weight: 700; color: var(--dark-brown); }
.thread-row-preview { display: block; margin-top: var(--space-sm); font-size: 14px; color: var(--dark-brown); max-width: 42em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Roster "what's outstanding" line: wraps to two lines on a phone, then clamps, so the second item is not cut off. */
.roster-needs-line { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; white-space: normal; }
.roster-needs-line--calm { opacity: 0.8; }
.roster-needs-wait { height: 14px; width: 40%; background: var(--beige); border-radius: var(--radius-lg); opacity: 0.6; }
.roster-row-text { flex: 1; min-width: 0; }
.roster-needs-note { margin: 0 0 var(--space-lg); font-size: 14px; color: var(--dark-brown); }
.roster-needs-note:empty { display: none; }
.unread-badge {
  flex-shrink: 0;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: var(--radius-pill);
  background: var(--badge-bg);
  color: var(--site-white);
  font-size: 12px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* "Your coaching resources" library grid (index.js's coachingResourcesHtml())
   -- portal-home-and-hub-infodesign-spec.md §5.2 folded this section's own
   heading into the shared "More resources" <h2> above it (see .free-resources
   below), so this class now carries only trailing spacing, moved down onto
   the grid itself. The two document items reuse .file-row/.file-row-dl
   verbatim (lesson.js's own file-list shape, unmodified). The one new class
   below is a small secondary caption under a card's file row(s) -- same
   visual value already established for .doc-row-date/.roster-row-email
   (14px, mid-brown secondary text), under its own name because it isn't a
   date or an email here, it's a timing note. */
.coaching-resources { margin-bottom: var(--space-xxl); }
/* portal-home-and-hub-infodesign-spec.md §5.1/§5.2: two new stacked-page
   groupings, same margin-bottom-only pattern (and the same --space-xxl
   token) as .coaching-resources above -- the heading's own margin-top is
   zeroed by the VERTICAL RHYTHM DEFAULTS' :first-child rule (it opens its
   own <section>), so the wrapper's own margin is what separates it from
   whatever follows. */
/* client-portal-information-design-spec.md §5.2: the same pattern again for
   "Your tools", which replaced the two standalone card rows that used to
   carry this spacing themselves (.symptom-tracker-card and .calm-tools,
   both now bare anchor classes on grid items with no styling of their own).
   No .your-courses rule is needed on the same pattern: that section sits
   between .your-forms' margin-bottom and .free-resources' margin-top, both
   already --space-xxl, so a third would only collapse into them. */
/* resourcesBodyHtml()'s available block (client-resources-page-spec.md
   §6.2) reuses .free-resources (above, this file) for its own
   section-level spacing -- deliberate reuse of an existing class, not a
   leftover, so no new spacing-token rule was ever needed for it. When
   coachingResourcesMarkup() also renders, its grid sits directly below
   freeResourcesMarkup()'s own -- this is the rule that gap needs, and it
   only ever matches when both grids are actually present. A descendant
   selector, so it went on matching through the <details> that briefly sat
   between the section and the grids and goes on matching now that the
   <details> is gone again. */
.free-resources .card-grid + .card-grid { margin-top: var(--space-xl); }
/* P3.3's "Ready when you are, no rush." subtitle. This is now the only
   heading/lede pairing left on this page (More resources lost its lede in
   information-design §5.5), and it is NOT tight: the mockup (client.html,
   STATE_FIRST_DAY) gives it the same
   16px rhythm as everything else -- the h2 default already does that, so
   no h2 override here (an earlier version added one, which left a stray
   4px gap above the cards on a run with no subtitle at all). The .lede
   itself only exists in the DOM when a form is outstanding, so this rule
   has no effect otherwise. */
.your-forms .lede { margin-bottom: var(--space-md); }
/* --green, not --mid-brown: this is the one line on the card that carries an
   instruction, and at --mid-brown on a beige card it read as disabled caption
   text and disappeared. --green is the palette's accent, already used for the
   Download buttons beside it, so it reads as "act on this" without --warn's
   alarm colour, which this audience should never see for a worksheet. */
.coaching-note { font-size: 14px; font-weight: 600; color: var(--green); margin: var(--space-sm) 0 0; }
/* .file-row:last-child above can't do this job here: .coaching-note is always
   the card's last child, so the trailing file row never matches it and keeps a
   divider that would sit right on top of the note. Scoped to this section only
   -- lesson.js's file list has no trailing sibling and must keep its own rule.
   Still :last-of-type rather than :last-child because a FINISHED card has no
   .coaching-note at all (client-resources-page-spec.md §6.3), so there the
   file row IS the last child and would lose its divider either way. */
.coaching-resources .file-row:last-of-type { border-bottom: none; }

/* The item lifecycle: one clear control per live coaching item
   (client-resources-page-spec.md §6.5, portal-item-lifecycle-spec.md §4.3).
   Both client surfaces, hence both ids -- the control renders on
   resources.html and on the library page, which lives
   under #library-view. Same comma-list discipline as the four rules in the
   CLIENT LIBRARY block above, for the same reason: no duplicate to drift.

   .coaching-item is the wrapper that takes the card's place in the grid,
   because a <button> cannot nest inside the video card's <a>. Column flex
   with the card flexing, so cards in the
   same row stay the same height as they were when they were grid items
   themselves. No new token, no new colour, no new radius.

   .card-clear is deliberately QUIETER than the bordered .file-row-dl pill
   beside it: the primary action on a live worksheet is still Download, and
   a second pill would compete with it. 44px is this stylesheet's own touch
   target; 15px and --dark-brown are both already in use. */
#library-view .coaching-item, #resources-view .coaching-item { display: flex; flex-direction: column; }
#library-view .coaching-item > .card, #resources-view .coaching-item > .card { flex: 1; }
#library-view .card-clear, #resources-view .card-clear {
  /* Left edge = the card's own content edge (19px padding + 1px border on .card). */
  align-self: flex-start;
  /* The layout gate caught this at 0px on its first run: the button sat flush
     against the card it belongs to, at every width. It reads as part of the
     card rather than an action on it, and 44px of tap target directly abutting
     a tappable card is its own problem on a phone. --space-sm matches the gap
     the grid already puts between cards. */
  margin-top: var(--space-sm);
  min-height: 44px;
  padding: 0 20px;
  border: none;
  background: none;
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 600;
  color: var(--dark-brown);
  cursor: pointer;
}
#library-view .card-clear:hover, #resources-view .card-clear:hover,
#library-view .card-clear:focus-visible, #resources-view .card-clear:focus-visible { text-decoration: underline; }

/* "Finished" (client-resources-page-spec.md §6.3). Same margin-bottom-only
   pattern as .up-next/.your-coach/.your-tools above, and the h2's own
   margin-top is zeroed by the VERTICAL RHYTHM DEFAULTS' :first-child rule
   because it opens its own <section>. The h2 carries tabindex="-1" as the
   post-clear focus target and gets NO focus style of its own: a
   programmatic focus() on it shows nothing (it is not :focus-visible), and
   a real keyboard focus keeps the browser's own ring. */

/* =========================================================================
   PRACTITIONER -- client hub (practitioner.js). Hero + a seven-tab strip;
   the seven panels below are all rendered up front and toggled with
   display (portal-shell-redesign-spec.md §7.4) -- .hub-nav's underlined-
   link jump row is gone, replaced by the real tab strip in the next block.
   ========================================================================= */
.hub-section { display: none; }
.hub-section.is-active { display: block; }
/* Documents (.publish-section below) is already capped at 640px and left-
   aligned (no margin-inline: auto). The other panels have no cap of their
   own and stretch to the full 1240px .shell (message bubbles/composer/
   datetime input included) -- same cap, same left alignment, so every
   non-Summary panel lines up under the tab bar. Summary keeps its own
   card-grid width. */
#hub-messages, #hub-video, #hub-graduation, #hub-forms, #hub-scheduling { max-width: 640px; }

/* ---------- hero (§7.1) ---------- */
.hub-hero {
  display: flex;
  align-items: flex-end;
  gap: var(--space-md);
  flex-wrap: wrap;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--beige);
}
.hub-hero-meta { font-size: 12.5px; color: var(--dark-brown); margin: 5px 0 0; }
.hub-pills { margin-left: auto; display: flex; align-items: center; gap: 7px; }
.hub-pill {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 4px 10px;
  border: var(--edge);
  border-radius: var(--radius-pill);
  background: var(--beige);
  background-image: var(--sheen);
  color: var(--dark-brown);
  box-shadow: var(--lift);
}
.hub-pill.is-active { background: var(--green-action); background-image: var(--sheen-midtone); color: var(--site-white); }

/* ---------- tab strip (§7.2) ---------- */
.hub-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  margin: 14px 0 var(--space-lg);
  border-bottom: 1px solid var(--beige);
}
.hub-tab {
  padding: 9px 14px;
  font-size: 13px;
  color: var(--dark-brown);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
}
.hub-tab:hover { color: var(--dark-brown); }
.hub-tab-badge { display: inline-flex; vertical-align: middle; margin-left: var(--space-sm); }

/* Summary "Messages" card: read-only last-3 list. Unread = bold sender + warn dot. */
.hub-msg-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-md); }
.hub-msg-item { display: grid; grid-template-columns: 1fr auto; column-gap: var(--space-sm); row-gap: var(--space-sm); font-size: 14px; color: var(--dark-brown); }
.hub-msg-who { font-weight: 600; }
.hub-msg-item.is-unread .hub-msg-who::before { content: ""; display: inline-block; width: 8px; height: 8px; margin-right: var(--space-sm); border-radius: 50%; background: var(--badge-bg); }
.hub-msg-item.is-unread .hub-msg-preview { font-weight: 600; }
.hub-msg-time { font-size: 12px; }
.hub-msg-preview { grid-column: 1 / -1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hub-msg-open { margin: var(--space-md) 0 0; display: flex; flex-wrap: wrap; gap: var(--space-sm) var(--space-md); align-items: center; font-size: 14px; }
.hub-tab.is-active { color: var(--dark-brown); font-weight: 600; border-bottom-color: var(--green); }

/* ---------- Summary card grid (§9/§11 "Grid mechanics") ----------
   Uses the shared .pgrid/.pcard primitives (PAGE CARDS, above). Card title
   is h3.pn-16 -- the Perfectly Nineties heading token, not a small-caps
   grotesque, which reads nearly identical to a field key and would fail
   AC-17's "no two of them share a class" requirement. */

/* --beige fill, --mid-brown text (§10.1) -- no green/warn variant in phase
   2; a coloured chip would be an editorial judgement about a client. */
.hub-chip {
  font-size: 9.5px;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--dark-brown);
  background: var(--beige);
  padding: 2px 8px;
  border: var(--edge);
  border-radius: var(--radius-pill);
  white-space: nowrap;
  flex-shrink: 0;
  box-shadow: var(--lift);
}
.hub-card-body > :first-child { margin-top: 0; }
.hub-card-source, .hub-soon {
  margin: 11px 0 0;
  padding-top: 10px;
  border-top: 1px solid var(--beige);
  font-size: 11px;
  color: var(--dark-brown);
}
.hub-card-empty { margin: 0 0 4px; font-size: 13px; color: var(--dark-brown); }

/* Session note card (Summary, every width). 16px text so iOS does not zoom on focus. */
.hub-note-input { width: 100%; font-size: 16px; resize: vertical; }
.hub-note-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm) var(--space-md); margin-top: var(--space-md); }
.hub-note-actions .btn { width: auto; min-height: 48px; padding-left: var(--space-xl); padding-right: var(--space-xl); }
.hub-note-status { margin: 0; color: var(--dark-brown); }
#session-note-list { margin-top: var(--space-lg); }
.hub-note-entry + .hub-note-entry, .hub-note-earlier > summary + .hub-note-entry { margin-top: var(--space-md); }
.hub-note-stamp { margin: 0 0 var(--space-sm); font-size: 12px; font-weight: 600; color: var(--dark-brown); }
.hub-note-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
.hub-note-earlier { margin-top: var(--space-md); border-top: 1px solid var(--beige); }
.hub-note-earlier > summary::-webkit-details-marker { display: none; }
.hub-note-earlier > summary::before { content: "+"; width: 16px; }
.hub-note-earlier[open] > summary::before { content: "\2013"; }
.hub-note-earlier > summary { list-style: none; display: flex; align-items: center; min-height: 48px; font-size: 14px; font-weight: 600; color: var(--dark-brown); cursor: pointer; }

/* Phone Summary: "Before her call" strip, ordered and foldable cards (practitioner.js mountPhoneCardCollapse).
   The toggle is a plain heading off the phone. Order: a flagged Fit check, the strip, Messages, Session note,
   Symptom tracking, Food & Mood, Core values, the rest folded, an unflagged Fit check last. Programme is
   placeholders only ("Started" is in the hero), so the phone leaves it out. */
.pcard-toggle { display: block; width: 100%; padding: 0; border: 0; background: none; font: inherit; color: inherit; text-align: left; cursor: default; }
.pcard-chevron, #hub-call-strip { display: none; }
@media (max-width: 640px), (pointer: coarse) and (max-height: 500px) {
  #hub-call-strip:not(:empty) { display: block; }
  #hub-call-strip .pcard { margin: 0; }
  .hub-call-lines { list-style: none; margin: 0; padding: 0; }
  .hub-call-lines li { display: flex; align-items: center; min-height: 32px; font-size: 15px; color: var(--dark-brown); }
  .hub-call-lines a { display: flex; align-items: center; min-height: 44px; font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }
  #hub-summary .pgrid > [data-card] { order: 6; min-width: 0; }
  #hub-summary .pgrid > .pcard--wide[data-card="fit-check"] { order: 0; }
  #hub-summary .pgrid > [data-card="call-strip"] { order: 1; }
  #hub-summary .pgrid > [data-card="messages"] { order: 2; }
  #hub-summary .pgrid > [data-card="session-note"] { order: 3; }
  #hub-summary .pgrid > [data-card="symptom-tracking"] { order: 4; }
  #hub-summary .pgrid > [data-card="food-mood"], #hub-summary .pgrid > [data-card="core-values"] { order: 5; }
  #hub-summary .pgrid > [data-card="fit-check"]:not(.pcard--wide) { order: 7; }
  #hub-summary .pgrid > [data-card="programme"] { display: none; }
  .pcard-head:has(.pcard-toggle) { flex-wrap: nowrap; cursor: pointer; }
  .pcard-toggle { display: flex; align-items: center; min-height: 48px; cursor: pointer; }
  .pcard-toggle:focus-visible { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
  .pcard-chevron { display: block; flex: 0 0 auto; width: 8px; height: 8px; border-right: 2px solid var(--dark-brown); border-bottom: 2px solid var(--dark-brown); transform: rotate(45deg); margin-right: var(--space-xs); }
  .pcard:not(.is-collapsed) > .pcard-head > .pcard-chevron { transform: rotate(-135deg); }
  .pcard.is-collapsed > :not(.pcard-head) { display: none; }
  .pcard.is-collapsed > .pcard-head { margin-bottom: 0; }
  #hub-summary .symptom-track-grid { max-width: 100%; }
}

/* Symptom tracking card (symptom-tracking-coach-view-spec.md §5) --
   1 label column + one column per day, sized inline via
   grid-template-columns (practitioner.js's symptomTrackingGridHtml()
   sets the day count). Zero new :root tokens: severity fills are
   var(--sev-*), unlogged cells reuse --edge/--lift, the phase underline
   reuses the client calendar's own rgba() values, period markers reuse
   --period. */
.symptom-track-grid { display: grid; gap: 3px; overflow-x: auto; align-items: stretch; }
.symptom-track-rowlabel { display: flex; align-items: center; font-size: 13px; font-weight: 600; color: var(--dark-brown); padding-right: 8px; white-space: nowrap; }
.symptom-track-daylabel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 2px 0 5px;
  font-size: 10px;
  color: var(--dark-brown);
  border-bottom: 3px solid transparent; /* overridden inline per day with the phase-tint rgba(), when a phase is known */
}
.symptom-track-date { font-weight: 700; color: var(--dark-brown); }
.symptom-track-cd { font-size: 9px; }
.symptom-track-period-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--period); margin-top: 1px; }
.symptom-track-cell { min-height: 28px; border-radius: var(--radius-sm); }
/* Coordinator's own standing rule for this exact bug class (symptom-
   mapping-portal-spec.md §7.4, round 3 item 5): an unlogged cell must
   never look like a logged-"none" cell. Spec §5.2 names `var(--lift)`
   for the fill, but --lift is a box-shadow value (`0 1px 3px
   rgba(...)`, style.css:100), not a color -- `background: var(--lift)`
   would be invalid CSS. --off-white used instead: a light, neutral fill
   distinct from both the card's own --site-white background and every
   --sev-* solid fill, paired with the dashed --edge border the spec
   also calls for. Flagged for Beetle rather than silently deviated
   from. */
.symptom-track-cell--unlogged { background: var(--off-white); border: 1px dashed var(--edge); }
.symptom-track-cell--mood {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 4px;
  background: var(--beige);
  border: var(--edge);
  font-size: 11px;
  font-weight: 600;
  color: var(--dark-brown);
}
/* Peterson re-audit: text-overflow doesn't apply to a flex container's
   own text run -- same fix .chart-label-cell-text (symptom-history.js)
   already needed. This nested span is what the ellipsis actually lives
   on. */
.symptom-track-mood-text { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; min-width: 0; }

/* ---------- fields (§10.4) ---------- */
.hub-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }
.hub-fields--one { grid-template-columns: 1fr; }
.hub-field-key {
  display: block;
  margin: 0;
  font-size: 10px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--green);
}
.hub-field-value { margin: var(--space-sm) 0 0; font-size: 13.5px; color: var(--dark-brown); }
/* Was lightBrown (2026-09-28 contrast sweep: 1.85:1 at the dark end of the
   card's sheen, effectively invisible). This renders real words -- "Not
   tracked yet" -- so the empty/filled distinction now rests on the wording
   rather than on a colour nobody could read anyway. */
.hub-field-value--empty { color: var(--dark-brown); }
.hub-field-caption { margin: 2px 0 0; font-size: 11px; color: var(--dark-brown); }
.hub-field textarea.text-input { margin-top: 2px; }

/* ---------- Quality of life pillars (§9 Card 3) ----------
   A continuous fill, not a segmented meter -- min-width keeps a score of 1
   from being visually zero. An unanswered pillar renders no fill element
   at all (JS-side), so a 0%-width fill and a real low score can never be
   mistaken for one another. */
.hub-pillar { display: grid; grid-template-columns: 92px 1fr 22px; align-items: center; gap: 10px; margin-bottom: var(--space-sm); }
.hub-pillar:last-child { margin-bottom: 0; }
.hub-pillar-label { font-size: 12.5px; color: var(--dark-brown); }
/* display:block is load-bearing, not tidiness. Both call sites render
   this as a <span> (product.js's course progress, practitioner.js's hub
   bar), and an inline box ignores height and paints no background, so
   the track collapsed to nothing and the absolutely-positioned fill
   below showed up as a stray 4px vertical sliver. Neither bar had ever
   rendered. Fixed here rather than by changing one call site's tag, so
   the component is correct for whichever element a future caller uses. */
.hub-bar { display: block; position: relative; height: 8px; background: var(--beige); border-radius: var(--radius-pill); overflow: hidden; }
.hub-bar-fill { position: absolute; inset: 0 auto 0 0; height: 100%; min-width: 4px; background: var(--green); }
#form-progress-label { margin-bottom: var(--space-sm); }
.hub-pillar-n { font-size: 11px; color: var(--dark-brown); text-align: right; }

/* Course progress -- course-progress-tracking-spec.md. Reuses .hub-bar/
   .hub-bar-fill above verbatim rather than a second bar component. */
.course-progress { margin: 16px 0 28px; max-width: 420px; }
.course-progress-label { font-size: 14px; color: var(--dark-brown); margin-bottom: 6px; }
.course-progress .hub-bar { height: 8px; }
.course-progress-continue { display: inline-flex; align-items: center; gap: 6px; margin-top: 10px; font-weight: 600; color: var(--green); text-decoration: none; }
.lesson-row-complete { margin-left: auto; display: inline-flex; color: var(--green); }
.lesson-complete-error { color: var(--warn); font-size: 13px; margin-top: 6px; }

/* ---------- disclosure (§10.2) ---------- */
details.hub-more { margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--beige); }
details.hub-more > summary {
  font-size: 9.5px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--dark-brown);
  cursor: pointer;
  list-style: none;
}
details.hub-more > summary::-webkit-details-marker { display: none; }
details.hub-more > summary:before { content: "+"; display: inline-block; width: 12px; font-size: 12px; letter-spacing: 0; }
details.hub-more[open] > summary:before { content: "\2013"; }
details.hub-more > summary:hover { color: var(--dark-brown); }
details.hub-more .hub-fields { margin-top: 12px; }
/* Staff Summary "Reference previews": one 44px tap target per link, a token apart. */
.ref-preview-list { display: grid; gap: var(--space-xs); }
.ref-preview-list a { display: flex; align-items: center; min-height: 44px; }

/* Used across the hub for a single quiet empty-state line (Messages,
   Video, Documents, care-team-boundary notices, etc). Was ALSO backing
   the Forms/Scheduling placeholder tabs (.hub-section--placeholder,
   .hub-placeholder-tag) until form-display-redesign-spec.md §13 deleted
   that dead markup and CSS -- this rule stays, still live everywhere
   else. */
.hub-placeholder-note { margin: 0; font-size: 15px; color: var(--dark-brown); line-height: 1.5; }

/* Content-not-yet-authored placeholders (portal-intake-forms-spec.md's
   "render in red, never silently omitted" fields) -- deliberately a
   different, louder color than --warn's amber, which this stylesheet
   already uses for real operational alerts. Red here means "nobody has
   written this yet," not "something is wrong." Used by form.html (a
   placeholder agreement) and practitioner.js's two summary screens
   (Symptom Burden formula, F&M Journal Insights, the final phrase list). */
.build-placeholder-tag {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--note-red);
  background: var(--note-red-bg);
  border-radius: var(--radius-pill);
  padding: 4px 10px;
  margin-bottom: var(--space-sm);
}
.build-placeholder-note { margin: 0; font-size: 15px; color: var(--note-red); line-height: 1.5; }

/* =========================================================================
   PRACTITIONER LANDING -- "Today's schedule" (Aug 2026, Shawn: page one
   has to answer "what does my day look like" before "which client do I
   want," which the client list alone never did). Fed by real Cal.com
   webhook data as of this pass (see practitioner.js's fetchUpcomingBookings
   and functions/api/cal-webhook.ts) -- the dashed/off-white "not real yet"
   treatment this used as a placeholder is gone; a solid border reads as an
   ordinary contained module now, same as everywhere else real content sits
   in a box on this page.
   ========================================================================= */
.schedule-list {
  list-style: none;
  margin: 0;
  padding: var(--space-sm) var(--space-lg);
  background: var(--off-white);
  border: 1px solid var(--light-brown);
  border-radius: var(--radius-lg);
}
.schedule-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--light-brown);
}
.schedule-row:last-child { border-bottom: none; }
.schedule-row-time { font-size: 15px; font-weight: 700; color: var(--dark-brown); min-width: 84px; flex-shrink: 0; }
.schedule-row-detail { font-size: 15px; color: var(--dark-brown); }
/* Phone: a row holding a time, a line of text and buttons stacks instead of
   squeezing the text into a sliver -- time, then the text at full width, then
   the buttons in a row. */
@media (max-width: 640px) {
  .schedule-row { flex-wrap: wrap; row-gap: var(--space-sm); }
  .schedule-row-time, .schedule-row-detail { flex: 1 0 100%; }
  .schedule-row .tm-notes-chip { margin-left: 0; }
}
/* Same stacking in a one-track card (3 per row at 1440 leaves ~300px of
   content), whatever the screen. Not container-type: that would make the
   card the containing block for any fixed overlay inside it. */
/* The card is the box; a list inside it is rows, not a second box. */
.pcard .schedule-list { padding: 0; background: none; border: 0; border-radius: 0; }
.pcard:not(.pcard--wide) .schedule-row { flex-wrap: wrap; row-gap: var(--space-sm); }
.pcard:not(.pcard--wide) .schedule-row-time, .pcard:not(.pcard--wide) .schedule-row-detail { flex: 1 0 100%; }
.pcard:not(.pcard--wide) .schedule-row .tm-notes-chip { margin-left: 0; }
.schedule-checklist { margin: 8px 0 16px; padding-left: 20px; }
.schedule-checklist li { margin: 4px 0; }
.schedule-checklist a { color: var(--dark-brown, inherit); font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }

/* =========================================================================
   PRACTITIONER MONTH CALENDAR (practitioner-calendar-spec.html) -- month
   grid + selected-day panel, stacked (not the left/right split
   #sessions-columns uses further down) so it stays inside .shell's
   existing 720px at every width, same reasoning that file's own spec §ui
   gives for not widening this page's shell. Day-panel session rows reuse
   .schedule-list/.schedule-row above unmodified.
   ========================================================================= */
.calendar-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}
.calendar-nav-months { display: flex; align-items: center; gap: var(--space-sm); }
/* Flex row, not block flow -- the heading rhythm default's margin-top/
   bottom (VERTICAL RHYTHM DEFAULTS, above) would otherwise add unwanted
   space around this flex item instead of collapsing the way it does
   between block siblings. */
.calendar-nav h2 { margin: 0; }
.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--space-xs);
}
.calendar-weekday {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--dark-brown); /* Coordinator, 2026-09-23 browser pass item 10: --mid-brown as text fails contrast */
  text-align: center;
  padding-bottom: var(--space-xs);
}
.calendar-cell {
  position: relative;
  min-height: 44px;
  padding: var(--space-xs) var(--space-sm);
  border: 1px solid var(--light-brown);
  border-radius: var(--radius-sm);
  background: var(--site-white);
  text-align: left;
}
button.calendar-cell { cursor: pointer; }
.calendar-cell-num { font-size: 13px; font-weight: 600; color: var(--dark-brown); }
/* Leading/trailing days from the adjacent month -- dimmed, not clickable
   (a plain div, not a button; see calendarGridHtml() in practitioner.js). */
.calendar-cell.is-outside { background: var(--off-white); }
/* Second survivor of the 2026-09-28 contrast sweep (2.44:1 on the outside
   cell's off-white fill). Held at midBrown because here the dimness IS the
   data: it is the only thing telling you this day belongs to the adjacent
   month. Promote it to darkBrown and an out-of-month day becomes
   indistinguishable from an in-month one, which on a symptom calendar is
   a worse error than a faint number. Fixing this needs a non-color
   affordance, which is a markup change and therefore Shawn's call. */
.calendar-cell.is-outside .calendar-cell-num { color: var(--mid-brown); }
/* --green for "this one is current," the same meaning .rail-client.is-active
   already carries in the nav rail. */
.calendar-cell.is-today { border: 2px solid var(--green); }
/* --green's own RGB (132, 130, 65) at low alpha -- same technique this
   file already uses for a --dark-brown-derived tint (.text-input:focus,
   above), not a new color. Today's ring and Selected's fill can both be
   true of the same cell (the default: today, freshly opened). */
.calendar-cell.is-selected { background: var(--select-tint); }
/* Busy-day count -- green fill (not --warn, which .unread-badge already
   owns for a different meaning: an unread message alert). Capped "9+" in
   practitioner.js's own render, not here. */
.calendar-badge {
  position: absolute;
  bottom: 4px;
  right: 4px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: var(--radius-pill);
  background: var(--green);
  color: var(--site-white);
  font-size: 10px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Failed-month marker -- every cell gets one, same --warn token
   .rail-retry already uses for a load failure. */
.calendar-warn-dot {
  position: absolute;
  bottom: 4px;
  right: 4px;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 1.5px solid var(--warn);
  color: var(--warn);
  background: var(--site-white);
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Loading marker -- same neutral fill .skeleton already uses, just small
   enough to sit in a day cell's corner. */
.calendar-skel-dot {
  position: absolute;
  bottom: 4px;
  right: 4px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--beige);
  opacity: 0.7;
}
.calendar-day-panel { margin-top: var(--space-xl); }
/* .skeleton itself carries no vertical-rhythm default (most call sites
   render exactly one), so 2-3 stacked here need a gap of their own --
   scoped to this one wrapper rather than a new global rule that would
   reach every other .skeleton in the app. */
.calendar-day-skeleton { display: flex; flex-direction: column; gap: var(--space-sm); }

/* "Your sessions" client page (sessions.js) ONLY, scoped under #sessions-list so
   practitioner.js's staff dashboard -- which renders this same .schedule-row
   markup under #client-sessions-list/#hub-video -- is untouched by any of this.
   justify-content is explicit, not left to flex's default, because it is the
   one thing that actually decides where the trailing button sits: packing
   every child at the row's start (time, then detail, then button, each
   separated only by `gap`) means the button lands immediately after the
   detail text, and any leftover width collects once, at the row's own right
   edge -- never as a gap in the middle of the row the way the removed
   `margin-left:auto` produced. */
#sessions-upcoming .schedule-row, #sessions-past .schedule-row {
  align-items: center;
  justify-content: flex-start;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-lg);
  padding: var(--space-md) 0;
}
.schedule-empty-note { margin: 0; font-size: 15px; color: var(--dark-brown); line-height: 1.5; }
/* Scoped to #sessions-list, not a bare .schedule-empty-note a rule --
   same bleed lesson this file already learned once with .schedule-row
   (see the #sessions-list .schedule-row comment above). Only sessions.js's
   agreement-gate messaging (in-portal-booking-spec.md §4) puts a real
   link inside this class today; `a { color: inherit; text-decoration:
   none; }` (top of this file) leaves a bare one invisible as a link. */
/* Also #sessions-booking-col -- the agreement-gate messages (no-agreement/
   agreement-unsubmitted, same spec section) that carry this link render
   into the booking column now (two-column layout below), not into
   #sessions-list. */
#sessions-upcoming .schedule-empty-note a, #sessions-past .schedule-empty-note a, #sessions-booking-col .schedule-empty-note a { text-decoration: underline; font-weight: 600; }

/* =========================================================================
   APPROVED SYSTEMS LIST -- Exhibit-B-style table + prose, shared by the
   live read-only view, any historical printable view, and the owner's
   edit rows (which reuse the same table shape with inline inputs). No
   table styling existed anywhere in this stylesheet before this feature,
   even though the protocol-document allowlist has permitted <table> in
   published client content since Aug 2026 -- that gap is real but out of
   scope here; these classes are scoped to .exhibit-table only, not a
   generic <table> reset.
   ========================================================================= */
.exhibit-table { width: 100%; border-collapse: collapse; margin-bottom: var(--space-xl); }
.exhibit-table th, .exhibit-table td { text-align: left; padding: var(--space-sm) var(--space-md); border-bottom: 1px solid var(--light-brown); vertical-align: top; font-size: 15px; }
.exhibit-table th { background: var(--additional-brown); color: var(--dark-brown); font-weight: 700; }
.exhibit-table td { color: var(--dark-brown); }
.exhibit-table tr:last-child td { border-bottom: none; }
.exhibit-row--retiring td { color: var(--dark-brown); }
.retiring-tag { display: inline-block; margin-left: var(--space-sm); padding: 2px 10px; border-radius: var(--radius-pill); background: var(--warn); background-image: var(--sheen-midtone); color: var(--site-white); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
.exhibit-notes p { font-size: 15px; line-height: 1.6; color: var(--dark-brown); margin: 0 0 var(--space-md); }
.exhibit-notes p:last-child { margin-bottom: 0; }

.unpublished-banner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap; }
/* Non-dismissible on purpose -- no close affordance anywhere in this
   markup. A published-but-unnotified change is genuinely not in effect
   (NDA 3.3); the owner must keep seeing this until a retry actually
   succeeds, not until she clicks it away. */
.banner.unnotified-banner { border-color: var(--warn); }

.history-list { list-style: none; margin: 0; padding: 0; }
.history-row { padding: var(--space-md) 0; border-bottom: 1px solid var(--light-brown); }
.history-row:last-child { border-bottom: none; }
.history-row-summary { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap; cursor: pointer; }
.history-row-date { font-size: 15px; font-weight: 700; color: var(--dark-brown); }
.history-row-meta { font-size: 14px; color: var(--dark-brown); }
.history-row-diff { margin-top: var(--space-sm); font-size: 14px; color: var(--dark-brown); display: none; }
.history-row-diff.is-open { display: block; }
.history-row-diff ul { list-style: disc; padding-left: 1.4em; margin: var(--space-xs) 0 var(--space-sm); }
.history-row-diff li { margin-bottom: var(--space-xs); }

.owner-edit-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--space-sm); padding: var(--space-md) 0; border-bottom: 1px solid var(--light-brown); }
.owner-edit-row .field { margin-bottom: 0; }
.owner-edit-row .field-label { margin-bottom: 4px; }

@media print {
  body * { visibility: hidden; }
  #printable-exhibit, #printable-exhibit * { visibility: visible; }
  #printable-exhibit { position: absolute; top: 0; left: 0; width: 100%; }
  .btn, .unpublished-banner, .banner.unnotified-banner, #approved-systems-header { display: none !important; }
}

/* =========================================================================
   PRACTITIONER -- Labs (public/labs.js, portal-lab-intake-spec.md S10).
   Badge colors reuse the existing two semantic tokens (--green, --warn)
   rather than introducing new ones -- this design system has no distinct
   red/amber tokens, and inventing them for one small staff screen isn't
   justified. Queued/processing both read as neutral/in-progress; ready is
   the one positive state; failed reuses --warn, the same tone every other
   error surface in this app already uses.
   ========================================================================= */
.labs-header-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap; margin-bottom: var(--space-lg); }
/* The h1 sits beside the Upload button in this row, not above a block of
   content -- the default's vertical margin would pull its text off-center
   against the button instead of spacing it from anything. */
.labs-header-row h1 { margin: 0; }
/* labs.js's Upload button is plain .btn, no .btn-inline -- without this,
   .btn's own width:100% stretches it to fill the row and wraps it onto
   its own line below the h1 instead of sitting beside it. Fixed here
   rather than in labs.js (adding .btn-inline at the call site) because
   labs.js isn't this round's file to touch; scoped to this row rather
   than changed on .btn itself because most of the site's .btn usage
   (Sign in, Set password, etc.) counts on the width:100% default in a
   narrow card. */
.labs-header-row .btn { width: auto; }

.lab-upload-panel { display: flex; flex-direction: column; gap: var(--space-md); }
.lab-upload-panel .field { margin-bottom: 0; }
.lab-upload-panel input[type="file"] { max-width: 100%; } /* the native control was wider than the card at 390 and 1440 */
.lab-nc-fields { flex-direction: column; gap: var(--space-md); }
/* The gap above is the only space between fields: no trailing margin from the last option row. */
.lab-nc-fields .option-row:last-child { margin-bottom: 0; }

.lab-job-list { list-style: none; margin: 0; padding: 0; }
.lab-job-row { padding: var(--space-md) 0; border-bottom: 1px solid var(--light-brown); }
.lab-job-row:last-child { border-bottom: none; }
.lab-job-row-main { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap; }
.lab-job-row-client { font-size: 16px; font-weight: 700; color: var(--dark-brown); }
.lab-job-row-date { font-size: 14px; color: var(--dark-brown); }
.lab-job-row-status { display: flex; align-items: center; gap: var(--space-sm); margin-top: var(--space-xs); }
.lab-job-row-copy { margin: var(--space-xs) 0 0; font-size: 14px; color: var(--dark-brown); }

.lab-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: var(--radius-pill); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--site-white); }
/* 2026-09-28 contrast sweep: siteWhite on midBrown measured 2.664:1, the
   worst text in the portal. Neither half alone rescues it -- darkBrown on
   midBrown is 3.831:1, and siteWhite is already the lightest token there
   is -- so both move: lightBrown fill, darkBrown text, 5.079:1. lightBrown
   is the nearest neighbour to the tan these already were, so the badges
   still read as the same "nothing has happened yet" pair rather than as a
   new state. Unlike --sev-* these fills are not synced to anything; they
   are this screen's own status colours and no one reads a value off them. */
.lab-badge--queued { background: var(--light-brown); color: var(--dark-brown); }
.lab-badge--processing { background: var(--light-brown); color: var(--dark-brown); }
.lab-badge--ready { background: var(--green); }
.lab-badge--failed { background: var(--warn); }
/* Spec calls for "distinct color, e.g. teal" -- this palette has no teal
   token, and adding one for a single badge isn't justified. dark-brown is
   unused as a badge background elsewhere on this screen (queued/
   processing are mid-brown, ready is green, failed is warn), so it's
   already visually distinct from all three without growing the palette. */
.lab-badge--released { background: var(--dark-brown); }
.lab-badge--pulsing { animation: lab-badge-pulse 1.6s ease-in-out infinite; }
@keyframes lab-badge-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

/* =========================================================================
   PRACTITIONER -- Revenue (public/revenue.js,
   _artifacts/sales-dashboard-spec.md S8).

   CHART COLOR IS NOT IN THIS FILE, and there is now only one of it:
   revenueFormat.js's SERIES_COLOR (#2a78d6, slot 1 of the dataviz skill's
   validated categorical palette) is applied directly as an SVG fill /
   inline background by revenue.js. The previous pass spent all eight of
   that palette's slots encoding stream identity inside a stacked bar; the
   redesign draws ONE series (total gross) and writes each stream's name
   beside its own bar, so hue stopped carrying anything and seven slots
   were deleted. Validated against this page's real surface, not the
   skill's default one:
     node scripts/validate_palette.js "#2a78d6" --mode light --surface "#f9f1e9"
     -> ALL CHECKS PASS (contrast included; the 8-color set carried a
        standing sub-3:1 WARN on four slots).
   Everything below is layout only and stays on the real brand tokens.

   Text color note: --mid-brown is NEVER used as a text color here, per
   this file's own CONTRAST DECISIONS block. Secondary text is --dark-brown,
   dimmed with opacity where it needs to recede (.revenue-muted).
   ========================================================================= */

/* A dashboard is not a reading column. 720px is right for prose and wrong
   for a 13-column chart next to a five-column table -- the previous pass's
   47 bars had ~640px of content width to live in, which is what made them
   8px wide. New selector, not a change to .shell: this cannot reach any
   other page. */

.revenue-caveat { font-size: 16px; margin-bottom: var(--space-xl); }
.revenue-stat-row + .pgrid { margin-top: var(--space-xl); }

/* ---- Headline stat tiles ------------------------------------------------
   The two numbers the page opens with. Values are set in the BODY sans,
   semibold, with proportional figures -- they are data figures, not
   headings, and tabular-nums makes a large standalone number look loose
   (the dataviz skill's figure rules). The page's serif stays on h1/h2,
   so the brand voice is intact where headings live. */
.revenue-stat-row {
  margin-top: var(--space-lg);
}
.revenue-stat-label {
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 600;
  color: var(--dark-brown);
  margin: 0;
}
.revenue-stat-value {
  font-family: var(--font-body);
  font-size: clamp(30px, 2.4vw + 18px, 40px);
  font-weight: 600;
  line-height: 1.15;
  color: var(--dark-brown);
  font-variant-numeric: proportional-nums;
  margin: var(--space-sm) 0 0;
}
.revenue-stat-note {
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--dark-brown);
  opacity: 0.8;
  margin: var(--space-sm) 0 0;
}
/* A plain stat inside a card: a divider above it, no box of its own. */
.revenue-stat-value + .revenue-stat-label { margin-top: var(--space-sm); }
.revenue-stat-cell { padding-top: var(--space-md); border-top: 1px solid var(--light-brown); }
.revenue-stat-comparisons {
  list-style: none;
  margin: var(--space-md) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  font-size: 14px;
}
.revenue-stat-vs { opacity: 0.8; margin-left: var(--space-sm); }

/* Direction lives in the arrow glyph AND the sign, never in color:
   measured against --off-white, neither --green (3.6:1) nor --warn (4.1:1)
   clears the 4.5:1 minimum for 14px text, so a green/red delta would be a
   contrast failure wearing a convention's clothes. */
.revenue-change { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.revenue-change-arrow { margin-right: 2px; }
.revenue-change--none { font-weight: 400; opacity: 0.6; }

/* ---- Range control -- the page's one filter row, above what it scopes -- */
/* The control belongs to what is BELOW it, so the big break sits above it
   and the chart follows close underneath -- otherwise it floats between
   two sections and reads as belonging to neither. */
.revenue-controls { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin: var(--space-xxl) 0 var(--space-lg); }
.revenue-range-btn {
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 600;
  min-height: 40px;
  padding: 8px 18px;
  border-radius: var(--radius-pill);
  border: 1.5px solid var(--dark-brown);
  background: transparent;
  color: var(--dark-brown);
  cursor: pointer;
}
.revenue-range-btn.is-active { background: var(--dark-brown); background-image: var(--sheen); color: var(--site-white); }

/* ---- Trend chart -------------------------------------------------------
   THE 8px-BAR BUG LIVED HERE. This file's global `img, svg { max-width:
   100% }` reset silently overrode the chart's own width attribute, so a
   deliberately-wider-than-the-column SVG was scaled DOWN to fit instead of
   scrolling -- 47 columns specced at 48px each rendered at ~13px and the
   overflow-x container never had anything to scroll. max-width: none is
   what makes an intrinsically-sized chart behave. */
.revenue-chart-scroll {
  overflow-x: auto;
  padding-bottom: var(--space-xs);
}
.revenue-chart { max-width: none; }
.revenue-subtitle {
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--dark-brown);
  opacity: 0.8;
  margin: var(--space-xs) 0 var(--space-md);
}
.revenue-subhead {
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 600;
  color: var(--dark-brown);
  margin: var(--space-xl) 0 var(--space-sm);
}
.revenue-footnote {
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 1.5;
  color: var(--dark-brown);
  opacity: 0.85;
  max-width: 62ch;
  margin: var(--space-sm) 0 0;
}
.revenue-muted { font-size: 13px; font-weight: 400; opacity: 0.7; }

/* ---- Growth card (Insights) -- reuses the Revenue stat/footnote/table
   classes; only the layout bits below are new. The toggle sits right under
   the card heading, so it drops Revenue's large top gap. Chart marks are
   flat fills; nothing here adds a gradient or shadow to data. */
.growth-controls { margin-top: var(--space-md); }
.growth-grid { margin-top: var(--space-md); }
.growth-chart-wrap, .growth-products { margin-top: var(--space-md); }
.growth-chart { display: block; max-width: 100%; height: auto; }
.growth-skeleton { height: 280px; }

/* ---- Tables ------------------------------------------------------------ */
/* Measured: the ranked stream table's min-content width is 372px and a
   390px phone gives this page a 350px content box. Without a scroller of
   its own the table pushes the whole document sideways and every
   paragraph scrolls with it. */
.revenue-table-scroll { overflow-x: auto; }
.revenue-funnel-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.revenue-funnel-table th { white-space: nowrap; text-align: left; padding: var(--space-sm) var(--space-md) var(--space-sm) 0; color: var(--dark-brown); font-weight: 600; border-bottom: 1px solid var(--light-brown); }
.revenue-funnel-table td { padding: var(--space-sm) var(--space-md) var(--space-sm) 0; border-bottom: 1px solid var(--light-brown); color: var(--dark-brown); }
.revenue-funnel-table th:last-child, .revenue-funnel-table td:last-child { padding-right: 0; }
/* Specificity matters here: `.revenue-funnel-table th` (0,1,1) beats a
   bare `.revenue-num` (0,1,0), so a right-aligned numeric column shipped
   with a left-aligned header floating over it. Caught by rendering. */
.revenue-num { text-align: right; font-variant-numeric: tabular-nums; }
.revenue-funnel-table th.revenue-num, .revenue-funnel-table td.revenue-num { text-align: right; }
/* The current year is partial -- the same lighter fill the trend chart's
   final column uses, and the same "so far" text carrying the actual
   statement, because opacity alone says nothing to a screen reader. */
.revenue-stream-bar-fill--partial { opacity: 0.45; }
/* Phone: drop the bar column so the year, the dollars and the change fit with no sideways scroll. */
@media (max-width: 640px) {
  .revenue-year-table th:nth-child(2), .revenue-year-table td:nth-child(2) { display: none; }
}
.revenue-year-table th[scope="row"] { font-weight: 600; border-bottom: 1px solid var(--light-brown); }
.revenue-year-table .revenue-stream-bar-cell { width: 46%; }
.revenue-year-table th:first-child, .revenue-year-table td:first-child { width: 110px; }

/* ---- Ranked stream breakdown -------------------------------------------
   The form that replaced the stacked bar. Every row carries its own name,
   its own bar, and its own exact dollar figure, so a stream at 2% of the
   total is still a full-width readable row instead of a two-pixel sliver
   in someone else's column. One series, one hue -> no legend: identity is
   the row header, which is the direct label. */
.revenue-stream-table th[scope="row"] { font-weight: 600; border-bottom: 1px solid var(--light-brown); }
/* Flex, not two inline-blocks: the whitespace between inline-blocks is a
   real space character, and it was enough to push the share percentage
   onto a second line under its own bar (found by rendering, not reading). */
.revenue-stream-bar-cell { width: 34%; }
.revenue-stream-bar-cell > .revenue-stream-bar-row { display: flex; align-items: center; gap: var(--space-sm); }
.revenue-stream-bar {
  flex: 1;
  min-width: 60px;
  height: 10px;
  background: var(--additional-brown);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
/* Rounded data-end, square at the baseline -- the skill's bar spec. */
.revenue-stream-bar-fill { display: block; height: 100%; border-radius: 0 4px 4px 0; }
.revenue-stream-share { width: 38px; flex: none; text-align: right; font-variant-numeric: tabular-nums; }
.revenue-spark-cell { width: 112px; text-align: right; }
.revenue-spark { display: inline-block; }

/* Below this width the sparkline and the row's own text start fighting for
   the same pixels. The shape is the least load-bearing thing in the row --
   the dollar figure and the direction chip both survive -- so it is what
   goes. 640px is the portal's phone/tablet breakpoint. */
@media (max-width: 640px) {
  .revenue-spark-cell { display: none; }
  .revenue-stream-bar-cell { width: 30%; }
}

/* "A zero count should render as a calm zero, not an alarm" -- calm
   reuses ordinary body text color; attention reuses --warn, the same
   tone every other error/needs-attention surface in this app already
   uses (see the Labs section above). No pill/background on either --
   these are stat-tile headline numbers, not status badges. --warn at
   these sizes (30px+, semibold) clears contrast comfortably; it is
   deliberately NOT used for the 14px delta chips above, which is why
   those carry an arrow instead. */
.revenue-count--calm { color: var(--dark-brown); }
.revenue-count--attention { color: var(--warn); }

/* Exact-figures table (revenue.js's figureTableHtml) -- the chart's
   table-view twin and the "just give me the number" path. Collapsed by
   default (native <details>), summary styled like this app's existing
   text-link affordance (.signout-link/.help-link) rather than a new button
   variant. It now holds only the periods currently on screen, newest
   first, so opening it lands on the number being looked for instead of on
   November 2022. */
.revenue-figure-details { margin-top: var(--space-md); }
/* Three short columns do not need the full dashboard width -- spread
   across 880px they stop reading as a list and start reading as a
   spreadsheet. This is a reference, so it stays compact. */
.revenue-figure-details .revenue-table-scroll { margin-top: var(--space-sm); }
.revenue-figure-table { max-width: 520px; }
.revenue-figure-details summary {
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 600;
  color: var(--dark-brown);
  cursor: pointer;
  padding: var(--space-sm) 0;
}
.revenue-figure-details summary:hover { text-decoration: underline; }
.revenue-figure-details[open] summary { margin-bottom: var(--space-xs); }

/* ---- Insights chat (insightsChat.js). Reuses the Revenue table, footnote and
   disclosure classes; only the layout below is new. The chart fill is flat, the
   same series blue as revenueFormat.js's SERIES_COLOR. */
.insights-chat-form { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.insights-chat-form .text-input { flex: 1 1 220px; min-width: 0; }
.insights-chat-chips { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-top: var(--space-md); }
.insights-chat-chips .btn-inline { white-space: normal; text-align: left; }
.insights-chat-status { font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); margin: var(--space-md) 0 0; }
.insights-chat-result { margin-top: var(--space-lg); }
.insights-chat-result:empty { margin-top: 0; }
.insights-chat-result .revenue-subhead { margin-top: 0; }
.insights-chat-result .insights-chat-showing { font-size: 15px; opacity: 1; margin: 0 0 var(--space-md); }
.insights-chat-result .revenue-table-scroll { margin-top: var(--space-md); }
.insights-chat-allowed { margin: var(--space-sm) 0 0; padding-left: var(--space-lg); font-family: var(--font-body); font-size: 15px; color: var(--dark-brown); }
.insight-chart-scroll { overflow-x: auto; padding-bottom: var(--space-xs); }
.insight-chart { max-width: none; }
.insight-hbar { list-style: none; margin: 0; padding: 0; }
.insight-hbar-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--space-md); row-gap: var(--space-xs); padding: var(--space-sm) 0; font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); }
.insight-hbar-label { overflow-wrap: anywhere; }
.insight-hbar-value { font-variant-numeric: tabular-nums; font-weight: 600; }
.insight-hbar-track { grid-column: 1 / -1; height: 10px; border-radius: 5px; background: var(--light-brown); overflow: hidden; }
.insight-hbar-fill { display: block; height: 100%; border-radius: 5px; }
/* A long count header wraps instead of squeezing the label column to a sliver. */
.insight-table th { white-space: normal; }
.insight-table th.revenue-num { min-width: 130px; max-width: 160px; }
.insight-table:has(th:nth-child(3)) th.revenue-num { min-width: 0; }
.insight-table .insight-total-row th { font-weight: 700; border-top: 1.5px solid var(--mid-brown); border-bottom: 0; }
.insights-chat-history { margin-top: var(--space-lg); }
.insights-chat-history-list { list-style: none; margin: 0; padding: 0; }
.insights-chat-history-row { display: flex; align-items: center; gap: var(--space-sm); border-bottom: 1px solid var(--light-brown); }
.insights-chat-history-open { flex: 1; min-width: 0; min-height: 44px; text-align: left; background: none; border: 0; padding: var(--space-sm) 0; font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); cursor: pointer; overflow-wrap: anywhere; }
.insights-chat-history-open:hover { text-decoration: underline; }
.insights-chat-history-delete { flex: 0 0 44px; min-width: 44px; min-height: 44px; background: none; border: 0; font-size: 20px; color: var(--dark-brown); cursor: pointer; }

/* ---- Insights report (reportView.js, insights-report-builder-spec.md section 10). The report opens
   full width in place of the chat card and the cards below; Close restores them. The data fills reuse
   the chat's chart classes. Print is the same DOM: see the @media print block below. */
.insights-chat-report-toggle { display: flex; align-items: center; gap: var(--space-sm); min-height: 44px; margin-top: var(--space-md); font-family: var(--font-body); font-size: 15px; color: var(--dark-brown); cursor: pointer; }
.insights-chat-report-toggle input { width: 20px; height: 20px; flex: 0 0 auto; }
#insights-chat-cancel { margin-top: var(--space-md); }
.insights-chat-history-tag { flex: 0 0 auto; font-family: var(--font-body); font-size: 12px; font-weight: 700; color: var(--dark-brown); border: 1px solid var(--mid-brown); border-radius: var(--radius-sm); padding: 2px var(--space-sm); }
.insights-reporting > :not(#insights-chat) { display: none; }
.insights-reporting #insights-chat > h3, .insights-reporting #insights-chat > .insights-chat { display: none; }
.insights-report-bar { display: flex; justify-content: flex-end; gap: var(--space-sm); margin-bottom: var(--space-lg); }
.insights-report-body { min-width: 0; }
/* A comparison table has five columns: scroll it sideways on a phone rather than wrapping every word. */
.report-block .insight-table:has(th:nth-child(4)) th.revenue-num { min-width: 120px; }
.report-block .insight-table:has(th:nth-child(4)) :is(th, td):first-child { min-width: 160px; }
.report-title { font-family: var(--font-heading); font-size: clamp(24px, 2vw + 16px, 32px); line-height: 1.2; color: var(--dark-brown); margin: 0 0 var(--space-lg); overflow-wrap: anywhere; }
.report-banner { font-family: var(--font-body); font-size: 14px; font-weight: 600; color: var(--dark-brown); border: 1.5px solid var(--warn); border-radius: var(--radius-sm); padding: var(--space-sm) var(--space-md); margin: 0 0 var(--space-lg); }
.report-heading { font-family: var(--font-body); font-size: 19px; font-weight: 700; color: var(--dark-brown); margin: var(--space-xl) 0 var(--space-md); overflow-wrap: anywhere; }
.report-prose { font-family: var(--font-body); font-size: 16px; line-height: 1.55; color: var(--dark-brown); margin: 0 0 var(--space-md); max-width: 72ch; overflow-wrap: anywhere; }
.report-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-md); margin: var(--space-lg) 0; }
.report-stat { display: flex; flex-direction: column; gap: var(--space-sm); border: var(--edge); border-radius: var(--radius-md); padding: var(--space-md); min-width: 0; }
.report-stat-value { font-family: var(--font-body); font-size: 26px; font-weight: 700; color: var(--dark-brown); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.report-stat-label { font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); }
.report-block { margin: var(--space-lg) 0; }
.report-block .revenue-subhead { margin-top: 0; }
.report-footer { margin-top: var(--space-xl); }
@media print {
  @page { size: Letter; margin: 0.6in; }
  .insights-report, .insights-report * { visibility: visible; }
  .insights-report { position: absolute; top: 0; left: 0; width: 100%; }
  .insights-report-bar { display: none; }
  body:has(.insights-report:not([hidden])) :is(#practitioner-rail, #practitioner-header) { display: none !important; }
  .insights-report-body.pcard { border: 0; box-shadow: none; padding: 0; background: transparent; background-image: none; }
  .report-stat, .report-block, .insight-chart-scroll, .report-banner { break-inside: avoid; }
  .report-heading { break-after: avoid; }
}

/* =========================================================================
   VIDEO SESSIONS -- call screen (public/session.html, public/lib/rtc.js,
   video-sessions-spec.md S4/S12). A wider shell than the default .shell
   (720px) -- a two-video call needs real width; everything else about the
   page (fonts, spacing tokens, button classes) is unchanged from the rest
   of the app.
   ========================================================================= */
/* padding-top/bottom match .product-main and .lesson-main, the established
   pattern for a page's own main element -- .call-shell had padding-inline
   only, so the video frame sat flush against the site header. */
.call-shell { max-width: 960px; margin-inline: auto; padding-inline: clamp(20px, 5vw, 40px); padding-top: var(--space-lg); padding-bottom: var(--space-xxl); }

/* #call-stage wraps the banner, video grid, status/recording-indicator
   row, AND .call-controls -- the fullscreen target
   (session.js's wireFullscreenControl()). Everything a person might need
   mid-call (mute, camera, Stop recording -- a consent-withdrawal control
   under Montana MCA 45-8-213 -- and Leave call) stays part of the
   fullscreened subtree; General's override, 2026-09-13, of an earlier
   "sensibly hidden" design that left .call-controls outside it. No layout
   change outside :fullscreen -- this is a plain wrapper the rest of the
   time. */
#call-stage:fullscreen,
#call-stage:-webkit-full-screen {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-md);
  width: 100vw;
  height: 100vh;
  padding: var(--space-lg);
  background: var(--fixed-dark);
}
#call-stage:fullscreen .call-video-grid,
#call-stage:-webkit-full-screen .call-video-grid {
  flex: 1;
  min-height: 0;
  margin-bottom: 0;
}
#call-stage:fullscreen .call-controls,
#call-stage:-webkit-full-screen .call-controls {
  margin-bottom: 0;
}
/* Idle-fade for .call-controls, fullscreen only (session.js's
   wireControlsIdleFade() toggles #call-stage.controls-idle on a plain
   timer, reset by any pointer/keyboard activity -- standard video-player
   convention). Outside fullscreen this selector never matches, so
   .call-controls stays exactly as visible as it always was. :focus-within
   is the real keyboard guarantee, independent of the timer: a control a
   keyboard user has tabbed into is never hidden by it, fullscreen or not. */
.call-controls { transition: opacity 0.2s ease; }
#call-stage.controls-idle:fullscreen .call-controls,
#call-stage.controls-idle:-webkit-full-screen .call-controls {
  opacity: 0;
  pointer-events: none;
}
.call-controls:focus-within {
  opacity: 1 !important;
  pointer-events: auto !important;
}

.call-video-grid {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--fixed-dark);
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-bottom: var(--space-lg);
}
.call-video--remote { width: 100%; height: 100%; object-fit: cover; }
.call-video--local {
  position: absolute;
  bottom: var(--space-md);
  right: var(--space-md);
  width: 25%;
  max-width: 220px;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--fixed-dark);
  border-radius: var(--radius-md);
  border: 2px solid var(--fixed-light);
}
/* multi-party-video-calls-spec.md S11 -- 3-6 participant calls (the SFU
   transport, rtc.js/session.js's own local-ready `transport` field).
   Unchanged for the ordinary 2-party case above: this is a MODIFIER class
   added to the same .call-video-grid container, never a second element,
   so aspect-ratio/border-radius/background/margin-bottom all still come
   from that shared rule. A plain, equal-size grid -- no active-speaker
   highlighting, pinning, or reordering (deliberately left out, see the
   build report). */
.call-video-grid--multi {
  aspect-ratio: 16 / 9;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-template-rows: repeat(2, 1fr);
  gap: var(--space-sm);
}
/* Exactly 3 tiles present leaves the 4th grid cell empty (S11's own
   diagram: "(blank if only 3)") -- CSS Grid does this for free with a
   fixed 2x2 template and no explicit placeholder needed; the container's
   own dark-brown background already reads as "blank" through the gap.
   Same at 5 tiles against the 3x2 template below. */
/* 5 and 6 tiles: a third COLUMN, never a third row. Without this the 5th
   tile opens an implicit third row, and because that row is content-sized
   while the two above it are 1fr, the grid goes lopsided -- measured at the
   880px desktop grid, 436x113 for the top four and 436x252 for the bottom
   two. 3x2 instead gives every tile 288x243. The rejected alternative was
   2x3 (436x158 each): same total area, but two thirds of the tile height,
   and height is what a face needs. Six tiles in a 16:9 box are genuinely
   smaller than four; that is the cost of the sixth seat, not a bug.
   Scoped to min-width 481px because this selector's :has() outspecifies
   the <=480px single-column rule below, which already handles any N.
   nth-of-TYPE, not nth-child: #fullscreen-btn is a direct child of this
   grid too (session.js re-appends it), so child positions are not tile
   positions -- but it is a <button>, and every .call-tile is a <div>, so
   counting divs counts tiles. */
@media (min-width: 481px) {
  .call-video-grid--multi:has(> .call-tile:nth-of-type(5)) {
    grid-template-columns: repeat(3, 1fr);
  }
}
.call-tile {
  position: relative;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--fixed-dark);
  /* A grid item's default min-width/min-height is `auto`, which for a
     replaced element (the <video> inside) resolves to ITS OWN intrinsic
     size -- a live camera track's native resolution, easily wider than a
     400px-viewport grid cell -- and would otherwise force this whole grid
     wider than its container regardless of object-fit/width:100% on the
     video itself. Overriding both to 0 is the standard fix; this is what
     actually makes "must work at 400px" true, not the video's own sizing
     rules alone. */
  min-width: 0;
  min-height: 0;
}
.call-tile-video { width: 100%; height: 100%; object-fit: cover; }
/* Position/size only -- color/weight/background match .call-fullscreen-btn's
   own "legible over live video" treatment above, the established pattern
   for anything overlaid directly on a video tile in this file. */
.call-tile-label {
  position: absolute;
  left: var(--space-sm);
  bottom: var(--space-sm);
  padding: 2px 10px;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  color: var(--fixed-light);
  background: rgba(0, 0, 0, 0.55);
  border-radius: var(--radius-pill);
}
/* S10.3: "a departure affects only that one tile" -- the video is cleared
   (session.js's markTileWaiting()) and this plain message covers the tile
   in its place; the tile itself stays put in the grid so the layout never
   reflows and a rejoin has somewhere to land. */
.call-tile--waiting::after {
  content: attr(data-waiting-text);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  text-align: center;
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--fixed-light);
  background: rgba(0, 0, 0, 0.55);
}
/* The plain, correct mobile fallback (S11) -- a single stacked column,
   scrollable if it overflows, not a second bespoke mobile layout. Each
   tile keeps a real video shape once the grid's own aspect-ratio no
   longer applies. */
@media (max-width: 480px) {
  .call-video-grid--multi {
    aspect-ratio: auto;
    grid-template-columns: 1fr;
    grid-template-rows: none;
    max-height: 70vh;
    overflow-y: auto;
  }
  .call-tile { aspect-ratio: 16 / 9; }
}

/* Overlaid on the video itself, not .call-controls -- it has to stay part
   of the fullscreened subtree (a descendant of #call-stage) to remain
   clickable as the in-fullscreen way to exit, alongside Escape. Sized and
   colored for legibility over live video, which none of this app's
   existing .btn variants were built for. */
.call-fullscreen-btn {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  z-index: 1;
  padding: 6px 14px;
  min-height: 36px;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  color: var(--fixed-light);
  background: rgba(0, 0, 0, 0.45);
  border: 1.5px solid var(--fixed-light);
  border-radius: var(--radius-pill);
  cursor: pointer;
}

.call-status-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-md); min-height: 24px; margin-bottom: var(--space-lg); }
/* CORRECTED (in-portal-booking-spec.md Problem 3): was var(--mid-brown).
   This app's own CONTRAST DECISIONS comment (top of this file) is explicit
   that midBrown is never a text color -- dividers, disabled fills, and
   resting placeholder text only. darkBrown is the real body-copy color. */
.call-status-text { font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); }
/* CORRECTED (in-portal-booking-spec.md Problem 3): was a hardcoded
   #b3261e, argued at the time as deliberate ("recording" as a universal
   signal that shouldn't read as an ordinary warm-toned accent). Brand
   conformance overrides that: --warn is this app's own token for every
   other alert state, and using it here instead of an undocumented
   one-off red keeps that true without visibly changing the color
   (--warn is #b85c00, already a strong, high-stakes orange-red). Pulse
   keyframes still reused verbatim from lab-badge-pulse (defined above)
   rather than a second, near-identical @keyframes block. */
.call-recording-indicator { display: flex; align-items: center; gap: 6px; font-family: var(--font-body); font-weight: 700; font-size: 14px; color: var(--warn); }
.call-recording-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--warn); animation: lab-badge-pulse 1.6s ease-in-out infinite; }

/* Staff-side local backup (dual-track-recording-spec.md S10, session.js's
   renderBackupStatus()) -- staff-only, persistent for the life of the
   call once shown, never a dismissible toast. Default (active/pending)
   stays the same plain dark-brown #call-status-text already uses --
   nothing is wrong, just informative. --off covers every state where the
   backup is NOT currently protecting this session (unsupported, declined,
   stopped, error) and reuses --warn, the same token
   .call-recording-indicator uses for "notice this". */
.call-backup-status { font-family: var(--font-body); font-weight: 700; font-size: 14px; color: var(--dark-brown); }
.call-backup-status--off { color: var(--warn); }

/* Screen sharing (screen-share-recording-spec.md, session.js's
   renderScreenShareStatus()) -- "This screen share is being recorded" is
   the same category of disclosure .call-recording-indicator already is
   (recording, not merely informative), so it reuses --warn rather than
   inventing a second "notice this" color. #screen-share-remote-notice
   ("They're sharing their screen.") is the plain #call-status-text look
   already declared above -- a fact, not an alert. Both live in
   .call-status-row, whose own flex/gap already spaces them from
   everything else there; neither needs layout rules of its own. */
.call-screenshare-disclosure { font-family: var(--font-body); font-weight: 700; font-size: 14px; color: var(--warn); }

/* display:flex/flex-wrap/gap, the .btn-inline flex:0 0 auto, and the
   adjacent-sibling margin reset all come from the shared .btn-row
   declaration in BUTTONS, above -- .call-controls only needs its own
   margin here, plus the idle-fade/fullscreen rules already below. */
.call-controls { margin-bottom: var(--space-lg); }

/* =========================================================================
   SYMPTOM MAPPING -- symptom-mapping-portal-spec.md, Commit A
   (symptom-mapping.js): first-run setup, edit-tracking (§6.6, the exact
   same picker markup as first-run), daily check-in, crisis modal (§6.4).
   §4.2 originally had severity as a 4-step brown ramp so orange never
   appeared for a data state; Shawn reversed that 2026-09-23 -- severity
   now uses the --sev-none/--sev-mild/--sev-moderate/--sev-severe tokens
   (:root, above), matching the app's severityColor() hex-for-hex.
   --period is still the one token with no app-color counterpart to
   match, and the period-start/history month grid reuses
   .calendar-grid/.calendar-weekday/.calendar-cell (CALENDAR, above)
   rather than a second, parallel set of grid rules.

   Peterson browser-pass revision (2026-09-18), TREATMENT still in force
   post-2026-09-23 reversal: the severity ramp below is NOT the original
   "text directly on the ramp fill" design -- dark-brown text directly on
   the mid-step of the old brown ramp measured 3.83:1, below AA for
   body-weight text, and --beige-on-beige (None vs its own card) measured
   1.00:1, i.e. no visible shape at all. Reworked to a constant white
   label pill (always dark-brown-on-site-white, ~10:1, passes regardless
   of step, and unaffected by the 09-23 fill swap since it never touches
   the fill) sitting on top of each step's own colored swatch, plus a
   border on every button so the lightest step still has a visible
   boundary against the card. The border itself is NOT one fixed color
   anymore -- re-measured per swatch below, since --sev-mild (olive) and
   --sev-severe (deep red) each fail 3:1 against the old default
   dark-brown border and need it overridden to site-white instead.

   Shawn's 2026-09-18 order (both portals): new card-like surfaces get
   --edge/--lift so they read as distinct raised surfaces, not flat
   color blocks -- applied below to every card-shaped element this build
   added (symptom-card, cycle-status-card, mood-section, summary pill,
   custom chip, cycle-length stepper, crisis resource row, crisis modal).
   ========================================================================= */

.symptom-mapping-main { padding-top: var(--space-lg); padding-bottom: var(--space-xxl); }

/* Symptom Mapping Kit branding (symptom-mapping.js kitHeroHtml/kitIntroHtml).
   Cover look from the kit PDF: beige ground, small spaced eyebrow, big serif
   title with an italic line. The line drawing is a CSS mask of a PNG, so it
   takes currentColor and works in light and dark. It sits beside the text,
   never behind it, so it cannot cost legibility. */
.kit-hero { display: flex; align-items: center; gap: var(--space-lg); background: var(--beige); background-image: none; padding: var(--space-xl) var(--space-lg); }
.kit-hero-text { flex: 1 1 0; min-width: 0; }
.kit-eyebrow { margin: 0 0 var(--space-md); font-family: var(--font-body); font-weight: 600; font-size: 12px; letter-spacing: 0.28em; text-transform: uppercase; color: var(--dark-brown); }
.kit-eyebrow::after { content: ""; display: block; width: 32px; height: 2px; margin-top: var(--space-sm); background: var(--green); }
.kit-title { margin: 0 0 var(--space-md); }
.kit-title em { display: block; font-style: italic; }
.kit-subtitle { margin: 0; font-family: var(--font-body); font-size: 16px; line-height: 1.5; color: var(--dark-brown); max-width: 36ch; }
.kit-drawing { flex: 0 0 min(46%, 420px); aspect-ratio: 1000 / 705; color: var(--dark-brown); background-color: currentColor; -webkit-mask: url("symptom-kit-drawing.png") center / contain no-repeat; mask: url("symptom-kit-drawing.png") center / contain no-repeat; }
@media (max-width: 640px) {
  .kit-hero { flex-direction: column; align-items: stretch; padding: var(--space-lg) var(--space-md); }
  .kit-drawing { flex: none; width: 100%; max-width: 300px; align-self: center; }
  .kit-hero--compact { padding: var(--space-md); }
  .kit-hero--compact .kit-eyebrow { display: none; }
  .kit-hero--compact .kit-title { font-size: 28px; margin-bottom: var(--space-sm); }
  .kit-hero--compact .kit-title em { display: inline; }
  .kit-hero--compact .kit-drawing { display: none; }
}

.kit-intro > summary { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); min-height: 44px; cursor: pointer; list-style: none; }
.kit-intro > summary::-webkit-details-marker { display: none; }
.kit-intro > summary h2 { margin: 0; flex: 1; min-width: 0; }
.kit-intro-hint { font-family: var(--font-body); font-weight: 600; font-size: 14px; color: var(--dark-brown); text-decoration: underline; }
.kit-intro[open] > summary { margin-bottom: var(--space-md); }
.kit-points { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: var(--space-md); align-items: stretch; }
.kit-point { background: var(--beige); border-radius: var(--radius-md); padding: var(--space-md); min-width: 0; }
.kit-point p { margin: 0; font-family: var(--font-body); font-size: 15px; line-height: 1.55; color: var(--dark-brown); }
.kit-point-title { margin: 0 0 var(--space-sm); font-family: var(--font-heading); font-weight: 400; font-style: italic; font-size: 20px; line-height: 1.2; color: var(--dark-brown); }
.kit-scale { margin: var(--space-sm) 0 0; display: grid; gap: var(--space-sm); }
.kit-scale li { display: flex; align-items: baseline; gap: var(--space-sm); font-family: var(--font-body); font-size: 14px; line-height: 1.45; color: var(--dark-brown); }
/* Flat data fills: no sheen (feedback_portal_flat_means_data). */
.kit-dot { flex: 0 0 10px; width: 10px; height: 10px; border-radius: 50%; }
.kit-dot--mild { background: var(--sev-mild); }
.kit-dot--moderate { background: var(--sev-moderate); }
.kit-dot--severe { background: var(--sev-severe); }
.kit-steps-title { margin: var(--space-lg) 0 var(--space-sm); }
.kit-steps { display: grid; gap: var(--space-sm); }
.kit-steps li { display: flex; align-items: flex-start; gap: var(--space-md); font-family: var(--font-body); font-size: 15px; line-height: 1.5; color: var(--dark-brown); }
.kit-step-num { flex: 0 0 28px; height: 28px; border-radius: 50%; border: 1px solid var(--dark-brown); display: inline-flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; }
.kit-fineprint { margin: var(--space-lg) 0 0; font-family: var(--font-body); font-size: 13px; line-height: 1.5; color: var(--dark-brown); }

/* Catalog picker rows (§6.2/§6.6) -- a <label> wrapping a native checkbox
   so the whole row is one tap target, matching this file's existing
   44px+ touch-target discipline (BUTTONS, above). Also used for the
   edit screen's "Previously tracked" archived-custom rows (same markup,
   a different data-* hook in symptom-mapping.js). */
.symptom-catalog-list { display: grid; gap: var(--space-sm); }
.symptom-catalog-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px;
  border-radius: var(--radius-sm);
  cursor: pointer;
}
.symptom-catalog-row.is-picked { background: var(--beige); }
.symptom-catalog-row.is-disabled { opacity: 0.45; cursor: not-allowed; }
.symptom-catalog-row input[type="checkbox"] { width: 20px; height: 20px; flex-shrink: 0; accent-color: var(--green); }
.symptom-catalog-row-label { font-family: var(--font-body); font-size: 16px; color: var(--dark-brown); flex: 1; }

.symptom-custom-row { display: flex; gap: var(--space-sm); margin-top: var(--space-sm); }
.symptom-custom-row .text-input { flex: 1; }
/* Peterson: the disabled custom input at the 10-cap looked fully live.
   Scoped to this page rather than a bare .text-input:disabled rule, so
   this doesn't change how a disabled input looks on any other screen
   that reuses the shared .text-input class. */
.symptom-mapping-main .text-input:disabled { opacity: 0.5; cursor: not-allowed; background: var(--additional-brown); }
.symptom-custom-chip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  background: var(--beige);
  background-image: var(--sheen);
  border: var(--edge);
  box-shadow: var(--lift);
  border-radius: var(--radius-md);
  padding: 10px 14px;
  margin-top: var(--space-sm);
}
.symptom-custom-chip-text { font-family: var(--font-body); font-size: 15px; color: var(--dark-brown); flex: 1; }
/* Peterson: 24px effective target. 44x44, icon centered. */
.symptom-custom-chip-remove {
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--dark-brown);
  cursor: pointer;
  flex-shrink: 0;
}
.symptom-counter { font-family: var(--font-body); font-weight: 700; font-size: 13px; letter-spacing: 1px; color: var(--green); margin: var(--space-sm) 0 0; }
/* Peterson: the 10-cap never explained itself. Shown/hidden by
   refreshPickerCapState() in symptom-mapping.js. */
.symptom-cap-note { font-family: var(--font-body); font-size: 13px; color: var(--dark-brown); margin: 4px 0 0; }

/* Cycle-length stepper (§6.2/§6.6) -- direct port of cycle-setup.tsx's
   own -/+ stepper, same shape as .revenue-range-btn's pill-button family
   but its own class since the value display between the two buttons
   (cycle-setup.tsx's stepperValueWrap) has no equivalent there. */
.cycle-length-stepper {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--beige);
  background-image: var(--sheen);
  border: var(--edge);
  box-shadow: var(--lift);
  border-radius: var(--radius-md);
  padding: var(--space-sm);
}
.cycle-length-btn {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
  background: var(--site-white);
  background-image: var(--sheen);
  border: var(--edge);
  font-family: var(--font-body);
  font-size: 20px;
  font-weight: 700;
  color: var(--dark-brown);
}
.cycle-length-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.cycle-length-value { font-family: var(--font-heading); font-size: 28px; color: var(--dark-brown); }
/* CONTRAST DECISIONS' own rule: midBrown is never a text color. This was
   one of the six Peterson found (a persistent "days" label, not a
   placeholder). */
.cycle-length-unit { font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); margin-left: 4px; }

/* Plain-text-link-looking button -- "Edit your tracked symptoms" (§6.3).
   A <button>, not an <a href="#">, since it swaps render state in place
   rather than navigating (§6.6). Peterson: 21px effective height and
   buried at the page's bottom, the only route into edit mode -- padded
   to a real 44px target and moved near the top of the check-in
   (renderDailyCheckin's own template order in symptom-mapping.js). */
.symptom-link-btn {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 10px 0;
  background: none;
  border: none;
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 600;
  color: var(--green);
  text-decoration: underline;
  white-space: nowrap;
  cursor: pointer;
}

/* Daily check-in (§6.3) */
.cycle-status-card { background: var(--beige); background-image: var(--sheen); border: var(--edge); box-shadow: var(--lift); border-radius: var(--radius-xl); padding: 16px; margin-bottom: var(--space-lg); }
.cycle-status-day { font-family: var(--font-heading); font-size: 20px; color: var(--dark-brown); margin: 0 0 var(--space-sm); }
.cycle-status-phase-row { display: flex; align-items: center; gap: var(--space-sm); }
.cycle-status-phase-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.cycle-status-phase-label { font-family: var(--font-body); font-weight: 700; font-size: 15px; color: var(--dark-brown); }
.cycle-status-days { font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); margin: var(--space-sm) 0 0; }

.symptom-card { background: var(--beige); background-image: var(--sheen); border: var(--edge); box-shadow: var(--lift); border-radius: var(--radius-lg); padding: 14px; margin-bottom: 10px; }
.symptom-card-name { font-family: var(--font-body); font-weight: 700; font-size: 16px; color: var(--dark-brown); margin: 0 0 10px; }
.symptom-save-error { font-family: var(--font-body); font-size: 13px; color: var(--warn); margin: 6px 0 0; }

/* Severity picker. Controls, so the portal's control depth is allowed
   (the count tiles below are data and stay flat).
   Unselected: quiet -- a low-alpha tint of the severity colour over the
   card, a 1px border in that colour, text in a contrast-safe shade of it,
   no shadow. Selected: full fill + gradient + --lift + --edge, the same
   border on all four steps. None has no hue to tint, so unselected is a
   neutral outline and selected is the inverse (--dark-brown fill), since
   its own fill (--sev-none) is the card colour. Mild selects to
   --green-action, not --sev-mild: neither white (4.0:1) nor dark text
   (4.2:1 with #2b1a03) reaches 4.5:1 on #848241; the action olive does. Text
   contrast measured 2026-10-03 on the tint (unselected) and the fill
   (selected), both themes: all >= 4.45:1 before the sheen, which only
   moves the fill toward more contrast under its text. */
.severity-picker { display: flex; gap: var(--space-sm); }
.severity-btn {
  flex: 1;
  min-height: 44px;
  padding: 6px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--light-brown);
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  color: var(--dark-brown);
  font-family: var(--font-body);
  cursor: pointer;
}
.severity-btn--none { border-color: var(--light-brown); }
.severity-btn--mild { background: color-mix(in srgb, var(--sev-mild) 18%, transparent); border-color: var(--sev-mild); color: var(--sev-mild-text); }
.severity-btn--moderate { background: color-mix(in srgb, var(--sev-moderate) 18%, transparent); border-color: var(--sev-moderate); color: var(--sev-moderate-text); }
.severity-btn--severe { background: color-mix(in srgb, var(--sev-severe) 18%, transparent); border-color: var(--sev-severe); color: var(--sev-severe-text); }
.severity-btn.is-selected { border: var(--edge); box-shadow: var(--lift); }
.severity-btn--none.is-selected { background: var(--dark-brown); background-image: var(--sheen); color: var(--site-white); }
.severity-btn--mild.is-selected { background: var(--green-action); background-image: var(--sheen-midtone); color: var(--on-mild); }
.severity-btn--moderate.is-selected { background: var(--sev-moderate); background-image: var(--sheen); color: var(--on-moderate); }
.severity-btn--severe.is-selected { background: var(--sev-severe); background-image: var(--sheen-midtone); color: var(--on-period); }
.severity-btn:focus-visible { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
.severity-btn-label {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-weight: 700;
  font-size: 13px;
  white-space: nowrap;
}

.symptom-summary-row { display: flex; gap: var(--space-sm); margin: var(--space-md) 0 var(--space-lg); }
.symptom-summary-pill { flex: 1; background: var(--beige); border: var(--edge); border-radius: var(--radius-md); padding: 10px; text-align: center; }
.symptom-summary-pill-count { display: block; font-family: var(--font-body); font-weight: 700; font-size: 18px; color: var(--dark-brown); }
/* Was --mid-brown (Peterson finding). */
.symptom-summary-pill-label { display: block; font-family: var(--font-body); font-size: 12px; color: var(--dark-brown); margin-top: var(--space-sm); }

/* Mood section (§6.3) -- chip row (mutually exclusive with the free-text
   box below it, enforced in symptom-mapping.js) + Save mood button +
   post-save confirmation (#mood-save-status: a persistent ".banner
   success" line that a further edit clears -- the soundscape suggestion
   this replaced is cut, 2026-09-23). */
.mood-section { background: var(--beige); background-image: var(--sheen); border: var(--edge); box-shadow: var(--lift); border-radius: var(--radius-lg); padding: 14px; }
.mood-sub-label { font-family: var(--font-body); font-weight: 700; font-size: 14px; color: var(--dark-brown); margin: 0 0 8px; }
.mood-chip-row { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-bottom: 10px; }
/* Peterson: both textareas need the same top margin -- one had it
   inline, the other didn't. Centralized here instead of two inline
   styles that can drift again. */
.mood-textarea { margin-top: var(--space-sm); }
.mood-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 8px 14px;
  border-radius: var(--radius-pill);
  border: 1.5px solid var(--light-brown);
  background: var(--site-white);
  background-image: var(--sheen);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 13px;
  color: var(--dark-brown);
}
.mood-chip.is-selected { background: var(--green-action); background-image: var(--sheen-midtone); border-color: var(--green); color: var(--site-white); }
/* "Recommended for you" card shown under the saved line (symptom-mapping.js). */
.reco-card { background: var(--site-white); border: 1.5px solid var(--light-brown); border-radius: var(--radius-lg); padding: var(--space-md); margin-top: var(--space-md); }
.reco-eyebrow { font-family: var(--font-body); font-weight: 700; font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dark-brown); margin: 0 0 var(--space-sm); }
.reco-title { font-family: var(--font-heading); font-size: 20px; color: var(--dark-brown); margin: 0; }
.reco-why { font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); margin: var(--space-sm) 0 0; }
.reco-duration { font-size: 13px; color: var(--dark-brown); margin: var(--space-sm) 0 0; }
.reco-actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-top: var(--space-md); }
.mood-divider { height: 1px; background: var(--light-brown); margin: var(--space-md) 0; }

/* Degraded-section notes (judgement call, this pass): a failed mood or
   cycle-info read no longer blanks the whole check-in -- symptom logging
   (the read the page can't work without) still renders, and each failed
   section says so honestly instead of looking empty. */
.symptom-section-error { font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); margin: 0 0 var(--space-md); }

/* A day in the picker that's after today -- disabled, non-interactive
   (a <div>, not a <button>, same treatment practitioner.js's own
   .calendar-cell.is-outside uses for a leading/trailing overflow day).
   New modifier class on the existing shared .calendar-grid/.calendar-cell
   rules (CALENDAR, above); additive only, inert everywhere else that
   reuses those rules. */
.calendar-cell.is-disabled { opacity: 0.35; }

/* Crisis modal (§6.4) -- a bottom sheet on phone-first widths, reusing
   --overlay (defined in :root, previously unused anywhere in this file)
   for the backdrop rather than inventing a new translucent color. */
.crisis-modal-backdrop {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: 50;
}
.crisis-modal-backdrop[hidden] { display: none; }
.crisis-modal {
  position: relative;
  background: var(--site-white);
  background-image: var(--sheen);
  border: var(--edge);
  box-shadow: var(--lift);
  border-radius: var(--radius-xxl) var(--radius-xxl) 0 0;
  padding: var(--space-xl) var(--space-lg) var(--space-xxl);
  max-width: 480px;
  width: 100%;
  max-height: 88vh;
  overflow-y: auto;
}
/* Peterson: 34px effective target. 44x44, icon centered. */
.crisis-modal-close {
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  top: 8px;
  right: 8px;
  background: none;
  border: none;
  color: var(--dark-brown);
  cursor: pointer;
}
.crisis-resource-row {
  display: block;
  background: var(--beige);
  background-image: var(--sheen);
  border: var(--edge);
  box-shadow: var(--lift);
  border-radius: var(--radius-md);
  padding: 14px 16px;
  margin-bottom: var(--space-sm);
  text-decoration: none;
}
.crisis-resource-title { display: block; font-family: var(--font-body); font-weight: 700; font-size: 16px; color: var(--dark-brown); }
/* Was --mid-brown -- Peterson's specific example, the crisis phone
   numbers at 2.08:1. This is the line that must never be hard to read. */
.crisis-resource-subtitle { display: block; font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); margin-top: 2px; }
/* Was --mid-brown (Peterson finding). */
.crisis-disclaimer { font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); text-align: center; font-style: italic; margin-top: var(--space-lg); }

/* =========================================================================
   SYMPTOM HISTORY -- symptom-mapping-portal-spec.md §7, Commit B
   (symptom-history.js): calendar, day detail, period marking, notes,
   severity chart. Carries forward every Commit A/Peterson-pass rule:
   --edge/--lift on new card-like surfaces, no --mid-brown text, 44px
   minimum tap targets, --sev-* for severity (the 2026-09-23 token swap,
   not the superseded brown ramp), --period for the one thing the
   severity palette can't carry. Reuses .calendar-nav/.calendar-grid/
   .calendar-weekday/.calendar-cell/.is-today/.is-disabled/.btn*/
   .symptom-section-error/.symptom-save-error/.symptom-link-btn/
   .mood-sub-label/.field-label verbatim from the SYMPTOM MAPPING section
   above rather than a second, parallel set of rules.
   ========================================================================= */

/* Calendar/Chart toggle, and the Month/Year toggle inside Chart -- its
   own class rather than reusing .revenue-range-btn (40px, sized for a
   different page that isn't this build's to resize). */
.history-toggle { display: flex; gap: var(--space-sm); }
.history-toggle-btn {
  flex: 1;
  min-height: 44px;
  padding: 10px 16px;
  border-radius: var(--radius-pill);
  border: 1.5px solid var(--dark-brown);
  background: var(--site-white);
  background-image: var(--sheen);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 14px;
  color: var(--dark-brown);
  cursor: pointer;
}
.history-toggle-btn.is-active { background: var(--dark-brown); background-image: var(--sheen); color: var(--site-white); }

/* Phase legend (§7.2) -- tap a chip to filter the calendar's phase tint
   to just that phase. Swatch previews the same rgba tint the calendar
   cells themselves use, at higher opacity so it reads as a solid dot. */
.phase-legend { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-top: var(--space-lg); }
.phase-legend-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 8px 14px;
  border-radius: var(--radius-pill);
  border: 1.5px solid var(--light-brown);
  background: var(--site-white);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 13px;
  color: var(--dark-brown);
  cursor: pointer;
}
.phase-legend-chip.is-active { border-color: var(--green); background: var(--beige); }
.phase-legend-swatch { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }

/* Period-day cell fill (§4.2/§7.2) -- the one --period token, replacing
   the app's own two-color split (beige fill / warn-colored start day)
   with a single consistent marker for every day a cycle covers. */
.calendar-cell--period { background: var(--period); }
.calendar-cell--period .calendar-cell-num { color: var(--on-period); }
.calendar-indicator-row { display: flex; align-items: center; justify-content: center; gap: 3px; margin-top: 2px; min-height: 6px; }
.calendar-mood-dot { width: 4px; height: 4px; border-radius: 2px; background: var(--green); }
/* Predicted-next-period ring -- outlined, not filled, so it reads as a
   forecast rather than a logged event; --period again rather than
   --warn (carried-over rule: orange means "blocked" elsewhere in this
   portal, never a cycle-data state). */
.calendar-predicted-ring { width: 6px; height: 6px; border-radius: 3px; border: 1px solid var(--period); background: transparent; }
/* Coordinator, round 3: a future calendar day is non-interactive, but the
   phase tint and predicted-ring are exactly what she's there to see --
   dim only the day number (NOT .calendar-cell.is-disabled, shared with
   symptom-mapping.js's period-start picker, which has no tint/ring of its
   own to protect). */
.calendar-cell-num--future { opacity: 0.4; }

/* Shared overlay -- the bottom sheet action list, period confirmation,
   and day detail all render into (symptom-history.js's own openOverlay()).
   Same visual treatment as symptom-mapping.js's .crisis-modal-backdrop/
   .crisis-modal, given its own name rather than reused directly: this
   overlay's content has nothing to do with the crisis path, and sharing
   that class name would read as though it did. */
.history-overlay-backdrop {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: 50;
}
.history-overlay-backdrop[hidden] { display: none; }
.history-overlay-sheet {
  position: relative;
  background: var(--site-white);
  background-image: var(--sheen);
  border: var(--edge);
  box-shadow: var(--lift);
  border-radius: var(--radius-xxl) var(--radius-xxl) 0 0;
  padding: var(--space-xl) var(--space-lg) var(--space-xxl);
  max-width: 480px;
  width: 100%;
  max-height: 88vh;
  overflow-y: auto;
}

/* Day-actions list (§7.2's four-item decision list). */
.history-action-list { display: flex; flex-direction: column; gap: var(--space-sm); }
.history-action-btn {
  min-height: 44px;
  padding: 12px 16px;
  border-radius: var(--radius-md);
  border: var(--edge);
  background: var(--beige);
  background-image: var(--sheen);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 15px;
  color: var(--dark-brown);
  text-align: left;
  cursor: pointer;
}
.history-action-btn--cancel { background: var(--site-white); background-image: var(--sheen); font-weight: 500; }
/* Same background/text pairing as the existing .btn-danger (already
   shipped elsewhere in this app) -- warn-as-TEXT on beige measured
   3.5:1, below AA; warn-as-BACKGROUND with site-white text is the
   validated combination this codebase already uses for its one other
   destructive action. */
.history-action-btn--danger { background: var(--warn); background-image: var(--sheen-midtone); color: var(--site-white); border-color: var(--warn); }

/* Day detail (§7.3) -- port of DayDetailSheet.tsx, no soundscape block
   (removed 2026-09-23). Close button reuses .crisis-modal-close's own
   44x44 treatment directly (same job, same shape) rather than a second
   near-identical rule -- position overridden to static since this
   button sits inline in a flex header instead of absolutely placed. */
.day-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-md); margin-bottom: var(--space-lg); }
.day-detail-cycle-day { font-family: var(--font-body); font-size: 14px; color: var(--green); margin: 2px 0 0; }
.day-detail-period-row { display: flex; align-items: center; gap: 8px; font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); margin: 0 0 var(--space-lg); }
.day-detail-period-swatch { width: 14px; height: 14px; border-radius: 4px; background: var(--period); flex-shrink: 0; }
.day-detail-badge-row { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
/* Dot + plain text on a neutral badge, same reasoning as the severity
   picker above: text never sits directly on a --sev-* fill. */
.day-detail-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: var(--radius-pill);
  background: var(--beige);
  background-image: var(--sheen);
  border: var(--edge);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 13px;
  color: var(--dark-brown);
}
/* Coordinator, 2026-09-23 browser pass item 10: the --sev-none dot was
   invisible on the --beige badge background (both near-identical
   beiges). Same border-contrast table as .severity-btn above -- darkBrown
   passes for none/moderate, siteWhite overrides for mild/severe. */
.day-detail-badge-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; border: 1px solid var(--fixed-dark); }
.day-detail-badge-dot--mild, .day-detail-badge-dot--severe { border-color: var(--fixed-light); }
.day-detail-mood-value { font-family: var(--font-body); font-weight: 700; font-size: 15px; color: var(--dark-brown); margin: 2px 0 0; }
.day-detail-mood-quote { font-family: var(--font-heading); font-style: italic; font-size: 15px; color: var(--dark-brown); margin: 4px 0 0; }
.day-detail-notes { min-height: 120px; }
.day-detail-note-saved { font-family: var(--font-body); font-size: 13px; color: var(--green); margin: 0; }
/* Round 3 item 1: the in-sheet "Discard changes?" choice, shown instead
   of closing when the notes box has unsaved text -- --warn (not --sev-*
   or --period) since this is a "you're about to lose something" prompt,
   matching this file's own carried-over rule (--warn means "blocked" or
   destructive, never a data state). */
.day-detail-discard-confirm { margin-top: var(--space-md); padding: var(--space-md); border-radius: var(--radius-lg); border: 1.5px solid var(--warn); background: var(--additional-brown); background-image: var(--sheen); }

/* Chart (§7.4) -- direct port of CycleBarChart.tsx as plain inline SVG.
   Horizontally scrollable at any width (replaces the app's landscape/
   orientation-lock mechanism, §2's cut) -- .chart-scroll is the one
   element that scrolls; the label column stays fixed to its left. */
.chart-outer { display: flex; }
.chart-label-col { width: 130px; padding-right: var(--space-sm); flex-shrink: 0; }
.chart-label-cell {
  display: flex;
  align-items: center;
  position: relative;
  width: 100%;
  text-align: left;
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--dark-brown);
}
/* Coordinator, round 3 item 5: text-overflow doesn't apply to a flex
   CONTAINER's own text run -- the browser wraps bare text in an
   anonymous flex item first, so putting overflow/ellipsis directly on
   .chart-label-cell above never actually truncated. This nested,
   non-flex span is what the ellipsis has to live on. */
.chart-label-cell-text { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; min-width: 0; }
.chart-label-tooltip {
  position: absolute;
  left: 0;
  top: 100%;
  z-index: 5;
  margin-top: 2px;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: var(--dark-brown);
  background-image: var(--sheen);
  color: var(--site-white);
  font-size: 13px;
  font-weight: 600;
  white-space: normal;
  max-width: 220px;
  box-shadow: var(--lift);
}
.chart-scroll { overflow-x: auto; padding-bottom: var(--space-sm); }
.chart-header-row { display: flex; align-items: stretch; }
.chart-header-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 1px;
  border-radius: 6px;
  border: 1px solid var(--light-brown);
  background: var(--site-white);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 11px;
  color: var(--dark-brown);
  padding: 0;
}
.chart-header-tile.is-today { background: var(--green); border-color: var(--green); color: var(--site-white); }
.chart-header-tile--period { background: var(--period); border-color: var(--period); color: var(--on-period); border-radius: 3px 3px 12px 12px; }
.chart-header-tile--year { margin: 0; border: none; background: transparent; justify-content: flex-start; font-size: 10px; color: var(--dark-brown); }
.chart-header-tile--year.is-today { background: var(--green); border-radius: 3px; color: var(--site-white); }
.chart-header-tile--period-year { background: var(--period); border-radius: 3px; color: var(--on-period); }
.chart-legend { display: flex; flex-wrap: wrap; gap: var(--space-md); margin-top: var(--space-sm); }
.chart-legend-item { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-body); font-size: 13px; color: var(--dark-brown); }
.chart-legend-swatch { width: 12px; height: 12px; border-radius: 3px; flex-shrink: 0; }

/* =========================================================================
   CALM TOOLS -- calm-tools-portal-spec.md, Commit A. Appended as one block
   so a merge with any concurrent style.css work is a clean tail addition,
   not an interleaved diff. Scoped entirely to new selectors (.calm-tools*,
   .breath-*, .round-selector*, .sr-only) -- nothing above this line is
   touched.
   ========================================================================= */

/* .sr-only (the #phase-announce live region, §12.3) is defined once, near SIGN IN. */

.calm-tools-main, .breath-main { padding-top: var(--space-xl); padding-bottom: var(--space-xxl); }

/* client-portal-information-design-spec.md §5.2: the "Calm tools" library
   card no longer has a section of its own to space -- it and the symptom
   tracker are two cards in the shared "Your tools" grid, which owns the
   spacing (.your-tools, above). The .calm-tools class survives only on the
   card itself and carries no styling
   here: a margin-bottom on a grid item would open a 48px hole under one
   column. Its own h2/lede rules went with the h2 and the lede.

   .calm-tools-main below is the hub PAGE and is a different class; it was
   never affected by either rule. */

/* Hub page's own three cards (calm-tools.js) -- deliberately NOT
   .resource-card (that shape is a single title+CTA row; these carry a
   description and duration too), so this can't collide with
   .resource-card's other callers. Plain vertical stack inside the
   existing .card frame. */
.calm-tool-card { display: flex; flex-direction: column; align-items: stretch; gap: var(--space-sm); text-decoration: none; }
.calm-tool-card .icon-chip, .calm-tool-card .card-cta { align-self: flex-start; }
.calm-tool-card .card-cta { margin-top: var(--space-sm); }
a.calm-tool-card:hover, a.calm-tool-card:focus-visible { box-shadow: 0 6px 18px rgba(89, 57, 4, 0.14); }

/* Box breathing / 4-7-8 -- a single, centered "focus" screen (matches the
   app's own centered layout for these two tools), unlike the rest of this
   portal's left-aligned pages. Scoped to .breath-main so nothing outside
   these two pages is affected. */
.breath-main h1 { text-align: center; }
.breath-main .help-text { text-align: center; max-width: 420px; margin-left: auto; margin-right: auto; }

/* Was --green, 3.58:1 -- browser-pass finding. */
.breath-round-status { font-family: var(--font-body); font-weight: 700; font-size: 14px; letter-spacing: 1.5px; color: var(--dark-brown); text-align: center; margin: 0 0 var(--space-md); }

/* Round selector (box-breathing.js/breathing-478.js idle state) -- same
   pill-group shape as the app's own roundSelector/roundSelectorOption
   (box-breathing.tsx:414-436), ported to this stylesheet's tokens.
   width: 100% -- browser-pass finding: .breath-controls is a centered
   flex column (align-items: center), which sizes a child to its own
   content by default; with no explicit width this shrank to ~59px per
   option instead of filling the row, truncating "4 rounds"/"8 rounds". */
.round-selector { display: flex; width: 100%; background: var(--beige); background-image: var(--sheen); border: var(--edge); box-shadow: var(--lift); border-radius: var(--radius-pill); padding: 4px; gap: 4px; margin-bottom: var(--space-md); }
.round-selector-option { flex: 1; min-height: 44px; display: flex; align-items: center; justify-content: center; border: none; background: transparent; border-radius: var(--radius-pill); font-family: var(--font-body); font-weight: 700; font-size: 15px; color: var(--dark-brown); }
/* Was --green background / --site-white text, 3.91:1 -- browser-pass
   finding. Fixed locally (not touching .btn-primary's identical green,
   sitewide and out of scope): dark-brown reaches ~10:1 against white. */
.round-selector-option.is-selected { background: var(--dark-brown); background-image: var(--sheen); color: var(--site-white); }

.breath-controls { display: flex; flex-direction: column; align-items: center; gap: var(--space-sm); max-width: 360px; margin: 0 auto var(--space-xl); }

/* Trace shape (square/triangle) -- ported from BreathTracePolygon.tsx
   (§7.1): a static "track" stroke under an animated "fill" stroke that
   draws in via stroke-dashoffset, box-breathing.js/breathing-478.js's own
   applyTrace() sets the actual offset/transition per phase. The SVG and
   its overlay text are aria-hidden (§12.3) -- #phase-announce above is
   what a screen reader hears instead.
   margin: var(--space-md) (was --space-xl) -- browser-pass finding:
   Begin sat below the fold at 390x844 (top edge 788px). Paired with the
   280px -> 200px SIZE reduction in box-breathing.js/breathing-478.js
   (that finding's own primary fix, the SVG canvas itself), not a
   standalone change. */
.breath-trace-wrap { position: relative; width: fit-content; margin: var(--space-md) auto; }
.breath-trace-content { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 4px; padding: 0 20px; }
.breath-trace-track { stroke: var(--additional-brown); }
.breath-trace-fill { stroke: var(--green); }
/* Triangle only (breathing-478.js) -- browser-pass regression fix, see
   insideShapeHtml()'s own comment in that file for the numbers this
   pairs with: anchors the label+countdown stack low in the shape, where
   it's wide enough, instead of centering it at the canvas's own vertical
   middle, where an apex-up triangle this size is too narrow. */
.breath-trace-content--low { justify-content: flex-end; padding-bottom: 65px; }

/* Sublabel-equivalent text, moved OUT of the shape entirely -- browser-
   pass regression (07-478-mid-exhale.png): it's long enough at some
   phases to wrap over a shape's edges from the inside (worst on the
   triangle, but fixed on both tools for consistency). A normal block
   below the drawing instead, where there's no edge to cross. Was
   --mid-brown, 2.44:1, as .breath-phase-sublabel. */
.breath-secondary { text-align: center; margin: 0 0 var(--space-md); }
.breath-secondary-text { font-family: var(--font-body); font-size: 14px; color: var(--dark-brown); margin: 0; }
/* "You did it" (done state) -- a real h2 now (browser-pass finding), not
   a <p> styled to look like one. Resets the global h2 margin-top/bottom
   (VERTICAL RHYTHM DEFAULTS, above) back to 0 -- it sits in the same
   tight, centered slot every other secondary-text state uses. */
.breath-secondary-heading { margin: 0; }

.breath-tip-box { background: var(--beige); background-image: var(--sheen); border: var(--edge); box-shadow: var(--lift); border-radius: var(--radius-lg); padding: 20px; max-width: 420px; margin: var(--space-lg) auto 0; }

/* Belt-and-suspenders on top of box-breathing.js/breathing-478.js's own
   prefers-reduced-motion check (which skips setting a timed transition at
   all): forces the trace to snap rather than animate regardless, the same
   "motion is opted IN, never assumed" discipline this file's REDUCED
   MOTION section above already applies everywhere else. */
@media (prefers-reduced-motion: reduce) {
  .breath-trace-fill { transition: none !important; }
}

/* =========================================================================
   BINAURAL BEATS -- calm-tools-portal-spec.md §9, Commit B. Reuses
   .breath-main/.breath-controls/.breath-tip-box/.round-selector/
   .breath-round-status/.help-text verbatim from the breathing tools above
   -- same "centered focus screen" shape, same fixes (dark-brown text
   only, full-width selector pills, 44px targets). Only what's genuinely
   new to this tool gets new rules here.
   ========================================================================= */

/* padding-top: --space-lg (was --space-xl, inherited from the shared
   .breath-main rule) -- browser-pass finding, one of several small
   trims toward getting Begin above the fold. Scoped to this page only
   (binaural-beats.html's own <main> carries both .breath-main and this
   class); box-breathing.js/breathing-478.js's own layout, already
   verified passing, is untouched. */
.binaural-main { padding-top: var(--space-lg); }

/* AC-10: visible at all times, on every state of the page -- static,
   outside #state-content, never rebuilt (binaural-beats.html). One
   element carries both .shell (this page's own content width/padding)
   and its own fill/border -- .shell has no background of its own
   (LAYOUT SHELL, above), so this doesn't collide with any other .shell
   caller. Matches binaural-beats.tsx's own persistent header banner. */
.binaural-headphones-banner { display: flex; align-items: center; justify-content: center; gap: 6px; padding-top: 8px; padding-bottom: 8px; background: var(--beige); border-bottom: 1px solid var(--light-brown); }
.binaural-headphones-banner span { font-family: var(--font-body); font-weight: 700; font-size: 13px; letter-spacing: 0.5px; color: var(--dark-brown); }
.binaural-headphones-banner .icon { color: var(--green); }

/* margin-top: --space-sm (was --space-md) -- browser-pass finding
   (Begin at 1089-1147px on 390x844): one of several small trims paired
   with the bigger structural fix below (the circle leaving idle, the
   preset list going from four two-line cards to four one-line rows). */
.binaural-section-label { font-family: var(--font-body); font-weight: 700; font-size: 14px; letter-spacing: 1.5px; color: var(--dark-brown); margin: var(--space-sm) 0 var(--space-xs); }
.binaural-section-label:first-child { margin-top: 0; }

/* Preset rows -- a native <button> (§12.2's aria-pressed toggle
   convention, same as .round-selector-option). Browser-pass finding:
   these used to be two-line cards (title + full description) and were
   the single biggest contributor to Begin sitting at 1089-1147px on
   390x844 -- one compact title+Hz row now, with the selected preset's
   own description shown once, below the list (binaural-beats.js's own
   #preset-description), instead of repeated on all four. */
.binaural-preset { display: flex; align-items: center; gap: var(--space-sm); width: 100%; min-height: 44px; text-align: left; padding: 10px 14px; border: 1.5px solid transparent; border-radius: var(--radius-md); background: var(--beige); background-image: var(--sheen); box-shadow: var(--lift); margin-bottom: 6px; font-family: inherit; cursor: pointer; }
.binaural-preset.is-selected { border-color: var(--green); }
.binaural-preset-title { flex: 1; min-width: 0; font-family: var(--font-heading); font-size: 15px; color: var(--dark-brown); }
.binaural-preset-hz { font-family: var(--font-body); font-weight: 700; font-size: 13px; color: var(--dark-brown); flex-shrink: 0; }
/* Browser-pass finding: selection was colour-only (the border alone).
   Always rendered, hidden via visibility (not display) so the Hz badge
   never shifts position when a row becomes/stops being selected --
   matches the round selector's own unambiguous pressed style, just a
   checkmark instead of a fill, since a solid fill behind a whole row
   read as heavier than these compact rows need. */
.binaural-preset-check { color: var(--green); flex-shrink: 0; visibility: hidden; }
.binaural-preset.is-selected .binaural-preset-check { visibility: visible; }
/* Browser-pass finding: the preset/duration controls stayed clickable
   during the fetch/decode await, letting a switch mid-load play one
   preset while showing another. Dimmed the same way .btn[aria-disabled]
   already is (Commit A) -- these aren't .btn-classed, so they need
   their own rule. box-shadow: none on .binaural-preset specifically
   (it's the one of the two with its own lift) -- browser-pass follow-up:
   opacity alone against an already-pale --beige card, next to an
   already-pale page background, didn't read clearly as disabled;
   flattening the card's elevation on top of the dim makes the state
   unambiguous. */
.binaural-preset[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; box-shadow: none; }
.round-selector-option[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

/* Pulsing circle -- §9's own spec: scale 0.85 to 1.05, 4s each
   direction (8s full cycle), ease-in-out. 140px, not the app's 220px --
   same "tighten the layout" discipline the breathing tools' SVG
   resize applied, so the preset list + duration selector + this circle
   still leave Begin above the fold at 390x844. Static (no animation)
   outside the .binaural-circle--pulsing modifier, which itself only
   applies inside the reduced-motion-safe query below -- "motion is
   opted IN, never assumed," this file's own REDUCED MOTION section's
   discipline, applied here the same way box-breathing.js/
   breathing-478.js's own trace transitions are. */
.binaural-circle-wrap { position: relative; width: 140px; height: 140px; margin: var(--space-md) auto; }
.binaural-circle { position: absolute; inset: 0; border-radius: 50%; background: var(--additional-brown); transform: scale(0.95); }
@media (prefers-reduced-motion: no-preference) {
  .binaural-circle--pulsing { animation: calm-circle-pulse 8s ease-in-out infinite; }
  @keyframes calm-circle-pulse {
    0%, 100% { transform: scale(0.85); }
    50% { transform: scale(1.05); }
  }
}
.binaural-circle-content { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 4px; padding: 0 12px; }

/* Load-error message (Web Audio fetch/decode failure). Was --warn text
   at 13px, 4.11:1 -- browser-pass finding; --warn is never a text colour
   in this portal (same fix as .breath-round-status/.breath-phase-
   sublabel/etc. in the earlier Commit A rounds). Dark-brown text now,
   signalled instead by a left border + fill, same shape
   .banner.error uses elsewhere in this file (border-color: --warn,
   never the text). Empty by default (binaural-beats.js only ever sets
   text on it after a real failure) -- :empty/:not(:empty) show/hide it
   without needing a JS class toggle. */
.binaural-load-error { display: none; font-family: var(--font-body); font-size: 13px; color: var(--dark-brown); margin: var(--space-sm) 0 0; padding: 8px 12px; border-left: 3px solid var(--warn); background: var(--additional-brown); border-radius: var(--radius-sm); text-align: left; }
.binaural-load-error:not(:empty) { display: block; }

/* Required answer still missing after a Submit attempt (form.js). */
.field.field-missing { border-left: 3px solid var(--warn); padding-left: 12px; }
.field.field-missing .question-label::after { content: " (please answer)"; color: var(--dark-brown); text-transform: none; letter-spacing: 0; }

/* Library form cards: status colour. weight 600 so the colour is not the
   only signal. portal-home-and-hub-infodesign-spec.md §4.4/AC-8:
   "assigned" (Not started) was #9b2c1f directly under "Ready when you are,
   no rush." -- a false alarm (red/orange means "blocked, cannot proceed"
   elsewhere in this file -- see --warn's own comment -- not "not done
   yet"). Now the same plain, neutral text .form-status--in_progress
   already uses; "Complete" keeps its own green. Still 4.5:1+ on the card
   beige at 14px. */
.form-status { color: var(--dark-brown); font-weight: 600; }
.form-status--assigned { color: var(--dark-brown); }
.form-status--in_progress { color: var(--dark-brown); }
.form-status--submitted { color: var(--ok-text); }

/* =========================================================================
   STAFF INTRO SESSION TAB (practitioner.js introSessionOverviewSectionHtml()
   and its helpers, portal task 2026-09-24 "intro-tab-layout") --
   presentation only, every selector scoped under .intro-overview so
   nothing else on the page changes.
   ========================================================================= */
/* Section headings: the brand olive token, --green (#848241), already used
   elsewhere in this file as an accent (.hub-field-key, .rail-client.is-active).
   3.58:1 against this tab's own page background (body's --off-white,
   #f9f1e9) -- passes WCAG AA's 3:1 large-text threshold at .pn-24's >=21px,
   no darker olive needed. */
.intro-overview .pn-24 { color: var(--green); }

/* Card grid (spec item 2): "Her Call" / "Forms" / "Fit check" / "Signed
   up", 2 columns >=900px (this file's own existing cutoff for the same
   collapse -- see #sessions-columns' @media (max-width: 899px) rule
   above), 1 column below. Cards reuse .card (beige, --edge/--lift)
   verbatim, no new card look. */
.intro-overview-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-lg); margin-bottom: var(--space-xl); }
@media (min-width: 900px) {
  .intro-overview-grid { grid-template-columns: 1fr 1fr; }
}

/* NAQ Summary bars (spec item 3): name | thin bar | percent, one row per
   system. min-width: 0 on the label lets a long system name wrap instead
   of forcing the row wider than its card (no horizontal scroll at
   375px). The track is --off-white, not --beige, so it reads against the
   --beige card behind it -- .hub-bar's own beige-on-beige track would be
   invisible here. */
.intro-naq-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(60px, 120px) 40px; align-items: center; gap: var(--space-sm); padding: 6px 0; }
.intro-naq-row-label { min-width: 0; font-size: 15px; color: var(--dark-brown); }
.intro-naq-row-pct { font-size: 13px; color: var(--dark-brown); text-align: right; }
.intro-naq-bar { display: block; position: relative; height: 8px; background: var(--off-white); border-radius: var(--radius-pill); overflow: hidden; }
.intro-naq-bar-fill { position: absolute; inset: 0 auto 0 0; height: 100%; min-width: 3px; background: var(--green); }

/* Intro Session Intake answers (form-display-redesign-spec.md §4.3):
   sentence-case, stacked label-above-value block per question, replacing
   the narrow ALL-CAPS two-column grid above (.intro-overview-answers,
   removed -- the "wide" variant existed only to keep a long textarea
   answer from being squeezed into that narrow column, and is no longer
   needed once nothing is squeezed into one). */
.intro-qa-list { display: flex; flex-direction: column; gap: var(--space-md); }
.intro-qa-label { margin: 0 0 2px; font-size: 13px; color: var(--dark-brown); }
.intro-qa-value { margin: 0; font-size: 16px; line-height: 1.5; color: var(--dark-brown); white-space: pre-wrap; }

/* Static description heading inside a .field-group: one step below the
   question label it introduces (form-display-redesign mockup). */
.static-desc-label { display: block; font-family: var(--font-body); font-weight: 600; font-size: 13px; color: var(--dark-brown); margin-bottom: 2px; }

/* =========================================================================
   PMDD RESEARCH LIBRARY (_artifacts/pmdd-research-library-spec.md §6/§8).
   Reuses .card (shape), .module-panel/.module-panel-inner (the existing
   expand/collapse mechanism), .mood-chip/.mood-chip-row (the single-
   select category chips, and the staff-only multi-select Chapter filter),
   .tag (pill shape), .banner/.question-label/.body-copy (the crisis
   block) verbatim -- only what those don't already cover is new below.
   ========================================================================= */
#research-list, #research-admin-list { display: flex; flex-direction: column; gap: var(--space-md); }

.research-card-trigger {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  position: relative;
}
.research-card-trigger .icon-chevron { position: absolute; top: 2px; right: 0; }
.research-card-trigger[aria-expanded="true"] .icon-chevron { transform: rotate(180deg); }
.research-card-meta-row { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; padding-right: 28px; }
.research-card-year { font-size: 13px; color: var(--dark-brown); font-weight: 600; }
.research-card-title { display: block; font-family: var(--font-body); font-weight: 700; font-size: 17px; color: var(--dark-brown); padding-right: 28px; }
.research-card-teaser { display: block; font-size: 14px; color: var(--dark-brown); line-height: 1.4; }

/* Category pills and the "Sensitive topic" heads-up both reuse the base
   .tag pill shape (uppercase, pill radius, --edge/--lift) with a neutral
   fill -- deliberately NOT --warn (spec §6.4: a heads-up, not an alarm;
   --warn is reserved for "blocked, cannot proceed" elsewhere in this
   file). */
.tag--neutral { background: var(--additional-brown); color: var(--dark-brown); }

.research-card-quotes blockquote { margin: 0 0 var(--space-sm); padding-left: var(--space-md); border-left: 3px solid var(--light-brown); color: var(--dark-brown); font-style: italic; }
.research-card-quotes blockquote:last-child { margin-bottom: 0; }
/* Links in citations/notes: --dark-brown is the site's text-link colour and flips with the dark theme. */
.research-link { color: var(--dark-brown); text-decoration: underline; overflow-wrap: anywhere; }

/* The crisis block is a plain .banner (not .banner.error -- see this
   file's own --warn comment above) -- no override needed beyond that. */

/* =========================================================================
   SIMULATED CLIENT VIEW control and sheet (simPill.js, mounted by app.js only
   while a staff member is previewing). A small "Preview" button in the phone
   title bar (where the bell sits, off in a preview) and in the desktop header;
   it opens the sheet, which is the only thing that ever overlays a page, and
   only while open. Neutral brand colours, never red: it is chrome, but it must
   not read as an error.
   ========================================================================= */
.sim-bar-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--dark-brown);
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.sim-bar-icon { display: grid; place-items: center; width: 32px; height: 32px; border: var(--edge); border-radius: 50%; background: var(--beige); background-image: var(--sheen); }
.sim-bar-icon svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
.sim-bar-label { display: none; }
.practitioner-topbar .sim-bar-label { display: inline; }
.phone-bar .sim-bar-btn { grid-column: 3; grid-row: 1; justify-self: end; }
.sim-sheet {
  position: fixed;
  z-index: 43;
  width: min(320px, calc(100vw - 16px));
  max-height: calc(100vh - 80px);
  overflow-y: auto;
  padding: var(--space-md);
  border: var(--edge);
  border-radius: var(--radius-md);
  background: var(--site-white);
  color: var(--dark-brown);
  box-shadow: var(--lift-hover);
}
.sim-sheet[hidden] { display: none; }
.sim-modes { margin: 0 0 var(--space-md); padding: 0; border: 0; }
.sim-modes legend { padding: 0; margin-bottom: var(--space-xs); font-size: 14px; font-weight: 600; }
.sim-mode { display: flex; align-items: center; gap: var(--space-sm); min-height: 44px; font-size: 16px; cursor: pointer; }
.sim-mode input { width: 20px; height: 20px; accent-color: var(--green-action); }
.sim-sheet .btn + .btn { margin-top: var(--space-sm); }

/* =========================================================================
   TEAM MEETING NOTES + TASKS (team-meeting-notes-tasks-spec.md, Phase A).
   Rendered by public/teamMeetingNotes.js into the Team tab. Controls carry
   the sheen (inputs reuse .text-input); status chips and the check box are
   VALUES, so they stay flat -- see the "flat means data" rule above.
   ========================================================================= */
.header-tab-badge.has-dot { display: inline-flex; min-width: 10px; width: 10px; height: 10px; padding: 0; background: var(--warn); }
.header-tab-badge.has-count.has-dot { width: auto; min-width: 18px; height: 18px; padding: 0 5px; }

.tm-inline-error { margin: var(--space-xs) 0 0; font-size: 14px; color: var(--warn); }
.tm-caught-up { margin: var(--space-md) 0; }

.tm-review-card { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap; }
.tm-review-card.is-amber { border-color: var(--warn); }
.tm-review-card-text { font-weight: 600; }

.tm-task-list { list-style: none; margin: 0 0 var(--space-md); padding: 0; }
.tm-task-row { display: flex; align-items: flex-start; gap: var(--space-sm); padding: var(--space-xs) 0; border-bottom: 1px solid var(--light-brown); }
.tm-task-row:last-child { border-bottom: none; }
.tm-task-row--screen { padding: var(--space-sm) 0; }
.tm-task-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; padding: 10px 0; }
.tm-task-row--screen .tm-task-body { padding: 4px 0; gap: var(--space-sm); }

/* 44px hit area; the visible box is 24px, drawn on the pseudo-elements. */
.tm-check { flex: 0 0 44px; width: 44px; height: 44px; padding: 0; border: 0; background: transparent; position: relative; cursor: pointer; }
.tm-check::before { content: ""; position: absolute; left: 10px; top: 10px; width: 20px; height: 20px; border: 2px solid var(--dark-brown); border-radius: 6px; background: var(--site-white); }
.tm-check.is-done::before { background: var(--green); border-color: var(--green); }
.tm-check.is-done::after { content: ""; position: absolute; left: 17px; top: 14px; width: 5px; height: 10px; border: solid var(--site-white); border-width: 0 2px 2px 0; transform: rotate(45deg); }
.tm-check--static { cursor: default; }
.tm-check--static::before { border-color: var(--light-brown); }
.tm-check:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: -4px; border-radius: var(--radius-sm); }

.tm-task-title { background: none; border: 0; padding: 0; text-align: left; font: inherit; font-size: 16px; font-weight: 600; color: var(--dark-brown); cursor: pointer; overflow-wrap: anywhere; min-height: 24px; }
.tm-task-title:hover { text-decoration: underline; }
.tm-task-title-static { font-size: 16px; font-weight: 600; color: var(--dark-brown); overflow-wrap: anywhere; white-space: pre-wrap; }
.is-struck { text-decoration: line-through; font-weight: 400; }
.tm-task-meta { font-size: 14px; color: var(--dark-brown); }

.tm-done-toggle { display: block; background: none; border: 0; font: inherit; font-size: 15px; font-weight: 600; color: var(--dark-brown); min-height: 44px; padding: 0; cursor: pointer; }
.pcols + .tm-done-toggle { margin-top: var(--space-md); }
.tm-done-toggle::before { content: "\25B8\00a0"; }
.tm-done-toggle[aria-expanded="true"]::before { content: "\25BE\00a0"; }

.tm-back { background: none; border: 0; font: inherit; font-size: 15px; font-weight: 600; color: var(--dark-brown); min-height: 44px; padding: 0; margin-bottom: var(--space-sm); cursor: pointer; }
.tm-screen-meta { margin-bottom: var(--space-md); }
.tm-status-chip { display: inline-block; margin-left: var(--space-sm); padding: 2px 10px; border-radius: var(--radius-pill); font-size: 13px; font-weight: 700; color: var(--dark-brown); }
.tm-status-chip--draft { background: var(--beige); }
.tm-status-chip--published { background: var(--green); color: var(--site-white); }

.tm-note-read { margin: 0 0 var(--space-lg); font-size: 16px; line-height: 1.6; color: var(--dark-brown); white-space: pre-wrap; overflow-wrap: anywhere; }
.tm-note-input { display: block; min-height: 180px; padding: var(--space-md); line-height: 1.5; resize: vertical; }
.tm-note-foot { display: flex; justify-content: space-between; min-height: 22px; font-size: 14px; color: var(--dark-brown); margin-bottom: var(--space-md); }

.tm-title-input { min-height: 44px; }
.tm-task-controls { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }
.tm-owner-picker { display: flex; flex-wrap: wrap; align-items: center; gap: 0 var(--space-md); min-width: 0; margin: 0; padding: 0; border: 0; }
.tm-owner-picker legend { width: 100%; padding: 0; }
.tm-owner-label { font-size: 14px; font-weight: 600; color: var(--dark-brown); }
.tm-owner-picker.needs-owner { border-left: 3px solid var(--warn); padding-left: var(--space-sm); }
.tm-delete { display: inline-flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }
.tm-delete-ask { font-size: 14px; }
.tm-add { margin: var(--space-md) 0; display: flex; flex-direction: column; gap: var(--space-sm); align-items: flex-start; }
.tm-add .tm-title-input { max-width: 520px; }

.tm-publish-bar { position: sticky; bottom: 0; z-index: 5; background: var(--site-white); border-top: 1px solid var(--light-brown); padding: var(--space-sm) 0; margin-top: var(--space-lg); }
.tm-publish-bar-inner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap; }
.tm-publish-reason { font-size: 14px; color: var(--dark-brown); flex: 1 1 200px; }
.tm-more { margin: var(--space-lg) 0; }
.tm-more summary { cursor: pointer; font-size: 14px; font-weight: 600; color: var(--dark-brown); min-height: 44px; display: flex; align-items: center; }
.tm-delete-notes-body { padding: var(--space-sm) 0; }

/* Past-meeting chips: flat fills (they encode status, not a control look). */
.tm-notes-chip { white-space: nowrap; margin-left: auto; min-height: 44px; padding: 0 14px; border-radius: var(--radius-pill); border: 1.5px solid var(--dark-brown); background: var(--site-white); font: inherit; font-size: 13px; font-weight: 700; color: var(--dark-brown); cursor: pointer; }
.tm-notes-chip--review { background: var(--beige); }
.tm-notes-chip--write { border-color: var(--warn); }
.tm-notes-chip--published { border-color: var(--light-brown); font-weight: 600; }
.tm-scrub-notice { margin: calc(var(--space-sm) * -1) 0 var(--space-md); font-size: 14px; color: var(--dark-brown); }

/* =============================================================================
   TEAM MEETING AGENDA (public/teamAgenda.js, public/agendaTimer.js).
   The "Next team meeting" card spans two columns of the Team grid on wide
   screens (so its On the agenda / Ideas columns have room) and stacks on a
   phone. The call strip sits under the video and never covers the controls.
   No animation anywhere in either, so prefers-reduced-motion needs no rule.
   ============================================================================= */
@media (min-width: 1100px) {
  #team-agenda-body { grid-column: span 2; }
}
.ag-when-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-sm); }
.ag-when { margin: 0; }
.ag-standing { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-lg); }
.ag-standing-text { margin: 0; font-size: 15px; color: var(--dark-brown); }
.ag-standing-form { display: flex; flex-direction: column; gap: var(--space-sm); width: 100%; }
.ag-standing-form .btn-row .text-input { width: auto; min-width: 0; }
.ag-hint { margin: 0; font-size: 14px; color: var(--dark-brown); }
.ag-conflict { margin: 0; font-size: 14px; color: var(--warn); }
.ag-link { background: none; border: 0; padding: 0 var(--space-xs); font: inherit; font-size: 14px; font-weight: 600; color: var(--dark-brown); text-decoration: underline; min-height: 44px; cursor: pointer; }
.ag-col > h3 { margin-bottom: var(--space-sm); }
.ag-empty { margin: 0 0 var(--space-md); font-size: 15px; color: var(--dark-brown); }
.ag-count { font-weight: 400; }
.ag-list { list-style: none; margin: 0 0 var(--space-md); padding: 0; display: flex; flex-direction: column; gap: var(--space-sm); }
.ag-card { border: var(--edge); border-radius: var(--radius-md); background: var(--site-white); box-shadow: var(--lift); padding: var(--space-xs) var(--space-md) var(--space-sm); display: flex; flex-direction: column; gap: var(--space-sm); }
.ag-card-head { display: flex; align-items: center; gap: var(--space-sm); }
.ag-title { display: flex; flex: 1 1 auto; align-items: center; justify-content: space-between; gap: var(--space-md); min-width: 0; min-height: 44px; padding: var(--space-sm) 0; border: 0; background: none; font: inherit; text-align: left; color: var(--dark-brown); cursor: pointer; }
.ag-title-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ag-title-text { font-size: 16px; font-weight: 700; overflow-wrap: anywhere; }
.ag-title:hover .ag-title-text, .ag-title:focus-visible .ag-title-text { text-decoration: underline; }
.ag-chevron { flex: 0 0 auto; width: 8px; height: 8px; margin-right: var(--space-xs); border-right: 2px solid var(--dark-brown); border-bottom: 2px solid var(--dark-brown); transform: rotate(45deg); }
.ag-title[aria-expanded="true"] .ag-chevron { transform: rotate(-135deg); }
.ag-meta { font-size: 14px; }
.ag-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm) var(--space-md); }
.ag-move { padding-left: 0; }
.ag-move:disabled { text-decoration: none; cursor: default; }
.ag-menu-wrap { position: relative; flex: 0 0 auto; }
.ag-menu-btn { position: relative; width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: var(--radius-sm); background: none; color: var(--dark-brown); cursor: pointer; }
.ag-menu-btn::after { content: ""; position: absolute; inset: -8px; }
.ag-menu-btn:hover, .ag-menu-btn[aria-expanded="true"] { background: var(--beige); }
.ag-menu { position: absolute; top: 100%; right: 0; z-index: 5; min-width: 150px; white-space: nowrap; display: flex; flex-direction: column; padding: var(--space-xs); background: var(--site-white); border: var(--edge); border-radius: var(--radius-md); box-shadow: var(--lift-hover); }
.ag-menu button { min-height: 44px; padding: 0 var(--space-md); border: 0; border-radius: var(--radius-sm); background: none; font: inherit; font-size: 14px; text-align: left; color: var(--dark-brown); cursor: pointer; }
.ag-menu button:hover, .ag-menu button:focus-visible { background: var(--beige); }
.ag-menu .ag-menu-danger { color: var(--warn); }
.ag-minutes { display: inline-flex; align-items: center; gap: var(--space-sm); }
.ag-step { width: 44px; height: 44px; padding: 0; border: 1.5px solid var(--dark-brown); border-radius: var(--radius-pill); background: var(--site-white); font: inherit; font-size: 15px; font-weight: 700; color: var(--dark-brown); cursor: pointer; }
.ag-step:disabled { opacity: 0.5; cursor: default; }
.ag-minutes-value { min-width: 3.5em; text-align: center; font-size: 15px; font-weight: 600; color: var(--dark-brown); }
.ag-alloc { display: flex; align-items: center; gap: var(--space-sm); margin: var(--space-sm) 0 var(--space-md); }
.ag-alloc-bar { flex: 1 1 auto; height: 6px; border-radius: 3px; background: var(--beige); overflow: hidden; }
.ag-alloc-fill { height: 100%; background: var(--green); }
.ag-alloc-fill.is-over { background: var(--warn); }
.ag-alloc-text { flex: 0 0 auto; white-space: nowrap; font-size: 14px; font-weight: 600; color: var(--dark-brown); }
.ag-alloc-text.is-over { color: var(--warn); }
.ag-thread { padding-top: var(--space-md); border-top: 1px solid var(--light-brown); display: flex; flex-direction: column; gap: var(--space-md); }
.ag-comments { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-md); }
.ag-comment { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-sm); }
.ag-comment-main { flex: 1 1 auto; min-width: 0; }
.ag-comment-meta { display: block; font-size: 13px; font-weight: 700; color: var(--dark-brown); margin-bottom: var(--space-sm); }
.ag-comment-body { margin: 0; font-size: 15px; line-height: 1.5; color: var(--dark-brown); white-space: pre-wrap; overflow-wrap: anywhere; }
.ag-delete { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); padding: var(--space-xs) 0; }
.ag-add { margin: 0 0 var(--space-md); }
.ag-composer { display: flex; align-items: flex-end; gap: var(--space-sm); }
.ag-composer-input { flex: 1 1 auto; min-width: 0; box-sizing: border-box; min-height: 44px; max-height: 144px; padding: 9px var(--space-md); background: var(--site-white); border: 1.5px solid var(--dark-brown); border-radius: var(--radius-md); font-family: var(--font-body); font-size: 16px; line-height: 24px; color: var(--dark-brown); resize: none; }
.ag-composer-input:focus { outline: none; box-shadow: 0 0 0 3px var(--focus-glow); }
.ag-send { flex: 0 0 auto; width: 44px; height: 44px; display: grid; place-items: center; border: 0; border-radius: var(--radius-pill); background: var(--dark-brown); color: var(--site-white); cursor: pointer; }
.ag-send:disabled { opacity: 0.5; cursor: default; }
.ag-insert { margin-bottom: var(--space-sm); }

.agenda-strip { background: var(--beige); border: 2px solid transparent; border-radius: var(--radius-md); padding: var(--space-md); margin-bottom: var(--space-lg); display: flex; flex-direction: column; gap: var(--space-md); }
.agenda-strip[hidden] { display: none; }
.agenda-strip.is-wrap { background: var(--wrap-bg); border-color: var(--warn); }
.agenda-strip.is-idle { border-color: var(--dark-brown); }
.agenda-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-sm); }
.agenda-topic { flex: 1 1 200px; min-width: 0; font-size: 16px; font-weight: 700; color: var(--dark-brown); overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.agenda-clock { flex: 0 0 auto; white-space: nowrap; font-size: 18px; font-weight: 700; color: var(--dark-brown); }
/* Text stays --dark-brown on the wrap fill (--warn on it is under 4.5:1 in light); the amber border, the fill and the words carry the state. */
.agenda-over { margin: 0; font-size: 14px; font-weight: 700; color: var(--dark-brown); }
.agenda-finished { margin: 0; font-size: 16px; font-weight: 700; color: var(--dark-brown); }
.agenda-bar { display: flex; gap: 3px; height: 12px; }
.agenda-seg { display: block; min-width: 4px; height: 100%; border-radius: 3px; background: var(--light-brown); overflow: hidden; }
.agenda-seg i { display: block; height: 100%; width: 0; background: var(--dark-brown); }
.agenda-seg.is-done { background: var(--green); }
.agenda-seg.is-done i { background: var(--green); }
.agenda-strip.is-wrap .agenda-seg.is-current i { background: var(--warn); }
.agenda-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-sm); }
.agenda-next { flex: 1 1 160px; min-width: 0; font-size: 14px; color: var(--dark-brown); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.agenda-buttons { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.agenda-buttons .btn-inline { margin-left: 0; }
.agenda-confirm { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); }
.agenda-confirm-text { font-size: 14px; font-weight: 700; color: var(--dark-brown); }
.agenda-ahead-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-sm); padding-top: var(--space-sm); }
.agenda-ahead summary { cursor: pointer; min-height: 44px; display: inline-flex; align-items: center; }
.agenda-tools { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); }
.agenda-hint { font-size: 13px; color: var(--dark-brown); }

/* Profile photos (profile-photos-spec.md S8). The initial stays in the DOM;
   an <img> is layered over it once the photo loads. Flat: no sheen on a
   photo. */
[data-avatar-user] { position: relative; overflow: hidden; }
.av-img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }
/* Click-to-change cue on every avatar circle that opens the photo dialog. */
.acct-photo-btn { cursor: pointer; }
.acct-photo-btn:hover { box-shadow: 0 0 0 2px var(--green); }
.acct-photo-btn:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.photo-dialog {
  max-width: 420px;
  width: calc(100% - 2 * var(--space-lg));
  padding: var(--space-lg);
  border: var(--edge);
  border-radius: var(--radius-md);
  background: var(--site-white);
  color: var(--dark-brown);
}
.photo-dialog { position: relative; }
.photo-dialog-x {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--dark-brown);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
}
.photo-dialog-x:focus-visible { outline: 2px solid var(--green); }
.photo-crop { margin: 0 0 var(--space-md); text-align: center; }
.photo-crop[hidden] { display: none; }
.photo-crop-frame {
  position: relative;
  width: min(240px, 100%);
  aspect-ratio: 1;
  margin: 0 auto var(--space-md);
  border-radius: 50%;
  overflow: hidden;
  background: var(--beige);
  touch-action: none;
  cursor: grab;
}
.photo-crop-frame:active { cursor: grabbing; }
.photo-crop-frame canvas { width: 100%; height: 100%; display: block; }
.photo-crop-zoom { display: flex; align-items: center; gap: var(--space-sm); justify-content: center; }
.photo-crop-zoom input { flex: 1; max-width: 220px; min-height: 44px; accent-color: var(--green); }
.photo-crop-hint { margin: var(--space-xs) 0 0; font-size: 13px; }
.photo-dialog::backdrop { background: var(--overlay); }
.photo-dialog-circle {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  background: var(--beige);
  color: var(--dark-brown);
  display: grid;
  place-items: center;
  font-size: 28px;
  font-weight: 700;
  margin: 0 auto var(--space-md);
}
.photo-dialog-consent { margin: 0 0 var(--space-md); }
.photo-dialog-error { color: var(--warn); margin: 0 0 var(--space-md); }
.photo-dialog-actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.photo-dialog-actions .btn-inline + .btn-inline { margin-left: 0; }

/* Staff AI chat (aiChat.js): the desktop-app layout (spec ai-assistant-desktop-feel-2026-10-04).
   One .pcard: a 260px chat sidebar, a hairline, the conversation. Only the sidebar list and
   the message list scroll. Tokens only: no colour literal, both themes through the variables. */
.ai-chat { display: flex; flex-direction: column; min-height: 0; }
.ai-chat [hidden] { display: none; }
.ai-chat-shell { position: relative; display: grid; grid-template-columns: 260px minmax(0, 1fr); column-gap: var(--space-lg); flex: 1 1 auto; min-height: 0; }
.ai-chat-side { display: flex; flex-direction: column; gap: var(--space-sm); min-width: 0; min-height: 0; padding-right: var(--space-lg); border-right: var(--edge); }
.ai-chat-side .btn-inline { flex: 0 0 auto; }
.ai-chat-side .btn-inline + .btn-inline { margin-left: 0; }
.ai-chat-side .btn.is-selected { background: var(--select-tint); }
.ai-chat-search { flex: 0 0 auto; box-sizing: border-box; min-height: 44px; font-size: 14px; }
.ai-chat-chats { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: var(--space-xs); overflow-y: auto; overscroll-behavior: contain; }
.ai-chat-chats > .body-copy { margin: 0; font-size: 14px; }
.ai-chat-retention { flex: 0 0 auto; margin: 0; font-size: 13px; }
.ai-chat-group { flex: 0 0 auto; margin: var(--space-sm) 0 0; padding: 0 var(--space-md); font-size: 12px; font-weight: 700; color: var(--dark-brown); }
.ai-chat-group:first-child { margin-top: 0; }
.ai-chat-rowwrap { position: relative; flex: 0 0 auto; overflow: hidden; border-radius: var(--radius-sm); }
.ai-chat-rowfront { position: relative; display: flex; align-items: center; min-height: 44px; border-radius: var(--radius-sm); touch-action: pan-y; transition: transform 0.18s ease; }
.ai-chat-rowwrap.is-dragging .ai-chat-rowfront { transition: none; }
/* Opaque only while it covers the tray. */
.ai-chat-rowwrap.is-open .ai-chat-rowfront, .ai-chat-rowwrap.is-dragging .ai-chat-rowfront { background-color: var(--site-white); }
.ai-chat-rowwrap.is-open .ai-chat-rowfront { transform: translateX(-144px); }
.ai-chat-rowwrap.is-current .ai-chat-rowfront { background-image: linear-gradient(var(--select-tint), var(--select-tint)); }
@media (hover: hover) { .ai-chat-rowwrap:hover .ai-chat-rowfront { background-color: var(--off-white); } }
.ai-chat-chat { flex: 1 1 auto; min-width: 0; min-height: 44px; padding: var(--space-sm) var(--space-md); border: 0; background: transparent; color: var(--dark-brown); font: inherit; font-size: 14px; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }
.ai-chat-chat[aria-current="true"] { font-weight: 700; }
.ai-chat-chat:disabled { cursor: default; opacity: 0.6; }
.ai-chat-tag { font-size: 12px; font-weight: 400; }
/* Always shown on touch (an alternative to swiping, and the only way to Rename); revealed on hover or focus where a pointer hovers. */
.ai-chat-more { display: block; flex: 0 0 auto; width: 44px; min-height: 44px; border: 0; background: transparent; color: var(--dark-brown); font-size: 18px; line-height: 1; cursor: pointer; }
@media (hover: hover) {
  .ai-chat-more { opacity: 0; }
  .ai-chat-rowwrap:hover .ai-chat-more, .ai-chat-rowwrap:focus-within .ai-chat-more, .ai-chat-more[aria-expanded="true"] { opacity: 1; }
}
.ai-chat-more:disabled { cursor: default; }
/* Swipe tray (touch only): two 72px buttons behind the row, unreachable until the row is open. */
.ai-chat-tray { position: absolute; top: 0; right: 0; bottom: 0; display: flex; width: 144px; visibility: hidden; transition: visibility 0s linear 0.18s; }
.ai-chat-rowwrap.is-open .ai-chat-tray, .ai-chat-rowwrap.is-dragging .ai-chat-tray { visibility: visible; transition-delay: 0s; }
.ai-chat-tray-btn { flex: 1 1 0; min-width: 0; border: 0; background: var(--beige); color: var(--dark-brown); font: inherit; font-size: 14px; cursor: pointer; }
/* --warn text on --beige is 3.5:1; the warn colour stays as the top edge. */
.ai-chat-tray-del { color: var(--dark-brown); font-weight: 700; box-shadow: inset 0 3px 0 var(--warn); }
.ai-chat-archived-toggle { font-feature-settings: "calt" 0, "liga" 0; flex: 0 0 auto; min-height: 44px; margin-top: var(--space-sm); padding: 0 var(--space-md); border: 0; background: transparent; color: var(--dark-brown); font: inherit; font-size: 14px; font-weight: 700; text-align: left; cursor: pointer; }
.ai-chat-archived-toggle:disabled { cursor: default; opacity: 0.6; }
.ai-chat-menu { z-index: 80; right: auto; }
.ai-chat-title-input { flex: 1 1 auto; min-width: 0; box-sizing: border-box; min-height: 44px; padding: var(--space-sm) var(--space-md); border: var(--edge); border-radius: var(--radius-sm); background: var(--site-white); color: var(--dark-brown); font: inherit; font-size: 14px; }

.ai-chat-main { display: flex; flex-direction: column; gap: var(--space-sm); min-width: 0; min-height: 0; }
.ai-chat-top { display: flex; align-items: center; gap: var(--space-sm); flex: 0 0 auto; min-height: 44px; }
.ai-chat-title { flex: 1 1 auto; min-width: 0; font-weight: 700; color: var(--dark-brown); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ai-chat-chats-btn { display: none; }
.ai-chat-menu-btn { align-items: center; justify-content: center; flex: 0 0 auto; width: 44px; height: 44px; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--dark-brown); cursor: pointer; }
.ai-chat-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.ai-chat-scroll > * { max-width: 52rem; }
.ai-chat-scroll > .ai-chat-blank { max-width: none; }
.ai-chat-blank { margin: 0; padding: var(--space-xl) 0; text-align: center; font-family: var(--font-heading); font-size: 24px; line-height: 1.2; color: var(--dark-brown); }
.ai-chat-list { display: flex; flex-direction: column; gap: var(--space-lg); margin-top: var(--space-md); margin-bottom: var(--space-md); }
.ai-chat-row { display: flex; flex-direction: column; gap: var(--space-sm); min-width: 0; }
.ai-chat-row--user { align-items: flex-end; }
.ai-chat-copy { min-height: 36px; padding: 6px 16px; }
/* Flat = data: a plain tint, no gradient, no sheen, no shadow, no border. */
.ai-chat-bubble { box-sizing: border-box; max-width: 80%; padding: var(--space-sm) var(--space-md); border-radius: var(--radius-md); background: var(--select-tint); color: var(--dark-brown); font-size: 15px; line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }
.ai-chat-md { min-width: 0; max-width: 100%; color: var(--dark-brown); font-size: 15px; line-height: 1.6; overflow-wrap: anywhere; }
.ai-chat-md:empty { display: none; }
.ai-chat-md > * { margin: 0 0 var(--space-sm); }
.ai-chat-md > :last-child { margin-bottom: 0; }
.ai-chat-md ul, .ai-chat-md ol { padding-left: var(--space-lg); }
.ai-chat-md ul { list-style: disc; }
.ai-chat-md ol { list-style: decimal; }
.ai-chat-md li + li { margin-top: var(--space-xs); }
.ai-md-h { font-family: var(--font-body); font-size: 16px; font-weight: 700; line-height: 1.4; }
.ai-chat-md > .ai-md-h { margin-top: var(--space-md); }
.ai-chat-md > .ai-md-h:first-child { margin-top: 0; }
.ai-md-code { padding: 1px var(--space-xs); border-radius: var(--radius-sm); background: var(--beige); font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 13px; }
.ai-md-pre { box-sizing: border-box; max-width: 100%; padding: var(--space-md); overflow-x: auto; border: var(--edge); border-radius: var(--radius-md); background: var(--site-white); font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.5; white-space: pre; overflow-wrap: normal; }
.ai-md-pre code { font-family: inherit; }
@media (hover: hover) {
  .ai-chat-row--assistant:not(:last-child) .ai-chat-copy { opacity: 0; }
  .ai-chat-row--assistant:hover .ai-chat-copy, .ai-chat-row--assistant:focus-within .ai-chat-copy { opacity: 1; }
}
/* Sticky and opaque so the client's name never scrolls away under a long reply. */
.ai-chat-clientwrap { position: sticky; top: 0; z-index: 2; background: var(--site-white); }
.ai-chat-clientline { display: flex; flex-wrap: wrap; align-items: center; column-gap: var(--space-md); row-gap: var(--space-xs); min-height: 44px; padding: 0 0 0 var(--space-md); border: var(--edge); border-radius: var(--radius-sm); color: var(--dark-brown); font-size: 14px; }
.ai-chat-clientname { flex: 0 1 auto; min-width: 0; max-width: 100%; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ai-chat-clientdrafts { flex: 1 1 auto; min-width: 0; }
.ai-chat-info { flex: 0 0 auto; width: 44px; height: 44px; border: 0; background: transparent; color: var(--dark-brown); font-family: serif; font-size: 18px; font-style: italic; font-weight: 700; cursor: pointer; }
.ai-chat-infopanel { margin-top: var(--space-sm); padding: var(--space-md); border: var(--edge); border-radius: var(--radius-sm); }
.ai-chat-infopanel .body-copy { margin: 0 0 var(--space-sm); font-size: 14px; }
.ai-chat-infopanel .body-copy:last-child { margin-bottom: 0; }
.ai-chat-foot { display: flex; flex-direction: column; flex: 0 0 auto; gap: var(--space-sm); width: 100%; max-width: 52rem; }
.ai-chat-foot .body-copy { margin: 0; }
.ai-chat-client { display: flex; align-items: center; gap: var(--space-sm); }
.ai-chat-client .text-input { flex: 1 1 auto; min-width: 0; }
.ai-chat-composer { margin: 0; display: flex; flex-direction: column; gap: var(--space-sm); padding: var(--space-sm) var(--space-md); border: var(--edge); border-radius: var(--radius-md); background: var(--site-white); }
.ai-chat-composer:focus-within { box-shadow: 0 0 0 3px var(--focus-glow); }
.ai-chat-input { box-sizing: border-box; width: 100%; max-height: 200px; padding: var(--space-xs) 0; border: 0; outline: none; resize: none; background: transparent; color: var(--dark-brown); font-family: var(--font-body); font-size: 16px; line-height: 24px; overflow-y: hidden; }
.ai-chat-composer-row { display: flex; align-items: center; gap: var(--space-sm); }
.ai-chat-composer-row .btn-inline + .btn-inline { margin-left: 0; }
.ai-chat-composer-row #ai-chat-attach { margin-right: auto; }
.ai-chat-attach-hint { font-size: 13px; }
.ai-chat-attachments { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.ai-chat-chip { position: relative; display: inline-flex; align-items: center; gap: var(--space-sm); min-height: 56px; padding: var(--space-sm) var(--space-md); border: var(--edge); border-radius: var(--radius-md); background: var(--site-white); color: var(--dark-brown); font-size: 14px; }
.ai-chat-thumb { display: block; width: 56px; height: 56px; object-fit: cover; border-radius: var(--radius-md); }
.ai-chat-chip-remove { min-width: 44px; min-height: 44px; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--dark-brown); font: inherit; font-size: 20px; cursor: pointer; }
.ai-chat .composer.is-dragover { background: var(--select-tint); border-radius: var(--radius-md); }
.ai-chat-client-label, .ai-chat-draft { font-size: 12px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: var(--dark-brown); }
.ai-chat-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); }
/* Mood Mail mode (aiMoodMail.js): the pane replaces the chat column; tokens only. */
.ai-chat-moodpane { display: none; }
.ai-chat-shell[data-mode="mood"] .ai-chat-main > :not(.ai-chat-moodpane) { display: none; }
.ai-chat-shell[data-mode="mood"] .ai-chat-moodpane { display: block; flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-bottom: var(--space-md); }
.ai-mood, .ai-mood-form, .ai-mood-results, .ai-mood-presets, .ai-mood-field, .ai-mood-block { display: flex; flex-direction: column; gap: var(--space-md); align-items: flex-start; }
.ai-mood-field, .ai-mood-presets { gap: var(--space-sm); align-self: stretch; }
.ai-mood-form, .ai-mood-results { align-self: stretch; }
.ai-mood-field .text-input, .ai-mood-field .composer-textarea { box-sizing: border-box; }
.ai-mood .body-copy { margin: 0; }
.ai-mood-error { font-weight: 700; }
.ai-mood .btn-inline + .btn-inline { margin-left: 0; }
/* The email button olive (--green-action, #787636). */
.ai-mood-primary { background: var(--green-action); background-image: var(--sheen-midtone); color: var(--site-white); }
.ai-mood-block { align-self: stretch; }
.ai-mood-code { box-sizing: border-box; width: 100%; max-height: 320px; margin: 0; padding: var(--space-md); overflow: auto; border: var(--edge); border-radius: var(--radius-md); background: var(--site-white); color: var(--dark-brown); font-size: 12px; line-height: 1.5; white-space: pre; }
/* Mood Mail v2 cards: preview, revise, copy. The frame ground uses --fixed-* so the email never inherits dark mode. */
.ai-mood-card { display: flex; flex-direction: column; gap: var(--space-md); align-self: stretch; align-items: flex-start; padding: var(--space-lg); border: var(--edge); border-radius: var(--radius-md); background: var(--site-white); }
.ai-mood-card .text-input { align-self: stretch; box-sizing: border-box; }
.ai-mood-frame-wrap { align-self: stretch; display: flex; justify-content: center; padding: var(--space-md); border-radius: var(--radius-md); background: var(--fixed-mid); transition: opacity 0.2s; }
.ai-mood-frame-wrap.is-updating { opacity: 0.5; }
.ai-mood-frame { display: block; width: 640px; max-width: 100%; height: 640px; border: 0; background: var(--fixed-light); color-scheme: light; }
.ai-mood-frame-wrap[data-device="phone"] .ai-mood-frame { width: 390px; }
.ai-mood-drafts { align-self: stretch; }
.ai-mood .btn[aria-pressed="true"] { border-color: var(--green); font-weight: 700; }
.ai-mood-drafts summary { cursor: pointer; padding: var(--space-sm) 0; color: var(--dark-brown); font-weight: 700; }
.ai-mood-drafts[open] summary { margin-bottom: var(--space-sm); }
.ai-mood-frame-wrap + .body-copy { margin: 0; }
@media (max-width: 640px) {
  .ai-chat-shell { grid-template-columns: minmax(0, 1fr); }
  .ai-chat-chats-btn { display: inline-flex; }
  /* Phone: the sidebar is a left slide-over drawer with a scrim. Closed, it is visibility:hidden so nothing in it takes focus. */
  .ai-chat-side { position: fixed; z-index: 70; top: 0; bottom: 0; left: 0; box-sizing: border-box; width: min(320px, 85vw); padding: var(--space-md); padding-top: calc(var(--space-md) + env(safe-area-inset-top, 0px)); border-right: var(--edge); background: var(--site-white); transform: translateX(-100%); visibility: hidden; transition: transform 0.2s ease, visibility 0s linear 0.2s; }
  .ai-chat-shell.is-drawer-open .ai-chat-side { transform: none; visibility: visible; transition: transform 0.2s ease; }
  .ai-chat-scrim { position: fixed; z-index: 65; inset: 0; background: var(--overlay); }
  .ai-chat-bubble { max-width: 90%; }
}
@media (prefers-reduced-motion: reduce) { .ai-chat-side, .ai-chat-rowfront, .ai-chat-tray { transition: none; } }
@media (min-width: 1041px) {
  #practitioner-view.is-chat #ai-chat-body { flex: 1 1 auto; min-height: 0; }
  #practitioner-view.is-chat #ai-chat-body > .pgrid { height: 100%; grid-template-rows: minmax(0, 1fr); }
}

/* =========================================================================
   TEAM CHAT (teamChat.js). One card; only .chat-scroll scrolls. Tokens only:
   no colour literal, both themes through the same variables.
   Desktop (above 1040px): the card fills the visible height and the page does
   not scroll (#practitioner-view.is-chat is set by the router while the chat
   view is showing). Phone: the thread is a full-screen layer with its own
   header, sized to the visual viewport by JS so the composer stays above the
   keyboard.
   ========================================================================= */
@media (min-width: 1041px) {
  #practitioner-view.is-chat { height: 100dvh; overflow: hidden; grid-template-rows: auto minmax(0, 1fr); }
  #practitioner-view.is-chat .practitioner-rail { height: auto; min-height: 0; }
  #practitioner-view.is-chat .practitioner-main-col { min-height: 0; }
  #practitioner-view.is-chat .practitioner-main-col > .shell { display: flex; flex-direction: column; height: 100%; padding-top: var(--space-lg); padding-bottom: var(--space-lg); }
  #practitioner-view.is-chat #chat-grid { flex: 1 1 auto; min-height: 0; grid-template-rows: minmax(0, 1fr); align-items: stretch; }
}
.chat-card { padding: 0; display: flex; flex-direction: column; overflow: hidden; position: relative; min-height: 0; }
.chat-pane { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; position: relative; }

.chat-head { display: flex; align-items: center; gap: var(--space-md); flex: 0 0 auto; padding: var(--space-md) var(--space-lg); padding-top: calc(var(--space-md) + env(safe-area-inset-top, 0px)); border-bottom: 1px solid var(--beige); }
.chat-head-text { min-width: 0; }
.chat-head-name { margin: 0; font-family: var(--font-heading); font-weight: 400; font-size: 22px; line-height: 1.15; color: var(--dark-brown); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-head-sub { margin: 2px 0 0; font-size: 13px; color: var(--dark-brown); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-head-sub:empty { display: none; }
.chat-back { display: none; align-items: center; justify-content: center; width: 44px; height: 44px; margin-left: calc(var(--space-sm) * -1); border-radius: var(--radius-md); color: var(--dark-brown); }
.chat-av { width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0; display: grid; place-items: center; background: var(--beige); color: var(--dark-brown); font-size: 14px; font-weight: 700; }
.chat-av--head { width: 40px; height: 40px; }

.chat-pane > div > .banner { margin: var(--space-sm) var(--space-lg) 0; }
.chat-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; position: relative; padding: var(--space-md) var(--space-lg); }
.chat-list { max-width: none; }
.chat-top-note { margin: 0 0 var(--space-md); text-align: center; font-size: 13px; color: var(--dark-brown); }
.chat-empty { padding: var(--space-xl) 0; text-align: center; color: var(--dark-brown); }
.chat-empty p { margin: 0; }
.chat-empty .chat-empty-note { margin-top: var(--space-sm); font-size: 13px; }
.chat-skel { display: flex; flex-direction: column; gap: var(--space-md); max-width: 52rem; }
.chat-skel .skeleton { height: 44px; }
.chat-skel .skeleton:nth-child(2) { width: 70%; }
.chat-skel .skeleton:nth-child(3) { width: 85%; }
.chat-skel .skeleton:nth-child(4) { width: 55%; }

.chat-day span, .chat-unread span { white-space: nowrap; }
.chat-day, .chat-unread { display: flex; align-items: center; gap: var(--space-md); margin: var(--space-md) 0; font-size: 12px; font-weight: 600; color: var(--dark-brown); }
.chat-day::before, .chat-day::after, .chat-unread::before, .chat-unread::after { content: ""; flex: 1; height: 1px; background: var(--light-brown); }
.chat-unread { font-weight: 700; }
.chat-unread::before, .chat-unread::after { height: 2px; background: var(--green); }

.chat-msg { position: relative; display: flex; align-items: flex-start; gap: var(--space-sm); padding: 1px 0; }
.chat-msg:not(.chat-msg--cont) { margin-top: var(--space-sm); }
.chat-msg--mine { justify-content: flex-end; }
.chat-msg-gutter { width: 36px; flex-shrink: 0; }
.chat-msg-stamp { display: block; margin-top: var(--space-sm); font-size: 12px; }
.chat-msg--mine .chat-msg-stamp { text-align: right; }
/* Flat fills: a bubble is data. Mine = olive tint on the right, others = card on the left. */
.chat-msg-main { min-width: 0; max-width: 75%; padding: var(--space-sm) var(--space-md); border: var(--edge); border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-sm); background: var(--site-white); color: var(--dark-brown); }
.chat-msg--mine .chat-msg-main { background: var(--bubble-mine); color: var(--dark-brown); border-radius: var(--radius-lg) var(--radius-lg) var(--radius-sm) var(--radius-lg); }
.chat-msg--mention .chat-msg-main { background: var(--select-tint); box-shadow: inset 3px 0 0 var(--green); }
.chat-msg--sending { opacity: 0.6; }
@media (max-width: 480px) { .chat-msg-main { max-width: calc(100% - 44px); } .chat-msg--mine .chat-msg-main { max-width: 90%; } }
.chat-msg-meta { display: flex; align-items: baseline; gap: var(--space-sm); }
.chat-msg-name { font-size: 15px; font-weight: 700; color: var(--dark-brown); white-space: nowrap; }
.chat-msg-time { font-size: 12px; color: var(--dark-brown); }
.chat-msg-body { margin: 0; font-size: 15px; line-height: 1.5; color: var(--dark-brown); white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
.chat-msg-body a { color: var(--dark-brown); text-decoration: underline; text-underline-offset: 2px; }
.chat-mention { padding: 0 3px; border-radius: var(--radius-sm); background: var(--select-tint); font-weight: 600; }
.chat-edited { font-size: 12px; color: var(--dark-brown); white-space: nowrap; }
.chat-msg-status { margin: var(--space-sm) 0 0; font-size: 13px; color: var(--dark-brown); }
.chat-msg-status--bad { color: var(--warn); }
.chat-msg-fix { display: flex; gap: var(--space-md); }
.chat-link-btn { min-height: 32px; padding: 0 var(--space-xs); font-size: 13px; font-weight: 600; color: var(--dark-brown); text-decoration: underline; text-underline-offset: 2px; }
.chat-msg .msg-attachment { max-width: min(320px, 100%); max-height: 320px; }
.chat-msg .msg-attachment--loading { width: 240px; height: 160px; }
.chat-msg .msg-attachment-pdf { max-width: 360px; }

.chat-msg-actions { position: relative; order: -1; align-self: center; }
.chat-msg-menu-btn { width: 32px; height: 32px; display: grid; place-items: center; border-radius: var(--radius-sm); background: var(--site-white); color: var(--dark-brown); border: var(--edge); box-shadow: var(--lift); opacity: 0; }
.chat-msg:hover .chat-msg-menu-btn, .chat-msg:focus-within .chat-msg-menu-btn, .chat-msg-menu-btn[aria-expanded="true"] { opacity: 1; }
@media (hover: none) {
  .chat-msg--mine:not(.chat-msg--cont) .chat-msg-menu-btn, .chat-msg.is-tapped .chat-msg-menu-btn { opacity: 1; }
}
@media (pointer: coarse) { .chat-msg-menu-btn { width: 44px; height: 44px; } }
.chat-menu { position: absolute; top: 100%; right: 0; z-index: 5; min-width: 150px; display: flex; flex-direction: column; padding: var(--space-xs); background: var(--site-white); border: var(--edge); border-radius: var(--radius-md); box-shadow: var(--lift-hover); }
.chat-menu button { min-height: 36px; padding: 0 var(--space-md); border-radius: var(--radius-sm); text-align: left; font-size: 14px; color: var(--dark-brown); }
.chat-menu button:hover, .chat-menu button:focus-visible { background: var(--beige); }
.chat-menu .chat-menu-danger { color: var(--warn); }
@media (pointer: coarse) { .chat-menu button { min-height: 44px; } }
/* Reactions: a bar of three over the bubble's top corner (hover/focus on desktop, tap on phone) and chips under the text. Flat fills, tokens only. */
.chat-msg-main { position: relative; }
.chat-react-bar { position: absolute; right: var(--space-sm); bottom: calc(100% - 12px); z-index: 2; display: flex; gap: var(--space-xs); padding: var(--space-xs); background: var(--site-white); border: var(--edge); border-radius: var(--radius-md); box-shadow: var(--lift); opacity: 0; pointer-events: none; -webkit-user-select: none; user-select: none; }
.chat-msg:hover .chat-react-bar, .chat-msg:focus-within .chat-react-bar, .chat-msg.is-tapped .chat-react-bar { opacity: 1; pointer-events: auto; }
@media (hover: none) { .chat-react-bar { visibility: hidden; } .chat-msg.is-tapped .chat-react-bar { visibility: visible; } }
.chat-react-btn { width: 32px; height: 32px; display: grid; place-items: center; border-radius: var(--radius-sm); font-size: 16px; line-height: 1; color: var(--dark-brown); }
.chat-react-btn:hover, .chat-react-btn:focus-visible { background: var(--beige); }
.chat-react-btn.is-mine, .chat-reaction.is-mine { background: var(--select-tint); border-color: var(--green); }
@media (pointer: coarse) { .chat-react-btn { width: 44px; height: 44px; } }
.chat-reactions { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-top: var(--space-sm); }
.chat-reaction { min-height: 28px; padding: 0 var(--space-sm); border: var(--edge); border-radius: var(--radius-md); background: var(--site-white); font-size: 13px; color: var(--dark-brown); }
@media (pointer: coarse) { .chat-reaction { min-height: 44px; padding: 0 var(--space-md); } }

.chat-edit-input { width: 100%; min-height: 84px; padding: var(--space-sm) var(--space-md); background: var(--site-white); border: 1.5px solid var(--dark-brown); border-radius: var(--radius-md); font-family: var(--font-body); font-size: 16px; line-height: 1.5; color: var(--dark-brown); resize: vertical; }
.chat-edit-input:focus { outline: none; box-shadow: 0 0 0 3px var(--focus-glow); }
.chat-edit-actions { display: flex; gap: var(--space-sm); margin-top: var(--space-sm); }

.chat-jump { position: sticky; bottom: var(--space-sm); z-index: 3; display: flex; align-items: center; gap: var(--space-sm); width: fit-content; min-height: 44px; margin: var(--space-sm) 0 0 auto; padding: 0 var(--space-md); border-radius: var(--radius-pill); background: var(--dark-brown); color: var(--site-white); box-shadow: var(--lift-hover); font-size: 13px; font-weight: 600; white-space: nowrap; border: 1.5px solid transparent; }

.chat-composer { position: relative; flex: 0 0 auto; padding: var(--space-md) var(--space-lg); padding-bottom: calc(var(--space-md) + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--beige); background: var(--site-white); }
.chat-composer-row { display: flex; align-items: flex-end; gap: var(--space-sm); max-width: 52rem; }
.chat-composer-input { flex: 1 1 auto; min-width: 0; min-height: 44px; padding: 9px var(--space-md); background: var(--site-white); border: 1.5px solid var(--dark-brown); border-radius: var(--radius-md); font-family: var(--font-body); font-size: 16px; line-height: 24px; color: var(--dark-brown); resize: none; overflow-y: auto; }
.chat-composer-input:focus { outline: none; box-shadow: 0 0 0 3px var(--focus-glow); }
.chat-icon-btn { width: 44px; height: 44px; flex: 0 0 auto; display: grid; place-items: center; border-radius: var(--radius-md); color: var(--dark-brown); }
.chat-icon-btn:hover { background: var(--beige); }
.chat-icon-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.chat-send { flex: 0 0 auto; width: auto; min-height: 44px; padding: 10px 24px; }
.chat-norm { margin: var(--space-sm) 0 0; font-size: 12px; color: var(--dark-brown); }
.chat-note { margin: 0 0 var(--space-sm); font-size: 13px; color: var(--dark-brown); }
#chat-composer-notes .banner { margin: 0 0 var(--space-sm); padding: var(--space-sm) var(--space-md); font-size: 14px; }
.chat-composer .composer-photo-preview { margin-bottom: var(--space-sm); }
.chat-tip { display: flex; align-items: center; gap: var(--space-md); max-width: 52rem; margin: 0 0 var(--space-sm); padding: var(--space-sm) var(--space-md); border-radius: var(--radius-md); background: var(--beige); color: var(--dark-brown); font-size: 13px; }
.chat-tip p { flex: 1; margin: 0; }
.chat-tip-dismiss { min-height: 44px; padding: 0 var(--space-sm); font-size: 13px; font-weight: 700; text-decoration: underline; text-underline-offset: 2px; color: var(--dark-brown); }
.chat-picker { position: absolute; left: var(--space-lg); bottom: 100%; z-index: 6; min-width: 220px; max-width: calc(100% - var(--space-xl) * 2); margin: 0 0 var(--space-xs); padding: var(--space-xs); background: var(--site-white); border: var(--edge); border-radius: var(--radius-md); box-shadow: var(--lift-hover); }
.chat-picker-row { min-height: 36px; display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); padding: 0 var(--space-md); border-radius: var(--radius-sm); font-size: 15px; font-weight: 600; color: var(--dark-brown); cursor: pointer; }
.chat-picker-row.is-active { background: var(--beige); }
.chat-picker-full { font-size: 12px; font-weight: 400; white-space: nowrap; }
@media (pointer: coarse) { .chat-picker-row { min-height: 44px; } }
.chat-drop { position: absolute; inset: 0; z-index: 8; display: grid; place-items: center; background: var(--overlay); color: var(--fixed-light); font-size: 18px; font-weight: 600; border: 2px dashed var(--fixed-light); border-radius: inherit; pointer-events: none; }

/* Phone list screen (the rail is hidden at this width). */
.chat-conv-row { display: flex; align-items: center; gap: var(--space-md); width: 100%; min-height: 56px; padding: var(--space-sm) 0; text-align: left; border-bottom: 1px solid var(--beige); }
.chat-conv-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.chat-conv-name { font-size: 16px; font-weight: 500; color: var(--dark-brown); }
.chat-conv-row.has-unread .chat-conv-name { font-weight: 700; }
.chat-conv-preview { font-size: 14px; color: var(--dark-brown); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-conv-side { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-xs); }
.chat-conv-time { font-size: 12px; color: var(--dark-brown); }
.chat-people-head { margin: var(--space-lg) 0 var(--space-sm); font-size: 12px; font-weight: 600; letter-spacing: 0.13em; text-transform: uppercase; color: var(--dark-brown); }
.chat-listscreen-card .btn-inline { margin-top: var(--space-lg); }
.rail-client.has-unread { font-weight: 700; }
.rail-chat-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Modal (delete, new group) and photo lightbox: one overlay at a time. */
.chat-modal-backdrop { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; padding: var(--space-md); background: var(--overlay); cursor: pointer; }
.chat-modal-backdrop > * { cursor: auto; }
html.chat-overlay-open, html.chat-lock { overflow: hidden; }
.chat-modal { width: min(440px, 100%); max-height: 90dvh; overflow-y: auto; padding: var(--space-lg); background: var(--site-white); background-image: var(--sheen); border: var(--edge); border-radius: var(--radius-lg); box-shadow: var(--lift-hover); }
.chat-modal h2 { margin: 0 0 var(--space-md); }
.chat-modal-actions { display: flex; gap: var(--space-sm); margin-top: var(--space-lg); }
.chat-group-members { margin: 0 0 var(--space-md); padding: 0; border: 0; }
.chat-group-member { display: flex; align-items: center; gap: var(--space-sm); min-height: 44px; font-size: 15px; color: var(--dark-brown); }
.chat-lightbox-dialog { display: flex; flex-direction: column; align-items: center; gap: var(--space-md); max-width: 100%; max-height: 100%; }
.chat-lightbox-img { max-width: 100%; max-height: calc(100dvh - 120px); object-fit: contain; border-radius: var(--radius-md); }
.chat-lightbox-bar { display: flex; gap: var(--space-sm); }
@media (max-width: 640px) {
  .chat-modal-backdrop.chat-confirm { place-items: end center; padding: 0; }
  .chat-confirm .chat-modal { width: 100%; border-radius: var(--radius-lg) var(--radius-lg) 0 0; padding-bottom: calc(var(--space-lg) + env(safe-area-inset-bottom, 0px)); }
}

@media (max-width: 1040px) {
  #chat-body.is-thread { position: fixed; left: 0; right: 0; top: 0; height: 100dvh; z-index: 50; margin: 0; border: 0; border-radius: 0; box-shadow: none; background-image: none; }
  .chat-back { display: flex; }
  .chat-scroll { padding: var(--space-md); }
  .chat-composer { padding-left: var(--space-md); padding-right: var(--space-md); }
  .chat-picker { left: var(--space-md); }
  .chat-head { padding-left: var(--space-md); padding-right: var(--space-md); }
  .chat-pane > div > .banner { margin-left: var(--space-md); margin-right: var(--space-md); }
}

/* PMDD Pep Talk Episodes and Blog posts cards (podcastEpisodes.js,
   podcastBlog.js). Rows reuse .schedule-row; the edit panel opens under its
   row. Status chips are DATA fills: flat, no sheen. */
[data-pa-stats]:not(:empty) { margin-bottom: var(--space-md); }
.pa-grow { flex: 1 1 0; min-width: 0; }
/* Open row: header on one line, the panel wraps onto its own full-width line below it. */
.pa-row--open { flex-wrap: wrap; row-gap: var(--space-sm); }
.pa-panel { flex: 1 0 100%; display: grid; gap: var(--space-md); padding: var(--space-md) 0; min-width: 0; }
.pa-panel > .body-copy { margin: 0; }
.pa-panel .banner { margin: 0; }
.pa-panel .btn-row { margin: 0; align-items: center; }
.pa-fields { display: grid; gap: var(--space-md); }
.pa-fields--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); }
.pa-field { min-width: 0; }
.pa-field .text-input { margin-top: var(--space-xs); }
textarea.pa-textarea { padding: var(--space-sm) var(--space-md); line-height: 1.5; resize: vertical; }
.pa-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 14px; }
.pa-status { font-size: 14px; }
.pa-chip { display: inline-block; padding: var(--space-xs) var(--space-sm); border-radius: var(--radius-sm); font-size: 13px; font-weight: 600; white-space: nowrap; border: 1.5px solid transparent; }
.pa-chip--private, .pa-chip--pending { background: var(--beige); color: var(--dark-brown); }
.pa-chip--scheduled { background: var(--sev-moderate); color: var(--fixed-dark); }
.pa-chip--live { background: var(--site-white); color: var(--dark-brown); border-color: var(--green); }
.pa-chip--failed { background: var(--sev-severe); color: var(--on-period); }

/* =========================================================================
   NOTIFICATION BELL (notificationBell.js). A popover, not a page. Colours are
   the existing tokens only: the bubble is .unread-badge's flat --warn fill (a
   count is data, so no sheen), the unread dot is --green. Unread is never colour
   alone: the dot, a heavier weight and visually-hidden "Unread, " text. On a phone
   the panel is a full-width sheet under the header with a Close button and scrim.
   ========================================================================= */
.notif { position: relative; display: flex; align-items: center; }
.notif:empty { display: none; }
.notif-bell { position: relative; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--dark-brown); cursor: pointer; }
.notif-bell:hover { background: var(--beige); }
.notif-badge { position: absolute; top: 3px; right: 1px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: var(--radius-pill); background: var(--badge-bg); color: var(--site-white); font-size: 11px; font-weight: 700; line-height: 1; display: inline-flex; align-items: center; justify-content: center; }
.notif-badge[hidden] { display: none; }
.notif-panel { position: absolute; right: 0; top: calc(100% + var(--space-sm)); z-index: 45; width: 360px; max-width: calc(100vw - var(--space-md) * 2); max-height: min(70vh, 520px); overflow-y: auto; background: var(--site-white); border: var(--edge); border-radius: var(--radius-md); box-shadow: var(--lift-hover); color: var(--dark-brown); }
.notif-panel[hidden], .notif-scrim { display: none; }
.notif-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-sm) var(--space-md); background: var(--site-white); border-bottom: 1px solid var(--beige); }
.notif-title { margin: 0 auto 0 0; font-family: var(--font-body); font-size: 16px; font-weight: 700; }
.notif-markall, .notif-close { min-height: 44px; padding: 0 var(--space-sm); border: 0; background: transparent; color: var(--dark-brown); font: inherit; font-size: 14px; font-weight: 600; text-decoration: underline; cursor: pointer; }
.notif-markall:disabled { opacity: 0.5; cursor: default; text-decoration: none; }
.notif-close { display: none; }
.notif-stale { margin: 0; padding: var(--space-sm) var(--space-md); background: var(--beige); font-size: 13px; }
.notif-stale[hidden] { display: none; }
.notif-note { margin: 0; padding: var(--space-md); font-size: 14px; }
.notif-retry { margin: 0 var(--space-md) var(--space-md); }
.notif-group + .notif-group { border-top: 1px solid var(--beige); }
.notif-group-title { margin: 0; padding: var(--space-md) var(--space-md) var(--space-xs); font-family: var(--font-body); font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
.notif-list { margin: 0; padding: 0 0 var(--space-xs); list-style: none; }
.notif-item { display: flex; align-items: flex-start; gap: var(--space-sm); min-height: 44px; padding: var(--space-sm) var(--space-md); color: var(--dark-brown); text-decoration: none; }
.notif-item:hover { background: var(--beige); }
.notif-item:focus-visible { outline-offset: -3px; }
.notif-dot { flex-shrink: 0; width: 8px; height: 8px; margin-top: 7px; border-radius: 50%; background: transparent; }
.notif-item.is-unread .notif-dot { background: var(--green); }
.notif-main { display: flex; flex-direction: column; gap: var(--space-xs); min-width: 0; }
.notif-text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; font-size: 14px; line-height: 1.4; overflow-wrap: anywhere; }
.notif-item.is-unread .notif-text { font-weight: 600; }
.notif-time { font-size: 12px; opacity: 0.75; }
/* The bell's sheet follows the shared phone query (mobileLogic.js PHONE_QUERY), so a landscape iPhone gets it too. */
@media (max-width: 640px), (pointer: coarse) and (max-height: 500px) {
  .notif-panel { position: fixed; left: 0; right: 0; top: var(--notif-top, 64px); bottom: 0; z-index: 45; width: auto; max-width: none; max-height: none; border-width: 1px 0 0; border-radius: 0; padding-bottom: env(safe-area-inset-bottom); }
  .notif-scrim:not([hidden]) { display: block; position: fixed; inset: 0; z-index: 44; background: var(--overlay); }
  .notif-close { display: inline-flex; align-items: center; }
  html.notif-lock body { overflow: hidden; }
}
@media (max-width: 640px) {
  /* The bell stays on the header's first row beside the wordmark; the links wrap under it. */
  .practitioner-topbar > .header-links { display: contents; }
  .practitioner-topbar .header-links > .notif { order: 1; margin-left: auto; }
  .practitioner-topbar .header-links > :not(.notif) { order: 2; }
}

/* Staff Pomodoro timer (pomodoro.js): a button beside the bell and a small popover. Idle is just the
   tomato icon; running adds the time and a Focus/Break word; done says "Time's up". The flash is a
   colour change, never movement, and holds still under reduced motion. */
.pomo { position: relative; display: flex; align-items: center; }
.pomo-btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-xs); min-width: 44px; height: 44px; padding: 0 var(--space-xs); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--dark-brown); font: inherit; font-size: 14px; font-weight: 600; cursor: pointer; }
.pomo-btn:hover { background: var(--beige); }
.pomo-time { font-variant-numeric: tabular-nums; white-space: nowrap; }
.pomo-tag { font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
.pomo-time[hidden], .pomo-tag[hidden] { display: none; }
.pomo-btn[data-state="done"] { background: var(--wrap-bg); }
.pomo-btn.is-flashing { background: var(--wrap-bg); outline: 2px solid var(--warn); outline-offset: -2px; }
@media (prefers-reduced-motion: no-preference) {
  .pomo-btn.is-flashing { animation: pomo-flash 1s ease-in-out infinite; }
}
@keyframes pomo-flash { 50% { background: transparent; } }
.pomo-panel { position: absolute; right: 0; top: calc(100% + var(--space-sm)); z-index: 45; width: 260px; max-width: calc(100vw - var(--space-md) * 2); max-height: min(80vh, 640px); overflow-y: auto; padding: var(--space-md); background: var(--site-white); border: var(--edge); border-radius: var(--radius-md); box-shadow: var(--lift-hover); color: var(--dark-brown); }
.pomo-panel[hidden] { display: none; }
.pomo-modes { display: flex; gap: var(--space-xs); }
.pomo-mode { flex: 1 1 0; min-height: 44px; border: var(--edge); border-radius: var(--radius-md); background: transparent; color: var(--dark-brown); font: inherit; font-size: 14px; font-weight: 600; cursor: pointer; }
.pomo-mode[aria-pressed="true"] { background: var(--select-tint); font-weight: 700; }
.pomo-clock { margin: var(--space-md) 0 0; font-size: 40px; font-weight: 700; line-height: 1.1; text-align: center; font-variant-numeric: tabular-nums; }
.pomo-mode-name { margin: var(--space-xs) 0 var(--space-md); font-size: 14px; text-align: center; }
.pomo-dots { display: flex; justify-content: center; gap: var(--space-sm); margin: 0 0 var(--space-md); }
.pomo-dot { width: 12px; height: 12px; border: 2px solid var(--dark-brown); border-radius: 50%; }
.pomo-dot.is-done { background: var(--green); border-color: var(--green); }
.pomo-long { display: flex; align-items: center; gap: var(--space-xs); margin: 0 0 var(--space-md); }
.pomo-long[hidden] { display: none; }
.pomo-long-label { flex: 1 1 auto; font-size: 14px; font-weight: 600; }
.pomo-long .pomo-mode { flex: 0 0 auto; padding: 0 var(--space-sm); }
/* The end-of-block popup: must be dismissed. Same footprint as the chat modals, above the header and panels. */
.pomo-alert-backdrop { position: fixed; inset: 0; z-index: 70; display: grid; place-items: center; padding: var(--space-md); background: var(--overlay); }
.pomo-alert-backdrop[hidden] { display: none; }
.pomo-alert { width: min(400px, 100%); padding: var(--space-lg); background: var(--site-white); border: var(--edge); border-radius: var(--radius-lg); box-shadow: var(--lift-hover); color: var(--dark-brown); }
.pomo-alert h2 { margin: 0 0 var(--space-sm); }
.pomo-alert p { margin: 0 0 var(--space-lg); }
.pomo-alert-actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.pomo-actions { display: flex; flex-direction: column; gap: var(--space-sm); }
/* margin-left: 0 beats the sibling rule .btn-inline + .btn-inline, which pushed Reset out past the card edge. */
.pomo-actions .btn { width: 100%; margin-left: 0; }
.pomo-actions [hidden] { display: none; }
.pomo-info { margin-top: var(--space-md); padding-top: var(--space-sm); border-top: 1px solid var(--beige); font-size: 14px; }
.pomo-info summary { display: flex; align-items: center; min-height: 44px; font-weight: 600; cursor: pointer; }
.pomo-info summary::-webkit-details-marker { display: none; }
/* display:flex drops the native triangle, so draw a caret that turns when open. */
.pomo-info summary::after { content: ""; width: 7px; height: 7px; margin-left: var(--space-sm); border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(45deg) translateY(-2px); transition: transform .15s; }
.pomo-info[open] summary::after { transform: rotate(-135deg) translateY(-2px); }
@media (prefers-reduced-motion: reduce) { .pomo-info summary::after { transition: none; } }
.pomo-info ul { margin: 0; padding: 0 0 0 var(--space-md); display: flex; flex-direction: column; gap: var(--space-sm); line-height: 1.4; }
@media (max-width: 640px) {
  .practitioner-topbar .header-links > .pomo { order: 1; margin-left: auto; }
  .practitioner-topbar .header-links > .pomo + .notif { margin-left: 0; }
}

/* Client podcast page (podcast.html). One player card, then a list of rows;
   rows are buttons, not .card tiles, so there is no box inside a box. */
.podcast-stack { display: flex; flex-direction: column; gap: var(--space-lg); max-width: 760px; }
.podcast-player { display: flex; flex-wrap: wrap; gap: var(--space-lg); align-items: flex-start; }
.podcast-art { width: 160px; height: 160px; border-radius: var(--radius-md); object-fit: cover; flex: 0 0 auto; }
.podcast-now { flex: 1 1 260px; min-width: 0; display: flex; flex-direction: column; gap: var(--space-sm); }
.podcast-now > * { margin: 0; }
.podcast-now audio { width: 100%; margin-top: var(--space-sm); }
.podcast-meta { font-size: 14px; color: var(--dark-brown); opacity: 0.8; }
.podcast-playing { font-size: 14px; font-weight: 700; color: var(--green); }
.podcast-audio-error { font-size: 14px; color: var(--dark-brown); }
.podcast-note { display: flex; flex-direction: column; gap: var(--space-md); align-items: flex-start; max-width: 760px; }
.podcast-note > * { margin: 0; }
.podcast-list { display: flex; flex-direction: column; gap: var(--space-sm); margin: var(--space-md) 0 0; }
.podcast-row + .podcast-row { border-top: var(--edge); padding-top: var(--space-md); }
.podcast-row { display: flex; flex-direction: column; gap: var(--space-sm); width: 100%; min-height: 44px; padding: var(--space-md) 0; background: none; border: 0; text-align: left; font: inherit; color: var(--dark-brown); cursor: pointer; }
.podcast-row:focus-visible { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
.podcast-row-title { font-size: 16px; font-weight: 700; overflow-wrap: anywhere; }
.podcast-row-desc { font-size: 14px; line-height: 1.4; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* Food & Mood journal (food-mood.html). Tokens only; the pressed state is the
   same fill the scale buttons use for "checked". */
.food-mood-main { padding-top: var(--space-lg); padding-bottom: var(--space-xxl); }
.fmj-chips { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-bottom: var(--space-lg); }
.fmj-chip, .scale-btn.fmj-toggle {
  min-height: 48px; padding: 10px 16px; font: inherit; font-weight: 700; color: var(--dark-brown); cursor: pointer;
  border: 1.5px solid var(--light-brown); background: var(--site-white); background-image: var(--sheen);
}
.fmj-chip { border-radius: var(--radius-pill); }
.fmj-toggle[aria-pressed="true"] { border-color: var(--green); background: var(--green-action); background-image: var(--sheen-midtone); color: var(--site-white); }
.fmj-toggle:focus-visible { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
#log-card .scale-buttons { gap: var(--space-sm); }
#log-card .scale-endcaps { margin: var(--space-sm) 0 var(--space-lg); }
.fmj-pill { margin: 0; white-space: nowrap; padding: 4px 12px; font-size: 14px; color: var(--dark-brown); background: var(--beige); border: var(--edge); border-radius: var(--radius-pill); }
.fmj-note { margin-bottom: var(--space-lg); }
.fmj-note .text-input { width: 100%; resize: vertical; }
.fmj-note-meta #fmj-count { white-space: nowrap; }
.fmj-note-meta { display: flex; justify-content: space-between; gap: var(--space-md); margin: var(--space-sm) 0 0; font-size: 14px; color: var(--dark-brown); }
.fmj-save { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md); }
.fmj-save-btn { width: auto; min-height: 44px; padding: 12px 32px; }
.fmj-hint { margin: 0; font-size: 14px; color: var(--dark-brown); }
.fmj-day { margin: var(--space-lg) 0 var(--space-sm); }
.fmj-list { display: flex; flex-direction: column; gap: var(--space-sm); margin-bottom: var(--space-md); }
.fmj-entry { border-top: var(--edge); }
.fmj-entry-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm) var(--space-md); width: 100%; min-height: 44px;
  padding: var(--space-md) 0; background: none; border: 0; text-align: left; font: inherit; color: var(--dark-brown); cursor: pointer;
}
.fmj-entry-head:focus-visible { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
.fmj-entry-time { font-size: 14px; white-space: nowrap; }
.fmj-entry-meal { font-size: 16px; font-weight: 700; }
.fmj-entry-rate { display: inline-flex; gap: var(--space-md); }
.fmj-dots { display: inline-flex; gap: 4px; }
.fmj-dot { width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid var(--dark-brown); background: transparent; }
.fmj-dot.is-on { background: var(--green-action); border-color: var(--green-action); }
.fmj-entry-tags { display: inline-flex; flex-wrap: wrap; gap: var(--space-sm); }
.fmj-tag { padding: 2px 10px; font-size: 13px; color: var(--dark-brown); background: var(--beige); border-radius: var(--radius-pill); }
.fmj-preview { flex-basis: 100%; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fmj-entry-detail { margin-top: var(--space-sm); padding: 0 0 var(--space-md); }
.fmj-fullnote { white-space: pre-wrap; overflow-wrap: anywhere; }
.fmj-meta { white-space: nowrap; margin: 0 0 var(--space-md); font-size: 14px; color: var(--dark-brown); }

/* ==================================================================
   iOS APP SHELL (html.native, set by native.js only inside the Capacitor app)
   viewport-fit=cover (every page) lets content run under the notch, Dynamic
   Island and home indicator; these rules keep it out from under them. On the
   web every env() below is 0 and the class is never set, so nothing changes.
   ========================================================================= */
html.native body {
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}
/* Scrolled content must not show through the status bar. Under the overlays (z 44+). */
html.native body::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-top);
  background: var(--site-white);
  z-index: 40;
}
/* Bottom-fixed pieces: the pieces below already pad their own bottom, so the body's is cancelled for them. */
html.native .tm-publish-bar { bottom: calc(-1 * env(safe-area-inset-bottom)); padding-bottom: calc(var(--space-sm) + env(safe-area-inset-bottom)); }
html.native .crisis-modal,
html.native .history-overlay-sheet { padding-bottom: calc(var(--space-xxl) + env(safe-area-inset-bottom)); }
/* The full-screen chat thread is position:fixed, so body padding does not reach it. */
html.native #chat-body.is-thread { padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); box-sizing: border-box; }
/* In-flow chat (conversation list) already sits inside the body padding; only the fixed thread needs its own insets. */
html.native #chat-body:not(.is-thread) .chat-head { padding-top: var(--space-md); }
html.native #chat-body:not(.is-thread) .chat-composer { padding-bottom: var(--space-md); }
html.native #practitioner-view.is-chat { height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); }
/* 1:1 coaching is sold on the website only: nothing in the app prices, sells or links to checkout. */
html.native [data-purchase],
html.native a[href*="stripe.com" i],
html.native a[href*="checkout" i] { display: none !important; }

/* One gentle, dismissible notifications offer on the client home (index.js, app only). */
.native-push-card { position: relative; margin-bottom: var(--space-lg); padding: var(--space-md) var(--space-lg); padding-right: 56px; background: var(--site-white); border: var(--edge); border-radius: var(--radius-lg); }
.native-push-card h2 { margin: 0 0 var(--space-xs); }
.native-push-dismiss { position: absolute; top: 0; right: 0; min-width: 44px; min-height: 44px; background: none; border: 0; font: inherit; font-size: 22px; color: var(--dark-brown); cursor: pointer; }
/* Onboarding page (client-onboarding spec §5.2): one narrow column, the next
   step as a single card, everything else tucked below it. */
#onboarding-body { max-width: 640px; }
#onboarding-body .lede { margin-bottom: var(--space-md); }
.onboard-card .btn { margin-top: var(--space-md); }
.onboard-skip { margin: var(--space-md) 0; }
.onboard-skip a, .onboard-all summary { color: var(--dark-brown); }
.onboard-all ol { margin: var(--space-sm) 0 0; padding-left: var(--space-lg); }
.onboard-all li { margin: var(--space-sm) 0; }
/* Decorative brand figure (the line drawing from the HMM logo mark, no lettering) behind the page, in the
   text colour at 6% so it is brown in light and cream in dark. Fixed to the right edge, past the 640px
   column on desktop and in the empty lower corner on a phone. Never receives touches, hidden from readers. */
.onboard-art {
  position: fixed; z-index: 0; right: 2vw; bottom: 0; width: min(36vw, 520px); aspect-ratio: 954 / 1200;
  background: var(--dark-brown); opacity: 0.06; pointer-events: none;
  -webkit-mask: url("onboarding-mark.png") center / contain no-repeat; mask: url("onboarding-mark.png") center / contain no-repeat;
}
#onboarding-view .shell { position: relative; z-index: 1; }
@media (max-width: 900px) { .onboard-art { width: 62vw; right: -8vw; bottom: var(--tab-h, 0px); } }

/* Page botanicals: the same treatment as .onboard-art (text colour at 6%, so brown in light and cream in
   dark), one shared layer. app.js mountPageArt adds <div class="page-art" data-art data-corner> from the
   page table. Large and bleeding off the right edge, behind the page (main is lifted over it, so cards cover
   it and it only shows in the margins). Desktop: 62vh tall, at the bottom (br) or just under the top bar
   (tr). Phone: 36vh, tucked above the tab bar, right for br and left for tr. "side" is for pages with
   inputs: desktop top right only, hidden on phones. */
.page-art {
  position: fixed; z-index: 0; right: -6vw; height: 62vh;
  background: var(--dark-brown); opacity: 0.06; pointer-events: none;
  -webkit-mask: var(--art) center / contain no-repeat; mask: var(--art) center / contain no-repeat;
}
.page-art[data-corner="br"] { bottom: -3vh; }
.page-art[data-corner="tr"], .page-art[data-corner="side"] { top: 64px; }
.page-art[data-art="fern"] { --art: url("page-art-fern.png"); aspect-ratio: 708 / 782; }
.page-art[data-art="daisy"] { --art: url("page-art-daisy.png"); aspect-ratio: 620 / 790; }
.page-art[data-art="lavender"] { --art: url("page-art-lavender.png"); aspect-ratio: 240 / 824; }
.page-art[data-art="eucalyptus"] { --art: url("page-art-eucalyptus.png"); aspect-ratio: 678 / 790; }
.page-art[data-art="curlfern"] { --art: url("page-art-curlfern.png"); aspect-ratio: 594 / 764; }
.page-art[data-art="chamomile"] { --art: url("page-art-chamomile.png"); aspect-ratio: 422 / 732; }
.page-art[data-art="figureback"] { --art: url("page-art-figureback.png"); aspect-ratio: 1106 / 1373; }
.page-art[data-art="figuresprig"] { --art: url("page-art-figuresprig.png"); aspect-ratio: 991 / 1375; }
.page-art ~ main { position: relative; }
@media (max-width: 900px) {
  .page-art[data-corner] { height: 36vh; right: -12vw; bottom: var(--tab-h, 0px); top: auto; }
  .page-art[data-corner="tr"] { right: auto; left: -12vw; }
  .page-art[data-corner="side"] { display: none; }
}

/* =========================================================================
   PHONE APP CHROME (mobile-app-redesign-2026-10-05.md)
   renderHeader() always emits both chromes; the desktop top bar and rail hide
   on a phone and the title bar and tab bar hide everywhere else. The phone is
   defined once, in mobileLogic.js PHONE_QUERY (mobileLogic.test.ts keeps this
   string equal to it): narrow, or a landscape iPhone (coarse and short).
   Tokens only, so both themes follow; no new colours.
   ========================================================================= */

/* ---- components, used on the phone hubs and Today ---- */
.phone-bar, .tabbar { display: none; }
.calendar-full-toggle { display: none; }
.chat-clients-card { display: none; }
.today-main, .hub-main { padding-top: var(--space-md); padding-bottom: var(--space-xxl); }
.phone-date { margin: 0 0 var(--space-md); font-size: 14px; line-height: 1.4; color: var(--dark-brown); }

/* Loading blocks sized like what replaces them, so nothing jumps. */
.skel { background: var(--beige); border-radius: var(--radius-xl); opacity: 0.6; }
.today-skel-cycle { height: 121px; }
.today-skel-checkin { height: 139px; }
.hub-skel { height: 180px; }
.me-skel { height: 240px; }
@media (prefers-reduced-motion: no-preference) {
  .skel { animation: skel-pulse 1.2s ease-in-out infinite; }
}
@keyframes skel-pulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 0.8; } }

/* Today */
.today-stack { display: flex; flex-direction: column; gap: var(--space-md); }
.today-stack > :empty { display: none; }
.today-strips { display: flex; flex-direction: column; gap: var(--space-sm); }
.strip { display: flex; align-items: center; gap: var(--space-md); min-height: 64px; padding: var(--space-sm) var(--space-md); background: var(--site-white); background-image: var(--sheen); border: var(--edge); border-radius: var(--radius-xl); box-shadow: var(--lift); color: var(--dark-brown); }
.strip-main { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: var(--space-sm); }
.strip-title { font-size: 16px; font-weight: 600; line-height: 1.35; }
/* Progress is a data fill: flat, never the sheen. */
.strip-bar { display: block; height: 6px; border-radius: var(--radius-pill); background: var(--additional-brown); overflow: hidden; }
.strip-bar-fill { display: block; height: 100%; background: var(--green); }
.strip-chev, .row-chev { color: var(--dark-brown); }

.pcard.today-card { display: flex; flex-direction: column; gap: var(--space-md); border-radius: var(--radius-xl); }
.pcard.today-cycle, .pcard.today-first { background: var(--beige); background-image: none; }
.today-row { display: flex; align-items: center; gap: var(--space-md); }
.today-text { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: var(--space-sm); }
.today-text > *, .today-card > p, .today-card > h2 { margin: 0; }
.today-text .kit-eyebrow { margin: 0; }
.today-day, .today-h2 { font-family: var(--font-heading); font-weight: 400; font-size: 20px; line-height: 1.2; color: var(--dark-brown); }
.today-h2 em { font-style: italic; }
.today-phase { display: flex; align-items: center; gap: var(--space-sm); font-size: 15px; font-weight: 700; line-height: 1.3; }
/* The phase colour is a data fill, flat. */
.today-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.today-sub { font-size: 14px; line-height: 1.4; color: var(--dark-brown); }
.today-link { display: inline-flex; align-items: center; align-self: flex-start; min-height: 44px; padding: 0; background: none; border: 0; font: inherit; font-size: 14px; font-weight: 600; color: var(--dark-brown); text-decoration: underline; cursor: pointer; }
.today-drawing { flex: 0 0 88px; width: 88px; aspect-ratio: 1000 / 705; color: var(--dark-brown); background-color: currentColor; -webkit-mask: url("symptom-kit-drawing.png") center / contain no-repeat; mask: url("symptom-kit-drawing.png") center / contain no-repeat; }
@media (max-width: 339px) { .today-drawing { display: none; } }

/* Hub rows: one tappable row = the whole link. Cards hold rows edge to edge. */
.pcard.row-card { padding: 0; overflow: hidden; border-radius: var(--radius-xl); }
.row-card-heading { margin: 0 0 var(--space-sm); padding: var(--space-md) var(--space-md) 0; }
.rows { display: flex; flex-direction: column; }
.row { position: relative; display: flex; align-items: center; gap: var(--space-md); width: 100%; min-height: 56px; padding: var(--space-sm) var(--space-md); background: none; border: 0; font: inherit; text-align: left; color: var(--dark-brown); cursor: pointer; }
.row:active { background: var(--additional-brown); }
.row:focus-visible, .strip:focus-visible, .tab:focus-visible, .phone-back:focus-visible { outline: 2px solid var(--dark-brown); outline-offset: -2px; }
.rows > .row + .row::before { content: ""; position: absolute; top: 0; left: var(--space-md); right: 0; height: 1px; background: var(--beige); }
.row-main { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.row-title { font-size: 16px; font-weight: 600; line-height: 1.35; }
.row-sub { font-size: 14px; line-height: 1.35; }
.row--static { cursor: default; }
.row--static:active { background: none; }
.row-control { margin-top: var(--space-sm); }
.row-control .push-note, .row-control .push-on, .row-control .push-recommend { margin-bottom: var(--space-sm); font-size: 14px; }
.row-control .push-error { margin-top: var(--space-sm); font-size: 14px; }
.row .theme-seg button { width: 44px; height: 44px; }
.me-head { display: flex; align-items: center; gap: var(--space-md); }
.me-avatar { position: relative; flex: 0 0 72px; width: 72px; height: 72px; border-radius: 50%; background: var(--beige); background-image: var(--sheen); display: grid; place-items: center; font-family: var(--font-heading); font-size: 28px; color: var(--dark-brown); cursor: pointer; }
.me-who { min-width: 0; }
.me-email { margin: 0 0 var(--space-sm); font-size: 16px; font-weight: 600; line-height: 1.35; overflow-wrap: anywhere; }

/* ---- the phone itself ---- */
@media (max-width: 640px), (pointer: coarse) and (max-height: 500px) {
  :root { --tab-h: calc(49px + env(safe-area-inset-bottom)); }
  html.tab-hidden { --tab-h: 0px; }
  /* AI chat (aiChat.js lockToVisualViewport): the document is pinned and the body is the visible
     area above the keyboard, so nothing scrolls under it. The fine print would sit between the
     composer and the keyboard, so it hides while a field is focused. */
  html.kbd-lock { position: fixed; inset: 0; overflow: hidden; }
  html.kbd-lock body { box-sizing: border-box; height: var(--vvh, 100%); min-height: 0; overflow: hidden; transform: translateY(var(--vv-top, 0px)); }
  html.kbd-lock.tab-hidden body { padding-bottom: 0; }
  html.tab-hidden .ai-chat-attach-hint { display: none; }

  /* Desktop chrome off, phone chrome on. Scoped to body.phone-chrome (set by mobile.js
     only when it emitted the bars), so the staff call header keeps what it has. */
  body.phone-chrome .client-header-slot .practitioner-topbar,
  body.phone-chrome #client-rail { display: none; }
  /* Signed out (or on a sign-in view) the staff bars are not shown; client pages always carry phone-chrome. */
  body:not(.phone-chrome) .phone-bar,
  body:not(.phone-chrome) .tabbar { display: none; }
  /* Client hub tab strip: one row, scrolls sideways, 44px targets. */
  .hub-tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
  .hub-tabs::-webkit-scrollbar { display: none; }
  .hub-tab { flex: 0 0 auto; display: inline-flex; align-items: center; min-height: 44px; }
  /* Staff: the stamped bar and tab bar replace the top bar, rail and every link in them
     (Me carries the account menu's items). The rail stays for 641 to 1040px. */
  body.phone-chrome #practitioner-header,
  body.phone-chrome #practitioner-rail { display: none; }
  .phone-back[hidden] { display: none; }
  .tab-badge.is-dot { min-width: 10px; width: 10px; height: 10px; padding: 0; background: var(--warn); }
  /* Today: Needs you first, then today's sessions; the month grid sits behind "Full calendar". */
  #schedule-body #schedule-assistant-section { order: 3; }
  #schedule-body #needs-you-section { order: 1; }
  #schedule-body #schedule-section { order: 2; }
  #schedule-section:not(.is-full) .calendar-nav,
  #schedule-section:not(.is-full) .calendar-grid { display: none; }
  /* With the nav and grid hidden the day panel is the first child; its desktop top margin becomes a gap. */
  #schedule-section:not(.is-full) .calendar-day-panel { margin-top: 0; }
  .calendar-full-toggle { display: inline-flex; margin-top: var(--space-md); }
  .chat-clients-card:not([hidden]) { display: block; }
  /* In-shell tap targets */
  .row--static:has(> #me-push:empty) { display: none; }

  /* In the iOS app the rail and its group chevrons never show on a phone, whatever the chrome state (a stray collapsed chevron was seen bottom-left). */
  html.native #client-rail, html.native #client-rail .rail-group-toggle { display: none; }
  body.phone-chrome .client-header-slot { position: sticky; top: 0; z-index: 42; background: var(--site-white); }
  html.native body.phone-chrome .client-header-slot { top: env(safe-area-inset-top); }

  .phone-bar { display: grid; grid-template-columns: 44px minmax(0, 1fr) 44px; align-items: center; height: 44px; padding: 0 var(--space-xs); background: var(--site-white); border-bottom: 1px solid var(--beige); }
  .phone-back { grid-column: 1; grid-row: 1; display: grid; place-items: center; width: 44px; height: 44px; color: var(--dark-brown); }
  .phone-title { grid-column: 2; grid-row: 1; padding: 0 var(--space-xs); font-size: 17px; font-weight: 600; line-height: 1.2; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--dark-brown); opacity: 0; transition: opacity 120ms ease-out; }
  .phone-title.is-shown { opacity: 1; }
  .phone-bell-slot { grid-column: 3; grid-row: 1; display: flex; justify-content: flex-end; }
  /* The staff timer sits left of the bell; its column grows with it (idle is one more 44px target). */
  .phone-bar:has(.pomo-slot) { grid-template-columns: 44px minmax(0, 1fr) auto; }
  .phone-back-icon, .tab-svg { width: 24px; height: 24px; stroke: currentColor; fill: none; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }

  .tabbar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 41; display: flex; height: var(--tab-h); padding-bottom: env(safe-area-inset-bottom); background: var(--site-white); border-top: var(--edge); }
  html.tab-hidden .tabbar { display: none; }
  .tab { flex: 1 1 0; min-width: 44px; min-height: 44px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; padding-top: 3px; color: var(--dark-brown); font-size: 11px; font-weight: 600; line-height: 1.1; }
  .tab.is-active { font-weight: 700; }
  .tab-icon { position: relative; display: grid; place-items: center; width: 48px; height: 28px; border-radius: var(--radius-pill); }
  .tab.is-active .tab-icon { background: var(--select-tint); }
  .tab-label { max-width: 100%; padding: 0 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .tab-badge { position: absolute; top: -4px; right: 0; min-width: 18px; height: 18px; padding: 0 5px; border-radius: var(--radius-pill); background: var(--badge-bg); color: var(--site-white); font-size: 12px; font-weight: 700; line-height: 1; display: inline-flex; align-items: center; justify-content: center; }
  .tab-badge[hidden] { display: none; }

  /* The app's own safe-area padding is cancelled at the bottom: the bar and main carry it. */
  html.native body.has-tabbar { padding-bottom: 0; }
  body.phone-chrome .shell { padding-inline: var(--space-md); }
  body.phone-chrome main { padding-top: var(--space-md); }
  body.has-tabbar main { padding-bottom: calc(var(--tab-h) + var(--space-md)); }

  /* The old "Back to your library" links: the title bar's chevron replaces them. */
  body.phone-chrome .back-link { display: none; }
  /* A page title that repeats the bar title stays for screen readers only. */
  .phone-title-dup { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

  /* Type, spacing and component pass (every client page, no per-page work). */
  body.phone-chrome main h1:not(.kit-title) { font-size: 30px; line-height: 1.15; margin: var(--space-sm) 0 var(--space-md); }
  body.phone-chrome main h1:not(.kit-title):first-child { margin-top: 0; }
  body.phone-chrome .today-main h1:not(.kit-title) { margin-bottom: var(--space-sm); }
  body.phone-chrome .lede { font-size: 16px; }
  body.phone-chrome .pcard { border-radius: var(--radius-xl); }
  body.phone-chrome .btn:not(.btn-inline) { min-height: 48px; padding-block: var(--space-sm); }

  /* A tab tap is a full page load. Where the browser supports cross-document view transitions
     (iOS 18.2+) the old page stays on screen until the new one is ready and the two cross-fade,
     with the tab bar held still; elsewhere navigation is instant. */
  @view-transition { navigation: auto; }
  .tabbar { view-transition-name: tabbar; }
  ::view-transition-group(root) { animation-duration: 160ms; }

  /* Native-only touches: chrome is not text. */
  html.native .phone-bar, html.native .tabbar { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; user-select: none; }
}
@media (prefers-reduced-motion: reduce) {
  .phone-title { transition: none; }
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none; }
}

/* Desktop-only items (e.g. the printable Symptom Mapping Kit: the phone has the tracker). Shared phone query. */
@media (max-width: 640px), (pointer: coarse) and (max-height: 500px) { .hide-on-phone { display: none !important; } }

/* The Manage in Privacy link on a tool whose health tracking is off must read as a link. */
.off-link { font-weight: 600; text-decoration: underline; }

/* =========================================================================
   Coaching tools (core-values.html, celebrate.html) and the in-app PDF viewer (document-view.html).
   Tokens only, so dark mode follows. Selected = the same green fill the food-mood toggles use.
   ========================================================================= */
.core-values-main, .celebrate-main, .doc-view-main { padding-top: var(--space-lg); padding-bottom: var(--space-xxl); }
.core-values-main .lede, .celebrate-main .lede { margin-bottom: var(--space-lg); }
.core-values-main .banner { margin-bottom: var(--space-lg); }
.cv-progress { margin: 0 0 var(--space-xs); font-size: 14px; font-weight: 700; color: var(--dark-brown); }
.cv-values { display: grid; grid-template-columns: 1fr; gap: var(--space-sm); margin: var(--space-md) 0 var(--space-lg); }
@media (min-width: 900px) { .cv-values { grid-template-columns: 1fr 1fr; } }
.cv-value {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-xs); min-height: 56px; padding: 10px 16px; text-align: left;
  font: inherit; color: var(--dark-brown); cursor: pointer; border-radius: var(--radius-md);
  border: 1.5px solid var(--light-brown); background: var(--site-white); background-image: var(--sheen);
}
.cv-value-name { font-weight: 700; }
.cv-value-def { font-size: 14px; }
.cv-value[aria-pressed="true"] { border-color: var(--green); background: var(--green-action); background-image: var(--sheen-midtone); color: var(--site-white); }
.cv-value:focus-visible { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
.cv-values--own { margin-top: var(--space-sm); }
.cv-add { display: flex; gap: var(--space-sm); align-items: center; margin-bottom: var(--space-sm); }
.cv-add .text-input { flex: 1; min-width: 0; }
.cv-hint, .cv-count, .cv-status { margin: var(--space-sm) 0 0; font-size: 14px; color: var(--dark-brown); min-height: 1.4em; }
.cv-why > summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; font-weight: 700; }
.cv-why[open] > summary { margin-bottom: var(--space-sm); }
.cv-why-list { margin: 0 0 var(--space-md); padding-left: var(--space-lg); display: flex; flex-direction: column; gap: var(--space-sm); }
.cv-top5-card { padding: var(--space-md); margin-bottom: var(--space-lg); background: var(--beige); border: var(--edge); border-radius: var(--radius-md); }
.cv-top5 { margin: 0 0 var(--space-md); padding-left: var(--space-lg); font-size: 18px; font-weight: 700; }
.cv-top5 li + li { margin-top: var(--space-sm); }
.cv-questions { display: flex; flex-direction: column; gap: var(--space-sm); margin-bottom: var(--space-lg); }
.cv-q { border: var(--edge); border-radius: var(--radius-md); background: var(--site-white); padding: 0 var(--space-md); }
.cv-q > summary { min-height: 56px; padding: var(--space-sm) 0; display: flex; flex-direction: column; justify-content: center; gap: var(--space-xs); cursor: pointer; }
.cv-q-label { font-weight: 700; }
.cv-q-prompt { font-size: 14px; }
.cv-q[open] { padding-bottom: var(--space-md); }
.cv-q .text-input { width: 100%; resize: vertical; margin-top: var(--space-sm); }
.cel-list { list-style: none; margin: var(--space-md) 0 0; padding: 0; display: grid; grid-template-columns: 1fr; gap: 0 var(--space-xl); }
@media (min-width: 900px) { .cel-list--all { grid-template-columns: 1fr 1fr; } }
.cel-row { display: flex; align-items: center; gap: var(--space-md); min-height: 56px; padding: var(--space-sm) 0; border-bottom: 1px solid var(--light-brown); }
.cel-num { flex: 0 0 auto; min-width: 2ch; font-size: 14px; font-weight: 700; color: var(--green); }
.cel-text { flex: 1; min-width: 0; font-size: 16px; }
.cel-pick { display: flex; align-items: center; gap: var(--space-md); padding: var(--space-md); background: var(--beige); border: var(--edge); border-radius: var(--radius-md); }
.cel-pick-text { flex: 1; min-width: 0; margin: 0; font-size: 20px; font-weight: 700; }
.cel-empty { margin: 0; }
.doc-page { display: block; width: 100%; height: auto; border: var(--edge); border-radius: var(--radius-sm); background: #fff; box-shadow: var(--lift); }
.cv-why > summary { justify-content: space-between; gap: var(--space-md); list-style: none; }
.cv-why > summary::-webkit-details-marker { display: none; }
.cv-why > summary::after { content: "Read"; font-size: 14px; font-weight: 600; text-decoration: underline; }
.cv-why[open] > summary::after { content: "Hide"; }
.cv-top5 li::marker { color: var(--green); }

/* Symptom assessment (naq.html). Tokens only, so dark mode follows. One scrolling form: a card per
   section, a fieldset per question, tappable option rows (.option-row, 44px+) two to a row on a phone. */
.naq-main { padding-top: var(--space-lg); padding-bottom: var(--space-xxl); }
.naq-main .lede, .naq-main .body-copy { margin-bottom: var(--space-lg); }
.naq-main .banner { margin-bottom: var(--space-lg); }
.naq-section > h2 { margin-bottom: var(--space-md); }
.naq-q { min-width: 0; margin: 0 0 var(--space-md); }
.naq-q > legend { padding: 0; float: left; width: 100%; margin-bottom: var(--space-sm); }
.naq-q > legend + * { clear: both; }
.naq-opts { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-sm); }
.naq-opts--two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 640px) { .naq-opts { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 900px) { .naq-opts:not(.naq-opts--two) { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.naq-opts .option-row { margin-bottom: 0; min-width: 0; }
.naq-opts .option-row > span { flex: 1; min-width: 0; }
.naq-how { margin-top: var(--space-md); }
.naq-how-label { margin: 0 0 var(--space-sm); font-size: 15px; font-weight: 600; }
.naq-block { margin: 0 0 var(--space-md); padding-left: var(--space-md); border-left: 3px solid var(--light-brown); }
.naq-text { display: block; width: 100%; box-sizing: border-box; min-height: 96px; padding: var(--space-sm) var(--space-md); background: var(--site-white); border: 1.5px solid var(--dark-brown); border-radius: var(--radius-md); font-family: var(--font-body); font-size: 16px; color: var(--dark-brown); resize: vertical; }
.naq-submit .body-copy { margin-bottom: var(--space-md); }
#naq-thanks:focus { outline: none; }
.naq-safety p { margin: 0 0 var(--space-sm); }
.naq-safety p:last-child { margin-bottom: 0; }

/* =========================================================================
   N-of-1 TRIAL ENGINE (n1-trial-engine-spec.md §4) -- symptom-mapping.html
   modes. Reuses the existing tokens throughout: the adherence buttons reuse
   .severity-btn's four-step ramp (--sev-none #efdece / --sev-mild #848241 /
   --sev-moderate #eab308 / --sev-severe #8b1a1a) so trial chrome never
   invents a new data color; bars use --green (the portal's "active/positive"
   hue) for her trial cycles and --light-brown for the baseline reference —
   no new hex values anywhere in this block.
   ========================================================================= */
.n1-variable-list { display: flex; flex-direction: column; gap: var(--space-sm); }
.n1-variable-card { display: flex; flex-direction: column; gap: var(--space-sm); min-height: 44px; padding: var(--space-sm) var(--space-md); background: var(--beige); border: var(--edge); border-radius: var(--radius-md); cursor: pointer; }
.n1-variable-card.is-selected { border-color: var(--green); box-shadow: 0 0 0 2px var(--green) inset; }
.n1-variable-card:has(input:focus-visible) { outline: 2px solid var(--dark-brown); outline-offset: 2px; }
.n1-variable-card input { position: absolute; opacity: 0; pointer-events: none; }
.n1-variable-label { font-weight: 700; }
.n1-variable-note { font-size: 13px; color: var(--dark-brown); }
.n1-variable-kind { font-size: 12px; font-weight: 600; color: var(--mid-brown); }
.n1-target-list { display: flex; flex-direction: column; gap: var(--space-sm); }
.n1-rules-panel { padding: var(--space-md); background: var(--beige); border: var(--edge); border-radius: var(--radius-md); }
.n1-adherence-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-sm); }
.n1-adherence-label { font-weight: 700; color: var(--dark-brown); }
.n1-adherence-btn { min-width: 96px; }
.n1-adherence-error-slot { flex-basis: 100%; }
.n1-bar-row { display: flex; align-items: center; gap: var(--space-md); padding: var(--space-xs) 0; }
.n1-bar-label { flex: 0 0 190px; font-size: 13px; color: var(--dark-brown); }
.n1-bar-track { flex: 1; min-width: 0; height: 18px; background: var(--beige); border-radius: var(--radius-pill); overflow: hidden; }
.n1-bar-fill { height: 100%; background: var(--green); border-radius: var(--radius-pill); }
.n1-bar-fill--baseline { background: var(--light-brown); }
.n1-bar-value { flex: 0 0 48px; text-align: right; font-variant-numeric: tabular-nums; font-size: 14px; color: var(--dark-brown); }
.n1-history-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-sm) var(--space-md); width: 100%; min-height: 44px; margin: 0; padding: var(--space-md) 0; border: 0; border-bottom: 1px solid var(--light-brown); background: none; cursor: pointer; text-align: left; font: inherit; color: inherit; }
.n1-history-row + .n1-history-row { margin-top: var(--space-sm); }
.n1-history-label { font-weight: 700; flex-basis: 100%; }
.n1-history-dates { flex: 1 1 120px; font-size: 13px; color: var(--dark-brown); }
.n1-history-status { flex: 0 1 auto; margin-left: var(--space-md); font-size: 13px; font-weight: 600; color: var(--mid-brown); }
.n1-end-link { max-width: 100%; white-space: normal; text-align: left; }
.n1-staff-actions { margin-top: var(--space-md); }
.n1-staff-actions .btn { min-height: 44px; }
.n1-staff-notes { display: flex; flex-direction: column; gap: var(--space-sm); margin-top: var(--space-md); }
@media (max-width: 640px), (pointer: coarse) and (max-height: 500px) { #hub-summary .pgrid > [data-card="n1-trial-request"] { order: 1; } }
@media (max-width: 480px) {
  .n1-bar-label { flex-basis: 100%; }
  .n1-history-status { flex-basis: 100%; margin-left: 0; }
}

/* Symptom Assessment Review (naq-review.html): a printable document, 3 to 4 Letter pages. Tokens only.
   Bars are data fills: flat, no sheen. */
.rv-main { max-width: 896px; margin: 0 auto; padding: var(--space-lg) var(--space-md) var(--space-xxl); }
.rv-actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-bottom: var(--space-lg); }
.rv-doc { color: var(--dark-brown); font-size: 16px; line-height: 1.5; }
.rv-header { margin-bottom: var(--space-lg); }
.rv-header h1 { margin: 0 0 var(--space-sm); font-size: 32px; }
.rv-meta { margin: 0; font-size: 15px; }
.rv-note { margin: 0 0 var(--space-lg); padding: var(--space-sm) var(--space-md); border: 1.5px solid var(--warn); border-radius: var(--radius-md); font-size: 15px; }
.rv-section { margin: 0 0 var(--space-lg); break-inside: avoid; }
.rv-section h2 { margin: 0 0 var(--space-sm); font-size: 22px; }
.rv-section h3 { margin: var(--space-md) 0 var(--space-sm); font-size: 17px; }
.rv-section p { margin: 0 0 var(--space-sm); }
.rv-fixed { font-size: 15px; }
.rv-counts { font-weight: 700; }
.rv-chart { display: block; width: 100%; height: auto; max-width: 620px; }
.rv-svg-label, .rv-svg-pct { font-family: var(--font-body); font-size: 14px; fill: var(--dark-brown); }
.rv-svg-track { fill: var(--beige); }
.rv-svg-bar--high { fill: var(--warn); }
.rv-svg-bar--med { fill: var(--sev-moderate); }
.rv-svg-bar--low { fill: var(--green); }
.rv-legend { display: flex; flex-wrap: wrap; gap: var(--space-md); list-style: none; margin: var(--space-sm) 0 0; padding: 0; font-size: 14px; }
.rv-legend li { display: flex; align-items: center; gap: var(--space-xs); }
.rv-swatch { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
.rv-swatch--high { background: var(--warn); }
.rv-swatch--med { background: var(--sev-moderate); }
.rv-swatch--low { background: var(--green); }
.rv-list, .rv-others { margin: 0 0 var(--space-sm); padding-left: var(--space-lg); }
.rv-list li + li { margin-top: var(--space-sm); }
.rv-others { list-style: none; padding-left: 0; }
.rv-others li { margin: 0 0 var(--space-sm); }
.rv-tag { font-size: 13px; font-weight: 600; }
.rv-options { display: grid; grid-template-columns: 1fr; gap: var(--space-md); }
@media (min-width: 640px) { .rv-options { grid-template-columns: 1fr 1fr; } }
.rv-option { padding: var(--space-md); border: 1.5px solid var(--light-brown); border-radius: var(--radius-md); }
.rv-option h3 { margin-top: 0; }
.rv-meds { font-size: 15px; }
.rv-cta { font-weight: 700; }
.rv-footer { margin-top: var(--space-lg); padding-top: var(--space-md); border-top: 1px solid var(--light-brown); font-size: 13px; }
@media print {
  @page { size: Letter; margin: 0.6in; }
  html, html[data-theme="dark"], body { background: transparent !important; }
  .rv-actions, .rv-screen-only { display: none; }
  .rv-main { max-width: none; padding: 0; }
  .rv-doc { font-size: 12pt; }
  .rv-header h1 { font-size: 24pt; }
}

/* Content page (content.js, contentCards.js). Cards stack their lines with a gap of their own;
   Taylor's titles can be long and unbroken, so they wrap anywhere. */
.content-card { display: flex; flex-direction: column; gap: var(--space-sm); }
.content-card .pcard-head { margin-bottom: 0; }
.content-card h2, .content-card .body-copy { overflow-wrap: anywhere; }
.content-card .body-copy { margin: 0; }
.content-retry { margin-left: var(--space-sm); }
/* Review and failed cards: slides on the left, the two editable fields on the right; stacked on a phone. */
.content-review { display: grid; grid-template-columns: 1fr; gap: var(--space-lg); }
@media (min-width: 900px) { .content-review { grid-template-columns: minmax(180px, 1fr) minmax(0, 2fr); } }
.content-thumbs { display: flex; flex-wrap: wrap; align-content: flex-start; gap: var(--space-sm); min-width: 0; }
.content-thumb { padding: 0; border: 1.5px solid var(--light-brown); border-radius: var(--radius-md); background: var(--site-white); cursor: zoom-in; width: 88px; overflow: hidden; }
.content-thumb img { display: block; width: 100%; height: auto; }
.content-thumb.is-large { width: 100%; cursor: zoom-out; }
.content-thumbs-skeleton { width: 100%; height: 120px; }
.content-canva-link { flex-basis: 100%; font-weight: 600; text-decoration: underline; color: inherit; }
.content-fields { display: flex; flex-direction: column; gap: var(--space-md); min-width: 0; }
.content-field { display: flex; flex-direction: column; gap: var(--space-xs); }
.content-field textarea { width: 100%; box-sizing: border-box; min-height: 160px; }
.content-chips { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.content-chips:empty { display: none; }
.content-chip { padding: 6px 12px; border-radius: var(--radius-md); border: 1.5px solid var(--light-brown); background: var(--additional-brown); font-size: 14px; line-height: 1.4; overflow-wrap: anywhere; }
.content-chip--hard { border-color: var(--warn); }
.content-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md); }
.content-status { font-size: 14px; color: var(--dark-brown); display: inline-flex; align-items: center; gap: var(--space-sm); }
.content-failed { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-md); }
.content-skeleton-lines { display: flex; flex-direction: column; gap: var(--space-sm); }
.content-skeleton-line { height: 18px; }
.content-dm-host { min-width: 0; }
.content-dm-host:empty { display: none; }
.content-dm { display: flex; flex-direction: column; gap: var(--space-sm); margin: 0; padding: var(--space-md); min-width: 0; border: 1.5px solid var(--light-brown); border-radius: var(--radius-md); }
.content-dm legend { padding: 0 var(--space-xs); }
.content-dm-option { display: flex; align-items: center; gap: var(--space-sm); min-height: 44px; cursor: pointer; }
.content-dm-option input { width: 20px; height: 20px; flex: none; }

/* /buy email step (buy.html). One field, one button; the note and problem lines use the warn colour. */
/* One calm column, centred; the HMM line drawing (.onboard-art) sits behind it and main is lifted over it. */
.buy-main { position: relative; z-index: 1; max-width: 560px; padding-top: var(--space-xxl); }
.buy-main h1 { margin-bottom: var(--space-sm); }
.buy-form, .buy-suggest, .buy-signin { margin-top: var(--space-lg); }
.buy-signin .lede + .body-copy { margin: var(--space-sm) 0 var(--space-md); }
.buy-form > .body-copy { margin: 0 0 var(--space-md); }
.buy-form .field { margin: var(--space-lg) 0; }
.buy-form .btn, .buy-actions .btn { min-height: 48px; }
.buy-note { margin: var(--space-sm) 0 0; font-size: 14px; color: var(--warn); }
.buy-problem { margin-top: var(--space-md); font-size: 16px; }
.buy-problem + .body-copy { margin-top: var(--space-sm); }
.buy-actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-top: var(--space-md); }

/* ---------- Extra access card on the Team tab (staffGrants.js) ---------- */
.sg-lede, .sg-note { margin: 0 0 var(--space-md); font-size: 15px; color: var(--dark-brown); }
.sg-people, .sg-grants { list-style: none; margin: 0 0 var(--space-md); padding: 0; display: flex; flex-direction: column; gap: var(--space-md); }
.sg-grants { margin: 0; gap: var(--space-sm); width: 100%; }
.sg-person { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-sm); }
.sg-person > h3 { margin: 0; }
.sg-grant { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-sm); width: 100%; }
.sg-grant-main { display: flex; flex-direction: column; gap: var(--space-sm); }
.sg-grant-label { font-size: 15px; font-weight: 600; color: var(--dark-brown); }
.sg-ends, .sg-grant-note, .sg-reach { margin: 0; font-size: 14px; color: var(--dark-brown); }
/* Same warning treatment as .push-error: a --warn edge, dark text (--warn text on white is 4.49:1, under AA). */
.sg-ends--soon { padding-left: var(--space-sm); border-left: 3px solid var(--warn); font-weight: 600; }
.sg-grant-note { flex-basis: 100%; }
.sg-error { margin: 0; padding-left: var(--space-sm); border-left: 3px solid var(--warn); font-size: 14px; color: var(--dark-brown); }
.sg-form { display: flex; flex-direction: column; gap: var(--space-sm); width: 100%; }
.sg-reach { margin-top: var(--space-sm); }

/* Page header band (Home, Resources, Sessions): solid panel with the title and one
   line on the left, an HMM photo inset on the right. Text never sits on the photo.
   Phones get a short strip above the text. */
.page-band { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 42%); gap: var(--space-lg); padding: var(--space-md); background: var(--site-white); background-image: var(--sheen); border: var(--edge); border-radius: var(--radius-md); box-shadow: var(--lift); margin-bottom: var(--space-lg); }
.page-band-text { padding: var(--space-sm) var(--space-sm) var(--space-sm) var(--space-md); align-self: center; min-width: 0; }
.page-band-text > :first-child { margin-top: 0; }
.page-band-text > :last-child { margin-bottom: 0; }
.page-band .band-line { margin: var(--space-sm) 0 0; font-family: var(--font-body); font-size: 17px; color: var(--dark-brown); }
.page-band-img { width: 100%; height: 100%; min-height: 168px; background: var(--additional-brown) center / cover no-repeat; border-radius: var(--radius-sm); }
.page-band-img[data-photo="home"] { background-image: url("band-home.webp"); }
.page-band-img[data-photo="resources"] { background-image: url("band-resources.webp"); }
.page-band-img[data-photo="sessions"] { background-image: url("band-sessions.webp"); }
:root[data-theme="dark"] .page-band-img, :root[data-theme="dark"] .section-strip { filter: brightness(0.78) saturate(0.9); }
@media (max-width: 899px) {
  .page-band { grid-template-columns: minmax(0, 1fr); gap: var(--space-md); }
  .page-band-img { order: -1; min-height: 0; height: 84px; }
  .page-band-text { padding: 0 var(--space-xs); }
}
/* Photo strip across the top of a Home section card. */
.section-strip { display: block; width: 100%; height: 92px; margin: 0 0 var(--space-md); object-fit: cover; border-radius: var(--radius-sm); }
@media (max-width: 899px) { .section-strip { height: 64px; margin-bottom: var(--space-sm); } }
/* Bigger tool art on the My tools and Resources tiles. */
.card-grid .your-tools .icon-chip--art, .your-tools .card-grid .resource-card .icon-chip--art, .free-resources .card-grid .resource-card .icon-chip--art { width: 64px; height: 64px; }
/* Empty Learn card: a botanical beside the line. */
.empty-row { display: flex; align-items: center; gap: var(--space-lg); }
.empty-row .body-copy { margin: 0; }
.empty-art { flex: 0 0 auto; width: 72px; height: 110px; background: var(--dark-brown); opacity: 0.55; -webkit-mask: url("page-art-eucalyptus.png") center / contain no-repeat; mask: url("page-art-eucalyptus.png") center / contain no-repeat; }
