/* ============================================================
   SOTERIA SANCTUARY — DESIGN FOUNDATION (Phase 1)
   ABA Soteria · "Everything ABA. Safely connected."

   WHAT THIS FILE IS
   -----------------
   The centralized token and component layer for the Soteria
   Sanctuary design system. It is a FOUNDATION ONLY: no production
   page links it yet, and linking it changes nothing on its own.

   THREE RULES MAKE THAT SAFE, AND EACH IS PINNED BY
   tests/soteria-sanctuary.test.js:

   1. EVERY custom property is prefixed `--ss-`. Not one name in
      this file collides with a token in style.css, so loading both
      cannot silently repaint a production page.
   2. EVERY selector is class-based and prefixed `.ss-`. There is
      no bare element selector anywhere below — no `body`, no `a`,
      no `table`. Nothing here can reach markup that has not opted
      in by class name.
   3. NO raw hex or px value appears outside the :root block. Every
      rule reads a token. Change a value once, here, and the whole
      system moves together.

   WHAT IT IS NOT
   --------------
   It is not a replacement for style.css and does not modify it.
   Phase 2 — mapping production pages onto these tokens — is a
   separate, approved step that has not been taken.

   COLOR HAS TWO SEPARATE JOBS IN THIS PRODUCT, AND THEY MUST NOT
   BLEED INTO EACH OTHER:

   - BRAND + STATUS colors (purple, sage, teal, amber, calm red)
     describe the INTERFACE: what is primary, what succeeded, what
     needs attention, what failed.
   - CLINICAL RESPONSE colors (independent / prompted / incorrect)
     describe a CHILD'S BEHAVIOR during a session. They are
     measurement, not decoration, and they are preserved here at
     their existing production values, unchanged.

   Using a clinical color for a banner, or a status color for a
   trial mark, breaks a meaning that clinicians read at a glance.
   See docs/design-system/soteria-sanctuary-v1.md.
   ============================================================ */

:root {

    /* ========================================================
       1. PALETTE — the eight approved Soteria Sanctuary colors,
       exactly as recorded on the visual board. These are raw
       values. Components never reference them directly; they
       reference the semantic roles in section 2.
       ======================================================== */

    --ss-purple: #4A4378;      /* brand, navigation, primary actions */
    --ss-sage: #5F8C78;        /* progress, success, reinforcement   */
    --ss-teal: #3E8583;        /* communication and supervision      */
    --ss-ivory: #FAF8F4;       /* primary background                 */
    --ss-lavender: #EFEDF6;    /* selected / supporting surfaces     */
    --ss-amber: #C48632;       /* attention                          */
    --ss-calm-red: #B8544A;    /* genuine errors                     */
    --ss-ink: #242333;         /* text                               */

    /* ---- Text-safe partners -------------------------------
       MEASURED, not guessed. As TEXT on ivory the board colors
       read: sage 3.59:1, teal 4.05:1, amber 2.91:1, calm red
       4.49:1 — all below the 4.5:1 minimum for body text. Each
       therefore gets a `-deep` partner used for TEXT, while the
       board color itself is used for FILLS, BARS and MARKS.
       This is the same base/deep split style.css already uses
       for the clinical response colors.

       Measured on ivory: sage-deep 5.85, teal-deep 5.95,
       amber-deep 5.84, red-deep 6.40, purple-deep 10.56. */

    --ss-purple-deep: #3B3563;
    --ss-sage-deep: #40695A;
    --ss-teal-deep: #2C6968;
    --ss-amber-deep: #845818;
    --ss-calm-red-deep: #9A3D34;

    /* ---- Tints — quiet backgrounds that carry deep text ----
       Each clears 4.5:1 against its own `-deep` partner
       (measured: 5.33, 5.41, 5.48, 5.82) and against ink. */

    --ss-purple-tint: #EFEDF6;   /* the board's soft lavender */
    --ss-sage-tint: #E7F0EB;
    --ss-teal-tint: #E4F0EF;
    --ss-amber-tint: #FAF0DF;
    --ss-calm-red-tint: #FAEAE7;

    /* ---- Interaction states of the brand ------------------- */

    --ss-purple-hover: #3F3966;
    --ss-purple-active: #332E52;


    /* ========================================================
       2. SEMANTIC COLOR ROLES
       Components read THESE. A component that reads a palette
       value directly has skipped the layer where meaning lives.
       ======================================================== */

    /* Surfaces */
    --ss-bg: var(--ss-ivory);
    --ss-surface: #FFFFFF;
    --ss-surface-raised: #FFFFFF;
    --ss-surface-selected: var(--ss-lavender);
    --ss-surface-sunken: #F3F0EA;
    --ss-scrim: rgba(36, 35, 51, 0.44);

    /* Text — muted measures 6.42:1 on ivory, so "quiet" never
       means "unreadable". */
    --ss-text: var(--ss-ink);
    --ss-text-muted: #5C5870;
    --ss-text-on-brand: #FFFFFF;
    --ss-text-link: var(--ss-purple);

    /* Lines */
    --ss-border: #E4E0D8;
    --ss-border-strong: #CFC9BE;
    --ss-border-brand: var(--ss-purple);

    /* Brand / primary action */
    --ss-brand: var(--ss-purple);
    --ss-brand-hover: var(--ss-purple-hover);
    --ss-brand-active: var(--ss-purple-active);
    --ss-brand-tint: var(--ss-purple-tint);
    --ss-brand-text: var(--ss-purple-deep);

    /* Status roles. Every one of these has a REQUIRED word and
       icon beside it in the markup — color is never the only
       carrier of the message. */
    --ss-success: var(--ss-sage);
    --ss-success-text: var(--ss-sage-deep);
    --ss-success-tint: var(--ss-sage-tint);

    --ss-info: var(--ss-teal);
    --ss-info-text: var(--ss-teal-deep);
    --ss-info-tint: var(--ss-teal-tint);

    --ss-attention: var(--ss-amber);
    --ss-attention-text: var(--ss-amber-deep);
    --ss-attention-tint: var(--ss-amber-tint);

    --ss-error: var(--ss-calm-red);
    --ss-error-text: var(--ss-calm-red-deep);
    --ss-error-tint: var(--ss-calm-red-tint);

    --ss-neutral: var(--ss-text-muted);
    --ss-neutral-text: var(--ss-text);
    --ss-neutral-tint: var(--ss-surface-sunken);

    /* Destructive actions stay QUIET, following the existing
       house rule: quiet placement prevents mis-taps better than
       a loud red button does. Calm red is reserved for
       reporting a genuine error, not for inviting one. */
    --ss-destructive-text: var(--ss-calm-red-deep);


    /* ========================================================
       3. CLINICAL RESPONSE COLORS — PRESERVED, NOT REDESIGNED
       These mean a child's response during a session. They are
       copied from the live style.css values so the two files
       cannot drift apart, and they are OFF LIMITS to branding,
       navigation, banners and decoration.
       ======================================================== */

    --ss-clinical-independent: #0E9F6E;
    --ss-clinical-independent-deep: #0A5F42;
    --ss-clinical-independent-tint: #E3F5EE;

    --ss-clinical-prompted: #C57604;
    --ss-clinical-prompted-deep: #8A5308;
    --ss-clinical-prompted-tint: #FBF0DE;

    --ss-clinical-incorrect: #C0392B;
    --ss-clinical-incorrect-deep: #98291D;
    --ss-clinical-incorrect-tint: #F9E9E7;


    /* ========================================================
       4. TYPOGRAPHY — the existing IBM Plex pairing and the
       existing size scale, carried forward unchanged so a page
       moved onto these tokens does not resize.
       ======================================================== */

    --ss-font-sans: 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
    --ss-font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;

    --ss-size-xs: 11px;
    --ss-size-sm: 13px;
    --ss-size-base: 15px;
    --ss-size-md: 16px;
    --ss-size-lg: 18px;
    --ss-size-xl: 22px;
    --ss-size-2xl: 34px;
    --ss-size-3xl: 40px;

    --ss-weight-regular: 400;
    --ss-weight-medium: 500;
    --ss-weight-bold: 600;

    --ss-leading-tight: 1.25;
    --ss-leading-normal: 1.6;

    /* Uppercase micro-labels get tracking; body text never does. */
    --ss-tracking-label: 0.06em;


    /* ========================================================
       5. SPACING — one scale. Pick from it; never invent.
       ======================================================== */

    --ss-space-1: 4px;
    --ss-space-2: 8px;
    --ss-space-3: 12px;
    --ss-space-4: 16px;
    --ss-space-5: 20px;
    --ss-space-6: 24px;
    --ss-space-7: 32px;
    --ss-space-8: 48px;
    --ss-space-9: 70px;


    /* ========================================================
       6. RADII
       ======================================================== */

    --ss-radius-sm: 8px;
    --ss-radius: 10px;
    --ss-radius-lg: 14px;
    --ss-radius-xl: 16px;
    --ss-radius-pill: 999px;


    /* ========================================================
       7. BORDERS
       ======================================================== */

    --ss-border-width: 1px;
    --ss-border-width-strong: 2px;
    --ss-border-line: var(--ss-border-width) solid var(--ss-border);
    --ss-border-line-strong: var(--ss-border-width) solid var(--ss-border-strong);
    /* The accent rail on a banner or a selected row. */
    --ss-rail-width: 3px;


    /* ========================================================
       8. SHADOWS — restrained by policy. Elevation separates a
       layer from the page; it is not texture. Three steps, and
       nothing glows.
       ======================================================== */

    --ss-shadow-none: none;
    --ss-shadow-sm: 0 1px 2px rgba(36, 35, 51, 0.06);
    --ss-shadow-card: 0 1px 3px rgba(36, 35, 51, 0.07);
    --ss-shadow-dialog: 0 12px 32px rgba(36, 35, 51, 0.22);


    /* ========================================================
       9. FOCUS — the clicker IS a keyboard. Focus is never
       removed, never reduced to a color change, and always
       offset so it reads against a filled control.
       ======================================================== */

    --ss-focus-color: var(--ss-purple);
    --ss-focus-width: 2px;
    --ss-focus-offset: 2px;
    --ss-focus-ring: var(--ss-focus-width) solid var(--ss-focus-color);
    /* On a purple fill the purple ring would vanish, so filled
       brand controls switch to the light ring plus a halo. */
    --ss-focus-color-inverse: #FFFFFF;
    --ss-focus-halo: 4px;

    /* The one translucent overlay in the system: a hover wash on
       an already-branded surface, where a solid token would have
       to guess what it sits on. */
    --ss-overlay-hover: rgba(255, 255, 255, 0.12);


    /* ========================================================
       10. TOUCH + MOTION
       ======================================================== */

    --ss-tap-min: 48px;
    /* Compact controls live in dense rows — a card footer, a table
       cell — and always sit beside a full-size action. 40px is the
       floor: comfortably above the 24px WCAG 2.2 minimum, and never
       the only way to complete a task. */
    --ss-tap-min-compact: 40px;
    --ss-speed-fast: 150ms;
    --ss-speed: 250ms;
    --ss-ease: ease;


    /* ========================================================
       11. BREAKPOINTS
       A custom property CANNOT be used inside a media query, so
       these are the DECLARED values and the @media rules below
       repeat the same literals. The test pins the two against
       each other, which is the only way they stay in step.
       They reuse the breakpoints style.css already uses rather
       than introducing a competing set.
       ======================================================== */

    --ss-bp-phone: 560px;
    --ss-bp-tablet: 720px;
    --ss-bp-wide: 900px;
    --ss-bp-max: 1100px;

    --ss-measure: 68ch;        /* readable line length for prose */
    --ss-dialog-max: 520px;    /* a dialog is a decision, not a page */
    --ss-card-min: 260px;      /* below this a card column is unreadable */


    /* ========================================================
       12. PLATFORM SHELL
       The application frame: sidebar, top bar, workspace
       switcher, dashboard surfaces. Three shell states —
       expanded, rail, drawer — are driven by the custom
       properties below, so one set of rules serves all three
       and the states cannot drift out of step.
       ======================================================== */

    --ss-side-w: 272px;        /* expanded sidebar */
    --ss-side-rail-w: 96px;    /* tablet rail: icon AND word, stacked */
    --ss-topbar-h: 68px;

    /* State variables. `--ss-shell--rail` and `--ss-shell--drawer`
       reassign these; nothing else changes. */
    --ss-side-now: var(--ss-side-w);
    --ss-side-axis: row;
    --ss-side-align: center;
    --ss-side-text: left;
    --ss-side-label-size: var(--ss-size-base);
    --ss-side-label-weight: var(--ss-weight-medium);
    --ss-side-pad: var(--ss-space-3);
    --ss-side-gap: var(--ss-space-3);
    --ss-side-detail: flex;    /* counts and group headings */

    /* The rest of the sidebar's state. EVERY difference between the
       expanded sidebar and the rail is one of these values — there is
       no rule that only a class can reach, which is what lets the
       media query produce exactly the same rail the demo control does. */
    --ss-side-brand-justify: flex-start;
    --ss-logo-full: block;     /* the horizontal wordmark */
    --ss-logo-mark: none;      /* the existing favicon mark */
    --ss-ws-others: flex;      /* the workspaces that are not current */
    --ss-ws-axis: row;
    --ss-ws-text: left;
    --ss-ws-now: inline;       /* the word "Current" */
    --ss-ws-name-size: var(--ss-size-sm);
    --ss-group-gap: var(--ss-space-5);
    --ss-group-sep: 0;         /* hairline between groups on the rail */
    --ss-group-sep-pad: 0;
    --ss-side-current-shadow: inset var(--ss-rail-width) 0 0 var(--ss-brand);
    --ss-guard-text: inline;

    /* Layered surfaces. The shell is built from tinted paper, not
       from slabs of brand color: a sidebar that is one large purple
       block shouts at a clinician all day. */
    --ss-shell-bg: #F4F1EB;
    --ss-side-bg: #FCFBF8;
    --ss-panel-bg: #FFFFFF;
    --ss-panel-quiet: #F7F5F0;

    /* Two translucent washes, used only where a solid token would
       have to guess what it sits on. */
    --ss-overlay-press: rgba(74, 67, 120, 0.08);
    --ss-hairline: rgba(36, 35, 51, 0.06);

    --ss-ring-track: #E6E2DA;   /* the unfilled part of a progress ring */
}


/* ============================================================
   REDUCED MOTION
   Some clients and staff are motion-sensitive. Scoped to this
   system's own components — a foundation file must not reach
   out and disable animation across a page it does not own.
   ============================================================ */

@media (prefers-reduced-motion: reduce) {

    [class*="ss-"],
    [class*="ss-"]::before,
    [class*="ss-"]::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
    }
}


/* ============================================================
   SHARED PRIMITIVES
   ============================================================ */

/* Border-box sizing, scoped to elements that opted into this system
   by class name. Without it `min-height: var(--ss-tap-min)` means
   "48px of content PLUS padding and border" — a control that claims
   a 48px target and measures 66. The attribute selector keeps the
   rule from reaching markup the system does not own. */
[class*="ss-"],
[class*="ss-"]::before,
[class*="ss-"]::after {
    box-sizing: border-box;
}

/* Every interactive component uses this one focus treatment. */
.ss-btn:focus-visible,
.ss-input:focus-visible,
.ss-select:focus-visible,
.ss-textarea:focus-visible,
.ss-nav__item:focus-visible,
.ss-dialog__close:focus-visible,
.ss-check__box:focus-visible,
.ss-table a:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

/* A filled brand control needs the inverse ring or the purple
   outline disappears into the purple button. */
.ss-btn--primary:focus-visible,
.ss-nav__item[aria-current="page"]:focus-visible {
    outline-color: var(--ss-focus-color-inverse);
    outline-offset: var(--ss-focus-offset);
    box-shadow: 0 0 0 var(--ss-focus-halo) var(--ss-brand-tint);
}

/* Icon slot. Icons are inline SVG supplied by the page — this
   file adds no dependency and ships no icon font. */
.ss-icon {
    width: 1em;
    height: 1em;
    flex: none;
    display: inline-block;
    vertical-align: -0.125em;
}

.ss-icon svg,
.ss-icon > svg {
    width: 100%;
    height: 100%;
    display: block;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.75;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Visually hidden, still read aloud. */
.ss-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}


/* ============================================================
   BUTTONS
   Four intents, one geometry. Primary is purple because purple
   is the brand and the primary action; success and info are NOT
   button intents, because a button is an instruction, not a
   status.
   ============================================================ */

.ss-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ss-space-2);
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-3) var(--ss-space-5);
    border: var(--ss-border-width) solid transparent;
    border-radius: var(--ss-radius);
    font-family: var(--ss-font-sans);
    font-size: var(--ss-size-base);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--ss-speed-fast) var(--ss-ease),
                border-color var(--ss-speed-fast) var(--ss-ease),
                color var(--ss-speed-fast) var(--ss-ease);
}

.ss-btn--primary {
    background: var(--ss-brand);
    color: var(--ss-text-on-brand);
    border-color: var(--ss-brand);
}

.ss-btn--primary:hover {
    background: var(--ss-brand-hover);
    border-color: var(--ss-brand-hover);
}

.ss-btn--primary:active {
    background: var(--ss-brand-active);
    border-color: var(--ss-brand-active);
}

.ss-btn--secondary {
    background: var(--ss-surface);
    color: var(--ss-brand-text);
    border-color: var(--ss-border-strong);
}

.ss-btn--secondary:hover {
    background: var(--ss-brand-tint);
    border-color: var(--ss-brand);
}

.ss-btn--quiet {
    background: transparent;
    color: var(--ss-brand-text);
    border-color: transparent;
    padding-inline: var(--ss-space-3);
}

.ss-btn--quiet:hover {
    background: var(--ss-brand-tint);
}

/* Destructive is deliberately NOT a filled red button. It reads
   as a quiet control in calm red text, so it is findable but
   never the thing a tired thumb hits first. */
.ss-btn--danger {
    background: var(--ss-surface);
    color: var(--ss-destructive-text);
    border-color: var(--ss-border-strong);
}

.ss-btn--danger:hover {
    background: var(--ss-error-tint);
    border-color: var(--ss-error);
}

.ss-btn--sm {
    min-height: var(--ss-tap-min-compact);
    padding: var(--ss-space-2) var(--ss-space-3);
    font-size: var(--ss-size-sm);
}

.ss-btn--block {
    display: flex;
    width: 100%;
}

.ss-btn[disabled],
.ss-btn[aria-disabled="true"] {
    opacity: 0.55;
    cursor: not-allowed;
    pointer-events: none;
}

.ss-btn-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ss-space-3);
    align-items: center;
}


/* ============================================================
   FORM CONTROLS
   A label is never a placeholder, an error is never color alone,
   and a hint sits under the control it explains.
   ============================================================ */

.ss-field {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-2);
    margin-bottom: var(--ss-space-5);
}

.ss-label {
    font-size: var(--ss-size-base);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text);
}

.ss-label__optional {
    font-weight: var(--ss-weight-regular);
    color: var(--ss-text-muted);
}

.ss-input,
.ss-select,
.ss-textarea {
    width: 100%;
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-3);
    font-family: var(--ss-font-sans);
    font-size: var(--ss-size-md);
    line-height: var(--ss-leading-normal);
    color: var(--ss-text);
    background: var(--ss-surface);
    border: var(--ss-border-line-strong);
    border-radius: var(--ss-radius-sm);
}

.ss-textarea {
    min-height: calc(var(--ss-tap-min) * 2);
    resize: vertical;
}

.ss-input::placeholder,
.ss-textarea::placeholder {
    color: var(--ss-text-muted);
}

.ss-input:hover,
.ss-select:hover,
.ss-textarea:hover {
    border-color: var(--ss-brand);
}

.ss-input[disabled],
.ss-select[disabled],
.ss-textarea[disabled] {
    background: var(--ss-surface-sunken);
    color: var(--ss-text-muted);
    cursor: not-allowed;
}

/* An invalid field is marked THREE ways: the border, the word,
   and the icon in .ss-field__error. Never the border alone. */
.ss-input[aria-invalid="true"],
.ss-select[aria-invalid="true"],
.ss-textarea[aria-invalid="true"] {
    border-color: var(--ss-error);
    border-width: var(--ss-border-width-strong);
}

.ss-hint {
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

.ss-field__error {
    display: flex;
    align-items: center;
    gap: var(--ss-space-2);
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-error-text);
}

.ss-check {
    display: flex;
    align-items: flex-start;
    gap: var(--ss-space-3);
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-2) 0;
}

.ss-check__box {
    width: var(--ss-space-5);
    height: var(--ss-space-5);
    margin-top: var(--ss-space-1);
    flex: none;
    accent-color: var(--ss-brand);
}

.ss-fieldset {
    border: var(--ss-border-line);
    border-radius: var(--ss-radius);
    padding: var(--ss-space-4);
    margin: 0 0 var(--ss-space-5);
}

.ss-legend {
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
    padding-inline: var(--ss-space-2);
}


/* ============================================================
   CARDS
   ============================================================ */

.ss-card {
    background: var(--ss-surface);
    border: var(--ss-border-line);
    border-radius: var(--ss-radius-lg);
    box-shadow: var(--ss-shadow-card);
    overflow: hidden;
}

.ss-card__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ss-space-4);
    flex-wrap: wrap;
    padding: var(--ss-space-4) var(--ss-space-5);
    border-bottom: var(--ss-border-line);
}

.ss-card__title {
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    margin: 0;
    color: var(--ss-text);
}

.ss-card__subtitle {
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
    margin: var(--ss-space-1) 0 0;
}

.ss-card__body {
    padding: var(--ss-space-5);
}

.ss-card__foot {
    padding: var(--ss-space-4) var(--ss-space-5);
    border-top: var(--ss-border-line);
    background: var(--ss-surface-sunken);
}

/* Selected state uses the lavender surface AND a brand rail —
   two signals, so selection survives a greyscale print. */
.ss-card--selected {
    background: var(--ss-surface-selected);
    border-color: var(--ss-brand);
    box-shadow: inset var(--ss-rail-width) 0 0 var(--ss-brand);
}

.ss-card-grid {
    display: grid;
    gap: var(--ss-space-4);
    grid-template-columns: repeat(auto-fit, minmax(var(--ss-card-min), 1fr));
}


/* ============================================================
   BANNERS
   A banner ALWAYS carries three things: an icon, a word naming
   the state, and the message. Remove the color and it still
   says what it means.
   ============================================================ */

.ss-banner {
    display: flex;
    align-items: flex-start;
    gap: var(--ss-space-3);
    padding: var(--ss-space-4);
    border: var(--ss-border-line);
    border-left: var(--ss-rail-width) solid var(--ss-neutral);
    border-radius: var(--ss-radius);
    background: var(--ss-neutral-tint);
    color: var(--ss-text);
    font-size: var(--ss-size-base);
    line-height: var(--ss-leading-normal);
}

.ss-banner__icon {
    font-size: var(--ss-size-lg);
    margin-top: var(--ss-space-1);
}

.ss-banner__body {
    flex: 1 1 auto;
    min-width: 0;
}

.ss-banner__title {
    display: block;
    font-weight: var(--ss-weight-bold);
    margin-bottom: var(--ss-space-1);
}

.ss-banner__text {
    margin: 0;
}

.ss-banner--info {
    background: var(--ss-info-tint);
    border-color: var(--ss-border);
    border-left-color: var(--ss-info);
    color: var(--ss-info-text);
}

.ss-banner--success {
    background: var(--ss-success-tint);
    border-color: var(--ss-border);
    border-left-color: var(--ss-success);
    color: var(--ss-success-text);
}

.ss-banner--attention {
    background: var(--ss-attention-tint);
    border-color: var(--ss-border);
    border-left-color: var(--ss-attention);
    color: var(--ss-attention-text);
}

.ss-banner--error {
    background: var(--ss-error-tint);
    border-color: var(--ss-border);
    border-left-color: var(--ss-error);
    color: var(--ss-error-text);
}


/* ============================================================
   STATUS PILLS
   Same contract as banners: icon + word. A pill is never a bare
   colored dot.
   ============================================================ */

.ss-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--ss-space-2);
    padding: var(--ss-space-1) var(--ss-space-3);
    border: var(--ss-border-width) solid transparent;
    border-radius: var(--ss-radius-pill);
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    white-space: nowrap;
    background: var(--ss-neutral-tint);
    color: var(--ss-text);
    border-color: var(--ss-border-strong);
}

.ss-pill__icon {
    font-size: var(--ss-size-base);
}

.ss-pill--progress {
    background: var(--ss-success-tint);
    color: var(--ss-success-text);
    border-color: var(--ss-success);
}

.ss-pill--supervision {
    background: var(--ss-info-tint);
    color: var(--ss-info-text);
    border-color: var(--ss-info);
}

.ss-pill--attention {
    background: var(--ss-attention-tint);
    color: var(--ss-attention-text);
    border-color: var(--ss-attention);
}

.ss-pill--error {
    background: var(--ss-error-tint);
    color: var(--ss-error-text);
    border-color: var(--ss-error);
}

.ss-pill--brand {
    background: var(--ss-brand-tint);
    color: var(--ss-brand-text);
    border-color: var(--ss-brand);
}

/* ---- CLINICAL RESPONSE PILLS --------------------------------
   A separate family on purpose. These report a child's response
   and read from the clinical tokens ONLY. No interface state
   may use this family, and these may not borrow a status color.
   ------------------------------------------------------------ */

.ss-pill--independent {
    background: var(--ss-clinical-independent-tint);
    color: var(--ss-clinical-independent-deep);
    border-color: var(--ss-clinical-independent);
}

.ss-pill--prompted {
    background: var(--ss-clinical-prompted-tint);
    color: var(--ss-clinical-prompted-deep);
    border-color: var(--ss-clinical-prompted);
}

.ss-pill--incorrect {
    background: var(--ss-clinical-incorrect-tint);
    color: var(--ss-clinical-incorrect-deep);
    border-color: var(--ss-clinical-incorrect);
}


/* ============================================================
   TABLES
   Scroll containers, not squeezed columns: a table that shrinks
   below legibility on a phone has lost the data.
   ============================================================ */

.ss-table-wrap {
    overflow-x: auto;
    border: var(--ss-border-line);
    border-radius: var(--ss-radius);
    background: var(--ss-surface);
}

.ss-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--ss-size-base);
}

.ss-table caption {
    text-align: left;
    padding: var(--ss-space-3) var(--ss-space-4);
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

.ss-table th,
.ss-table td {
    padding: var(--ss-space-3) var(--ss-space-4);
    text-align: left;
    border-bottom: var(--ss-border-line);
    vertical-align: top;
}

.ss-table thead th {
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
    background: var(--ss-surface-sunken);
    white-space: nowrap;
}

.ss-table tbody tr:last-child th,
.ss-table tbody tr:last-child td {
    border-bottom: 0;
}

.ss-table__num {
    font-family: var(--ss-font-mono);
    font-variant-numeric: tabular-nums;
    text-align: right;
}

.ss-table--rows tbody tr:nth-child(even) {
    background: var(--ss-bg);
}


/* ============================================================
   DIALOGS
   ============================================================ */

.ss-dialog {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--ss-space-4);
    background: var(--ss-scrim);
    z-index: 100;
}

.ss-dialog__panel {
    width: 100%;
    max-width: var(--ss-dialog-max);
    max-height: 90vh;
    overflow-y: auto;
    background: var(--ss-surface);
    border-radius: var(--ss-radius-xl);
    box-shadow: var(--ss-shadow-dialog);
}

.ss-dialog__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--ss-space-4);
    padding: var(--ss-space-5);
    border-bottom: var(--ss-border-line);
}

.ss-dialog__title {
    font-size: var(--ss-size-xl);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    margin: 0;
}

.ss-dialog__close {
    min-width: var(--ss-tap-min);
    min-height: var(--ss-tap-min);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: var(--ss-border-width) solid transparent;
    border-radius: var(--ss-radius-sm);
    color: var(--ss-text-muted);
    cursor: pointer;
}

.ss-dialog__close:hover {
    background: var(--ss-surface-sunken);
    color: var(--ss-text);
}

.ss-dialog__body {
    padding: var(--ss-space-5);
}

.ss-dialog__foot {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--ss-space-3);
    padding: var(--ss-space-4) var(--ss-space-5);
    border-top: var(--ss-border-line);
    background: var(--ss-surface-sunken);
}

/* A dialog rendered inline in the preview, without fixed
   positioning, so the page can show one without trapping focus. */
.ss-dialog--static {
    position: static;
    inset: auto;
    padding: var(--ss-space-6);
    border-radius: var(--ss-radius-lg);
    z-index: auto;
}


/* ============================================================
   EMPTY STATES
   An empty state names what is missing and offers the one action
   that fills it. It never says "No data".
   ============================================================ */

.ss-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--ss-space-3);
    padding: var(--ss-space-8) var(--ss-space-5);
    background: var(--ss-surface);
    border: var(--ss-border-width) dashed var(--ss-border-strong);
    border-radius: var(--ss-radius-lg);
}

.ss-empty__icon {
    font-size: var(--ss-size-2xl);
    color: var(--ss-brand);
    line-height: 1;
}

.ss-empty__title {
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-medium);
    margin: 0;
}

.ss-empty__text {
    max-width: var(--ss-measure);
    margin: 0;
    color: var(--ss-text-muted);
    font-size: var(--ss-size-base);
}


/* ============================================================
   NAVIGATION — icon AND word, always
   An icon-only rail is a memory test. Every item carries its
   label; the icon is recognition, the word is the answer.
   ============================================================ */

.ss-nav {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-1);
    padding: var(--ss-space-3);
    background: var(--ss-brand);
    border-radius: var(--ss-radius-lg);
}

.ss-nav__item {
    display: flex;
    align-items: center;
    gap: var(--ss-space-3);
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-2) var(--ss-space-3);
    border: var(--ss-border-width) solid transparent;
    border-radius: var(--ss-radius);
    color: var(--ss-text-on-brand);
    font-size: var(--ss-size-base);
    font-weight: var(--ss-weight-medium);
    text-decoration: none;
    cursor: pointer;
    background: transparent;
    width: 100%;
    text-align: left;
}

.ss-nav__item:hover {
    background: var(--ss-overlay-hover);
}

/* The current page is marked by a filled surface AND the
   aria-current attribute that names it to a screen reader —
   never by color alone. */
.ss-nav__item[aria-current="page"] {
    background: var(--ss-surface);
    color: var(--ss-brand-text);
    font-weight: var(--ss-weight-bold);
}

.ss-nav__icon {
    font-size: var(--ss-size-lg);
    flex: none;
}

.ss-nav__label {
    flex: 1 1 auto;
    min-width: 0;
}

.ss-nav__badge {
    flex: none;
    min-width: var(--ss-space-5);
    padding: 0 var(--ss-space-2);
    border-radius: var(--ss-radius-pill);
    background: var(--ss-attention);
    color: var(--ss-ink);
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    text-align: center;
}

/* Horizontal variant for a top bar. */
.ss-nav--bar {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
}

.ss-nav--bar .ss-nav__item {
    width: auto;
}


/* ============================================================
   RESPONSIVE
   The literals below MUST match --ss-bp-* above; a custom
   property cannot be read inside a media query, and the test
   pins the pair together.
   ============================================================ */

/* --ss-bp-wide: 900px */
@media (max-width: 900px) {

    .ss-dialog__panel {
        max-width: 100%;
    }
}

/* --ss-bp-tablet: 720px */
@media (max-width: 720px) {

    .ss-card-grid {
        grid-template-columns: 1fr;
    }

    .ss-nav--bar {
        flex-direction: column;
        align-items: stretch;
    }

    .ss-nav--bar .ss-nav__item {
        width: 100%;
    }
}

/* --ss-bp-phone: 560px */
@media (max-width: 560px) {

    .ss-card__head,
    .ss-card__body,
    .ss-card__foot,
    .ss-dialog__head,
    .ss-dialog__body,
    .ss-dialog__foot {
        padding: var(--ss-space-4);
    }

    /* Buttons go full width so a thumb has the whole row. */
    .ss-btn-row .ss-btn,
    .ss-dialog__foot .ss-btn {
        width: 100%;
    }

    .ss-btn-row,
    .ss-dialog__foot {
        flex-direction: column;
        align-items: stretch;
    }

    .ss-empty {
        padding: var(--ss-space-6) var(--ss-space-4);
    }

    .ss-table th,
    .ss-table td {
        padding: var(--ss-space-3);
    }
}


/* ============================================================
   PLATFORM SHELL — the application frame
   ============================================================ */

.ss-shell {
    display: grid;
    grid-template-columns: var(--ss-side-now) minmax(0, 1fr);
    min-height: 100%;
    background: var(--ss-shell-bg);
    color: var(--ss-text);
    font-family: var(--ss-font-sans);
    position: relative;
}

/* ---- The three states ---------------------------------------
   Each reassigns variables; no layout rule is written twice, so
   a change to the sidebar reaches all three at once.
   ------------------------------------------------------------ */

/* THE RAIL, in one place. The demo control sets this class; the
   media query below sets the identical list. Nothing else differs. */
.ss-shell--rail {
    --ss-side-now: var(--ss-side-rail-w);
    --ss-side-axis: column;
    --ss-side-align: center;
    --ss-side-text: center;
    --ss-side-label-size: var(--ss-size-xs);
    --ss-side-label-weight: var(--ss-weight-bold);
    --ss-side-pad: var(--ss-space-2);
    --ss-side-gap: var(--ss-space-1);
    --ss-side-detail: none;
    --ss-side-brand-justify: center;
    --ss-logo-full: none;
    --ss-logo-mark: block;
    --ss-ws-others: none;
    --ss-ws-axis: column;
    --ss-ws-text: center;
    --ss-ws-now: none;
    --ss-ws-name-size: var(--ss-size-xs);
    --ss-group-gap: var(--ss-space-4);
    --ss-group-sep: var(--ss-border-width);
    --ss-group-sep-pad: var(--ss-space-4);
    --ss-side-current-shadow: inset 0 var(--ss-rail-width) 0 var(--ss-brand);
    --ss-guard-text: none;
}

.ss-shell--drawer {
    grid-template-columns: minmax(0, 1fr);
}

/* ---- Sidebar ------------------------------------------------ */

.ss-side {
    display: flex;
    flex-direction: column;
    min-height: 100%;
    background: var(--ss-side-bg);
    border-right: var(--ss-border-width) solid var(--ss-border);
    box-shadow: var(--ss-shadow-sm);
    position: relative;
    z-index: 20;
}

.ss-side__brand {
    display: flex;
    align-items: center;
    justify-content: var(--ss-side-brand-justify);
    gap: var(--ss-space-3);
    min-height: var(--ss-topbar-h);
    padding: var(--ss-space-3) var(--ss-space-4);
    border-bottom: var(--ss-border-width) solid var(--ss-hairline);
}

/* The real logo asset, shown at its own proportions. Nothing in
   this system redraws, recolors or crops it. */
.ss-side__logo {
    display: var(--ss-logo-full);
    height: var(--ss-space-6);
    width: auto;
}

.ss-side__logo--mark {
    display: var(--ss-logo-mark);
    height: var(--ss-space-7);
    width: auto;
}

.ss-side__scroll {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: var(--ss-space-4) var(--ss-side-pad);
}

.ss-side__group + .ss-side__group {
    margin-top: var(--ss-group-gap);
    padding-top: var(--ss-group-sep-pad);
    border-top: var(--ss-group-sep) solid var(--ss-hairline);
}

.ss-side__group-label {
    display: var(--ss-side-detail);
    padding: 0 var(--ss-space-3) var(--ss-space-2);
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
}

/* ---- Sidebar item: icon AND word in every state ------------- */

.ss-side__item {
    display: flex;
    flex-direction: var(--ss-side-axis);
    align-items: var(--ss-side-align);
    gap: var(--ss-side-gap);
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-2) var(--ss-space-3);
    margin-bottom: var(--ss-space-1);
    border: var(--ss-border-width) solid transparent;
    border-radius: var(--ss-radius);
    color: var(--ss-text);
    font-size: var(--ss-size-base);
    font-weight: var(--ss-weight-regular);
    text-decoration: none;
    text-align: var(--ss-side-text);
    cursor: pointer;
    background: transparent;
    width: 100%;
    transition: background var(--ss-speed-fast) var(--ss-ease),
                color var(--ss-speed-fast) var(--ss-ease);
}

.ss-side__item:hover {
    background: var(--ss-overlay-press);
    color: var(--ss-brand-text);
}

.ss-side__item:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

.ss-side__icon {
    font-size: var(--ss-size-lg);
    flex: none;
    color: var(--ss-text-muted);
}

.ss-side__label {
    flex: 1 1 auto;
    min-width: 0;
    font-size: var(--ss-side-label-size);
    font-weight: var(--ss-side-label-weight);
    line-height: var(--ss-leading-tight);
}

/* A count is detail, not meaning — it disappears on the rail,
   where the attention pill on the item itself carries the state. */
.ss-side__count {
    display: var(--ss-side-detail);
    flex: none;
    min-width: var(--ss-space-6);
    padding: 0 var(--ss-space-2);
    border-radius: var(--ss-radius-pill);
    background: var(--ss-neutral-tint);
    border: var(--ss-border-width) solid var(--ss-border);
    color: var(--ss-text-muted);
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    text-align: center;
    line-height: var(--ss-space-5);
}

.ss-side__count--attention {
    background: var(--ss-attention-tint);
    border-color: var(--ss-attention);
    color: var(--ss-attention-text);
}

/* THE SELECTED STATE. A lavender surface, a purple rail, purple
   bold text and aria-current — four signals, none of them color
   on its own. */
.ss-side__item[aria-current="page"] {
    background: var(--ss-surface-selected);
    border-color: var(--ss-brand-tint);
    color: var(--ss-brand-text);
    font-weight: var(--ss-weight-bold);
    box-shadow: var(--ss-side-current-shadow);
}

.ss-side__item[aria-current="page"] .ss-side__icon {
    color: var(--ss-brand);
}

.ss-side__foot {
    padding: var(--ss-space-3) var(--ss-side-pad);
    border-top: var(--ss-border-width) solid var(--ss-hairline);
    background: var(--ss-panel-quiet);
}

/* ---- Workspace switcher ------------------------------------ */

.ss-ws {
    padding: var(--ss-space-3) var(--ss-side-pad) 0;
}

.ss-ws__label {
    display: var(--ss-side-detail);
    padding: 0 var(--ss-space-3) var(--ss-space-2);
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
}

.ss-ws__list {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-1);
    padding: var(--ss-space-1);
    background: var(--ss-panel-quiet);
    border: var(--ss-border-width) solid var(--ss-border);
    border-radius: var(--ss-radius);
}

.ss-ws__item {
    display: flex;
    flex-direction: var(--ss-ws-axis);
    align-items: center;
    justify-content: center;
    text-align: var(--ss-ws-text);
    gap: var(--ss-space-2);
    min-height: var(--ss-tap-min-compact);
    padding: var(--ss-space-2);
    border: var(--ss-border-width) solid transparent;
    border-radius: var(--ss-radius-sm);
    background: transparent;
    color: var(--ss-text-muted);
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-medium);
    text-align: left;
    text-decoration: none;
    cursor: pointer;
    width: 100%;
}

.ss-ws__item:hover {
    background: var(--ss-overlay-press);
    color: var(--ss-brand-text);
}

.ss-ws__item:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

/* On the rail there is no room to list four destinations without
   reducing them to guessable icons, so only the current one shows —
   icon AND word — and the rest come back with the sidebar. */
.ss-ws__item:not([aria-current="true"]) {
    display: var(--ss-ws-others);
}

/* The current workspace: white card, purple text, a shield, and
   the word "Current" for anyone who cannot see either. */
.ss-ws__item[aria-current="true"] {
    background: var(--ss-panel-bg);
    border-color: var(--ss-brand-tint);
    color: var(--ss-brand-text);
    font-weight: var(--ss-weight-bold);
    box-shadow: var(--ss-shadow-sm);
}

.ss-ws__icon {
    flex: none;
    font-size: var(--ss-size-base);
}

.ss-ws__name {
    flex: 1 1 auto;
    min-width: 0;
    font-size: var(--ss-ws-name-size);
    line-height: var(--ss-leading-tight);
}

.ss-ws__now {
    display: var(--ss-ws-now);
    flex: none;
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-brand);
}

/* ---- Active context: which organization is in scope ----------
   Choosing the wrong tenant is the costlier mistake, so the
   organization is the louder element and the person's name sits
   quieter beneath it. Platform is a different KIND of context, not a
   higher-ranked clinic, so it reads in neutral ink rather than brand
   colour. Long names truncate visually but keep their full value in
   `title`, so nothing is lost to a screen reader or a hover. */

.ss-ctx {
    display: flex;
    align-items: center;
    gap: var(--ss-space-3);
    min-width: 0;
    max-width: 340px;
}

.ss-ctx__mark {
    flex: none;
    width: 34px;
    height: 34px;
    display: grid;
    place-items: center;
    border-radius: var(--ss-radius-sm);
    background: var(--ss-brand-tint);
    color: var(--ss-brand-text);
}

.ss-ctx--platform .ss-ctx__mark {
    background: var(--ss-surface-sunken);
    color: var(--ss-text-muted);
}

.ss-ctx__text {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.ss-ctx__name {
    font-size: var(--ss-size-base);
    font-weight: var(--ss-weight-bold);
    color: var(--ss-text);
    line-height: var(--ss-leading-tight);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ss-ctx__meta {
    display: flex;
    align-items: center;
    gap: var(--ss-space-2);
    min-width: 0;
    font-size: var(--ss-size-xs);
    color: var(--ss-text-muted);
}

.ss-ctx__role {
    flex: none;
    padding: 1px var(--ss-space-2);
    border: var(--ss-border-width) solid var(--ss-brand-tint);
    border-radius: var(--ss-radius-pill);
    background: var(--ss-panel-bg);
    color: var(--ss-brand-text);
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    white-space: nowrap;
}

.ss-ctx--platform .ss-ctx__role {
    border-color: var(--ss-border-strong);
    color: var(--ss-text-muted);
}

.ss-ctx__who {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ss-ctx__switch {
    flex: none;
    min-height: var(--ss-tap-min);
}

/* The unfinished-work refusal. One live region, focusable so the reason
   can be reached, and never a bypass. */
.ss-tenant-block {
    margin: 0;
    padding: var(--ss-space-4) var(--ss-space-5);
    border-bottom: var(--ss-border-width) solid var(--ss-border-strong);
    border-left: var(--ss-rail-width) solid var(--ss-text);
    background: var(--ss-panel-bg);
    color: var(--ss-text);
    font-size: var(--ss-size-sm);
}

.ss-tenant-block:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: calc(var(--ss-focus-offset) * -1);
}

/* On a phone the context strip keeps the organization visible but the
   person's name yields the room. --ss-bp-phone, not a new literal. */
@media (max-width: 560px) {
    .ss-ctx { max-width: none; }
    .ss-ctx__who { display: none; }
}

/* ---- Guardian: a shield, used only for protection facts ----- */

.ss-guard {
    display: inline-flex;
    align-items: center;
    gap: var(--ss-space-2);
    padding: var(--ss-space-2) var(--ss-space-3);
    border-radius: var(--ss-radius-pill);
    border: var(--ss-border-width) solid var(--ss-border);
    background: var(--ss-panel-bg);
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text-muted);
    line-height: var(--ss-leading-tight);
}

.ss-guard__icon {
    flex: none;
    font-size: var(--ss-size-base);
}

.ss-guard--protected {
    background: var(--ss-info-tint);
    border-color: var(--ss-info);
    color: var(--ss-info-text);
}

.ss-guard--signed {
    background: var(--ss-success-tint);
    border-color: var(--ss-success);
    color: var(--ss-success-text);
}

.ss-guard--warn {
    background: var(--ss-attention-tint);
    border-color: var(--ss-attention);
    color: var(--ss-attention-text);
}

.ss-guard--quiet {
    background: transparent;
    border-color: transparent;
    padding-inline: 0;
}

.ss-guard__text {
    display: var(--ss-guard-text);
}

/* ---- Top bar ------------------------------------------------ */

.ss-shell__body {
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 100%;
}

.ss-topbar {
    display: flex;
    align-items: center;
    gap: var(--ss-space-4);
    min-height: var(--ss-topbar-h);
    padding: var(--ss-space-2) var(--ss-space-5);
    background: var(--ss-panel-bg);
    border-bottom: var(--ss-border-width) solid var(--ss-border);
    position: sticky;
    top: 0;
    z-index: 10;
}

.ss-topbar__crumb {
    display: flex;
    align-items: center;
    gap: var(--ss-space-2);
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

.ss-topbar__crumb-here {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: var(--ss-size-base);
    font-weight: var(--ss-weight-bold);
    color: var(--ss-text);
}

.ss-topbar__sep {
    color: var(--ss-border-strong);
}

.ss-topbar__spacer {
    flex: 1 1 auto;
}

.ss-search {
    display: flex;
    align-items: center;
    gap: var(--ss-space-2);
    flex: 0 1 340px;
    min-width: 0;
    padding: 0 var(--ss-space-3);
    background: var(--ss-panel-quiet);
    border: var(--ss-border-width) solid var(--ss-border);
    border-radius: var(--ss-radius-pill);
}

.ss-search:focus-within {
    border-color: var(--ss-brand);
    background: var(--ss-panel-bg);
}

.ss-search__icon {
    flex: none;
    color: var(--ss-text-muted);
}

.ss-search__input {
    flex: 1 1 auto;
    min-width: 0;
    min-height: var(--ss-tap-min-compact);
    border: 0;
    background: transparent;
    font-family: var(--ss-font-sans);
    font-size: var(--ss-size-base);
    color: var(--ss-text);
}

.ss-search__input:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

.ss-search__key {
    flex: none;
    padding: 0 var(--ss-space-2);
    border: var(--ss-border-width) solid var(--ss-border);
    border-radius: var(--ss-radius-sm);
    background: var(--ss-panel-bg);
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-xs);
    color: var(--ss-text-muted);
}

.ss-iconbtn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ss-space-2);
    min-width: var(--ss-tap-min);
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-2);
    border: var(--ss-border-width) solid transparent;
    border-radius: var(--ss-radius);
    background: transparent;
    color: var(--ss-text-muted);
    cursor: pointer;
}

.ss-iconbtn:hover {
    background: var(--ss-overlay-press);
    color: var(--ss-brand-text);
}

.ss-iconbtn:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

/* An unread marker is a dot AND a number AND a spoken label. */
.ss-iconbtn__dot {
    position: absolute;
    top: var(--ss-space-1);
    right: var(--ss-space-1);
    min-width: var(--ss-space-4);
    padding: 0 var(--ss-space-1);
    border-radius: var(--ss-radius-pill);
    background: var(--ss-attention);
    color: var(--ss-ink);
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    line-height: var(--ss-space-4);
    text-align: center;
}

.ss-account {
    display: flex;
    align-items: center;
    gap: var(--ss-space-3);
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-1) var(--ss-space-2) var(--ss-space-1) var(--ss-space-1);
    border: var(--ss-border-width) solid var(--ss-border);
    border-radius: var(--ss-radius-pill);
    background: var(--ss-panel-bg);
    cursor: pointer;
    text-align: left;
}

.ss-account:hover {
    border-color: var(--ss-brand);
}

.ss-account:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

.ss-avatar {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--ss-space-7);
    height: var(--ss-space-7);
    border-radius: var(--ss-radius-pill);
    background: var(--ss-brand-tint);
    color: var(--ss-brand-text);
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-bold);
}

.ss-account__name {
    display: block;
    white-space: nowrap;
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-bold);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

.ss-account__role {
    display: block;
    white-space: nowrap;
    font-size: var(--ss-size-xs);
    color: var(--ss-text-muted);
}

/* The sidebar control only exists where the sidebar can move. */
.ss-navtoggle {
    display: none;
}

.ss-shell--rail .ss-navtoggle,
.ss-shell--drawer .ss-navtoggle {
    display: inline-flex;
}

/* The close control belongs to the drawer alone — a rail has
   nothing to close. */
.ss-side__close {
    display: none;
    margin-left: auto;
}

.ss-shell--drawer .ss-side__close {
    display: inline-flex;
}

/* ---- Main region -------------------------------------------- */

.ss-main {
    flex: 1 1 auto;
    min-width: 0;
    padding: var(--ss-space-6) var(--ss-space-5) var(--ss-space-8);
}

.ss-main__inner {
    max-width: var(--ss-bp-max);
    margin: 0 auto;
}

/* ---- Greeting and protected-status header ------------------- */

.ss-hero {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--ss-space-5);
    flex-wrap: wrap;
    margin-bottom: var(--ss-space-5);
}

.ss-hero__greet {
    font-size: var(--ss-size-2xl);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    margin: 0;
}

.ss-hero__sub {
    margin: var(--ss-space-2) 0 0;
    font-size: var(--ss-size-lg);
    color: var(--ss-text-muted);
}

.ss-hero__aside {
    display: flex;
    align-items: center;
    gap: var(--ss-space-3);
    flex-wrap: wrap;
}

/* The protected-status strip. Teal-tinted paper, a shield, and
   three facts — a calm statement, not an alarm. */
.ss-status {
    display: flex;
    align-items: center;
    gap: var(--ss-space-5);
    flex-wrap: wrap;
    padding: var(--ss-space-4) var(--ss-space-5);
    margin-bottom: var(--ss-space-6);
    background: var(--ss-info-tint);
    border: var(--ss-border-width) solid var(--ss-info);
    border-left: var(--ss-rail-width) solid var(--ss-info);
    border-radius: var(--ss-radius-lg);
}

.ss-status__lead {
    display: flex;
    align-items: center;
    gap: var(--ss-space-3);
    flex: 1 1 260px;
    min-width: 0;
}

.ss-status__shield {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--ss-space-8);
    height: var(--ss-space-8);
    border-radius: var(--ss-radius-pill);
    background: var(--ss-panel-bg);
    border: var(--ss-border-width) solid var(--ss-info);
    color: var(--ss-info-text);
    font-size: var(--ss-size-xl);
}

.ss-status__title {
    display: block;
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-bold);
    color: var(--ss-info-text);
    line-height: var(--ss-leading-tight);
}

.ss-status__note {
    display: block;
    font-size: var(--ss-size-sm);
    color: var(--ss-info-text);
}

.ss-status__facts {
    display: flex;
    gap: var(--ss-space-6);
    flex-wrap: wrap;
}

.ss-fact__value {
    display: block;
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-xl);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-info-text);
    line-height: var(--ss-leading-tight);
}

.ss-fact__label {
    display: block;
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-info-text);
}

/* ---- Dashboard grid ----------------------------------------- */

.ss-dash {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: var(--ss-space-4);
}

.ss-col-3 { grid-column: span 3; }
.ss-col-4 { grid-column: span 4; }
.ss-col-5 { grid-column: span 5; }
.ss-col-7 { grid-column: span 7; }
.ss-col-8 { grid-column: span 8; }
.ss-col-12 { grid-column: span 12; }

/* ---- Panels ------------------------------------------------- */

.ss-panel {
    display: flex;
    flex-direction: column;
    background: var(--ss-panel-bg);
    border: var(--ss-border-width) solid var(--ss-border);
    border-radius: var(--ss-radius-lg);
    box-shadow: var(--ss-shadow-card);
    overflow: hidden;
}

.ss-panel--quiet {
    background: var(--ss-panel-quiet);
    box-shadow: var(--ss-shadow-none);
}

.ss-panel--lavender {
    background: var(--ss-surface-selected);
    border-color: var(--ss-brand-tint);
    box-shadow: var(--ss-shadow-none);
}

.ss-panel__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ss-space-3);
    flex-wrap: wrap;
    padding: var(--ss-space-4) var(--ss-space-5);
    border-bottom: var(--ss-border-width) solid var(--ss-hairline);
}

.ss-panel__title {
    margin: 0;
    font-size: var(--ss-size-md);
    font-weight: var(--ss-weight-bold);
    line-height: var(--ss-leading-tight);
}

.ss-panel__hint {
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

.ss-panel__body {
    flex: 1 1 auto;
    padding: var(--ss-space-4) var(--ss-space-5);
}

.ss-panel__body--flush {
    padding: 0;
}

.ss-panel__foot {
    padding: var(--ss-space-3) var(--ss-space-5);
    border-top: var(--ss-border-width) solid var(--ss-hairline);
    background: var(--ss-panel-quiet);
}

/* ---- Compact metric tiles ----------------------------------- */

.ss-metric {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-2);
    padding: var(--ss-space-4);
    background: var(--ss-panel-bg);
    border: var(--ss-border-width) solid var(--ss-border);
    border-radius: var(--ss-radius-lg);
    box-shadow: var(--ss-shadow-sm);
}

.ss-metric__top {
    display: flex;
    align-items: center;
    gap: var(--ss-space-2);
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text-muted);
}

.ss-metric__icon {
    flex: none;
    font-size: var(--ss-size-base);
}

.ss-metric__value {
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-2xl);
    font-weight: var(--ss-weight-medium);
    line-height: 1;
    color: var(--ss-text);
}

.ss-metric__unit {
    font-size: var(--ss-size-lg);
    color: var(--ss-text-muted);
}

.ss-metric__foot {
    display: flex;
    align-items: center;
    gap: var(--ss-space-2);
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

/* A trend is a word and an arrow, never a bare colored number. */
.ss-trend {
    display: inline-flex;
    align-items: center;
    gap: var(--ss-space-1);
    font-weight: var(--ss-weight-medium);
}

.ss-trend--up { color: var(--ss-success-text); }
.ss-trend--down { color: var(--ss-attention-text); }

.ss-metric--accent {
    background: var(--ss-success-tint);
    border-color: var(--ss-success);
}

.ss-metric--accent .ss-metric__top,
.ss-metric--accent .ss-metric__value,
.ss-metric--accent .ss-metric__foot {
    color: var(--ss-success-text);
}

/* ---- Rows, schedule and feed -------------------------------- */

.ss-rows {
    display: flex;
    flex-direction: column;
}

.ss-row {
    display: flex;
    align-items: center;
    gap: var(--ss-space-3);
    padding: var(--ss-space-3) var(--ss-space-5);
    border-bottom: var(--ss-border-width) solid var(--ss-hairline);
}

.ss-row:last-child {
    border-bottom: 0;
}

.ss-row--attention {
    background: var(--ss-attention-tint);
    box-shadow: inset var(--ss-rail-width) 0 0 var(--ss-attention);
}

.ss-row--error {
    background: var(--ss-error-tint);
    box-shadow: inset var(--ss-rail-width) 0 0 var(--ss-error);
}

.ss-row--info {
    background: var(--ss-info-tint);
    box-shadow: inset var(--ss-rail-width) 0 0 var(--ss-info);
}

.ss-row__main {
    flex: 1 1 auto;
    min-width: 0;
}

.ss-row__title {
    display: block;
    font-size: var(--ss-size-base);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
}

.ss-row__meta {
    display: block;
    margin-top: var(--ss-space-1);
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

.ss-row__end {
    flex: none;
    display: flex;
    align-items: center;
    gap: var(--ss-space-2);
}

.ss-time {
    flex: none;
    width: var(--ss-space-9);
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text-muted);
}

/* The schedule spine: one quiet vertical line, one mark per
   appointment, colored by TYPE and labelled by word. */
.ss-spine {
    flex: none;
    width: var(--ss-space-1);
    align-self: stretch;
    border-radius: var(--ss-radius-pill);
    background: var(--ss-border-strong);
}

.ss-spine--care { background: var(--ss-teal); }
.ss-spine--supervision { background: var(--ss-purple); }
.ss-spine--admin { background: var(--ss-text-muted); }
.ss-spine--fieldwork { background: var(--ss-sage); }

.ss-feed {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-4);
    padding: var(--ss-space-4) var(--ss-space-5);
}

.ss-feed__item {
    display: flex;
    gap: var(--ss-space-3);
    position: relative;
    padding-bottom: var(--ss-space-4);
    border-bottom: var(--ss-border-width) solid var(--ss-hairline);
}

.ss-feed__item:last-child {
    padding-bottom: 0;
    border-bottom: 0;
}

.ss-feed__mark {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--ss-space-7);
    height: var(--ss-space-7);
    border-radius: var(--ss-radius-pill);
    background: var(--ss-neutral-tint);
    border: var(--ss-border-width) solid var(--ss-border);
    color: var(--ss-text-muted);
    font-size: var(--ss-size-base);
}

.ss-feed__mark--signed {
    background: var(--ss-success-tint);
    border-color: var(--ss-success);
    color: var(--ss-success-text);
}

.ss-feed__mark--access {
    background: var(--ss-info-tint);
    border-color: var(--ss-info);
    color: var(--ss-info-text);
}

.ss-feed__when {
    display: block;
    margin-top: var(--ss-space-1);
    font-size: var(--ss-size-xs);
    color: var(--ss-text-muted);
}

/* ---- Progress ------------------------------------------------ */

.ss-progress {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-4);
}

.ss-bar__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--ss-space-3);
    margin-bottom: var(--ss-space-2);
    font-size: var(--ss-size-sm);
}

.ss-bar__name {
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text);
}

.ss-bar__value {
    font-family: var(--ss-font-mono);
    color: var(--ss-text-muted);
}

.ss-bar {
    height: var(--ss-space-2);
    border-radius: var(--ss-radius-pill);
    background: var(--ss-ring-track);
    overflow: hidden;
}

.ss-bar__fill {
    height: 100%;
    border-radius: var(--ss-radius-pill);
    background: var(--ss-success);
}

.ss-bar__fill--info { background: var(--ss-info); }
.ss-bar__fill--attention { background: var(--ss-attention); }
.ss-bar__fill--brand { background: var(--ss-brand); }

.ss-ring {
    display: flex;
    align-items: center;
    gap: var(--ss-space-4);
}

.ss-ring__svg {
    flex: none;
    width: var(--ss-space-9);
    height: var(--ss-space-9);
    transform: rotate(-90deg);
}

.ss-ring__track {
    fill: none;
    stroke: var(--ss-ring-track);
}

.ss-ring__value {
    fill: none;
    stroke: var(--ss-sage);
    stroke-linecap: round;
}

.ss-ring__label {
    min-width: 0;
}

.ss-ring__big {
    display: block;
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-xl);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

.ss-ring__note {
    display: block;
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

/* ---- Ecosystem reference strip ------------------------------ */

.ss-eco {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--ss-space-3);
    margin-top: var(--ss-space-7);
    padding-top: var(--ss-space-5);
    border-top: var(--ss-border-width) solid var(--ss-border);
}

.ss-eco__lead {
    grid-column: 1 / -1;
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
}

.ss-eco__item {
    display: flex;
    align-items: center;
    gap: var(--ss-space-3);
    padding: var(--ss-space-3) var(--ss-space-4);
    background: var(--ss-panel-quiet);
    border: var(--ss-border-width) solid var(--ss-border);
    border-radius: var(--ss-radius);
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

.ss-eco__item--now {
    background: var(--ss-surface-selected);
    border-color: var(--ss-brand);
    color: var(--ss-brand-text);
    font-weight: var(--ss-weight-bold);
}

.ss-eco__name {
    display: block;
}

.ss-eco__state {
    display: block;
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-regular);
    color: var(--ss-text-muted);
}

/* ---- Drawer state ------------------------------------------- */

.ss-shell--drawer .ss-side {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    width: min(var(--ss-side-w), 86%);
    transform: translateX(-101%);
    visibility: hidden;
    box-shadow: var(--ss-shadow-dialog);
    transition: transform var(--ss-speed) var(--ss-ease),
                visibility var(--ss-speed) var(--ss-ease);
    z-index: 60;
}

.ss-shell--drawer.ss-shell--open .ss-side {
    transform: translateX(0);
    visibility: visible;
}

.ss-scrim {
    display: none;
    position: fixed;
    inset: 0;
    background: var(--ss-scrim);
    z-index: 50;
}

.ss-shell--drawer.ss-shell--open .ss-scrim {
    display: block;
}

.ss-shell--drawer .ss-main {
    padding: var(--ss-space-5) var(--ss-space-4) var(--ss-space-8);
}

.ss-shell--drawer .ss-topbar {
    padding-inline: var(--ss-space-4);
}

.ss-shell--drawer .ss-search,
.ss-shell--drawer .ss-topbar__crumb-trail,
.ss-shell--drawer .ss-topbar__help,
.ss-shell--drawer .ss-account__text {
    display: none;
}

/* On a phone the page name is the most useful thing in the bar, so
   it gets the space the search and the help control gave up. */
.ss-shell--drawer .ss-topbar__crumb {
    flex: 1 1 auto;
}

.ss-shell--drawer .ss-topbar {
    gap: var(--ss-space-2);
}

.ss-shell--drawer .ss-dash > * {
    grid-column: span 12;
}

.ss-shell--drawer .ss-eco {
    grid-template-columns: minmax(0, 1fr);
}

.ss-shell--drawer .ss-status__facts {
    gap: var(--ss-space-5);
}

/* ---- Automatic states, for a viewport with no demo control ---
   The literals repeat the breakpoint tokens; the test pins them
   against each other.
   ------------------------------------------------------------ */

/* --ss-bp-max: 1100px — below the maximum content width a 272px
   sidebar is taking a quarter of the screen, so it becomes the
   rail: same items, same words, stacked under their icons. */
@media (max-width: 1100px) {

    .ss-shell {
        --ss-side-now: var(--ss-side-rail-w);
        --ss-side-axis: column;
        --ss-side-align: center;
        --ss-side-text: center;
        --ss-side-label-size: var(--ss-size-xs);
        --ss-side-label-weight: var(--ss-weight-bold);
        --ss-side-pad: var(--ss-space-2);
        --ss-side-gap: var(--ss-space-1);
        --ss-side-detail: none;
        --ss-side-brand-justify: center;
        --ss-logo-full: none;
        --ss-logo-mark: block;
        --ss-ws-others: none;
        --ss-ws-axis: column;
        --ss-ws-text: center;
        --ss-ws-now: none;
        --ss-ws-name-size: var(--ss-size-xs);
        --ss-group-gap: var(--ss-space-4);
        --ss-group-sep: var(--ss-border-width);
        --ss-group-sep-pad: var(--ss-space-4);
        --ss-side-current-shadow: inset 0 var(--ss-rail-width) 0 var(--ss-brand);
        --ss-guard-text: none;
    }

    /* P7 correction (F4): NO navigation toggle at this width.
       Between 561px and 1100px the sidebar is the 96px RAIL — always
       on screen, labelled, and operable — so there is nothing for a
       toggle to open. app.js's soteriaShellDrawer() treats only
       `(max-width: 560px)` as a drawer, and no rule outside the 560px
       query moves `.ss-side`, so a control shown here flipped
       aria-expanded to "true" while the navigation did not move. A
       control that lies to a screen reader is worse than no control,
       and the rail needs none. The base `.ss-navtoggle { display:
       none }` therefore stands at this width; the 560px query below
       turns the toggle on where it genuinely works, and the
       prototype's own `.ss-shell--rail` / `.ss-shell--drawer` classes
       keep their demo toggles unchanged. */

    /* Four metrics in a row would be 200px each here — two rows of
       two reads better and keeps the numbers large. */
    .ss-col-3 { grid-column: span 6; }

    /* The trail collapses to the page you are on. */
    .ss-shell:not(.ss-shell--drawer) .ss-topbar__crumb-trail { display: none; }

    .ss-search { flex-basis: var(--ss-space-9); }
}

/* --ss-bp-wide: 900px — the dashboard drops to two columns */
@media (max-width: 900px) {

    .ss-col-4, .ss-col-5 { grid-column: span 6; }
    .ss-col-7, .ss-col-8 { grid-column: span 12; }

    .ss-eco { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --ss-bp-phone: 560px — the sidebar becomes a closed drawer */
@media (max-width: 560px) {

    /* A drawer is a FULL panel, not a rail: every rail value goes
       back to the expanded one, so the phone menu reads like the
       desktop sidebar rather than like a squeezed strip. */
    .ss-shell {
        grid-template-columns: minmax(0, 1fr);
        --ss-side-axis: row;
        --ss-side-align: center;
        --ss-side-text: left;
        --ss-side-label-size: var(--ss-size-base);
        --ss-side-label-weight: var(--ss-weight-medium);
        --ss-side-pad: var(--ss-space-3);
        --ss-side-gap: var(--ss-space-3);
        --ss-side-detail: flex;
        --ss-side-brand-justify: flex-start;
        --ss-logo-full: block;
        --ss-logo-mark: none;
        --ss-ws-others: flex;
        --ss-ws-axis: row;
        --ss-ws-text: left;
        --ss-ws-now: inline;
        --ss-ws-name-size: var(--ss-size-sm);
        --ss-group-gap: var(--ss-space-5);
        --ss-group-sep: 0;
        --ss-group-sep-pad: 0;
        --ss-side-current-shadow: inset var(--ss-rail-width) 0 0 var(--ss-brand);
        --ss-guard-text: inline;
    }

    .ss-shell .ss-side {
        position: fixed;
        top: 0;
        bottom: 0;
        left: 0;
        width: min(var(--ss-side-w), 86%);
        transform: translateX(-101%);
        visibility: hidden;
        z-index: 60;
    }

    .ss-shell.ss-shell--open .ss-side {
        transform: translateX(0);
        visibility: visible;
    }

    /* The scrim belongs to the breakpoint as much as the drawer does.
       Without this rule a production page — which reaches the drawer
       through the media query and not through .ss-shell--drawer — would
       open a panel over the page with nothing behind it to dim or to
       tap, and the outside-tap dismissal would silently not exist. */
    .ss-shell.ss-shell--open .ss-scrim {
        display: block;
    }

    .ss-shell .ss-navtoggle { display: inline-flex; }

    /* P7 correction (F5): the close control belongs to the
       BREAKPOINT, not only to the demo class — the same fact the
       scrim rule above records. A production page reaches the drawer
       through this media query and never through
       `.ss-shell--drawer`, so `.ss-shell--drawer .ss-side__close` was
       the only rule that ever revealed it and it never matched. The
       drawer opened with no visible way to close it. It stays
       `.ss-iconbtn`, so its 48px tap target and its focus ring come
       from that component unchanged, and it is only ever ON SCREEN
       when the drawer is open — a closed drawer is translated
       off-canvas, visibility: hidden and inert. */
    .ss-shell .ss-side__close { display: inline-flex; }

    .ss-shell .ss-search,
    .ss-shell .ss-topbar__crumb-trail,
    .ss-shell .ss-topbar__help,
    .ss-shell .ss-account__text { display: none; }

    .ss-shell .ss-topbar__crumb { flex: 1 1 auto; }

    .ss-shell .ss-topbar { gap: var(--ss-space-2); }

    .ss-dash > * { grid-column: span 12; }

    .ss-eco { grid-template-columns: minmax(0, 1fr); }
}


/* ============================================================
   PRODUCTION INTEGRATION — OPT-IN ONLY (Phase 2B)

   EVERY selector below is rooted at [data-soteria-shell="true"],
   the marker a page must set on <body> to receive the shell. A
   page without it is not matched by a single rule here, which is
   what keeps every legacy page byte-for-byte as it was.

   These rules do one job: undo the centered reading column the
   production stylesheet gives <body> so the shell can hold the
   full window, and let the existing page content sit inside it
   unchanged. No production content is restyled by this phase.
   ============================================================ */

body[data-soteria-shell="true"] {
    max-width: none;
    margin: 0;
    padding: 0;
    background: var(--ss-shell-bg);
    font-family: var(--ss-font-sans);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ss-shell {
    min-height: 100vh;
}

/* The page's own <main> is moved into the shell, not rewritten. It
   keeps every class it was authored with; the shell only gives it
   the frame's padding and a maximum measure. */
[data-soteria-shell="true"] .ss-main {
    display: block;
}

/* The legacy header element becomes the top bar. Its old class is
   replaced, so nothing of .site-header survives — this rule exists
   only to state that the bar spans the shell body. */
[data-soteria-shell="true"] .ss-topbar {
    max-width: none;
    margin: 0;
    border-bottom: var(--ss-border-width) solid var(--ss-border);
}

/* The brand link wrapping the logo is the app's existing one; it
   must not pick up the legacy header's block layout here. */
[data-soteria-shell="true"] .ss-side__brand .brand {
    display: inline-flex;
    align-items: center;
    line-height: 0;
    text-decoration: none;
}

/* style.css sizes `.brand img` and would show BOTH marks at once —
   the wordmark and the rail's favicon — so which one is showing is
   settled here, at a specificity that beats it. Neither image is
   altered: one is displayed, the other is not. */
[data-soteria-shell="true"] .ss-side__brand .ss-side__logo {
    display: var(--ss-logo-full);
    height: var(--ss-space-6);
    width: auto;
}

[data-soteria-shell="true"] .ss-side__brand .ss-side__logo--mark {
    display: var(--ss-logo-mark);
    height: var(--ss-space-7);
    width: auto;
}

/* ***** THE MOST IMPORTANT RULE IN THIS SECTION *****
   `.hidden` is how this application says "not now" — a role-scoped
   destination, a panel with nothing in it yet, a form that needs a
   profile first. Every `.ss-` component sets its own `display`, at the
   same specificity, from a stylesheet that loads LATER, so on an
   opted-in page `.hidden` would silently stop working and every hidden
   section would appear. One rule, scoped to the marker, settles it for
   the whole page. */
[data-soteria-shell="true"] .hidden {
    display: none;
}

/* ***** THE SAME GUARANTEE FOR THE `hidden` ATTRIBUTE *****
   `.hidden` is this application's class; `hidden` is the HTML
   attribute, and the shell uses BOTH — `#ssSwitchCtx`, `#ssContext`,
   `#ssScrim`, the chooser's list and its blocked notice all ship with
   the attribute and are revealed by script.

   The attribute's `display: none` comes from the USER AGENT
   stylesheet, and an author declaration beats a user-agent one at any
   specificity. So `.ss-btn { display: inline-flex }` and
   `.ss-ctx { display: flex }` silently defeated it: a one-workspace
   account got a fully rendered "Switch organization or Platform"
   button that carried `hidden`, had no handler, did nothing when
   pressed, and on a phone crowded the context strip and the account
   control. The empty context strip appeared the same way during
   workspace initialization.

   One author rule, scoped to the marker, at a specificity no `.ss-`
   component reaches, settles it for every element on the page —
   including the pages' own content, which sets display through the
   same components. `hidden` means not relevant; it is a correctness
   fact, not a style preference, so no component may outrank it. */
[data-soteria-shell="true"] [hidden] {
    display: none;
}

/* The same guarantee, stated for the two navigation families the
   shell itself builds. *****

   `.hidden { display: none }` lives in style.css and is what
   paintAccountBar uses to keep a role-scoped destination out of
   sight until the actor's role is known. `.ss-side__item` sets
   `display: flex` at the SAME specificity from a stylesheet that
   loads LATER, so without the two rules below a shell page would
   show every role-scoped destination to everyone — a nav promising
   doors that slam, and the exact failure the class exists to
   prevent. Higher specificity, so hidden stays hidden. */
[data-soteria-shell="true"] .ss-side__item.hidden,
[data-soteria-shell="true"] .ss-ws__item.hidden,
/* The whole Areas panel, hidden in a Platform context: Areas are
   shortcuts inside an ORGANIZATION, and Platform is not one. */
[data-soteria-shell="true"] .ss-ws.hidden {
    display: none;
}

/* A section heading with nothing under it is noise. Rather than
   re-deriving who may see what, this reads the DOM that
   paintAccountBar produced: a group whose every destination is
   still .hidden collapses. Browsers without :has() simply show the
   heading — a cosmetic fallback, never a disclosure. */
[data-soteria-shell="true"] .ss-side__group:not(:has(.ss-side__item:not(.hidden))) {
    display: none;
}

/* The account block holds the app's existing #whoami line, its
   existing sign-out control, and the workspace-switch link
   paintAccountBar inserts after #whoami on multi-workspace
   accounts. */
[data-soteria-shell="true"] .ss-account {
    gap: var(--ss-space-2);
    padding-inline: var(--ss-space-3);
    flex-wrap: nowrap;
}

[data-soteria-shell="true"] .ss-account .nav-who {
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-bold);
    color: var(--ss-text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

[data-soteria-shell="true"] .ss-account__mark {
    font-size: var(--ss-size-lg);
    color: var(--ss-text-muted);
}

/* The compact control belongs to the phone alone; on a wide screen the
   account block itself is in the bar and this would be a second door
   to the same room. */
[data-soteria-shell="true"] .ss-account-toggle {
    display: none;
}

/* Sign out is a single line wherever the block ends up. */
[data-soteria-shell="true"] .ss-account #signOutBtn {
    white-space: nowrap;
    flex: none;
}

/* The drawer footer's account slot is empty until the phone layout
   moves the one account block into it. */
[data-soteria-shell="true"] .ss-side__account:empty {
    display: none;
}

[data-soteria-shell="true"] .ss-side__account {
    margin-bottom: var(--ss-space-3);
    padding-bottom: var(--ss-space-3);
    border-bottom: var(--ss-border-width) solid var(--ss-hairline);
}

[data-soteria-shell="true"] .ss-account #switchWorkspace {
    font-size: var(--ss-size-sm);
    color: var(--ss-brand-text);
    white-space: nowrap;
}

/* --ss-bp-max: 1100px — the sidebar becomes the labeled rail, so the
   brand block swaps the wordmark for the existing favicon mark. */
@media (max-width: 1100px) {

    /* A name clipped to "Alex Ri…" tells you less than no name at all,
       so the rail's account control is the compact one: the avatar and
       Sign out are visible, and the full name stays in the
       accessibility tree rather than being cut in half on screen. */
    [data-soteria-shell="true"] .ss-account .nav-who {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }
}

/* --ss-bp-phone: 560px
   The phone top bar carries three things and no more: the menu, the
   page you are on, and one compact account control. The account block
   itself is moved into the drawer footer, where its name, role and
   Sign out have room to be read. */
@media (max-width: 560px) {

    [data-soteria-shell="true"] .ss-account-toggle {
        display: inline-flex;
    }

    [data-soteria-shell="true"] .ss-topbar {
        min-height: var(--ss-space-8);
        padding-inline: var(--ss-space-3);
        gap: var(--ss-space-1);
    }

    /* With the account gone from the bar the page name has room to be
       shown whole rather than trailed off. */
    [data-soteria-shell="true"] .ss-topbar__crumb {
        flex: 1 1 auto;
        min-width: 0;
    }

    [data-soteria-shell="true"] .ss-topbar__crumb-here {
        font-size: var(--ss-size-base);
    }

    /* In the drawer footer the block is a full row again: avatar,
       name and role on one line, Sign out beside them. */
    [data-soteria-shell="true"] .ss-side__account .ss-account {
        width: 100%;
        padding: 0;
        border: 0;
        border-radius: 0;
        background: transparent;
        flex-wrap: wrap;
        gap: var(--ss-space-2) var(--ss-space-3);
    }

    /* The name and role are read here, not squeezed here: they take
       the width beside the avatar and wrap if they need to, and Sign
       out takes the line below — one line of its own, full width, the
       way a drawer's last action should sit. */
    [data-soteria-shell="true"] .ss-side__account .nav-who {
        position: static;
        width: auto;
        height: auto;
        margin: 0;
        overflow: visible;
        clip-path: none;
        max-width: none;
        flex: 1 1 auto;
        white-space: normal;
        line-height: var(--ss-leading-tight);
        font-size: var(--ss-size-base);
    }

    [data-soteria-shell="true"] .ss-side__account #signOutBtn {
        flex: 1 0 100%;
        justify-content: center;
    }

    /* The workspace-switch link paintAccountBar inserts for
       multi-workspace accounts follows the name, not the button. */
    [data-soteria-shell="true"] .ss-side__account #switchWorkspace {
        flex: 0 0 auto;
    }

    /* ---- Phone content scale (opted-in shell pages only) ----
       The page keeps every word, every control and every workflow;
       only the space around them tightens, so the first thing a
       trainee can act on arrives sooner on a small screen. */

    [data-soteria-shell="true"] .ss-main {
        padding: var(--ss-space-4) var(--ss-space-4) var(--ss-space-7);
    }

    /* 40px already sits inside the 38-42 band this pass asked for;
       what was costing height was the 48px line box around it. */
    [data-soteria-shell="true"] .ss-main h1 {
        font-size: var(--ss-size-3xl);
        line-height: 1.1;
    }

    [data-soteria-shell="true"] .ss-main .lede {
        font-size: var(--ss-size-lg);
        line-height: 1.45;
    }

    [data-soteria-shell="true"] .ss-main .dash-head {
        margin-bottom: var(--ss-space-4);
    }

    [data-soteria-shell="true"] .ss-main .dash-card {
        padding: var(--ss-space-4);
    }
}


/* ============================================================
   FIELDWORK HOME (Phase 3A) — opt-in only

   The composition of one production page, built from the components
   above. Every selector is rooted at the shell marker, so none of it
   can reach a legacy page. It changes presentation only: no rule here
   hides a control, changes a label, or alters what a form sends.
   ============================================================ */

/* ---- The compact introduction ---- */

[data-soteria-shell="true"] .fw-intro {
    max-width: var(--ss-measure);
    margin-bottom: var(--ss-space-5);
}

[data-soteria-shell="true"] .fw-intro h1 {
    margin: 0 0 var(--ss-space-2);
    font-size: var(--ss-size-2xl);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .fw-intro .lede {
    margin: 0;
    font-size: var(--ss-size-base);
    line-height: 1.5;
    color: var(--ss-text-muted);
}

/* ---- The dashboard grid ---- */

[data-soteria-shell="true"] .fw-home {
    align-items: start;
}

[data-soteria-shell="true"] .fw-metrics {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--ss-space-4);
}

/* ---- 1. Next action ---- */

/* A grid, so the phone can put the ACTION above the explanation
   without the markup having to say it twice. */
[data-soteria-shell="true"] .fw-next__body {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
        "mark main go"
        ".    help help";
    align-items: start;
    gap: var(--ss-space-3) var(--ss-space-4);
}

[data-soteria-shell="true"] .fw-next__mark { grid-area: mark; }
[data-soteria-shell="true"] .fw-next__main { grid-area: main; }
[data-soteria-shell="true"] .fw-next__go { grid-area: go; }
[data-soteria-shell="true"] .fw-next__help { grid-area: help; }

[data-soteria-shell="true"] .fw-next__mark {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--ss-space-8);
    height: var(--ss-space-8);
    border-radius: var(--ss-radius-pill);
    font-size: var(--ss-size-xl);
    background: var(--ss-panel-bg);
    border: var(--ss-border-width) solid var(--ss-border);
}

[data-soteria-shell="true"] .fw-next__main {
    min-width: 0;
}

[data-soteria-shell="true"] .fw-next__head {
    margin: 0;
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .fw-next__text {
    margin: var(--ss-space-1) 0 0;
    font-size: var(--ss-size-xl);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .fw-next__meta {
    display: flex;
    align-items: center;
    gap: var(--ss-space-3);
    flex-wrap: wrap;
    margin: var(--ss-space-3) 0 0;
    font-size: var(--ss-size-sm);
}

[data-soteria-shell="true"] .fw-next__where {
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .fw-next__help {
    margin: var(--ss-space-2) 0 0;
    max-width: var(--ss-measure);
    font-size: var(--ss-size-base);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .fw-next__go:empty {
    display: none;
}

/* Something is waiting: amber rail, amber mark, and the words
   "Needs you" beside them. */
[data-soteria-shell="true"] .fw-next--action {
    border-left: var(--ss-rail-width) solid var(--ss-attention);
}

[data-soteria-shell="true"] .fw-next--action .fw-next__mark {
    background: var(--ss-attention-tint);
    border-color: var(--ss-attention);
    color: var(--ss-attention-text);
}

/* Nothing to work in yet: lavender and a brand rail. Neither amber
   (nothing is wrong) nor sage (nothing has been achieved yet) — this
   is simply where the record begins. */
[data-soteria-shell="true"] .fw-next--start {
    background: var(--ss-surface-selected);
    border-color: var(--ss-brand-tint);
    border-left: var(--ss-rail-width) solid var(--ss-brand);
    box-shadow: var(--ss-shadow-none);
}

[data-soteria-shell="true"] .fw-next--start .fw-next__mark {
    background: var(--ss-panel-bg);
    border-color: var(--ss-brand);
    color: var(--ss-brand);
}

[data-soteria-shell="true"] .fw-next--start .fw-next__head,
[data-soteria-shell="true"] .fw-next--start .fw-next__text {
    color: var(--ss-brand-text);
}

/* Nothing is waiting: sage, quiet, and reinforcing rather than empty. */
[data-soteria-shell="true"] .fw-next--clear {
    background: var(--ss-success-tint);
    border-color: var(--ss-success);
    border-left: var(--ss-rail-width) solid var(--ss-success);
    box-shadow: var(--ss-shadow-none);
}

[data-soteria-shell="true"] .fw-next--clear .fw-next__mark {
    background: var(--ss-panel-bg);
    border-color: var(--ss-success);
    color: var(--ss-success-text);
}

[data-soteria-shell="true"] .fw-next--clear .fw-next__head,
[data-soteria-shell="true"] .fw-next--clear .fw-next__text,
[data-soteria-shell="true"] .fw-next--clear .fw-next__help {
    color: var(--ss-success-text);
}

/* ---- 2. Summary tiles ---- */

[data-soteria-shell="true"] .ss-metric__value--text {
    font-family: var(--ss-font-sans);
    font-size: var(--ss-size-xl);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
}

[data-soteria-shell="true"] .ss-metric--warn {
    background: var(--ss-attention-tint);
    border-color: var(--ss-attention);
}

[data-soteria-shell="true"] .ss-metric--warn .ss-metric__top,
[data-soteria-shell="true"] .ss-metric--warn .ss-metric__value,
[data-soteria-shell="true"] .ss-metric--warn .ss-metric__foot {
    color: var(--ss-attention-text);
}

/* ---- 3. Experience cards ---- */

[data-soteria-shell="true"] .fw-exp {
    display: flex;
    align-items: flex-start;
    gap: var(--ss-space-4);
    flex-wrap: wrap;
    padding: var(--ss-space-4) var(--ss-space-5);
    border-bottom: var(--ss-border-width) solid var(--ss-hairline);
}

[data-soteria-shell="true"] .fw-exp:last-child {
    border-bottom: 0;
}

[data-soteria-shell="true"] .fw-exp__main {
    flex: 1 1 260px;
    min-width: 0;
}

[data-soteria-shell="true"] .fw-exp__title {
    margin: 0;
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .fw-exp__chips {
    display: flex;
    gap: var(--ss-space-2);
    flex-wrap: wrap;
    margin: var(--ss-space-2) 0 0;
}

[data-soteria-shell="true"] .fw-exp__facts {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--ss-space-2) var(--ss-space-4);
    margin: var(--ss-space-3) 0 0;
}

[data-soteria-shell="true"] .fw-exp__facts dt {
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .fw-exp__facts dd {
    margin: 0;
    font-size: var(--ss-size-base);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .fw-exp__ready {
    display: flex;
    align-items: flex-start;
    gap: var(--ss-space-2);
    margin: var(--ss-space-3) 0 0;
    padding: var(--ss-space-2) var(--ss-space-3);
    background: var(--ss-info-tint);
    border-left: var(--ss-rail-width) solid var(--ss-info);
    border-radius: var(--ss-radius-sm);
    font-size: var(--ss-size-sm);
    color: var(--ss-info-text);
}

[data-soteria-shell="true"] .fw-exp__months {
    margin: var(--ss-space-3) 0 0;
    font-size: var(--ss-size-base);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .fw-exp__months--none {
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .fw-exp__go {
    flex: 0 0 auto;
    margin-left: auto;
}

/* ---- 4. Profile ---- */

[data-soteria-shell="true"] .fw-setup {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--ss-space-3);
}

[data-soteria-shell="true"] .fw-setup .ss-field {
    width: 100%;
    max-width: var(--ss-dialog-max);
}

[data-soteria-shell="true"] .fw-setup__mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--ss-space-8);
    height: var(--ss-space-8);
    border-radius: var(--ss-radius-pill);
    font-size: var(--ss-size-xl);
    background: var(--ss-brand-tint);
    color: var(--ss-brand);
}

[data-soteria-shell="true"] .fw-setup__lede {
    margin: 0;
    font-size: var(--ss-size-base);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .fw-facts {
    margin: 0 0 var(--ss-space-4);
    display: grid;
    gap: var(--ss-space-3);
}

[data-soteria-shell="true"] .fw-facts dt {
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .fw-facts dd {
    margin: var(--ss-space-1) 0 0;
    font-size: var(--ss-size-base);
    color: var(--ss-text);
}

/* Not an error — a field nobody has filled in yet. */
[data-soteria-shell="true"] .fw-fact--empty {
    color: var(--ss-text-muted);
    font-style: italic;
}

/* ---- Disclosures ---- */

[data-soteria-shell="true"] .fw-summary {
    display: flex;
    align-items: baseline;
    gap: var(--ss-space-3);
    flex-wrap: wrap;
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-3) var(--ss-space-5);
    cursor: pointer;
    list-style: none;
}

[data-soteria-shell="true"] .fw-summary::-webkit-details-marker {
    display: none;
}

[data-soteria-shell="true"] .fw-summary::before {
    content: "▸";
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] details[open] > .fw-summary::before {
    content: "▾";
}

[data-soteria-shell="true"] .fw-summary:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: calc(var(--ss-focus-offset) * -1);
}

[data-soteria-shell="true"] .fw-summary__label {
    font-size: var(--ss-size-md);
    font-weight: var(--ss-weight-bold);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .fw-summary__hint {
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .fw-summary--sm {
    padding: var(--ss-space-2) 0;
}

[data-soteria-shell="true"] .fw-summary--sm .fw-summary__label {
    font-size: var(--ss-size-base);
}

[data-soteria-shell="true"] .fw-edit .fw-form {
    margin-top: var(--ss-space-3);
}

/* ---- Forms ---- */

[data-soteria-shell="true"] .fw-form {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 0 var(--ss-space-4);
}

[data-soteria-shell="true"] .fw-form--one {
    grid-template-columns: minmax(0, 1fr);
}

[data-soteria-shell="true"] .fw-new__lede {
    display: block;
    margin: 0 0 var(--ss-space-4);
    max-width: var(--ss-measure);
}

/* ---- Cancelled archive: quiet, never alarming ---- */

[data-soteria-shell="true"] .fw-arch {
    padding: var(--ss-space-3) 0;
    border-bottom: var(--ss-border-width) solid var(--ss-hairline);
}

[data-soteria-shell="true"] .fw-arch:last-child {
    border-bottom: 0;
}

[data-soteria-shell="true"] .fw-arch__title {
    margin: 0;
    font-size: var(--ss-size-base);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .fw-arch__meta {
    display: flex;
    align-items: center;
    gap: var(--ss-space-2);
    flex-wrap: wrap;
    margin: var(--ss-space-1) 0 0;
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

/* ---- Empty state ---- */

[data-soteria-shell="true"] .fw-empty {
    border: 0;
    background: transparent;
}

/* ============================================================
   FIELDWORK HOME — responsive
   Either hand, one thumb: the primary action on every card and
   every form is full width and centred rather than tucked into a
   corner only one thumb reaches.
   ============================================================ */

/* --ss-bp-max: 1100px */
@media (max-width: 1100px) {

    [data-soteria-shell="true"] .fw-metrics {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    [data-soteria-shell="true"] .fw-exp__facts {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* --ss-bp-phone: 560px */
@media (max-width: 560px) {

    [data-soteria-shell="true"] .fw-metrics {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--ss-space-2);
    }

    /* All four facts stay, and every label stays readable — only the
       space around them tightens. The number keeps the largest type
       in the tile so it is still the thing the eye lands on. */
    [data-soteria-shell="true"] .fw-metrics .ss-metric {
        padding: var(--ss-space-3);
        gap: var(--ss-space-1);
    }

    [data-soteria-shell="true"] .fw-metrics .ss-metric__top {
        align-items: flex-start;
        font-size: var(--ss-size-xs);
        line-height: var(--ss-leading-tight);
    }

    [data-soteria-shell="true"] .fw-metrics .ss-metric__value {
        font-size: var(--ss-size-2xl);
    }

    [data-soteria-shell="true"] .fw-metrics .ss-metric__value--text {
        font-size: var(--ss-size-lg);
        overflow-wrap: break-word;
    }

    [data-soteria-shell="true"] .fw-metrics .ss-metric__foot {
        font-size: var(--ss-size-xs);
        line-height: var(--ss-leading-tight);
    }

    [data-soteria-shell="true"] .fw-action {
        display: flex;
        width: 100%;
    }

    [data-soteria-shell="true"] .fw-exp__go {
        margin-left: 0;
        width: 100%;
    }

    /* On a phone the action comes before the explanation of it. */
    [data-soteria-shell="true"] .fw-next__body {
        grid-template-columns: auto minmax(0, 1fr);
        grid-template-areas:
            "mark main"
            "go   go"
            "help help";
    }

    [data-soteria-shell="true"] .fw-exp {
        padding: var(--ss-space-4);
    }

    [data-soteria-shell="true"] .fw-summary {
        padding: var(--ss-space-3) var(--ss-space-4);
    }

    [data-soteria-shell="true"] .fw-exp__facts {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    [data-soteria-shell="true"] .fw-intro h1 {
        font-size: var(--ss-size-xl);
    }

    [data-soteria-shell="true"] .fw-intro .lede {
        font-size: var(--ss-size-base);
    }
}


/* ============================================================
   EXPERIENCE OVERVIEW (Phase 3B) — opt-in only

   The Overview section of one production page. Every selector is
   rooted at the shell marker; the other five sections of that page
   keep the presentation they already had.
   ============================================================ */

/* ***** ONE PRIMARY STATUS MESSAGE *****

   The Overview's priority panel says what this Experience needs, in
   one place. The page-wide banners above the tabs say the same thing
   in the setup, cancelled and ended states, and two banners agreeing
   with each other read as noise rather than emphasis.

   They are NOT removed: their derivations still run, they keep their
   ids and their hidden-state, and they come back the moment the reader
   leaves Overview for a tab the panel cannot speak for. This rule only
   suppresses the DUPLICATE, and only while the panel is on screen. */
[data-soteria-shell="true"] #exBody:has(#exPanelOverview:not(.hidden)) #exGateA,
[data-soteria-shell="true"] #exBody:has(#exPanelOverview:not(.hidden)) #exReadiness,
[data-soteria-shell="true"] #exBody:has(#exPanelOverview:not(.hidden)) #exCancelledBanner {
    display: none;
}

[data-soteria-shell="true"] .ex-overview {
    align-items: start;
    margin-bottom: var(--ss-space-6);
}

/* ---- 1. Identity ---- */

[data-soteria-shell="true"] .ex-ident__chips {
    display: flex;
    align-items: center;
    gap: var(--ss-space-2);
    flex-wrap: wrap;
    margin: 0 0 var(--ss-space-3);
}

[data-soteria-shell="true"] .ex-ident__facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: var(--ss-space-3) var(--ss-space-5);
    margin: 0;
}

[data-soteria-shell="true"] .ex-ident__facts dt {
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-ident__facts dd {
    margin: var(--ss-space-1) 0 0;
    font-size: var(--ss-size-base);
    color: var(--ss-text);
}

/* ---- 2. Priority panel ---- */

[data-soteria-shell="true"] .ex-priority__body {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
        "mark main go"
        ".    help help";
    align-items: start;
    gap: var(--ss-space-3) var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-priority__mark {
    grid-area: mark;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--ss-space-8);
    height: var(--ss-space-8);
    border-radius: var(--ss-radius-pill);
    font-size: var(--ss-size-xl);
    background: var(--ss-panel-bg);
    border: var(--ss-border-width) solid var(--ss-border);
}

[data-soteria-shell="true"] .ex-priority__main { grid-area: main; min-width: 0; }
[data-soteria-shell="true"] .ex-priority__go { grid-area: go; }
[data-soteria-shell="true"] .ex-priority__go:empty { display: none; }
[data-soteria-shell="true"] .ex-priority__help { grid-area: help; }

[data-soteria-shell="true"] .ex-priority__head {
    margin: 0;
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-priority__text {
    margin: var(--ss-space-1) 0 0;
    font-size: var(--ss-size-xl);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-priority__help {
    margin: 0;
    max-width: var(--ss-measure);
    font-size: var(--ss-size-base);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-priority__help:empty { display: none; }

/* Amber ONLY where something is proven to be waiting. */
[data-soteria-shell="true"] .ex-priority--action {
    border-left: var(--ss-rail-width) solid var(--ss-attention);
}

[data-soteria-shell="true"] .ex-priority--action .ex-priority__mark {
    background: var(--ss-attention-tint);
    border-color: var(--ss-attention);
    color: var(--ss-attention-text);
}

/* Setting up is instruction, not fault: lavender and a brand rail. */
[data-soteria-shell="true"] .ex-priority--setup {
    background: var(--ss-surface-selected);
    border-color: var(--ss-brand-tint);
    border-left: var(--ss-rail-width) solid var(--ss-brand);
    box-shadow: var(--ss-shadow-none);
}

[data-soteria-shell="true"] .ex-priority--setup .ex-priority__mark {
    background: var(--ss-panel-bg);
    border-color: var(--ss-brand);
    color: var(--ss-brand);
}

[data-soteria-shell="true"] .ex-priority--setup .ex-priority__head,
[data-soteria-shell="true"] .ex-priority--setup .ex-priority__text {
    color: var(--ss-brand-text);
}

/* Genuinely current: sage. */
[data-soteria-shell="true"] .ex-priority--current {
    background: var(--ss-success-tint);
    border-color: var(--ss-success);
    border-left: var(--ss-rail-width) solid var(--ss-success);
    box-shadow: var(--ss-shadow-none);
}

[data-soteria-shell="true"] .ex-priority--current .ex-priority__mark {
    background: var(--ss-panel-bg);
    border-color: var(--ss-success);
    color: var(--ss-success-text);
}

[data-soteria-shell="true"] .ex-priority--current .ex-priority__head,
[data-soteria-shell="true"] .ex-priority--current .ex-priority__text,
[data-soteria-shell="true"] .ex-priority--current .ex-priority__help {
    color: var(--ss-success-text);
}

/* Ended or cancelled: NEUTRAL. Calm red is for an error or a
   destructive consequence, and a deliberate ending is neither. */
[data-soteria-shell="true"] .ex-priority--terminal {
    background: var(--ss-panel-quiet);
    box-shadow: var(--ss-shadow-none);
}

[data-soteria-shell="true"] .ex-priority--terminal .ex-priority__mark {
    background: var(--ss-panel-bg);
    border-color: var(--ss-border-strong);
    color: var(--ss-text-muted);
}

/* ---- 3. Month facts and 4. relationship ---- */

[data-soteria-shell="true"] .ex-facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: var(--ss-space-4);
    margin: 0 0 var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-facts--stack {
    grid-template-columns: minmax(0, 1fr);
}

[data-soteria-shell="true"] .ex-fact dt {
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-fact__value {
    margin: var(--ss-space-1) 0 0;
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-xl);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-fact__note {
    margin: var(--ss-space-1) 0 0;
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-chips {
    display: flex;
    gap: var(--ss-space-2);
    flex-wrap: wrap;
    margin: 0 0 var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-state,
[data-soteria-shell="true"] .ex-rel__kind,
[data-soteria-shell="true"] .ex-rel__gate {
    display: flex;
    align-items: flex-start;
    gap: var(--ss-space-2);
    margin: 0 0 var(--ss-space-3);
    padding: var(--ss-space-3);
    border-radius: var(--ss-radius-sm);
    font-size: var(--ss-size-base);
}

[data-soteria-shell="true"] .ex-state,
[data-soteria-shell="true"] .ex-rel__kind {
    background: var(--ss-info-tint);
    border-left: var(--ss-rail-width) solid var(--ss-info);
    color: var(--ss-info-text);
}

[data-soteria-shell="true"] .ex-rel__gate {
    background: var(--ss-attention-tint);
    border-left: var(--ss-rail-width) solid var(--ss-attention);
    color: var(--ss-attention-text);
}

[data-soteria-shell="true"] .ex-rel__count,
[data-soteria-shell="true"] .ex-empty {
    margin: 0;
    font-size: var(--ss-size-base);
    color: var(--ss-text-muted);
}

/* ---- Responsive ---- */

/* --ss-bp-max: 1100px */
@media (max-width: 1100px) {

    [data-soteria-shell="true"] .ex-ident__facts {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* --ss-bp-phone: 560px
   Every Overview fact stays; the layout reorganises around them, and
   the one action becomes full width so either thumb reaches it. */
@media (max-width: 560px) {

    [data-soteria-shell="true"] .ex-priority__body {
        grid-template-columns: auto minmax(0, 1fr);
        grid-template-areas:
            "mark main"
            "go   go"
            "help help";
    }

    [data-soteria-shell="true"] .ex-action {
        display: flex;
        width: 100%;
    }

    [data-soteria-shell="true"] .ex-priority__go {
        width: 100%;
    }

    [data-soteria-shell="true"] .ex-facts {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--ss-space-3);
    }

    [data-soteria-shell="true"] .ex-fact__value {
        font-size: var(--ss-size-lg);
    }
}


/* ============================================================
   EXPERIENCE ENTRIES (Phase 3C) — opt-in only

   The Entries section of one production page. Every selector is
   rooted at the shell marker; the other two sections of that page
   (Forms and files, People) keep the presentation they already had.
   `.ex-facts`, `.ex-fact`, `.ex-chips` and `.ss-empty`
   are Phase 3B/foundation components, reused here rather than
   redeclared.
   ============================================================ */

[data-soteria-shell="true"] .ex-entries {
    align-items: start;
    margin-bottom: var(--ss-space-6);
}

/* ---- 1. Header ---- */

[data-soteria-shell="true"] .ex-entries__head {
    margin: 0 0 var(--ss-space-1);
    font-size: var(--ss-size-2xl);
    font-weight: var(--ss-weight-bold);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-entries__lede {
    margin: 0;
    max-width: var(--ss-measure);
    font-size: var(--ss-size-base);
    color: var(--ss-text-muted);
}

/* ---- 2. Month navigation ---- */

[data-soteria-shell="true"] .ex-entries__nav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-entries__nav .ss-field {
    margin-bottom: 0;
    min-width: 200px;
    max-width: 280px;
}

/* ---- Form fields shared by the add-entry disclosure and the two
   inline row forms (edit / review). ---- */

[data-soteria-shell="true"] .ex-entries__form-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 0 var(--ss-space-4);
}

/* ---- 4. Add an entry: the one primary action. A native disclosure
   so it can be tucked away once today's entry is in, without
   touching the form or the insert path underneath it. Open by
   default, so nothing that worked before this redesign changes. ---- */

[data-soteria-shell="true"] .ex-entries__disclosure summary {
    /* <summary> carries no `ss-` class, so it falls outside the shared
       box-sizing rule at the top of this file — without this, its own
       padding is added ON TOP of its auto-resolved width instead of
       being carved out of it, and it overflows its container by
       exactly the horizontal padding. Proven by measurement, not
       assumed: real 390px render right-edge exceeded the container's
       by 32px until this line was added. */
    box-sizing: border-box;
    width: 100%;
    display: flex;
    align-items: center;
    gap: var(--ss-space-3);
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-4) var(--ss-space-5);
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-brand-text);
    cursor: pointer;
    list-style: none;
}

[data-soteria-shell="true"] .ex-entries__disclosure summary::-webkit-details-marker {
    display: none;
}

[data-soteria-shell="true"] .ex-entries__disclosure summary:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

[data-soteria-shell="true"] .ex-entries__disclosure-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--ss-space-7);
    height: var(--ss-space-7);
    flex: none;
    border-radius: var(--ss-radius-pill);
    background: var(--ss-brand-tint);
    color: var(--ss-brand-text);
}

[data-soteria-shell="true"] .ex-entries__disclosure-chevron {
    margin-left: auto;
    flex: none;
    color: var(--ss-text-muted);
    transition: transform var(--ss-speed-fast) var(--ss-ease);
}

[data-soteria-shell="true"] .ex-entries__disclosure[open] .ex-entries__disclosure-chevron {
    transform: rotate(90deg);
}

[data-soteria-shell="true"] .ex-entries__disclosure-body {
    padding: 0 var(--ss-space-5) var(--ss-space-5);
    border-top: var(--ss-border-line);
}

[data-soteria-shell="true"] .ex-entries__list-head {
    margin: 0 0 var(--ss-space-4);
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text);
}

/* ---- 5/6. The entries list, and what can be done to each row ---- */

[data-soteria-shell="true"] .ex-entry-list {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-entry__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--ss-space-1) var(--ss-space-4);
    margin: 0 0 var(--ss-space-2);
}

[data-soteria-shell="true"] .ex-entry__date {
    margin: 0;
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-bold);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-entry__duration {
    margin: 0;
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-lg);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-entry__meta {
    margin: 0 0 var(--ss-space-3);
    font-size: var(--ss-size-base);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-entry__desc {
    margin: 0 0 var(--ss-space-3);
    font-size: var(--ss-size-base);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-entry__note {
    display: flex;
    align-items: flex-start;
    gap: var(--ss-space-2);
    margin: 0 0 var(--ss-space-2);
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-entry__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ss-space-2);
    margin-top: var(--ss-space-3);
}

/* Amber rail ONLY where a supervisor's or a trainee's action is
   genuinely waiting (needs_review / returned) — never for a plain
   draft or an ordinary recorded row. */
[data-soteria-shell="true"] .ex-entry--attention {
    border-left: var(--ss-rail-width) solid var(--ss-attention);
}

[data-soteria-shell="true"] .ex-entry__inline {
    margin-top: var(--ss-space-3);
    padding-top: var(--ss-space-3);
    border-top: var(--ss-border-line);
}

/* ---- Responsive ---- */

/* --ss-bp-max: 1100px */
@media (max-width: 1100px) {

    [data-soteria-shell="true"] .ex-entries__nav .ss-field {
        min-width: 160px;
    }
}

/* --ss-bp-phone: 560px
   Every fact and every control stays; only the layout reflows, and
   the disclosure and the primary save action become full width so
   either thumb reaches them. */
@media (max-width: 560px) {

    [data-soteria-shell="true"] .ex-entries__head {
        font-size: var(--ss-size-xl);
    }

    [data-soteria-shell="true"] .ex-entries__nav .ss-field {
        min-width: 0;
        max-width: none;
        width: 100%;
    }

    [data-soteria-shell="true"] .ex-entries__disclosure summary {
        padding: var(--ss-space-4);
    }

    [data-soteria-shell="true"] .ex-entries__disclosure-body {
        padding: 0 var(--ss-space-4) var(--ss-space-4);
    }

    /* Every primary Entries action — the add-entry disclosure toggle,
       the Save entry submit, and the empty state's one CTA — reaches
       the FULL 48px tap minimum, never the 40px compact floor
       reserved for secondary/dense-row controls, and spans the full
       width so either thumb reaches it. Save draft and the per-row
       actions are not this class: they are non-primary and keep their
       existing (already-compliant) sizing untouched. */
    [data-soteria-shell="true"] .ex-entries__phone-primary {
        width: 100%;
        min-height: var(--ss-tap-min);
    }

    [data-soteria-shell="true"] .ex-entry__date,
    [data-soteria-shell="true"] .ex-entry__duration {
        font-size: var(--ss-size-base);
    }
}


/* ============================================================
   EXPERIENCE SUPERVISOR INVOLVEMENT (Phase 3D) — opt-in only

   The Supervisor involvement section of one production page. Every
   selector is rooted at the shell marker; the remaining two sections
   of that page (Forms and files, People) keep the presentation they
   already had. `.ex-facts`, `.ex-fact`,
   `.ex-chips`, `.ex-empty` and `.ss-panel` are Phase 3B/foundation
   components, reused here rather than redeclared.
   ============================================================ */

[data-soteria-shell="true"] .ex-events {
    align-items: start;
    margin-bottom: var(--ss-space-6);
}

/* ---- 1. Header ---- */

[data-soteria-shell="true"] .ex-events__head {
    margin: 0 0 var(--ss-space-1);
    font-size: var(--ss-size-2xl);
    font-weight: var(--ss-weight-bold);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-events__lede {
    margin: 0;
    max-width: var(--ss-measure);
    font-size: var(--ss-size-base);
    color: var(--ss-text-muted);
}

/* ---- 3. Record supervisor involvement: the one primary action, kept
   permanently expanded above the list at every width — never a
   details disclosure, unlike Entries' add-entry card. ---- */

[data-soteria-shell="true"] .ex-events__form-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 0 var(--ss-space-4);
}

/* ---- 4. The events list, and what can be done to each row ---- */

[data-soteria-shell="true"] .ex-events__list-headrow {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--ss-space-2) var(--ss-space-4);
    margin: 0 0 var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-events__list-head {
    margin: 0;
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-event-list {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-event__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--ss-space-1) var(--ss-space-4);
    margin: 0 0 var(--ss-space-2);
}

[data-soteria-shell="true"] .ex-event__date {
    margin: 0;
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-bold);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-event__duration {
    margin: 0;
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-lg);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-event__meta {
    margin: 0 0 var(--ss-space-3);
    font-size: var(--ss-size-base);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-event__desc {
    margin: 0 0 var(--ss-space-3);
    font-size: var(--ss-size-base);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-event__note {
    display: flex;
    align-items: flex-start;
    gap: var(--ss-space-2);
    margin: 0 0 var(--ss-space-2);
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-event__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ss-space-2);
    margin-top: var(--ss-space-3);
}

/* ---- Responsive ---- */

/* --ss-bp-max: 1100px */
@media (max-width: 1100px) {

    [data-soteria-shell="true"] .ex-events__form-grid {
        grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    }
}

/* --ss-bp-phone: 560px
   Every fact, every event and every control stays; the record form's
   fields collapse to a single compact column — a two-up auto-fit grid
   still fits two 160px tracks inside a 390px viewport, so the phone
   layout is forced explicitly rather than left to auto-fit — and the
   form's one primary action reaches the FULL 48px tap minimum at full
   width so either thumb reaches it. Per-row actions are not this
   class: they are non-primary and keep their existing (already-
   compliant) sizing untouched, matching the Entries precedent. */
@media (max-width: 560px) {

    [data-soteria-shell="true"] .ex-events__head {
        font-size: var(--ss-size-xl);
    }

    [data-soteria-shell="true"] .ex-events__form-grid {
        grid-template-columns: minmax(0, 1fr);
    }

    [data-soteria-shell="true"] .ex-events__phone-primary {
        width: 100%;
        min-height: var(--ss-tap-min);
    }

    [data-soteria-shell="true"] .ex-event__date,
    [data-soteria-shell="true"] .ex-event__duration {
        font-size: var(--ss-size-base);
    }
}


/* ============================================================
   EXPERIENCE MONTH END (Phase 3E) — opt-in only

   The Month end section of one production page. Every selector is
   rooted at the shell marker; the remaining two sections of that page
   (Forms and files, People) keep the presentation they already had.
   `.ex-facts`, `.ex-fact`, `.ex-chips`, `.ex-state`, `.ex-empty`,
   `.ss-panel`, `.ss-pill`, `.ss-btn-row`, `.ss-field` and `.ss-empty`
   are Phase 3B/3C/3D or foundation components, reused here rather
   than redeclared. The primary actions (Freeze, Sign, Record the
   resolution) need no phone-specific class of their own: `.ss-btn`
   inside `.ss-btn-row` already reaches the 48px tap minimum and goes
   full width under 560px at the foundation level.
   ============================================================ */

[data-soteria-shell="true"] .ex-month {
    align-items: start;
    margin-bottom: var(--ss-space-6);
}

/* ---- 1. Header ---- */

[data-soteria-shell="true"] .ex-month__head {
    margin: 0 0 var(--ss-space-1);
    font-size: var(--ss-size-2xl);
    font-weight: var(--ss-weight-bold);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-month__lede {
    margin: 0;
    max-width: var(--ss-measure);
    font-size: var(--ss-size-base);
    color: var(--ss-text-muted);
}

/* ---- 2. Month navigation ---- */

[data-soteria-shell="true"] .ex-month__nav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-month__nav .ss-field {
    margin-bottom: 0;
    min-width: 200px;
    max-width: 280px;
}

/* ---- 5. Version history ---- */

[data-soteria-shell="true"] .ex-month__list-head {
    margin: 0 0 var(--ss-space-4);
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-version-list {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-version__title {
    margin: 0 0 var(--ss-space-2);
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-bold);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-version__meta {
    margin: 0 0 var(--ss-space-2);
    font-size: var(--ss-size-base);
    color: var(--ss-text-muted);
}

/* The fingerprint proves a version has not changed. It stays in its
   own technical disclosure, in the small monospace this composition
   otherwise reserves for exact figures, and wraps rather than
   overflowing its card on a narrow phone. */
[data-soteria-shell="true"] .ex-version__hash {
    margin: var(--ss-space-2) 0 0;
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-xs);
    color: var(--ss-text-muted);
    word-break: break-all;
}

/* ---- Responsive ---- */

/* --ss-bp-max: 1100px */
@media (max-width: 1100px) {

    [data-soteria-shell="true"] .ex-month__nav .ss-field {
        min-width: 160px;
    }
}

/* --ss-bp-phone: 560px
   Every total, every state and every control stays; only the layout
   reflows. Actions already reach the full 48px tap minimum and go
   full width through the shared `.ss-btn-row` foundation rule, so
   nothing display:none's here. */
@media (max-width: 560px) {

    [data-soteria-shell="true"] .ex-month__head {
        font-size: var(--ss-size-xl);
    }

    [data-soteria-shell="true"] .ex-month__nav .ss-field {
        min-width: 0;
        max-width: none;
        width: 100%;
    }
}


/* ============================================================
   EXPERIENCE PEOPLE (Phase 3G) — opt-in only, REV 2

   The People section of one production page, joining the same
   Sanctuary composition as Overview, Entries, Supervisor involvement
   and Month end. `.ex-facts`, `.ex-chips`, `.ex-empty`, `.ss-panel`
   and `.ss-pill` are foundation/earlier-phase components, reused
   rather than redeclared. Forms and files is the only remaining
   section of this page kept legacy, with its own narrow fix below.

   Every supervisor-status, qualification, invitation-lifecycle and
   "Current"/"Previously responsible" chip below is drawn through
   `.ss-pill` + `exIcon(...)`, so none of them is ever colour alone.
   ============================================================ */

[data-soteria-shell="true"] .ex-people-list {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-4);
    margin: 0 0 var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-person__head {
    margin: 0 0 var(--ss-space-2);
}

[data-soteria-shell="true"] .ex-person__name {
    margin: 0;
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-bold);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-people__facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: var(--ss-space-3) var(--ss-space-5);
    margin: 0 0 var(--ss-space-3);
}

[data-soteria-shell="true"] .ex-people__facts dt {
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-people__facts dd {
    margin: var(--ss-space-1) 0 0;
    font-size: var(--ss-size-base);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-person__note {
    margin: 0 0 var(--ss-space-2);
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

/* ---- Invitations ---- */

[data-soteria-shell="true"] .ex-invite {
    margin: 0 0 var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-invite__email {
    margin: 0 0 var(--ss-space-2);
    font-size: var(--ss-size-base);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text);
    /* A long address has no natural break point; wrap it rather than
       overflow the card on a narrow phone. */
    overflow-wrap: anywhere;
}

[data-soteria-shell="true"] .ex-invite__actions {
    display: flex;
    margin-top: var(--ss-space-3);
}

/* Revoke is a real, standalone action slot — never the 40px compact
   floor `.ss-btn--sm` uses elsewhere on this page. `.ss-btn` alone
   already reaches the full 48px tap minimum at the foundation level. */
[data-soteria-shell="true"] .ex-invite__actions .ss-btn {
    min-height: var(--ss-tap-min);
}

/* The one-time invitation link is shown once, in full, so it must
   never be the reason a phone page scrolls sideways. */
[data-soteria-shell="true"] #exInviteMsg .numeric {
    overflow-wrap: anywhere;
}

/* ---- Responsibility history ---- */

[data-soteria-shell="true"] .ex-history {
    margin: var(--ss-space-2) 0 0;
}

[data-soteria-shell="true"] .ex-history__summary {
    cursor: pointer;
    padding: var(--ss-space-2) 0;
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-history__summary:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

[data-soteria-shell="true"] .ex-history__list {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-2);
    margin: var(--ss-space-2) 0 0;
    padding: 0;
    list-style: none;
}

[data-soteria-shell="true"] .ex-history__row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--ss-space-2) var(--ss-space-3);
    font-size: var(--ss-size-sm);
    color: var(--ss-text);
}

[data-soteria-shell="true"] .ex-history__who {
    font-weight: var(--ss-weight-medium);
}

[data-soteria-shell="true"] .ex-history__when {
    color: var(--ss-text-muted);
}

/* ---- Responsive ---- */

/* --ss-bp-phone: 560px */
@media (max-width: 560px) {

    [data-soteria-shell="true"] .ex-invite__actions .ss-btn {
        width: 100%;
    }
}


/* ============================================================
   FIELDWORK INVITATION (Phase 3H) — opt-in only

   The whole of fieldwork-invite.html, composed with the design
   system for the first time under Rev 2. `.ss-panel`, `.ss-btn`,
   `.ss-field`, `.ss-fieldset` and `.ex-empty` are foundation
   components, reused rather than redeclared. Every selector below
   is rooted at the shell marker or class-based, and every colour
   resolves to an existing token — no hex, no new palette value.
   ============================================================ */

[data-soteria-shell="true"] .ex-invite__panel {
    margin: 0 0 var(--ss-space-5);
}

/* ---- 2. Invitation summary — icon-plus-words expiry fact, and
   3. the privacy/security explanation, promoted to a quiet rule
   line rather than a footnote. Same icon-plus-text row shape. ---- */

[data-soteria-shell="true"] .ex-invite__privacy,
[data-soteria-shell="true"] .ex-invite__expiry {
    display: flex;
    align-items: flex-start;
    gap: var(--ss-space-2);
    margin: 0 0 var(--ss-space-4);
    font-size: var(--ss-size-base);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-invite__privacy .ss-icon,
[data-soteria-shell="true"] .ex-invite__expiry .ss-icon {
    margin-top: var(--ss-space-1);
    color: var(--ss-text-muted);
}

/* An inviter's own display name, a site label they chose, and a
   server's own error text all have no natural break point; wrap
   them rather than overflow the card on a narrow phone — the
   file_hash fix above is the precedent. */
[data-soteria-shell="true"] .ex-invite__wrap {
    overflow-wrap: anywhere;
}

/* 6. Terminal outcome — Accepted/Declined never rest on the
   heading text alone; this icon sits beside it, and the body text
   names the outcome again. */
[data-soteria-shell="true"] .ex-invite__outcome-icon {
    display: inline-flex;
    font-size: var(--ss-size-xl);
    color: var(--ss-text-muted);
}

/* ---- Responsive ---- */

/* --ss-bp-phone: 560px
   Accept and Decline are one mis-tap away from a supervision
   assignment, so on a phone they are never side by side: both
   reach the full 48px tap minimum and stack, full width. */
@media (max-width: 560px) {

    [data-soteria-shell="true"] .ex-invite__btns {
        flex-direction: column;
    }

    [data-soteria-shell="true"] .ex-invite__phone-primary {
        width: 100%;
        min-height: var(--ss-tap-min);
    }
}


/* ============================================================
   EXPERIENCE FORMS AND FILES (Phase 3I) — opt-in only, REV 2

   Composes only the access-log section of #exPanelFiles with the
   same Sanctuary language as People and Invitation above. `.ss-panel`,
   `.ss-panel__body` and `exIcon(...)` are foundation/earlier-phase
   components, reused rather than redeclared. The official-form and
   contract-evidence upload/list markup, and the fingerprint-wrap fix
   immediately below, stay on the pre-existing `.dash-card` /
   `.note-grid` presentation — this block does not touch them.

   Each access-log row is exactly three labelled facts — when issued,
   artifact kind, access relationship — never a `·`-joined run of
   text, and every fact carries an icon beside its words.
   ============================================================ */

[data-soteria-shell="true"] .ex-files__access-list {
    display: flex;
    flex-direction: column;
    gap: var(--ss-space-3);
    margin: 0;
}

[data-soteria-shell="true"] .ex-files__access-row .ss-panel__body {
    padding: var(--ss-space-3) var(--ss-space-4);
}

[data-soteria-shell="true"] .ex-files__access-facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: var(--ss-space-2) var(--ss-space-5);
    margin: 0;
}

[data-soteria-shell="true"] .ex-files__access-fact dt {
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ex-files__access-fact dd {
    display: flex;
    align-items: flex-start;
    gap: var(--ss-space-2);
    margin: var(--ss-space-1) 0 0;
    font-size: var(--ss-size-base);
    color: var(--ss-text);
    /* An unmapped domain or relationship renders its raw stored
       value (P2.6); that value has no natural break point, so it
       must wrap rather than overflow the card on a narrow phone. */
    overflow-wrap: anywhere;
}

[data-soteria-shell="true"] .ex-files__access-icon {
    margin-top: var(--ss-space-1);
    color: var(--ss-text-muted);
}

/* ---- Responsive ---- */

/* --ss-bp-phone: 560px
   The three facts already reflow through the auto-fit grid; at
   phone width they stack one per line so none is ever clipped or
   hidden. */
@media (max-width: 560px) {

    [data-soteria-shell="true"] .ex-files__access-facts {
        grid-template-columns: 1fr;
    }
}


/* ============================================================
   WORKSPACE LANDING (Phase 3J) — opt-in only

   Shell adoption for dashboard.html and workspace.html — the first
   non-Fieldwork pages to opt into the Soteria Sanctuary shell. Two
   switching surfaces coexist here and must read as different things
   (P3(b)): #switchWorkspace, injected by app.js's paintAccountBar next
   to #whoami on ANY multi-workspace page, switches the ACTIVE TENANT
   through enter_workspace(); the shell's own SOTERIA_WORKSPACES row
   (labelled "Organization", unchanged) is navigation only. This block
   styles the injected #switchWorkspace control and workspace.html's
   own chooser rows — nothing here touches app.js, and the inline
   marginLeft app.js already sets on #switchWorkspace is left alone
   (P3(a)).

   dashboard.html needs no rule of its own below: <main id="main"> is a
   structural landmark change only, #startBtn already measures the
   48px minimum from style.css's .btn-cta, and no new badge is added to
   its content (P3(e)) — #whoami stays the page's only role statement.
   ============================================================ */

/* ---- #switchWorkspace: a tenant control, not a nav link ---- */

[data-soteria-shell="true"] #switchWorkspace {
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    gap: var(--ss-space-2);
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-2) var(--ss-space-4);
    border: var(--ss-border-width) solid var(--ss-border-strong);
    border-radius: var(--ss-radius-pill);
    background: var(--ss-surface);
    color: var(--ss-text);
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-medium);
    text-decoration: none;
    white-space: nowrap;
}

[data-soteria-shell="true"] #switchWorkspace::before {
    content: "\21c4";
    font-weight: var(--ss-weight-bold);
}

[data-soteria-shell="true"] #switchWorkspace:hover {
    background: var(--ss-neutral-tint);
}

[data-soteria-shell="true"] #switchWorkspace:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

/* ---- workspace.html chooser rows ----
   Organization name prominent, membership role as an icon-plus-words
   badge underneath — never color alone. Platform rows stay visually
   distinct (a quiet info-toned rail and a different icon) without
   reading as a privileged tier. */

[data-soteria-shell="true"] .ws-land__option {
    box-sizing: border-box;
    display: block;
    width: 100%;
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-4) var(--ss-space-5);
    background: var(--ss-surface);
    border: var(--ss-border-width) solid var(--ss-border-strong);
    border-radius: var(--ss-radius);
}

[data-soteria-shell="true"] .ws-land__option:hover {
    background: var(--ss-neutral-tint);
}

[data-soteria-shell="true"] .ws-land__option:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

/* The chooser row already in scope: a rail, a tint and the WORD
   "Current" — never colour alone. Platform rows carry a plain-language
   kind instead, so nothing implies a higher rank. */
[data-soteria-shell="true"] .ws-land__option--current {
    border-color: var(--ss-brand-tint);
    background: var(--ss-surface-selected);
    box-shadow: var(--ss-side-current-shadow);
    font-weight: var(--ss-weight-bold);
}

[data-soteria-shell="true"] .ws-land__now {
    margin-left: auto;
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-bold);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    color: var(--ss-brand);
}

[data-soteria-shell="true"] .ws-land__kind {
    display: block;
    font-family: var(--ss-font-mono);
    font-size: var(--ss-size-xs);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .ws-land__lede {
    max-width: 56ch;
}

[data-soteria-shell="true"] .ws-land__option--platform {
    box-shadow: inset var(--ss-rail-width) 0 0 var(--ss-info);
}

[data-soteria-shell="true"] .ws-land__main {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--ss-space-2);
    min-width: 0;
    /* .ws-option also carries the legacy .btn-secondary class, whose
       own white-space: nowrap (style.css) is inherited by every child
       here — restated normal so it cannot silently defeat wrapping. */
    white-space: normal;
}

[data-soteria-shell="true"] .ws-land__name {
    display: block;
    width: 100%;
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-bold);
    color: var(--ss-text);
    overflow-wrap: anywhere;
    /* A flex item's automatic minimum width defaults to its content's
       unbroken max size, which silently defeats overflow-wrap — this
       is what let a long organization name overflow the card instead
       of wrapping. */
    min-width: 0;
}

[data-soteria-shell="true"] .ws-land__badge {
    display: inline-flex;
    align-items: center;
    gap: var(--ss-space-2);
    padding: var(--ss-space-1) var(--ss-space-3);
    border-radius: var(--ss-radius-pill);
    background: var(--ss-neutral-tint);
    color: var(--ss-text);
    border: var(--ss-border-width) solid var(--ss-border-strong);
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-medium);
    opacity: 1;
}

[data-soteria-shell="true"] .ws-land__badge--platform {
    background: var(--ss-info-tint);
    color: var(--ss-info-text);
    border-color: var(--ss-info);
}

[data-soteria-shell="true"] .ws-land__badge-icon {
    font-size: var(--ss-size-base);
}

/* ---- Responsive ---- */

/* --ss-bp-phone: 560px
   #switchWorkspace moves into the drawer footer beside the relocated
   account block at this width (app.js's placeAccount()); full width
   there reads as a deliberate control rather than a stray link. */
@media (max-width: 560px) {

    [data-soteria-shell="true"] #switchWorkspace {
        width: 100%;
        justify-content: center;
    }
}


/* ============================================================
   AUTHENTICATION SURFACE (Phase 3K) — opt-in only, REV 2

   Chrome-free Sanctuary composition for login.html, signup.html and
   reset.html. These three pages carry NO data-soteria-shell attribute
   and load no shell script (P4: no signed-in application shell,
   sidebar, navigation or workspace switcher). The opt-in hook is a
   DIFFERENT attribute, `data-ss-auth`, never the shell marker — every
   selector below is rooted at it, so this block can never reach a
   shell page and the shell marker can never reach these three.

   This is also why every rule below reaches its target through the
   attribute root plus a plain element type, an EXISTING class already
   in the markup (`.lede`, `.login-step`, `.login-identity`,
   `.note-hint`, `.msg`), or plain HTML structure — never a class this
   file invents. tests/fieldwork-ui.test.js pins that no page outside
   the three Fieldwork pages carries a `class="…ss-…"` attribute in
   its STATIC markup, and that freeze is correct to keep: it is what
   stops a class name from silently becoming a second, undocumented
   contract on a page a future phase does not expect to touch. An
   attribute nobody scans for the word "ss-" composes with that freeze
   instead of fighting it, while every SELECTOR here still reads
   `--ss-` tokens exclusively, same as every other block in this file.

   One shared layout — brand mark, single card, one <h1>, lede, form,
   message region, secondary links — serves all three pages; nothing
   here needs a page-specific modifier, since login's two-step and
   reset's dead-end are already distinguished by their own ids.

   P4(b): ONE shared CSS pattern, NEVER a shared script. Every id,
   form, field name, autocomplete value, message class and existing
   script is left completely unchanged — this block is presentation
   laid over the unchanged markup. Message elements keep their
   severity styling from style.css verbatim; this block only
   guarantees long server text wraps.
   ============================================================ */

body[data-ss-auth] {
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    max-width: none;
    margin: 0;
    padding: var(--ss-space-7) var(--ss-space-4);
    background: var(--ss-bg);
    font-family: var(--ss-font-sans);
    color: var(--ss-text);
}

/* The wrap (direct child of body) and the card (its own second direct
   child, after the brand header) are plain, unclassed structural
   <div>s in the markup — see the note above on why no class names
   were added to reach them. */
body[data-ss-auth] > div {
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    width: 100%;
    max-width: 420px;
}

body[data-ss-auth] > div > header {
    display: flex;
    justify-content: center;
    margin: 0 0 var(--ss-space-7);
    padding: 0;
    border: 0;
}

body[data-ss-auth] > div > header img {
    display: block;
    height: 40px;
    width: auto;
}

body[data-ss-auth] > div > div {
    box-sizing: border-box;
    background: var(--ss-surface);
    border: var(--ss-border-line);
    border-radius: var(--ss-radius-lg);
    box-shadow: var(--ss-shadow-card);
    padding: var(--ss-space-7) var(--ss-space-6);
}

body[data-ss-auth] h1 {
    font-size: var(--ss-size-2xl);
    font-weight: var(--ss-weight-bold);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
    margin: 0 0 var(--ss-space-2);
    text-align: center;
    overflow-wrap: anywhere;
}

body[data-ss-auth] .lede {
    font-size: var(--ss-size-base);
    color: var(--ss-text-muted);
    text-align: center;
    margin: 0 0 var(--ss-space-6);
    overflow-wrap: anywhere;
}

body[data-ss-auth] label {
    display: block;
    font-size: var(--ss-size-base);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text);
    margin-bottom: var(--ss-space-2);
}

/* :not(.visually-hidden) keeps this from ever fighting the password-
   manager decoy input's own hiding rule — a plain type selector here
   would otherwise out-specificity style.css's `.visually-hidden` and
   make the decoy field visible again. */
body[data-ss-auth] input[type="email"]:not(.visually-hidden),
body[data-ss-auth] input[type="password"] {
    box-sizing: border-box;
    width: 100%;
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-3);
    margin-bottom: var(--ss-space-5);
    font-family: var(--ss-font-sans);
    font-size: var(--ss-size-md);
    line-height: var(--ss-leading-normal);
    color: var(--ss-text);
    background: var(--ss-surface);
    border: var(--ss-border-line-strong);
    border-radius: var(--ss-radius-sm);
}

body[data-ss-auth] input[type="email"]:not(.visually-hidden):hover,
body[data-ss-auth] input[type="password"]:hover {
    border-color: var(--ss-brand);
}

body[data-ss-auth] input:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

body[data-ss-auth] button[type="submit"] {
    box-sizing: border-box;
    display: flex;
    width: 100%;
    align-items: center;
    justify-content: center;
    gap: var(--ss-space-2);
    min-height: var(--ss-tap-min);
    padding: var(--ss-space-3) var(--ss-space-5);
    border: var(--ss-border-width) solid var(--ss-brand);
    border-radius: var(--ss-radius);
    background: var(--ss-brand);
    color: var(--ss-text-on-brand);
    font-family: var(--ss-font-sans);
    font-size: var(--ss-size-base);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    cursor: pointer;
    transition: background var(--ss-speed-fast) var(--ss-ease),
                border-color var(--ss-speed-fast) var(--ss-ease);
}

body[data-ss-auth] button[type="submit"]:hover {
    background: var(--ss-brand-hover);
    border-color: var(--ss-brand-hover);
}

body[data-ss-auth] button[type="submit"]:active {
    background: var(--ss-brand-active);
    border-color: var(--ss-brand-active);
}

body[data-ss-auth] button[type="submit"]:focus-visible {
    outline: var(--ss-focus-color-inverse) solid var(--ss-focus-width);
    outline-offset: var(--ss-focus-offset);
    box-shadow: 0 0 0 var(--ss-focus-halo) var(--ss-brand-tint);
}

body[data-ss-auth] button[type="submit"][disabled] {
    opacity: 0.55;
    cursor: not-allowed;
}

/* style.css's bare `form` selector already gives every form on this
   page a flex column layout (needed, since a label and an input are
   inline-level by default) — but it ALSO gives the form its own
   max-width, padding, border and background, built for an era where
   the FORM was the card. Now the wrapping div above is the card, so
   that would draw a second, narrower card inside it. This neutralizes
   only the box-model properties, leaving the layout ones alone, for
   every form on the page (signup's has no `.login-step` class at all,
   so a class-scoped exclusion would miss it). Visibility toggling
   ("login-step hidden" / "login-step") is untouched — this rule sets
   no display property, so style.css's own hidden-state rule for that
   class keeps deciding it. */
body[data-ss-auth] form {
    box-sizing: border-box;
    max-width: none;
    margin: 0;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: 0;
}

/* Login's step-two identity row — the address shown back, and a
   change control beside it. `.login-identity` already exists in the
   markup with its own flex layout, but style.css's own
   `.login-identity span` truncates with an ellipsis (built for a
   short, fixed-width echo) — every property that truncation depends
   on is overridden here so a long address wraps instead. */
body[data-ss-auth] .login-identity span {
    overflow: visible;
    text-overflow: clip;
    white-space: normal;
    overflow-wrap: anywhere;
    min-width: 0;
}

/* The "Forgot your password?" quiet link sits in the one <p> that
   immediately follows the sign-in submit button — reached structurally
   rather than by a class of our own. */
body[data-ss-auth] button[type="submit"] + p {
    text-align: center;
    margin-top: var(--ss-space-4);
}

/* reset.html's #deadEnd is shown by the SAME whole-className
   reassignment pattern as its form above (`"note-hint login-note"` /
   the hidden variant), so this keys off the `.note-hint.login-note`
   pair that all three pages' secondary-link paragraphs already carry
   in their unchanged markup. */
body[data-ss-auth] .note-hint.login-note {
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
    text-align: center;
    line-height: var(--ss-leading-normal);
    margin-top: var(--ss-space-6);
    overflow-wrap: anywhere;
}

body[data-ss-auth] .note-hint.login-note a {
    color: var(--ss-text-link);
}

/* Long server-supplied text (a raw error.message, a long email echoed
   back in an acknowledgement) must wrap inside the card at 390px
   rather than overflow it — severity styling, ids and content are
   untouched; this only adds a wrap rule. */
body[data-ss-auth] .msg {
    overflow-wrap: anywhere;
}

/* ---- Responsive ---- */

/* --ss-bp-phone: 560px */
@media (max-width: 560px) {

    body[data-ss-auth] {
        padding: var(--ss-space-5) var(--ss-space-4);
    }

    body[data-ss-auth] > div > div {
        padding: var(--ss-space-6) var(--ss-space-4);
    }
}


/* ============================================================
   PLATFORM OWNER CONSOLE (Phase 3L) — opt-in only

   platform.html's read-only, cross-organisation roster — the
   smallest authenticated page in the product. This phase adds the
   `<main>` landmark (structural only, no rule needed here) and a
   polite `#platMessage` (style already exists on `.msg`, no rule
   needed here). The one rule this phase owns is a visible focus
   ring for the table's local scroll container: `.plat__table-scroll`
   sits ALONGSIDE the existing `.table-scroll` class from style.css
   (which already supplies the horizontal-scroll and print behaviour)
   rather than replacing it, and everything else the table needs —
   borders, header styling, numeric alignment — already comes from
   `.admin-table` / `.numeric` in style.css.

   `.plat__table-scroll` is temporary, page-specific evidence for the
   later P11 table foundation, not a foundation itself: it is scoped
   to this one marker and this one page, is never named `.ss-table`,
   and establishes no shared table API, global table component or
   design-system abstraction. P11 remains the only phase authorised
   to build the reusable table.
   ============================================================ */

[data-soteria-shell="true"] .plat__table-scroll:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

/* A long organisation name has no natural break point once it
   exceeds the column width; plain text wrapping only breaks at
   whitespace, so this lets it wrap instead of overflowing the table
   at 390px — the same reasoning as the fingerprint-wrap fix below,
   applied here instead of there because that fix is byte-identical
   and this table did not exist when it was written. `table-layout:
   fixed` is required alongside it: an auto-layout table sizes its
   columns from each cell's UNBROKEN content width first, so a single
   long word still forces the table wider than its scroll container
   even with overflow-wrap set — fixed layout makes the columns (and
   therefore the wrap) respect the container's own width instead. */
[data-soteria-shell="true"] .plat__table-scroll table {
    table-layout: fixed;
}

[data-soteria-shell="true"] .plat__table-scroll td {
    overflow-wrap: anywhere;
}


/* ============================================================
   STAFF READINESS (Phase 3M) — opt-in only

   my-onboarding.html's own-requirements self-service view and
   readiness.html's supervisee/staff/owner readiness view adopt the
   Soteria Sanctuary shell. Both stay card-based — no table, no
   table-like grid, no P11 dependency (decision d). Every production-
   code change here is markup: the shell opt-in, a stable <h1> on
   readiness.html with the runtime mode presented as a separate
   subtitle instead of a dynamic rewrite (decision a), the progress
   bar's accessible name unified with its own visible count sentence
   (decision b), a dedicated polite result-count region replacing
   #rdList's own live-region responsibility (decision c), and an icon
   beside every status word so no state rests on colour alone. app.js
   is untouched; every readiness/onboarding computation, RPC and query
   shape is exactly as before.
   ============================================================ */

[data-soteria-shell="true"] .rd__mode {
    margin: 0 0 var(--ss-space-2);
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .rd__result-count {
    margin: 0 0 var(--ss-space-3);
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .onb__pill-icon,
[data-soteria-shell="true"] .rd__status-icon {
    margin-right: var(--ss-space-1);
}

/* A staff full name or an org-authored requirement name has no
   guaranteed break point once it exceeds its card width — the same
   reasoning as the fingerprint-wrap fix below, applied here instead
   of there because these cards did not exist when it was written. */
[data-soteria-shell="true"] .onb-item-name,
[data-soteria-shell="true"] .rd-name,
[data-soteria-shell="true"] .rd-facts dd {
    overflow-wrap: anywhere;
}

/* --ss-bp-phone: 560px
   The upload dialog's OWN actions — scoped to #uploadModal, never the
   generic .amend-actions pattern other Fieldwork panels share, so
   this can never reach any other page's amendment actions. Upload and
   Cancel are one mis-tap away from a wrong evidence submission, so on
   a phone they stack, full width, at the full 48px tap minimum. */
@media (max-width: 560px) {

    [data-soteria-shell="true"] #uploadModal .amend-actions {
        flex-direction: column;
    }

    [data-soteria-shell="true"] #uploadGo,
    [data-soteria-shell="true"] #uploadCancel {
        width: 100%;
        min-height: var(--ss-tap-min);
    }
}


/* ============================================================
   PROGRESS REPORTS (Phase 3N) — opt-in only

   reports.html's roster of what is written and what is owed, and
   report.html's single progress report, adopt the Soteria Sanctuary
   shell. Every production-code change is markup or presentation:
   the shell opt-in, the <main> landmark the shell relocates, polite
   regions for the page message, the autosave state and a dedicated
   result count, an icon beside every status word, and the
   page-scoped classes below. No report loading, filtering,
   assembly, calculation, draft, autosave, preview, signature or
   signed-record behaviour changes, no Supabase projection changes,
   and app.js is untouched.

   (a) Signed and Draft are INTERFACE states, so they read the
       Sanctuary status tokens. The legacy `.sc-signed` /
       `.sc-unsigned` badges in style.css tint themselves with
       `--independent-*` and `--prompted-*` — a CHILD'S RESPONSE
       colour standing in for a document's state, which is the one
       thing the design foundation's §1 forbids. `.rp__status` fixes
       that on these two pages only; style.css is untouched, so no
       other page's presentation moves in this phase.

   (b) No state rests on colour alone: signed/draft, the attention
       heading, the safety pill and the improvement trend each carry
       a mark or a word that survives greyscale.

   (c) One concise result count per render, in its own polite region
       — never one announcement per row.

   (d) `.rp__table-scroll` sits ALONGSIDE style.css's `.table-scroll`
       (which already supplies the horizontal scroll and print
       behaviour) rather than replacing it, and adds only a visible
       focus ring plus a wrap rule for a long unbroken name. It is
       page-scoped, temporary evidence for the later P11 table
       foundation, never named `.ss-table`, and establishes no shared
       table API. Deliberately NO `table-layout: fixed` here: unlike
       the platform roster these tables carry numeric columns whose
       comparison depends on staying on one line, and the scroll
       container — now keyboard-reachable — is the right fallback for
       a name with no break point.

   (e) A signed report says so before it is read. `.rp__locked`
       states the EXISTING signed fact; it introduces no new report
       state, and an "Amended" state remains P14's to design.
   ============================================================ */

[data-soteria-shell="true"] .rp__result-count {
    margin: 0 0 var(--ss-space-3);
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

[data-soteria-shell="true"] .rp__status {
    display: inline-flex;
    align-items: center;
    padding: var(--ss-space-1) var(--ss-space-2);
    border-radius: var(--ss-radius-pill);
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-medium);
    letter-spacing: var(--ss-tracking-label);
    text-transform: uppercase;
    white-space: nowrap;
}

[data-soteria-shell="true"] .rp__status--signed {
    color: var(--ss-success-text);
    background: var(--ss-success-tint);
}

/* An unsigned report is not a finished record. Amber, not red — it
   needs a person, it has not failed. */
[data-soteria-shell="true"] .rp__status--draft {
    color: var(--ss-attention-text);
    background: var(--ss-attention-tint);
}

[data-soteria-shell="true"] .rp__locked {
    display: flex;
    align-items: center;
    margin: 0 0 var(--ss-space-5);
    padding: var(--ss-space-3) var(--ss-space-4);
    border-left: var(--ss-rail-width) solid var(--ss-success);
    border-radius: var(--ss-radius-sm);
    background: var(--ss-success-tint);
    color: var(--ss-success-text);
    font-size: var(--ss-size-sm);
}

[data-soteria-shell="true"] .rp__due-icon,
[data-soteria-shell="true"] .rp__status-icon,
[data-soteria-shell="true"] .rp__pill-icon,
[data-soteria-shell="true"] .rp__locked-icon {
    margin-right: var(--ss-space-1);
}

[data-soteria-shell="true"] .rp__table-scroll:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

/* A client name, a service code and an org-authored target name each
   have no guaranteed break point once they exceed their column — the
   same reasoning as the fingerprint-wrap fix below, applied here
   instead of there because these tables did not exist when it was
   written. */
[data-soteria-shell="true"] .rp__table-scroll td {
    overflow-wrap: anywhere;
}

/* Save draft and Sign report are the two decisions this page exists
   for, and one of them is irreversible. Both clear the full 48px tap
   minimum at every width, not only on a phone. */
[data-soteria-shell="true"] #saveDraft,
[data-soteria-shell="true"] #signReport {
    min-height: var(--ss-tap-min);
}

/* --ss-bp-phone: 560px
   The two roster filters are the only controls on reports.html, and a
   mis-tap between them costs a re-read of the whole roster. Scoped to
   their own ids so this can never reach another page's filter row. */
@media (max-width: 560px) {

    [data-soteria-shell="true"] #statusFilter,
    [data-soteria-shell="true"] #clientFilter {
        min-height: var(--ss-tap-min);
    }
}


/* ============================================================
   STAFF ADMINISTRATION (Phase 3O) — opt-in only

   The administrator's half of the staff credentialing lifecycle
   adopts the Soteria Sanctuary shell: requirements.html (what staff
   must satisfy), onboarding.html (the roster and its invitations),
   staff-onboarding.html (one person's evidence and its review) and
   documents.html (the credential file cabinet). Phase 3M redesigned
   the same workflow seen from the staff member's own chair; this is
   the other chair, and the two share a database, a status engine and
   — see (a) below — several class names.

   Every production-code change in this phase is markup or
   presentation: the shell opt-in, polite result-count regions, an
   icon beside every status word, one keyboard-reachable scroll
   container, a stable <h1> on staff-onboarding.html, and the
   page-scoped classes below. No query, projection, RPC, gate,
   signed-URL TTL or review contract changes, and app.js is untouched.

   (a) THE PREFIXES ARE NEW ON PURPOSE. `.onb__` and `.rd__` belong
       to Phase 3M and are NOT extended here. That matters more than
       it looks: `.onb-item-name` is legacy markup shared by
       my-onboarding.html AND staff-onboarding.html, and `.rd-name` /
       `.rd-facts` are shared by readiness.html AND documents.html,
       because each pair renders the same thing from the same engine.
       Phase 3M's closing rule is marker-rooted but not page-scoped,
       so opting these pages in makes it match them too. That is
       INTENDED REUSE — it is one `overflow-wrap: anywhere` wrapping
       fix, and a long credential name needs it on both pages — but
       intended is not the same as unnoticed, so the complete set of
       pre-existing rules that newly match is enumerated and pinned
       in tests/staff-admin-ui.test.js rather than left to a browser
       to discover. Phase 3O's own four prefixes cannot join that set
       because no earlier phase names them.

   (b) One concise result count per render, in its own polite region.
       Phase 3M decision (c) again, and for the same reason: on
       documents.html the LIST ITSELF was the live region, so a
       filter that changed forty cards announced forty cards. The
       count region takes that job; #docList gives it up.

   (c) No state rests on colour alone. The invitation states (Active,
       Accepted, Expired, Revoked), the document review and expiry
       states, the requirement flags and the review-queue counts each
       carry a mark beside the word they already had.

   (d) `.oa__table-scroll` supplies the invitations table's
       horizontal scroll ITSELF, and that is a correction rather than
       a decoration. Phase 3N's `.rp__table-scroll` could add only a
       focus ring because it sits beside `.table-scroll`, which
       really does carry `overflow-x: auto`. The invitations table is
       marked `.table-wrap` — a class that **is declared NOWHERE in
       style.css** and has therefore never done anything. Local
       browser QA caught this: making the container focusable and
       giving it `role="region"` PROMISES a scrollable region, so a
       container that cannot scroll is the same defect the P7 F4
       correction removed from the navigation toggle — a control
       telling a screen reader something the page does not do. The
       overflow, the focus ring and the wrap rule are therefore all
       declared here, page-scoped and marker-rooted, so no other page
       carrying `.table-wrap` (note.html, ioa.html) is touched.

       It is never named `.ss-table` and establishes no shared table
       API — the table foundation remains P11's to design. Only the
       invitations table needs any of this; the staff roster and the
       document cabinet are CARDS, which is a privacy decision Phase
       3M made and tests/onboarding.test.js already pins for their
       siblings.
   ============================================================ */

[data-soteria-shell="true"] .oa__result-count,
[data-soteria-shell="true"] .req__result-count,
[data-soteria-shell="true"] .doc__result-count {
    margin: 0 0 var(--ss-space-3);
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

/* (c) One icon rule for the whole phase. Every status mark is an
   inline mark before a word that is already there — it never becomes
   the message on its own. */
[data-soteria-shell="true"] .oa__status-icon,
[data-soteria-shell="true"] .oa__note-icon,
[data-soteria-shell="true"] .req__flag-icon,
[data-soteria-shell="true"] .sd__status-icon,
[data-soteria-shell="true"] .sd__queue-icon,
[data-soteria-shell="true"] .doc__status-icon {
    margin-right: var(--ss-space-1);
}

/* An invitation that is waiting is not a problem to solve — amber,
   never red. Red is reserved for what has failed. */
[data-soteria-shell="true"] .oa__note {
    display: inline-flex;
    align-items: center;
    color: var(--ss-attention-text);
}

/* style.css gives `.btn-secondary` `flex: 1`, so wherever one is a flex
   item it GROWS to fill the row. In the legacy centred reading column
   that was barely visible; inside the shell's much wider content
   column the page-header link stretched into an empty box the width of
   the page, and the requirement row's Edit button pushed its siblings
   to the far edge. Local browser QA caught both at 1440px.

   The fix is deliberately NOT `[marker] .btn-secondary`: five pages
   that adopted the shell in earlier phases use that class too, and a
   marker-rooted rule would restyle every one of them. Each control is
   given its own page-scoped class instead, so the correction reaches
   exactly the four controls that are wrong and nothing else. The same
   latent stretch on those earlier pages is REPORTED rather than fixed
   here — it is not this phase's surface. */
[data-soteria-shell="true"] .oa__head-action,
[data-soteria-shell="true"] .req__head-action,
[data-soteria-shell="true"] .sd__head-action,
[data-soteria-shell="true"] .req__row-action {
    flex: 0 0 auto;
}

/* (d) The scroll the container was already promising. */
[data-soteria-shell="true"] .oa__table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

[data-soteria-shell="true"] .oa__table-scroll:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

/* An email address and an org-authored requirement name have no
   guaranteed break point once they exceed their column — the same
   reasoning as Phase 3N's table rule and the fingerprint-wrap fix
   below. */
[data-soteria-shell="true"] .oa__table-scroll td {
    overflow-wrap: anywhere;
}

/* staff-onboarding.html's heading is now STABLE (it used to be
   rewritten from "Loading staff onboarding…" to a name and then to
   "Not available" — three documents to a screen reader). The runtime
   value moved into a subtitle beneath it, which is Phase 3M decision
   (a) applied to the fourth page that had the same defect. */
[data-soteria-shell="true"] .sd__subject {
    margin: 0 0 var(--ss-space-2);
    font-size: var(--ss-size-sm);
    font-weight: var(--ss-weight-medium);
    color: var(--ss-text-muted);
}

/* The review queue is the reason to open this page. It reads as
   attention, not as failure: nothing here has gone wrong, somebody
   is simply waiting on a person. */
[data-soteria-shell="true"] .sd__queue-head {
    display: flex;
    align-items: center;
}

/* A requirement's own flags — required, restricted, acknowledgment,
   review — are facts about the requirement, not states of a person,
   so they stay quiet and read as labels rather than as status. */
[data-soteria-shell="true"] .req__flag {
    display: inline-flex;
    align-items: center;
    white-space: nowrap;
}

/* --ss-bp-phone: 560px
   Filters are the primary control on three of these four pages, and
   a mis-tap costs a re-read of the whole list. Every rule below is
   scoped to its own ids so it can never reach another page's filter
   row — the same confinement Phase 3N used for #statusFilter and
   #clientFilter. */
@media (max-width: 560px) {

    [data-soteria-shell="true"] #fSearch,
    [data-soteria-shell="true"] #fRole,
    [data-soteria-shell="true"] #fStatus,
    [data-soteria-shell="true"] #docSearch,
    [data-soteria-shell="true"] #docReq,
    [data-soteria-shell="true"] #docScope,
    [data-soteria-shell="true"] #docStatus {
        min-height: var(--ss-tap-min);
    }

    /* The three modals' OWN action rows, scoped to their own ids and
       never to the generic `.amend-actions` pattern other pages
       share. Approve and Reject in particular must not sit side by
       side at a width where a thumb can reach both at once. */
    [data-soteria-shell="true"] #inviteModal .amend-actions,
    [data-soteria-shell="true"] #reqModal .amend-actions,
    [data-soteria-shell="true"] #reviewModal .amend-actions {
        flex-direction: column;
        align-items: stretch;
    }

    [data-soteria-shell="true"] #inviteSend,
    [data-soteria-shell="true"] #inviteCancel,
    [data-soteria-shell="true"] #reqSave,
    [data-soteria-shell="true"] #reqCancel,
    [data-soteria-shell="true"] #reviewApprove,
    [data-soteria-shell="true"] #reviewReject,
    [data-soteria-shell="true"] #reviewCancel {
        min-height: var(--ss-tap-min);
    }
}


/* ============================================================
   SCHEDULING WORKFLOW (Phase 3P) — opt-in only

   schedule.html — the week calendar — adopts the Soteria Sanctuary
   shell. It is a cohort of ONE, and that is a finding rather than an
   omission: `sessions.html` and `session.html` are clinical session
   documentation, `client.html` and `dashboard.html` read appointments
   without being about the schedule, and the brief excludes clinical
   documentation by name. `schedule.html` is the only page carrying the
   appointment create/edit/cancel/delete dialogs and the only nav
   destination with the `ssi-calendar` mark.

   Every production-code change in this phase is markup or
   presentation: the shell opt-in, a `<main>` landmark, a stable <h1>,
   visible state words on the calendar chips, an announced scroll
   region, a polite count, and the page-scoped classes below. No
   query, projection, filter, ordering, RPC, write, gate or lifecycle
   transition changes; `app.js` and `style.css` are untouched.

   (a) THE WEEK RANGE WAS THE <h1>. `render()` overwrote the page's only
       top-level heading with the current week, so it announced
       "Loading…", then a date range, and never said what the page was
       — Phase 3M's `readiness.html` correction, in a second place. The
       stable `<h1>` is new markup; the week range KEEPS `id="weekLabel"`
       and its `.cal-title` class and merely stops being a heading, so
       the existing textContent write binds to the same element.

   (b) NO STATE RESTED ON A WORD. The chip encoded service type and
       cancellation in ONE colour channel (`.t-billable` … `.t-cancelled`),
       completion as a `✓` and a stripe, and an actual-vs-scheduled time
       difference as a `•` whose only explanation was a `title`
       attribute — unavailable on touch and unreliable to a screen
       reader. The legend translated the hues, but a legend is a lookup
       table, not a label, and collapsing a STATUS (cancelled) into the
       same channel as the four TYPES made a cancelled travel block and
       a cancelled billable one indistinguishable.

       Each chip now carries the word beside the colour. `status` is
       treated as its own axis: SQL/06 constrains it to four values, and
       `completed` and `no_show` had no visible carrier at all, because
       the colour rule only ever expressed `cancelled`.

       THE LIMIT IS STATED, NOT HIDDEN. A 15-minute appointment is 14px
       tall and clips its client and staff names TODAY; the state line
       clips with them, because chip height encodes duration and this
       phase must not change it. The `aria-label` is what makes state
       unconditional; the visible word is what removes the colour-only
       dependence at every readable size. Neither alone would be enough.

   (c) THE SCROLL WAS REAL AND UNREACHABLE. `.cal-scroll` carries
       `overflow-x: auto` in style.css and wraps a `.cal-grid` with
       `min-width: 900px`, so below ~900px there is genuinely hidden
       content — with no tabindex, no role and no name. This is the
       INVERSE of the Phase 3O `.table-wrap` defect: there a container
       promised a scroll it did not have; here it had one it never
       announced. The overflow itself is NOT redeclared here — style.css
       already supplies it correctly — so this phase adds only the
       focus ring, exactly as Phase 3N's `.rp__table-scroll` did.

   (d) The colour language is UNCHANGED. `t-billable … t-cancelled`,
       the completion stripe and the chip geometry are the page's
       existing meaning, and the brief requires the visual relationships
       between dates, times, staff, clients and statuses to survive.
       This phase adds a word beside the colour; it does not restyle
       the calendar.
   ============================================================ */

[data-soteria-shell="true"] .sc__title {
    margin: 0 0 var(--ss-space-3);
    font-size: var(--ss-size-2xl);
    font-weight: var(--ss-weight-bold);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

/* (d) Phase 3M decision (c) and Phase 3O decision (b), a third time. */
[data-soteria-shell="true"] .sc__result-count {
    margin: var(--ss-space-3) 0 0;
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

/* (b) The state line. `--ss-size-xs` and the chip's own inherited
   colour keep it subordinate to the client name, which stays the
   thing you read first. */
[data-soteria-shell="true"] .sc__chip-state {
    display: flex;
    align-items: center;
    gap: var(--ss-space-1);
    font-size: var(--ss-size-xs);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The mark is 1em so it rides the state line's own size rather than
   the page's, and it never grows past the word it accompanies. */
[data-soteria-shell="true"] .sc__chip-icon,
[data-soteria-shell="true"] .sc__status-icon {
    flex: 0 0 auto;
    width: 1em;
    height: 1em;
}

[data-soteria-shell="true"] .sc__status-icon {
    margin-right: var(--ss-space-1);
}

[data-soteria-shell="true"] .sc__detail-state {
    display: inline-flex;
    align-items: center;
}

/* (c) The focus ring the container needed. The overflow is style.css's
   and stays there. */
[data-soteria-shell="true"] .sc__grid-scroll:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}


/* ============================================================
   OPERATIONS (Phase 3Q) — opt-in only

   admin.html (clients, staff, supervision, assignments and the
   organisation record) and setup.html (the guided first pass over
   the same record) adopt the Soteria Sanctuary shell. They are one
   cohort on the repository's own evidence: `app.js` files BOTH under
   `group: "Operations"` and gives them the SAME `ssi-admin` mark, and
   of the eight pages that read `organizations` these are the only two
   that WRITE it — each scoped by `.eq("id", me.active_org_id)`.

   Every production-code change in this phase is markup or
   presentation: the shell opt-in, admin.html's <main> landmark, a
   polite region on the message line that carries every result,
   keyboard reach and a name for five real scroll containers, a mark
   beside each existing status word, and setup.html losing one
   duplicated live region. No query, projection, filter, ordering,
   RPC, write, gate or invitation rule changes; app.js and style.css
   are untouched, and no copy on either page was rewritten.

   (a) admin.html's <h1> WAS the landmark — `<h1 id="main">Admin</h1>`.
       renderSoteriaShell RELOCATES `#main`, so opting in without
       moving the id would have carried the heading into the shell and
       left the entire page — organisation form, four tab panels, every
       table — outside it. The `<main>` takes the id; the heading keeps
       its text and its position. This is the Phase 3P correction in a
       second place, and for a different reason: there the heading was
       being REWRITTEN, here it was doing the landmark's job.

   (b) #adminMessage carries every success and every failure on the
       page through showMessage(), and had no role and no aria-live —
       "Organisation saved", "Could not revoke", "Keep the logo under
       2 MB" were all announced to nobody. It becomes the page's one
       polite region. No second announcer is added: the five lists keep
       quiet, which is Phase 3M decision (c) and Phase 3O decision (b)
       arriving at the same answer from the other direction.

   (c) FIVE real scroll containers, none reachable. `.table-scroll`
       genuinely carries `overflow-x: auto` in style.css, and the
       assignment and supervision grids are the widest tables in the
       product. Each container gets `tabindex`, `role="region"` and its
       OWN name — five regions announced as "Table" would be five
       identical announcements. The overflow is NOT redeclared here:
       this is the Phase 3N/3L situation (a real scroll needing a ring)
       and not the Phase 3O one (a container promising a scroll it did
       not have). Never named `.ss-table`; P11 still owns the table
       foundation.

   (d) setup.html LOST a live region rather than gaining one.
       `#stepPanel` was `aria-live="polite"` and CONTAINS `#pStatus`
       and `#fStatus`, which are themselves `role="status"
       aria-live="polite"` — so one save announced twice, and a step
       change read the whole panel aloud. The three purpose-built
       regions keep the job. The step change is already announced
       correctly: both wizard buttons move focus to `#main`.

   (e) TWO PRE-EXISTING MARKER-ROOTED RULES NEWLY MATCH, enumerated
       here and pinned in tests/operations-ui.test.js rather than left
       to a browser to find. `[marker] .hidden` is INTENDED and
       required — it is the guard that keeps `.hidden` working once a
       later stylesheet's `.ss-` components set their own display, and
       both pages lean on it (#setupDenied, #setupBody, #adminMessage,
       four tab panels). `[marker] #fStatus { min-height: --ss-tap-min }`
       at <=560px is an ACCIDENTAL ID COLLISION: Phase 3O sized
       onboarding.html's filter SELECT `#fStatus`, and setup.html
       independently uses `#fStatus` for its finish-status PARAGRAPH.
       It is accepted rather than fixed — renaming would change a
       behaviour-bearing id this page's own script reads, and a
       counter-rule would add a selector to undo something cosmetically
       harmless on an element that is `.hidden` unless a save just
       happened. Phase 3Q's own `.op__` prefix cannot join that set,
       because no earlier phase names it.
   ============================================================ */

/* (c) The ring the containers needed. The overflow is style.css's and
   stays there. */
[data-soteria-shell="true"] .op__table-scroll:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

/* (b)/(d) One mark rule for the whole phase. Every status mark is an
   inline mark before a word that is already there — it never becomes
   the message on its own. */
[data-soteria-shell="true"] .op__status-icon {
    margin-right: var(--ss-space-1);
}

/* The pills and wizard states these marks sit inside are legacy
   inline elements; aligning the mark to the text baseline keeps the
   word and its mark on one line at every width. */
[data-soteria-shell="true"] .pill .op__status-icon,
[data-soteria-shell="true"] .wiz-state .op__status-icon {
    vertical-align: -0.125em;
}


/* ============================================================
   CLINICAL ROSTER (Phase 3R) — opt-in only

   clients.html (the roster) and client.html (the roster entry
   opened) adopt the Soteria Sanctuary shell. The cohort is TWO, not
   the four pages `NAV_PARENT` hangs under clients.html: target.html,
   behavior.html and training.html are clinical PROGRAMME editors —
   each with its own subject and its own tables — that merely share a
   nav parent. Measured by data surface, clients.html reads two tables
   and client.html seventeen, and client.html's are the roster
   entry's: guardians, assignments, authorisations, the care team.
   sessions.html / session.html / note.html are the clinical core and
   a later phase.

   Every production-code change is markup or presentation: the shell
   opt-in, a <main> landmark on both, the stable-record heading on
   client.html, polite regions on the two message lines that carry
   every result, one polite count for the filtered roster, dialog
   semantics on four overlays, keyboard reach and a name for one real
   scroll container, and a mark beside status words that already
   existed. No query, projection, filter, ordering, mutation, gate,
   signed-URL TTL or clinical rule changes; app.js and style.css are
   untouched; no copy was reworded on either page.

   (a) client.html had NO `id="main"` AT ALL. renderSoteriaShell does
       `const main = document.getElementById("main"); if (main) …`, so
       opting in without adding one would have built the shell around
       nothing and rendered the whole client record outside it. Both
       pages now carry the landmark; on clients.html it took the id
       OFF the <h1>, which had been doing the landmark's job.

   (b) THE HEADING WAS THE CLIENT'S NAME. client.html's <h1> was
       `#clientName`, rewritten at runtime from "…" to a person's
       name — Phase 3M decision (a) and Phase 3O's staff-onboarding
       correction, now on the third page with the same defect, and
       here it is also a PRIVACY point: the page's top-level heading
       was a client identity. The heading is stable; #clientName keeps
       its id, its "…" loading text and its single writer, and only
       the element it names moved. No accessible name on either page
       carries a client identity — the four dialogs are labelled by
       their own static headings.

   (c) BOTH MESSAGE LINES ANNOUNCED NOTHING. #listMessage and
       #clientMessage carry every error these pages report through
       showMessage() and had no role and no aria-live. Each becomes
       its page's polite region. clients.html additionally gets ONE
       count region for the filtered grid — the search and status
       filter repainted it silently, and an empty result sounded
       exactly like a failed load. #scopeNote is NOT made live: it
       states the organisation total and does not move with the
       filter, so the two are different facts rather than a
       duplicated announcement.

   (d) ONE real scroll container, on client.html's authorisation
       service lines. `.table-scroll` genuinely carries `overflow-x:
       auto` in style.css, so this is the Phase 3N/3L/3Q situation and
       the ring is all that is added here — the overflow is not
       redeclared. It is never named `.ss-table`; P12 and beyond still
       own any shared table foundation. clients.html gets NO scroll
       region: it renders a card grid, and a container that does not
       scroll must not claim to be one.

   (e) A BILLING FIGURE WAS EXAMINED AND DELIBERATELY LEFT ALONE. The
       service-line table marks a Remaining cell `is-tight` — a colour
       — at 90% utilisation. It is NOT colour-only: the same row
       states "X.X% used" as visible text one cell away, so the fact
       is already written down and the colour is redundant emphasis.
       Adding a word would mean naming a billing policy, and the
       code's own comment is careful that 90% is "the point at which
       someone needs to request a renewal, NOT the point at which
       sessions stop". Inventing "low" or "renewal due" is a product
       decision this phase may not take. The redundancy is pinned
       instead.

   (f) THE CLINICAL COLOUR NAMESPACE IS UNTOUCHED. No `--ss-clinical-*`
       token is read here, and no clinical colour is reused as an
       interface status colour. The programme `phase-*` pills and the
       measurement pills are labels rather than statuses and keep
       exactly the presentation they had.

   NOT DONE, recorded: the four overlays gain role, aria-modal and
   aria-labelledby, which is markup. A focus trap, initial focus and
   focus restoration are BEHAVIOUR, and no earlier Sanctuary phase has
   added them to a legacy `.dialog`. That gap is left open, not closed
   quietly here.
   ============================================================ */

/* (b) The runtime client identity, no longer the page heading. */
[data-soteria-shell="true"] .cr__subject {
    margin: 0 0 var(--ss-space-1);
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

/* (c) Phase 3M decision (c), Phase 3O (b) and Phase 3Q again. */
[data-soteria-shell="true"] .cr__result-count {
    margin: 0 0 var(--ss-space-3);
    font-size: var(--ss-size-sm);
    color: var(--ss-text-muted);
}

/* One mark rule for the whole phase. Every mark is an inline mark
   before a word that is already there — it never becomes the message
   on its own. */
[data-soteria-shell="true"] .cr__status-icon {
    margin-right: var(--ss-space-1);
    vertical-align: -0.125em;
}

/* Local browser QA at 1101px and below found DOCUMENT-LEVEL horizontal
   overflow on client.html — the aside pushed past the viewport.

   The cause is a viewport breakpoint inside a narrowed container.
   style.css sets `.client-layout { grid-template-columns: 1fr 320px }`
   and collapses it to one column only at `max-width: 900px`, which
   measures the VIEWPORT. The shell spends 272px (or 96px) of that
   viewport on the sidebar, so at a 1101px viewport the content column
   is about 989px while the media query still sees 1101px and keeps two
   columns. The `1fr` track then cannot shrink below its own min-content
   — a grid item defaults to `min-width: auto`, and the authorisation
   table's min-content is wide — so the fixed 320px aside is pushed off
   the page.

   `minmax(0, 1fr)` releases that floor, so the track shrinks and the
   table scrolls inside the `.table-scroll` container that already has
   `overflow-x: auto` — which is also what makes this phase's scroll
   region genuinely scrollable rather than merely labelled.

   Guarded at `min-width: 901px` so style.css's own single-column
   collapse below 900px is left completely alone, and page-scoped so no
   other grid on any adopted page is touched. This is the Phase 3O
   `.btn-secondary { flex: 1 }` lesson again: a legacy rule that was
   harmless in the full-width layout is not harmless in the shell's
   narrower column. */
@media (min-width: 901px) {

    [data-soteria-shell="true"] .cr__layout {
        grid-template-columns: minmax(0, 1fr) 320px;
    }
}

/* The same floor, one level down and at EVERY width. `.client-main` is
   the grid item inside that track, and a grid item also defaults to
   `min-width: auto` — so below 900px, where style.css collapses the
   layout to a single column, the item still refused to shrink and the
   page overflowed again at 768, 561 and 560.

   What was forcing it wide is the tab row: seven tabs have a
   min-content of about 770px, and `.client-tabs` ALREADY carries
   `overflow-x: auto` in style.css for exactly that reason. Releasing
   the item's floor is what lets that existing rule finally do its job —
   the tab row scrolls instead of the document.

   The tab row is deliberately NOT given `role="region"` and a
   tabindex: it is a `role="tablist"`, its tabs are individually
   focusable, and a browser scrolls a focused tab into view. Wrapping a
   tablist in a scroll region would break the tab semantics to solve a
   problem the tab semantics already solve. */
[data-soteria-shell="true"] .cr__main {
    min-width: 0;
}

/* Local browser QA measured these four close controls at 31x26 — under
   the 48px Sanctuary tap target. The fix is page-scoped on purpose: a
   marker-rooted `.dialog-close` rule would resize the legacy dialogs on
   every page adopted since Phase 3J, including schedule.html's two.
   The same latent shortfall on those pages is REPORTED, not fixed here.
   Sizing only — no dialog behaviour changes. */
[data-soteria-shell="true"] .cr__dialog-close {
    min-width: var(--ss-tap-min);
    min-height: var(--ss-tap-min);
}

/* (d) The ring the container needed. The overflow is style.css's and
   stays there. */
[data-soteria-shell="true"] .cr__table-scroll:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}


/* ============================================================
   CLINICAL CORE (Phase 3S) — opt-in only, TWO DIFFERENT OPT-INS

   sessions.html (the session list), session.html (the recorder) and
   note.html (the clinical note) are one workflow: a closed triangle of
   handoffs, with `NAV_PARENT` mapping note.html to sessions.html.
   They do NOT take the same opt-in, and that is the shape of this
   phase.

   ONLY sessions.html takes the SHELL. session.html and note.html have
   no `#appHeader` and never call renderNav(), and renderNav() opens
   `if (!header) return;` — so the shell cannot reach them without
   ADDING a header and a nav call. That would put full application
   navigation onto an in-progress clinical recording screen and onto
   the signed-note surface, which this codebase already refuses to do:
   paintAccountBar omits the workspace switch on session.html because
   "unfinished clinical work must never be carried into another
   organization"; guardTenantExit blocks leaving with unsaved data;
   the bottom-bar mark is "deliberately NOT a link — a stray tap
   mid-session must never navigate away from unsaved data"; and the
   beforeunload guard is documented as best-effort on mobile/PWA.
   The owner reviewed that evidence and authorized this split.

   So the two chrome-free pages take PHASE 3K'S PATTERN exactly: a
   DIFFERENT attribute, `data-ss-clinical`, never the shell marker —
   every rule for them is rooted at it, so this half can never reach a
   shell page and the shell marker can never reach these two. Phase 3K
   also records WHY it reaches targets through the attribute root plus
   existing markup rather than classes it invents: tests/fieldwork-ui
   freezes static `class="…ss-…"` attributes, and an attribute nobody
   scans for "ss-" composes with that freeze instead of fighting it.
   The `.cc__` prefix used here contains no "ss-" and is new to this
   phase, so no earlier block can reach it and it trips no freeze.

   NO navigation, header, renderNav(), sidebar, workspace switch or
   new exit route is added to either chrome-free page. session.html's
   existing exits — the account-bar "← Dashboard" and "Past sessions",
   and the writer-block overlay's "← Back to dashboard" — are
   untouched, as are unsavedClinicalWork(), guardTenantExit() and the
   beforeunload guard.

   (a) THREE HEADINGS, THREE DIFFERENT PROBLEMS. sessions.html's <h1>
       was the landmark (`id="main"`), so the shell would have
       relocated the heading alone — the Phase 3Q correction again.
       session.html's <h1> WAS the target name, rewritten on every
       target change: a moving heading carrying clinical identity, and
       the fourth page with this defect after 3M, 3O and 3R. Its two
       writers use `querySelector(".target-name")`, so the CLASS is
       the contract and it is preserved exactly; only the tag moved.
       note.html had SEVEN <h1> elements and no page-level heading at
       all — five document titles and two bundle cover titles.

   (b) THE STABLE HEADINGS ARE PLACED WHERE THEY COST NOTHING.
       session.html's is visually hidden: the counting screen is a
       clinical instrument whose density is part of its safety, and a
       visible banner above the target display would push the tap
       targets down. note.html's sits inside the toolbar that is
       ALREADY `.no-print`, so the printed record gains nothing and
       loses nothing; its document titles became h2 keeping their
       class and text, and since `.note-title` and `.cover-title` are
       styled by CLASS in both style.css and print.css — there is no
       `h1.note-title` rule anywhere — the printed output is unchanged
       in appearance.

   (c) THE THREE MESSAGE LINES ANNOUNCED NOTHING. #listMessage,
       #sessionMessage and #noteMessage carry every result, error and
       REFUSAL these pages report through showMessage(), with no role
       and no aria-live — including "Press Start session before
       logging data." and every amendment refusal. Each becomes its
       page's one polite region. Nothing else on any of the three is
       made live, and in particular the trial strip is NOT: a rapidly
       changing clinical-data container must never announce itself.

   (d) THE TRIAL STRIP GETS A NAME, NOT A WORD. It paints the last 15
       responses as bare colour dots. Visible words inside a 15-dot
       sparkline would destroy the density the tallying screen depends
       on — so each dot gets `role="img"` and an aria-label carrying
       THE PAGE'S OWN EXISTING WORD: the three response buttons
       directly below are labelled Independent / Prompted / Incorrect
       in those same three colours, and the aggregate is already text
       in #pctDisplay + #scoreUnit. No new clinical vocabulary is
       invented, no policy is defined and no pixel moves. The
       colour→word legend is therefore on screen at all times, which
       is what keeps the strip from being colour-alone visually.

   (e) NINE real scroll containers on note.html, all genuinely
       scrolling and none reachable or named. Each is named for its
       own SUBJECT — never for a client, a note or any clinical
       identifier. The overflow is NOT redeclared: style.css already
       gives `.table-scroll` `overflow-x: auto`, so this is the Phase
       3N/3L/3Q/3R situation and only the ring is added here.
       sessions.html and session.html get no scroll region — neither
       has a table, and a container that does not scroll must not
       claim to be one.

   (f) THE CLINICAL-RESPONSE COLOUR NAMESPACE IS UNTOUCHED. No
       `--ss-clinical-*` token and no `--independent` / `--prompted` /
       `--incorrect` value is read here, and no clinical colour is
       reused as an ordinary interface status colour.

   NOT DONE, recorded: no focus trap, initial focus or Escape handling
   was added to session.html's legacy `.s-overlay` cards. That is
   BEHAVIOUR on a surface that gates clinical recording, no earlier
   phase has added it to a legacy overlay, and it could not be proven
   presentation-only here.
   ============================================================ */

/* (b) The stable page heading on the two chrome-free pages. On
   session.html it is visually hidden; on note.html it sits in the
   already-no-print toolbar and is seen. */
body[data-ss-clinical="session"] .cc__page-title {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

body[data-ss-clinical="note"] .cc__page-title {
    margin: 0;
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text-muted);
}

/* (e) The ring the containers needed. The overflow is style.css's and
   stays there. */
body[data-ss-clinical="note"] .cc__table-scroll:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

/* One mark rule for the shell half of this phase. The mark is an
   inline mark before a word that is already there. */
[data-soteria-shell="true"] .cc__status-icon {
    margin-right: var(--ss-space-1);
    vertical-align: -0.125em;
}


/* ============================================================
   CLINICAL PROGRAMME EDITORS (Phase 3T) — opt-in only

   target.html, behavior.html and training.html — the three programme
   editors Phases 3R and 3S each excluded together, by name and for the
   same reason: measured by data surface, each reads its own subject's
   tables and none reads the roster entry's guardians, assignments or
   authorisations. `NAV_PARENT` maps all three under clients.html, so
   they are detail pages of the roster rather than destinations.

   All three already carry `#appHeader` and `<body class="wide">`, so
   all three take the FULL SHELL — not Phase 3S's chrome-free pattern.
   That is the decisive difference from session.html and note.html, and
   it means NO NAVIGATION IS ADDED, REMOVED, REGROUPED OR REWRITTEN:
   renderNav() already ran on these pages and now renders the shell
   instead of the legacy header.

   Every production-code change is markup: the shell opt-in, a <main>
   landmark, a stable <h1>, a polite region on each page's own message
   line, dialog semantics on the one dialog each, and the two classes
   below. No query, projection, filter, ordering, write, phase-history
   rule, chart calculation, refusal string or gate changes; app.js,
   style.css and print.css are untouched; and no clinical or
   user-facing copy was rewritten.

   (a) TWO MORE DYNAMIC HEADINGS. target.html's <h1> WAS #targetName
       and behavior.html's WAS #behName — each rewritten at runtime
       with a clinical subject's name, and each also the skip-link
       target. This is the Phase 3M decision (a) correction on the
       fifth and sixth page, after readiness.html, staff-onboarding
       .html, client.html and session.html. Both ids keep their exact
       "…" loading text and every writer; only the element moved, and
       the skip link retargets to the new landmark like every other
       adopted page. training.html's heading TEXT was already stable
       and is untouched — only the landmark id moved off it.

   (b) ONE POLITE REGION PER INDEPENDENTLY WRITTEN LINE. Each page has
       two message ids: a page-level one (#targetMessage, #behMessage,
       #ptMessage) and a dialog-level one (#phMessage, #gMessage), and
       ALL SIX are polite status regions.

       This paragraph originally said only the page-level line should
       announce, citing the non-duplication rule Phases 3M, 3O, 3Q and
       3S applied. Independent review found the citation misapplied,
       and the correction shipped in P13 itself — merged to `main`
       through PR #54, together with the markup it describes. Only this
       comment was left behind, and it is corrected here. Those dialog
       lines are INDEPENDENT WRITERS whose validation and save refusals
       reach no other line, so silence there duplicated nothing — it
       meant the refusal was never announced at all, inside the dialog
       the user had just acted in. The rule counts WRITERS, not pages.
       Phase 3M correctly REMOVED a region because two regions
       announced the same save; Phase 3Q's setup.html correctly
       carries THREE.

   (c) NO SCROLL REGION IS CLAIMED. None of the three has a table or a
       `.table-scroll`; target.html and behavior.html render SVG charts
       and training.html renders forms. A container that does not
       scroll must not claim to be one, so this phase adds none.

   (d) THE PHASE 3R LAYOUT TRAP DID NOT REPRODUCE, and the fix was
       REMOVED rather than shipped. All three use the same
       `.client-layout` > `.client-main` + `aside` grid that overflowed
       on client.html, so page-scoped `minmax(0, 1fr)` / `min-width: 0`
       classes were written first — and then offline QA measured NO
       document overflow at any width on any of the three, before the
       fix. The reason is specific: on client.html the floor was forced
       by the authorisation service-line TABLE's min-content, and these
       three have no tables at all. The classes were deleted. A rule
       with no defect behind it is not a smaller change, it is an
       unexplained one.

   (e) CLINICAL STATE ALREADY CARRIES A WORD. The phase pills render
       `PHASE_LABEL[p.phase]` beside the colour on both phase editors,
       training.html renders its goal status the same way, and a signed
       note reads "Signed <datetime>". Nothing here rests on colour or
       an icon alone, so this phase adds no status mark and restyles no
       pill — the `phase-*` colours are the pages' existing clinical
       vocabulary and are untouched.

   NOT DONE, recorded: no focus trap, initial focus, Escape handling or
   focus restoration was added to these legacy `.dialog` overlays. That
   is behaviour, it is not shared behaviour that could be proven
   unchanged, and no earlier Sanctuary phase has added it.
   ============================================================ */

/* (a) The runtime clinical subject, no longer the page heading. */
[data-soteria-shell="true"] .pe__subject {
    margin: 0 0 var(--ss-space-1);
    font-size: var(--ss-size-lg);
    font-weight: var(--ss-weight-medium);
    line-height: var(--ss-leading-tight);
    color: var(--ss-text);
}

/* The Phase 3R measurement again: the legacy `.dialog-close` is 31x26,
   under the 48px tap target. Page-scoped on purpose — a marker-rooted
   `.dialog-close` rule would resize the legacy dialogs on every page
   adopted since Phase 3J. Sizing only; no dialog behaviour changes. */
[data-soteria-shell="true"] .pe__dialog-close {
    min-width: var(--ss-tap-min);
    min-height: var(--ss-tap-min);
}


/* ============================================================
   PHASE 3U — CLINICAL OVERSIGHT
   supervision.html and ioa.html: the role-scoped Care destination
   and the page NAV_PARENT maps beneath it. They link directly to
   each other and form one workflow, so they adopt together.

   Both already carry #appHeader, so both take the FULL SHELL and no
   navigation is added, removed, regrouped or rewritten. Their
   AUTHORIZATION CONTRACTS DIFFER AND STAY DIFFERENT: supervision
   .html gates on `requireAuth({ roles: ["bcba", "admin"] })` and
   ioa.html on a bare `requireAuth()`. Cohorting two pages does not
   unify their gates, and this phase changes neither.

   Every HTML change is markup: the shell opt-in, a <main> landmark on
   each, the landmark id moved off two headings whose literal text is
   unchanged, a polite region on every independently written message
   line, dialog semantics on supervision.html's two dialogs, two named
   scroll regions, one <label> corrected to a <p>, and a page-specific
   class on each of the two page-header links. The stylesheet adds
   those THREE page-specific classes — `.co__dialog-close`,
   `.co__table-scroll` and `.co__head-action` — in the FIVE rules
   below: close sizing, the scroll itself, a focus-visible ring,
   table-cell wrapping, and page-header action containment. No
   query, projection, filter, ordering, limit, RPC, insert payload,
   write order, IOA calculation rule, signature contract, refusal
   string or gate changes; app.js, style.css and print.css are
   untouched.

   (a) NEITHER HEADING WAS DYNAMIC — the defect was the LANDMARK.
       Both pages already had a stable literal <h1> ("Supervision",
       "Interobserver Agreement"), so this is NOT the Phase 3M
       decision (a) case. What both had instead was `id="main"` on the
       <h1> ITSELF and NO <main> element at all: the skip link landed
       on a heading, and neither page had a main landmark to skip to.
       Measured before the change on both pages: `main` count 0, skip
       target `h1`. The id moved onto a real <main id="main"
       tabindex="-1">; both heading texts are untouched.

   (b) FOUR WRITERS, FOUR ANNOUNCERS — the corrected P13 rule, applied
       from the start. supervision.html writes THREE message lines
       from separate call sites (#bcbaMessage page-level, #rvMessage
       in the review dialog, #spMessage in the note dialog) and
       ioa.html writes ONE (#ioaMessage). None was a live region
       before; measured live-region count was 0 on both pages. Every
       one of them is an INDEPENDENT writer — no page copies a message
       into another — so each becomes a polite status region declared
       exactly once. The unit is the writer, not the page: leaving a
       dialog line silent would not prevent a duplicate announcement,
       it would prevent its own refusals ("Sign to countersign this
       note.", "RBT and date are required.") being announced at all.
       #ioaMessage additionally carries this application's only
       "IOA result saved." success message.

   (c) TWO UNNAMED DIALOGS, AND THE 31x26 CLOSE CONTROL AGAIN.
       supervision.html's review and supervision-note overlays carried
       no role, no aria-modal and no accessible name; both already had
       an <h2> ("Review note", "Supervision note"), so each is now
       labelled by the heading it already displayed — no new copy.
       Their close controls measured 31x26, the same figure Phases 3R
       and 3T recorded, and are raised PAGE-SCOPED: a marker-rooted
       `.dialog-close` rule would resize the legacy dialogs on every
       page adopted since Phase 3J. Sizing and naming only — no focus
       trap, no Escape handling, no workflow change.

   (d) TWO TABLES, TWO DIFFERENT DEFECTS, MEASURED APART. This is the
       Phase 3N/3O contrast on one page pair, and the measurement is
       what separates them:

       supervision.html's notes table is `.table-scroll`, which
       style.css really does give `overflow-x: auto`. Measured, it
       genuinely scrolls (589 > 285 at 390, 589 > 455 at 560) and the
       DOCUMENT never overflowed — but the container was neither
       reachable by keyboard nor named. It gets the focus ring and the
       name only, exactly like Phase 3N's `.rp__table-scroll`.

       ioa.html's history table is `.table-wrap` — a class that is
       declared NOWHERE in style.css and has therefore never done
       anything. That is Phase 3O decision (d) on a second page, and
       here it was not latent: at 390 the table escaped its container
       and overflowed the DOCUMENT, measured 425 > 375. The rule below
       supplies the scroll the container needed, which removes the
       document overflow AND lets it honestly carry `tabindex` and
       `role="region"`. Both share one class because the correction is
       the same shape; neither is `.ss-table`, and this establishes no
       shared table API.

   (e) A <label> CANNOT LABEL A <canvas>. supervision.html captioned
       the review signature pad with a bare `<label class="field-
       label">Your signature</label>` that labels nothing — offline QA
       counted 24 of 25 labels wired, which is how it surfaced, and it
       is the Phase 3T finding on a second page. It is now a <p> with
       the same class, text and appearance. NO accessible name was
       invented for either <canvas>: naming a signing control is a
       clinical decision this phase did not take.

   (f) NO STATUS RESTS ON COLOUR. The notes table renders "Signed" /
       "Unsigned" pills, the supervisee list renders "N unsigned",
       an overdue review row carries style.css's generated "over a
       week", and the IOA table renders its method, its percentage or
       "N/A", and its source versions as words. Nothing here rests on
       colour or an icon alone, so this phase adds no status mark and
       restyles no pill.

   (g) EXACTLY ONE PRE-EXISTING SANCTUARY SELECTOR NEWLY MATCHES EACH
       PAGE, and it is the required one: `[marker] .hidden`. Derived
       mechanically from the stylesheet against every id and class in
       both files, including those emitted at runtime. That guard is
       what keeps `.hidden` working once a page opts in — both pages
       depend on it heavily (`.dialog.hidden`, `#rvSignWrap`,
       `#spPauseWrap`, `#ioaCompat`, `#ioaMethodBlock`, `#ioaResult`
       and every `.msg.hidden`). No other rule from any earlier phase
       reaches either page.

   (h) HOSTED QA FOUND ONE PRESENTATION DEFECT, and it is the Phase 3O
       `.btn-secondary` stretch plus the 74px height that phase
       reported and left alone. Both page-header links grew to fill
       their row and stood 74px tall beside a 48px primary action.
       Fixed with a third page-scoped class rather than a marker-rooted
       `.btn-secondary` rule; the measurement and the reasoning are at
       the rule itself, below.

   NOT DONE, recorded: no focus trap, initial focus, Escape handling
   or focus restoration was added to these legacy `.dialog` overlays,
   and no accessible name was invented for a signature canvas.
   ============================================================ */

/* (h) HOSTED QA · the Phase 3O `.btn-secondary` stretch, and the 74px
   height that phase reported and left alone.

   `style.css` gives every `.btn-secondary` `flex: 1` and `min-height:
   var(--tap-min)` with NO `box-sizing`, so an anchor carrying it keeps
   the initial `content-box`. Both page-header links are flex items —
   ioa.html's sits directly in `.dash-head`, supervision.html's in
   `.head-actions` — so both grew, and on both the 48px minimum applied
   to the CONTENT box: 48 + 12 + 12 padding + 2 border = 74px OUTER.

   Measured before: ioa.html's "← Supervision" 1113x74 at 1440 and
   617x74 at 768; supervision.html's "Interobserver agreement" 160x74
   beside the `.btn-cta` it sits with, which is `border-box` and lands
   at exactly 195x48. The button next to it was already right, which is
   what made the anchor's height wrong rather than the design's.

   Phase 3O fixed only the STRETCH, with `flex: 0 0 auto` on four
   page-scoped classes, and REPORTED the 74px height as needing a
   `style.css` edit. It does not: `box-sizing` page-scoped settles it
   without touching the legacy class, so this rule fixes both halves.

   Page-scoped ON PURPOSE. A marker-rooted `[marker] .btn-secondary`
   rule would restyle that class on every page adopted since Phase 3J
   AND both dialog Cancel buttons on supervision.html itself, where
   `flex: 1` inside `.dialog-actions` is the INTENDED sizing. This
   reaches exactly the two page-header links and nothing else. */
[data-soteria-shell="true"] .co__head-action {
    flex: 0 0 auto;
    box-sizing: border-box;
}


/* (c) The Phase 3R measurement a third time: the legacy
   `.dialog-close` is 31x26, under the 48px tap target. Page-scoped on
   purpose. Sizing only; no dialog behaviour changes. */
[data-soteria-shell="true"] .co__dialog-close {
    min-width: var(--ss-tap-min);
    min-height: var(--ss-tap-min);
}

/* (d) The scroll ioa.html's container was promising and did not have.
   Harmless on supervision.html's, which already had it from
   style.css — the declaration is the same either way. */
[data-soteria-shell="true"] .co__table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

/* A focusable region must show where the focus is. */
[data-soteria-shell="true"] .co__table-scroll:focus-visible {
    outline: var(--ss-focus-ring);
    outline-offset: var(--ss-focus-offset);
}

/* A client name, an RBT name and an org-authored target name have no
   guaranteed break point once they exceed their column — the same
   reasoning as Phase 3N's and Phase 3O's table rules. */
[data-soteria-shell="true"] .co__table-scroll td {
    overflow-wrap: anywhere;
}


/* ============================================================
   FORMS AND FILES — REV 2 fingerprint-wrap fix
   The Forms and files section keeps its pre-existing .dash-card /
   .note-grid presentation (see the Month end section's own note
   above) — this is not a shell-gated redesign, just one targeted
   fix. A file_hash is one unbroken token with no natural break
   point; plain text wrapping only breaks at whitespace, so a long
   hash overflowed its card on a narrow phone instead of wrapping.
   ============================================================ */

[data-soteria-shell="true"] #exOfficialList .note-hint.numeric,
[data-soteria-shell="true"] #exContractList .note-hint.numeric {
    overflow-wrap: anywhere;
}


/* ============================================================
   PRINT
   A signed record printed in greyscale must still be readable,
   which is why every status carries a word.
   ============================================================ */

@media print {

    .ss-card,
    .ss-banner,
    .ss-table-wrap {
        box-shadow: var(--ss-shadow-none);
        border-color: var(--ss-border-strong);
    }
}
