/* The White Orchid - Mobile-First Styles */

/* ═══ 0. BRAND FONTS ═══ */

@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-Light.woff2') format('woff2'), url('/fonts/FiraSans-Light.ttf') format('truetype');
    font-weight: 300;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-LightItalic.woff2') format('woff2'), url('/fonts/FiraSans-LightItalic.ttf') format('truetype');
    font-weight: 300;
    font-style: italic;
    font-display: swap;
}
@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-Regular.woff2') format('woff2'), url('/fonts/FiraSans-Regular.ttf') format('truetype');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-Italic.woff2') format('woff2'), url('/fonts/FiraSans-Italic.ttf') format('truetype');
    font-weight: 400;
    font-style: italic;
    font-display: swap;
}
@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-Medium.woff2') format('woff2'), url('/fonts/FiraSans-Medium.ttf') format('truetype');
    font-weight: 500;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-MediumItalic.woff2') format('woff2'), url('/fonts/FiraSans-MediumItalic.ttf') format('truetype');
    font-weight: 500;
    font-style: italic;
    font-display: swap;
}
@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-SemiBold.woff2') format('woff2'), url('/fonts/FiraSans-SemiBold.ttf') format('truetype');
    font-weight: 600;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-SemiBoldItalic.woff2') format('woff2'), url('/fonts/FiraSans-SemiBoldItalic.ttf') format('truetype');
    font-weight: 600;
    font-style: italic;
    font-display: swap;
}
@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-Bold.woff2') format('woff2'), url('/fonts/FiraSans-Bold.ttf') format('truetype');
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-BoldItalic.woff2') format('woff2'), url('/fonts/FiraSans-BoldItalic.ttf') format('truetype');
    font-weight: 700;
    font-style: italic;
    font-display: swap;
}
@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-ExtraBold.woff2') format('woff2'), url('/fonts/FiraSans-ExtraBold.ttf') format('truetype');
    font-weight: 800;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: 'Fira Sans';
    src: url('/fonts/FiraSans-ExtraBoldItalic.woff2') format('woff2'), url('/fonts/FiraSans-ExtraBoldItalic.ttf') format('truetype');
    font-weight: 800;
    font-style: italic;
    font-display: swap;
}
/* Modern-emoji gap filler: Windows 10's Segoe UI Emoji predates Unicode 14 (2021), so every
   Emoji 14.0, 15.0, 15.1 and 16.0 codepoint (introduced 2021 to 2024) renders as a tofu box
   there, while iOS/Android render fine. unicode-range restricts this font to ONLY those
   codepoints, compiled from the official unicode.org emoji-test.txt files for emoji versions
   14.0, 15.0, 15.1 and 16.0 (cumulative version tags in the 16.0 file), so pre-2021 emoji keep
   rendering from the system font everywhere, and the font downloads only when a page actually
   renders a covered codepoint. Font file: @fontsource/noto-color-emoji 5.3.0, the single
   non-subsetted "emoji" variant (self-hosted, not a CDN hotlink, so it does not depend on
   fonts.googleapis.com being reachable). Family name matches the existing 'Noto Color Emoji'
   entry already in --font-family below, so no other change is needed to wire it in. */
@font-face {
    font-family: 'Noto Color Emoji';
    src: url('/fonts/NotoColorEmoji-Modern.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    unicode-range: U+1F6DC-1F6DF, U+1F7F0, U+1F979, U+1F9CC, U+1FA75-1FA77, U+1FA7B-1FA7C, U+1FA87-1FA89, U+1FA8F, U+1FAA9-1FAAF, U+1FAB7-1FABF, U+1FAC3-1FAC6, U+1FACE-1FACF, U+1FAD7-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8;
}

/* ═══ 1. RESET & BASE ═══ */

:root {
    /* Drives the UA canvas/backdrop colour. Without this the browser's base
       layer stays light, so iOS WebKit flashes a pale-cream backdrop during
       cross-document navigation (dashboard ↔ messenger) before the new page's
       dark body paints. [data-theme="dark"] flips it to dark below. */
    color-scheme: light;
    /* Engines that support this keyword transition height:auto for real (no
       JS measuring, no max-height hack). Consumers gate the hack behind
       @supports not (interpolate-size: allow-keywords) so unsupporting
       engines keep today's behaviour exactly (huoma example: .cal-detail
       in css/style-05.css). See docs/frontier-26-techniques.md #7 (ported
       from canopy commit ebe10858). */
    interpolate-size: allow-keywords;
    --color-primary: #E8691E;
    --color-primary-light: #F2914E;
    --color-primary-dark: #B84E0F;
    --color-on-primary: #fff;
    --color-accent: #F7D3BE;
    --color-whatsapp: #25D366;
    --color-bg: #F5F6E7;
    --color-card: #FFFFFF;
    --color-surface: #FFFFFF;
    --color-text: #2D2D2D;
    --color-text-light: #6B6B6B;
    --color-muted: var(--color-text-light);
    --color-bg-subtle: rgba(0, 0, 0, 0.03);
    --color-border-subtle: rgba(0, 0, 0, 0.08);
    --color-success: #1E6B3D;
    --color-success-bg: #e8f7ee;
    --color-error: #C0392B;
    --color-error-bg: #fdecea;
    --color-border: #e1e2d3;
    --color-shadow: 0 2px 8px rgba(232, 105, 30, 0.1);
    --color-danger: #C0392B;
    --color-msgr-tick-read: #4fc3f7;
    --color-primary-soft: rgba(232, 105, 30, 0.12);
    --color-warning: #f59e0b;
    --color-warning-bg: #fff8e1;
    /* Search-result / jump-to-message highlight mark (ported from canopy). Opaque pair
       (own background + own text colour), deliberately not reusing --color-warning-bg:
       a highlight is drawn on surfaces the token system does not control, including the
       primary-coloured own message bubble whose text is hardcoded #fff, so anything
       that inherits the surrounding colour or relies on a translucent tint fails
       somewhere. Painting both halves makes it legible on a light bubble, a dark
       bubble and a brand-coloured one alike. Overridden per theme in style-04 only to
       soften the glare on dark, never to change the contrast relationship. */
    --color-search-mark-bg: #ffd24a;
    --color-search-mark-text: #2a2100;
    /* Theme-invariant accents: the attention/pulse red shared by every pending
       dot, and the fixed HuoMa brand orange (never follows per-org --color-primary). */
    --color-pulse-dot: #ef4444;
    --color-huoma-brand: #E8691E;
    --font-family: 'Fira Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Color Emoji', sans-serif;
    --color-surface-alt: #EDEEE0;
    --color-hover: rgba(0, 0, 0, 0.04);
    --color-title-color: var(--color-primary);
    --color-title-font: inherit;

    /* Font size tokens - 5 categories, desktop defaults */
    --font-size-heading: 1.25rem;
    --font-size-subheading: 0.75rem;
    --font-size-body: 0.88rem;
    --font-size-label: 0.82rem;
    --font-size-small: 0.72rem;
    /* Messenger message body. Desktop matches --font-size-body (unchanged); the
       mobile override below bumps it one step larger for comfortable phone reading. */
    --font-size-msgr: var(--font-size-body);

    /* Animation easing tokens */
    --ease-out: cubic-bezier(0.4, 0, 0.2, 1);
    --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
    --ease-keyboard: cubic-bezier(0.38, 0.7, 0.125, 1); /* approximates the iOS soft-keyboard slide curve */

    /* Soft-keyboard inset (px). Runtime value driven by _wireKeyboardInset() in
       js/shared.js from visualViewport — height of the on-screen keyboard, or 0
       when hidden / on desktop. Consumed by .modal-overlay + .modal so a focused
       field is always lifted clear of the keyboard. See js/shared.js for why. */
    --kb-inset: 0px;

    /* Animation duration tokens (orion_motion). The companion to the easing
       tokens above; the Motion Library primitives reference these by name so
       js/animations.js ANIMATION_REGISTRY durations stay parity-checkable. */
    --dur-micro: 120ms;       /* press / tactile give */
    --dur-enter: 220ms;       /* card / list entrance */
    --dur-feedback: 250ms;    /* pop / pulse / value bump */
    --dur-transition: 300ms;  /* tab / view swap */
    --dur-celebrate: 450ms;   /* success */
    --dur-swipe: 500ms;       /* full-panel push / pop (matches Telegram iOS) */

    /* ── DESIGN TOKENS (--dt-*) ──────────────────────────────────────────────
       App-wide design geometry. The values here are the CANONICAL DEFAULTS,
       the single source of truth each token falls back to when no override is
       saved. Overrides are stored centrally in the design_settings table
       (tokens JSONB keyed by CSS var name), edited on the HuoMa HQ > Design
       Management tab, and applied as inline documentElement styles by
       js/design-tokens.js (inline shadows these defaults). The Design
       Management UI reads each default via getComputedStyle, so never restate
       these values anywhere else. Adding a token: define the default here,
       consume it in the rules, register it in DESIGN_TOKENS
       (js/design-tokens.js). */

    /* Bottom nav bar. The row is the tappable button area; the gap +
       safe-area inset sit BELOW it (never eat into the row - global
       box-sizing:border-box would otherwise squeeze the buttons - and the gap
       keeps slots off the curved screen edge, L224 family). 44px row + 4px gap
       matches Chrome iOS's ~48pt bottom toolbar while keeping the Apple-HIG
       44px tap target. Mobile and desktop are independent tokens; the derived
       --bottom-nav-row below repoints per width. */
    --dt-bottom-nav-row-mobile: 44px;
    --dt-bottom-nav-row-desktop: 44px;
    --dt-bottom-nav-gap: 4px;
    --dt-bottom-nav-icon: 22px;

    /* Header org logo (.org-logo-img, top-left of the app header). */
    --dt-header-logo-height: 54px;

    /* Buttons (.btn base geometry; .btn-small keeps its own padding). */
    --dt-btn-radius: 8px;
    --dt-btn-pad-v: 12px;

    /* Semantic button fills - cross-org CONSTANTS, deliberately NOT the per-org
       brand colour. An approve/confirm button (.btn-success) is the same green on
       every org, and a decline/delete button (.btn-danger) the same red, even on a
       red-themed org where --color-primary would make "Approve" read as "Delete".
       Defaults track the global --color-success / --color-error (one source for the
       green/red, and the dark-mode variants resolve automatically while no override
       is set); an HQ override pins one literal colour for both modes. Editable on
       HuoMa HQ > Design Management. */
    --dt-btn-success-bg: var(--color-success);
    --dt-btn-danger-bg:  var(--color-error);

    /* Cards (.card). */
    --dt-card-radius: 12px;
    --dt-card-pad: 24px;

    /* Form inputs (.form-group input / select / textarea). */
    --dt-input-radius: 8px;

    /* Derived internals consumed by the bar + every clearance rule (body
       padding, messenger app-container). The desktop media swap below
       repoints the row var at the desktop token. */
    --bottom-nav-row: var(--dt-bottom-nav-row-mobile);
    --bottom-nav-gap: var(--dt-bottom-nav-gap);
    /* Minimum clearance below the bottom-nav icon row on EVERY platform. The 4px
       --bottom-nav-gap alone left Android/web cramped (env(safe-area-inset-bottom)
       is 0 there) while iOS got gap + ~34px home-indicator inset. The clearance
       rules below floor at this value with max(), still growing to clear the home
       indicator where env() reports one. See css/CLAUDE.md "Safe-Area Insets". */
    --bottom-nav-min-clear: 30px;
}

/* Desktop bottom-nav height: repoint the derived row var at the desktop token. */
@media (min-width: 641px) {
    :root { --bottom-nav-row: var(--dt-bottom-nav-row-desktop); }
}

/* Font size tokens - mobile defaults */
@media (max-width: 767px) {
    :root {
        --font-size-heading: 1.1rem;
        --font-size-subheading: 0.7rem;
        --font-size-body: 0.85rem;
        --font-size-label: 0.78rem;
        --font-size-small: 0.68rem;
        /* Messenger bubble text — half a step above mobile body (0.85rem) so chat
           reads easier on a phone without making bubbles chunky or fitting less. */
        --font-size-msgr: 0.9rem;
    }
}

/* Prevent iOS Safari/Chrome auto-zoom on input focus (requires font-size ≥ 16px).
   !important is needed because many module-scoped selectors (e.g.
   #tab-settings .form-group input) have higher specificity and would otherwise
   pull font-size back down to var(--font-size-body) ≈ 13.6px on mobile. */
@media (max-width: 767px) {
    input, select, textarea {
        font-size: 16px !important;
    }
}

/* ═══ 2. TYPOGRAPHY & FONT SCALE ═══ */

/* Typography utility classes */
.text-heading    { font-size: var(--font-size-heading) !important; }
.text-subheading { font-size: var(--font-size-subheading) !important; }
.text-body       { font-size: var(--font-size-body) !important; }
.text-label      { font-size: var(--font-size-label) !important; }
.text-small      { font-size: var(--font-size-small) !important; }

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

/* Paint the root canvas itself (not just <body>) so the html element behind
   the body is the theme bg, never the UA default. Covers the navigation gap and
   the first pre-body-paint frame, where body's background hasn't drawn yet. */
html {
    background-color: var(--color-bg);
    /* Always reserve the vertical-scrollbar gutter so layout width is constant
       whether or not a tab's content overflows. Without this, switching between a
       short tab (no scrollbar) and a tall one (scrollbar present) changed the
       available width by the scrollbar's ~15px, re-centring the max-width app
       shell and shifting everything sideways. No-op where scrollbars are overlay
       (iOS Safari, the Capacitor webview, Android) so it never affects mobile. */
    scrollbar-gutter: stable;
}

body {
    font-family: var(--font-family);
    background-color: var(--color-bg);
    color: var(--color-text);
    min-height: 100vh;   /* fallback for browsers without dvh support */
    min-height: 100dvh;  /* fits the current visible viewport on mobile */
    display: flex;
    flex-direction: column;
}

/* ═══ 3. LAYOUT (grid, flex, containers) ═══ */

/* App Shell */
.app-container {
    margin: 0 auto;
    padding: 16px;
    width: 100%;
    min-width: 0; /* flex item: prevent expanding past viewport width */
    flex: 1;
    display: flex;
    flex-direction: column;
    position: relative;
}

/* Header */
.app-header {
    text-align: left;
    padding: 24px 0 16px;
    border: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}

.app-header .logo {
    font-size: var(--font-size-heading);
    font-weight: 700;
    color: var(--color-primary);
    letter-spacing: -0.5px;
    display: flex;
    align-items: center;
    gap: 5px;
}

.org-logo-img {
    height: var(--dt-header-logo-height);
    width: auto;
    max-width: 210px;
    object-fit: contain;
    flex-shrink: 0;
}

#orgLogoName {
    color: var(--color-title-color);
    font-family: var(--color-title-font);
}

.app-header .logo small {
    display: block;
    font-size: var(--font-size-subheading);
    font-weight: 400;
    color: var(--color-text-light);
    letter-spacing: 1px;
    text-transform: uppercase;
    margin-top: 4px;
}

/* Navigation bar */
.nav-bar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 0;
    margin-bottom: 16px;
    border: none;
}

.nav-bar .user-info {
    font-size: var(--font-size-body);
    color: var(--color-text-light);
}

.nav-bar .nav-links {
    display: flex;
    align-items: center;
    gap: 12px;
}

.nav-bar .nav-links a {
    font-size: var(--font-size-body);
    color: var(--color-primary);
    text-decoration: none;
}



.nav-bar .nav-links a:hover {
    text-decoration: underline;
}

/* Hide desktop inline links - navigation is the bottom nav bar + More page at
   all sizes (Edit Profile / Update Password / Sign Out live on the More page). */
.nav-bar .nav-links > a {
    display: none;
}

.nav-bar .nav-links > .view-toggle {
    display: none;
}

/* View Toggle */
.view-toggle {
    display: flex;
    background: rgba(232, 105, 30, 0.1);
    border-radius: 20px;
    padding: 3px;
    gap: 2px;
    font-size: var(--font-size-label);
}

.view-toggle a,
.view-toggle span {
    font-size: inherit;
}

.view-toggle-btn {
    padding: 5px 12px;
    border: none;
    border-radius: 18px;
    background: transparent;
    color: var(--color-text-light);
    font-size: inherit;
    font-weight: 500;
    font-family: inherit;
    cursor: pointer;
    transition: all 0.2s;
    white-space: nowrap;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1.4;
    text-align: center;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

.view-toggle-btn:hover {
    background: rgba(232, 105, 30, 0.12);
    color: var(--color-primary);
    text-decoration: none;
}

.view-toggle-btn.active {
    background: var(--color-primary);
    color: white;
    cursor: default;
}

.view-toggle-btn.active:hover {
    background: var(--color-primary);
    color: white;
}

/* ═══ 4. COMPONENTS (buttons, badges, cards, modals, forms, alerts) ═══ */

/* Cards */
.card {
    background: var(--color-card);
    border-radius: var(--dt-card-radius);
    padding: var(--dt-card-pad);
    margin-bottom: 16px;
    box-shadow: var(--color-shadow);
    border: 1px solid var(--color-border);
}

.card h2 {
    font-size: var(--font-size-heading);
    margin-bottom: 16px;
    color: var(--color-primary-dark);
}

/* Forms */
.form-group {
    margin-bottom: 16px;
}

.form-group label {
    display: block;
    font-size: var(--font-size-label);
    font-weight: 600;
    color: var(--color-text);
    margin-bottom: 6px;
}

.form-group input,
.form-group select,
.form-group textarea {
    width: 100%;
    padding: 12px 14px;
    border: 1px solid var(--color-border);
    border-radius: var(--dt-input-radius);
    font-size: var(--font-size-body);
    background: var(--color-card);
    color: var(--color-text);
    transition: border-color 0.2s;
}

.form-group textarea {
    resize: none;
    overflow-y: auto;
}

/* Date/time inputs: force them to respect width:100% on iOS.
   WebKit's native date/time picker chrome sets an intrinsic min-width
   that overrides width:100%, causing overflow on narrow screens. */
input[type="date"],
input[type="time"] {
    min-width: 0;
    max-width: 100%;
}

/* Hide native number input spinners */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
input[type="number"] {
    -moz-appearance: textfield;
}

.form-group select {
    padding-right: 36px;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236B6B6B' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
    cursor: pointer;
}

.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px rgba(232, 105, 30, 0.15);
}

.form-group input:disabled,
.form-group select:disabled,
.form-group textarea:disabled {
    opacity: 0.45;
    cursor: not-allowed;
    pointer-events: none;
}

/* CRM country-code + local-number phone widget (New Prospect modal, campaign
   WhatsApp-number field, js/admin-crm.js crmPhoneWidget*). Nested inside a
   .form-group so the select/input inherit the base border/background/dark
   styling above; only the row layout + widths are added here. */
/* flex-wrap is the fallback for a narrow HOST container (the campaign form
   packs this widget alongside three sibling .form-groups in one flex row,
   css/style-01.css's Manage CRM layout in dashboard.html, so this group can
   get squeezed toward its own min-width): when there is not enough room for
   select + input side by side, the input wraps to its own full-width line
   below the select instead of both being crushed illegibly thin. */
.crm-phone-row {
    display: flex;
    gap: 8px;
    align-items: flex-start;
    flex-wrap: wrap;
}

/* Specificity note: .form-group select / .form-group input (above) are
   0,2,0 and set width:100%, which would otherwise make BOTH the select and
   the input claim the full row width. Matching that specificity here (and
   declaring after) is what lets the row's flex sizing actually win. */
.form-group .crm-phone-country {
    flex: 0 0 auto;
    width: 150px;
    min-width: 120px;
}

.form-group .crm-phone-local {
    flex: 1 1 140px;
    width: auto;
    min-width: 140px;
}

.crm-phone-hint {
    margin: 6px 0 0;
    min-height: 0;
}

.crm-phone-hint:not(:empty) {
    color: var(--color-error);
}

@media (max-width: 480px) {
    .crm-phone-country {
        width: 116px;
        min-width: 104px;
    }
}

.radio-group {
    display: flex;
    gap: 16px;
    padding: 8px 0;
}

.radio-label {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--font-size-body);
    cursor: pointer;
    color: var(--color-text);
}

/* Global: constrain all checkboxes/radios - Safari iOS ignores flex sizing without this */
input[type="checkbox"],
input[type="radio"] {
    width: 16px;
    height: 16px;
    min-width: 16px;
    min-height: 16px;
    flex-shrink: 0;
    accent-color: var(--color-primary);
    margin: 0;
    vertical-align: middle;
    cursor: pointer;
}

.radio-label input[type="radio"] {
    accent-color: var(--color-primary);
    width: 16px;
    height: 16px;
    margin: 0;
    vertical-align: middle;
    cursor: pointer;
}

/* Announcement audience pill selector */
.ann-audience-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    padding: 4px 0;
}
.ann-pill {
    padding: 5px 14px;
    border-radius: 20px;
    border: 1.5px solid var(--color-border);
    background: var(--color-card);
    color: var(--color-text-light);
    font-size: var(--font-size-label);
    cursor: pointer;
    transition: all 0.15s ease;
}
.ann-pill:hover { border-color: var(--color-primary); color: var(--color-primary); }
.ann-pill.active {
    background: var(--color-primary);
    border-color: var(--color-primary);
    color: #fff;
}

/* Document body formatting toolbar */
.doc-format-toolbar {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 5px 8px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-bottom: none;
    border-radius: 8px 8px 0 0;
}
.doc-format-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 4px 8px;
    min-width: 30px;
    border: none;
    background: transparent;
    border-radius: 4px;
    cursor: pointer;
    color: var(--color-text);
    font-size: var(--font-size-small);
    line-height: 1;
    transition: background 0.12s;
}
.doc-format-btn:hover { background: var(--color-bg); }
.doc-format-btn-italic { font-style: italic; }

/* Documents: form builder (kind='form'; js/admin-documents-forms.js).
   Ported deliberately from canopy, including the fix for .form-group
   input/select { width:100% } (style-01.css above) reaching these as
   descendants of #docFormBuilderGroup's .form-group. */
.doc-form-builder-section {
    border: 1px solid var(--color-border);
    border-radius: 8px;
    padding: 12px;
    margin-bottom: 12px;
    background: var(--color-surface);
}
.doc-form-builder-section-head {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
}
.doc-form-section-title-input {
    flex: 1;
    font-weight: 600;
    font-size: var(--font-size-body);
    border: none;
    border-bottom: 1px solid var(--color-border);
    background: transparent;
    padding: 4px 2px;
    color: var(--color-text);
}
.doc-form-section-desc-input {
    width: 100%;
    border: none;
    background: transparent;
    color: var(--color-text-light);
    font-size: var(--font-size-small);
    resize: vertical;
    padding: 2px;
    margin-bottom: 8px;
}
.doc-form-builder-controls { display: flex; gap: 2px; flex-shrink: 0; }
.doc-form-builder-questions { display: flex; flex-direction: column; gap: 8px; margin-bottom: 8px; }
.doc-form-builder-question {
    border: 1px solid var(--color-border);
    border-radius: 6px;
    padding: 10px;
    background: var(--color-bg);
}
.doc-form-builder-question-row { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
.doc-form-q-label-input {
    flex: 1 1 auto !important;
    width: auto !important;
    min-width: 0;
    border: 1px solid var(--color-border);
    border-radius: 6px;
    padding: 6px 8px;
    background: var(--color-surface);
    color: var(--color-text);
}
.doc-form-q-type-select {
    flex: 0 0 170px !important;
    width: 170px !important;
    border: 1px solid var(--color-border);
    border-radius: 6px;
    padding: 6px 8px;
    background: var(--color-surface);
    color: var(--color-text);
}
.doc-form-options-editor { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 8px; }
.doc-form-option-row { display: flex; align-items: center; gap: 4px; }
.doc-form-option-row input {
    flex: 1;
    border: 1px solid var(--color-border);
    border-radius: 6px;
    padding: 5px 8px;
    background: var(--color-surface);
    color: var(--color-text);
}
.doc-form-builder-question-foot { display: flex; align-items: center; justify-content: space-between; }
.doc-form-required-toggle {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    cursor: pointer;
}

/* Documents: form fill/preview renderer (shared by builder Preview + filler) */
.doc-form-section { margin-bottom: 20px; }
.doc-form-section-title { margin: 0 0 4px; }
.doc-form-section-desc { margin: 0 0 12px; }
.doc-form-question { margin-bottom: 14px; }
.doc-form-q-label { display: block; font-weight: 500; margin-bottom: 6px; }
.doc-form-q-input {
    width: 100%;
    border: 1px solid var(--color-border);
    border-radius: 6px;
    padding: 8px 10px;
    background: var(--color-surface);
    color: var(--color-text);
}
/* Ported from canopy's audit fix: the dropdown-type question (a bare
   <select>, rendered outside any .form-group wrapper) never inherits
   .form-group select's appearance:none + chevron, and the browser-default
   chevron can be invisible in dark mode on Chrome/iOS. */
select.doc-form-q-input {
    padding-right: 36px;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236B6B6B' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
    cursor: pointer;
}
.doc-form-choice-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 0;
    font-weight: normal;
    cursor: pointer;
}
.doc-form-preview-frame {
    border: 1px dashed var(--color-border);
    border-radius: 8px;
    padding: 14px;
}
.doc-form-tallies { display: flex; flex-wrap: wrap; gap: 16px; }
.doc-form-tally-block {
    border: 1px solid var(--color-border);
    border-radius: 8px;
    padding: 10px 12px;
    min-width: 200px;
    flex: 1;
}
.doc-form-tally-row {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    font-size: var(--font-size-small);
    padding: 3px 0;
}

.doc-format-divider {
    width: 1px;
    height: 16px;
    background: var(--color-border);
    margin: 0 4px;
}
.doc-format-toolbar + textarea {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

/* Message/Document type toggle pill buttons */
.msg-type-btn {
    padding: 5px 14px;
    border-radius: 20px;
    border: 1.5px solid var(--color-border);
    background: transparent;
    color: var(--color-text-light);
    font-size: var(--font-size-body);
    cursor: pointer;
    transition: all 0.15s ease;
}
.msg-type-btn:hover { border-color: var(--color-primary); color: var(--color-primary); }
.msg-type-btn.active {
    background: var(--color-primary);
    border-color: var(--color-primary);
    color: #fff;
}

/* Announcement segmented control */
.ann-seg-control {
    display: flex;
    border: 1.5px solid var(--color-border);
    border-radius: 8px;
    overflow: hidden;
}
.ann-seg {
    flex: 1;
    padding: 7px 6px;
    border: none;
    background: var(--color-card);
    color: var(--color-text-light);
    font-size: var(--font-size-label);
    cursor: pointer;
    transition: background 0.15s var(--ease-out), color 0.15s var(--ease-out);
    text-align: center;
}
.ann-seg:not(:last-child) { border-right: 1.5px solid var(--color-border); }
.ann-seg:hover { background: var(--color-bg); }
.ann-seg.active {
    background: var(--color-primary);
    color: #fff;
}
/* Mobile touch target — desktop padding (7px) yields ~28px height; thumb taps need 44px. */
@media (max-width: 767px) {
    .ann-seg { min-height: 44px; }
}

.form-group input::placeholder {
    color: #B0A0C0;
}

.form-group .hint {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    margin-top: 4px;
}

/* Password field with show/hide toggle */
.password-field-wrap {
    position: relative;
    display: flex;
    align-items: stretch;
}

.password-field-wrap input {
    flex: 1;
    padding-right: 44px !important;
}

.pw-toggle {
    position: absolute;
    right: 0;
    top: 0;
    bottom: 0;
    width: 42px;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    color: var(--color-text-light);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 0 8px 8px 0;
    transition: color 0.15s;
}

.pw-toggle:hover {
    color: var(--color-text);
}

.pw-toggle svg {
    width: 18px;
    height: 18px;
    pointer-events: none;
}

/* Info Icon - the fi-rr-info glyph, click to open the info modal. The markup
   carries `fi fi-rr-info`, so the glyph itself stays sourced from the Flaticon
   CDN (the single source of truth for every icon glyph) rather than a hardcoded
   codepoint; this class only adds the interactive affordance (muted colour,
   hover, click target, inline alignment). font-size:0 collapses the legacy
   literal 'i' that older markup still nests inside the span, so only the
   Flaticon glyph renders. */
.info-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0;
    color: var(--color-text-light);
    cursor: pointer;
    margin-left: 4px;
    flex-shrink: 0;
    user-select: none;
    vertical-align: middle;
    position: relative;
    top: -1px;
    transition: color 0.15s;
    line-height: 1;
}
.info-icon::before { font-size: 15px; }
.info-icon:hover {
    color: var(--color-primary);
}

/* Student Profile Photos */
.profile-photo-thumb {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    object-fit: cover;
    display: block;
}

.profile-photo-placeholder {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--color-border);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--color-primary);
}

.profile-photo-section {
    text-align: center;
    margin-bottom: 16px;
}

img.profile-photo-large,
.profile-photo-large img {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    object-fit: cover;
    display: block;
    margin: 0 auto 8px;
}

.profile-photo-placeholder-lg {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    background: var(--color-border);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--color-primary);
    margin: 0 auto;
}

/* Profile photo in detail/edit modals - 128px, no border */
.modal-profile-photo img {
    width: 128px;
    height: 128px;
    border-radius: 50%;
    object-fit: cover;
    display: block;
    margin: 0 auto 8px;
}

.modal-profile-photo .profile-photo-placeholder-lg {
    width: 128px;
    height: 128px;
    font-size: 2.4rem;
    margin-bottom: 8px;
}

.profile-photo-upload-btn {
    cursor: pointer;
    font-size: 0.8rem !important;
}

/* Settings School List */
.settings-school-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 0;
    border-bottom: 1px solid var(--color-border);
}

.settings-school-item:last-child {
    border-bottom: none;
}

.school-item-actions {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-shrink: 0;
}

.school-item .school-name {
    font-size: var(--font-size-body);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.school-item .btn {
    font-size: var(--font-size-label);
    padding: 5px 12px;
}

/* Settings tab - compact sizing */
#tab-settings .card h2 {
    font-size: var(--font-size-body);
    margin-bottom: 10px;
}

#tab-settings .card > p.text-muted {
    font-size: var(--font-size-label);
}

#tab-settings .form-group label {
    font-size: var(--font-size-label);
}

#tab-settings .form-group select,
#tab-settings .form-group input {
    font-size: var(--font-size-body);
    padding: 9px 12px;
}

#tab-settings .btn {
    font-size: var(--font-size-label);
    padding: 9px 18px;
}

#tab-settings .btn.btn-small {
    font-size: var(--font-size-label);
    padding: 5px 12px;
}

/* History tab - compact Load More */
#tab-history .btn {
    font-size: var(--font-size-label);
    padding: 9px 18px;
}

/* Flaticon icons - ensure vertical centering inside any container */
.fi {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    /* Icon glyphs are icon-font characters (selectable text), so a long-press or
       drag would select them and paint the blue text-selection box / raise the
       iOS callout. Icons are never meant to be selected anywhere - kill it
       globally for every .fi glyph. */
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}

/* Buttons */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--dt-btn-pad-v) 24px;
    border: none;
    border-radius: var(--dt-btn-radius);
    font-size: var(--font-size-body);
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s cubic-bezier(0.22, 1, 0.36, 1), transform 0.15s cubic-bezier(0.22, 1, 0.36, 1);
    text-decoration: none;
    width: 100%;
}

.btn:active {
    transform: scale(0.98);
}

.btn-primary {
    background-color: var(--color-primary);
    color: white;
}

.btn-primary:hover {
    background-color: var(--color-primary-dark);
    transform: translateY(-1px);
}

.btn-primary:disabled {
    background-color: var(--color-primary-light);
    opacity: 0.6;
    cursor: not-allowed;
}

.btn-secondary {
    background-color: transparent;
    color: var(--color-primary);
    border: 1px solid var(--color-primary);
}

.btn-secondary:hover {
    background-color: rgba(232, 105, 30, 0.05);
}

.btn-secondary:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.btn-danger {
    background-color: var(--dt-btn-danger-bg);
    color: white;
}

.btn-danger:hover {
    filter: brightness(0.9);
}

.btn-ghost-danger {
    background-color: transparent;
    color: var(--color-error);
    border: none;
    padding-left: 8px;
    padding-right: 8px;
}

.btn-ghost-danger:hover {
    background-color: rgba(var(--color-error-rgb, 180, 50, 50), 0.08);
}

.btn-success {
    background-color: var(--dt-btn-success-bg);
    color: #fff;
}

.btn-success:hover {
    filter: brightness(1.15);
}

.btn-small {
    padding: 10px 16px;
    font-size: var(--font-size-body);
    width: auto;
}

/* Icon + label button: a real flex gap and a line-height:1 icon so the glyph
   shares the label's vertical centre. Use with <i class="fi ..."></i><span>Label</span>
   instead of the brittle <i></i>&ensp;Label spacing, which leaves the icon riding
   optically high (most visible on borderless/small buttons like btn-ghost-danger). */
.btn-iconed {
    gap: 7px;
}
.btn-iconed > i {
    line-height: 1;
}

.btn-group {
    display: flex;
    gap: 8px;
    margin-top: 16px;
}

.btn-group .btn {
    flex: 1;
}

.btn-group--wrap {
    flex-wrap: wrap;
}
.btn-group--wrap .btn {
    flex: 1 1 calc(50% - 4px);
}

/* Links */
.text-link {
    color: var(--color-primary);
    text-decoration: none;
    font-size: var(--font-size-body);
    text-align: center;
    display: block;
    margin-top: 12px;
}

.text-link:hover {
    text-decoration: underline;
}

/* Login-card language switch (i18n, index.html) - small, unobtrusive EN | 中文
   text switch below the login button. Deliberately plainer than the
   .ann-seg pill control used elsewhere: this sits on a public, pre-auth page
   where the toggle is a minor affordance, not a settings control. Ported
   from canopy. */
.login-lang-switch {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin-top: 12px;
}
.login-lang-switch-btn {
    background: none;
    border: none;
    padding: 2px 4px;
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    cursor: pointer;
    font-family: inherit;
}
.login-lang-switch-btn:hover { color: var(--color-primary); text-decoration: underline; }
.login-lang-switch-btn.active { color: var(--color-primary); font-weight: 600; }
.login-lang-switch-sep { font-size: var(--font-size-small); color: var(--color-border); }

/* Alerts */
.alert {
    padding: 12px 16px;
    border-radius: 8px;
    margin-bottom: 16px;
    font-size: var(--font-size-body);
    display: none;
}

.alert.show {
    display: block;
}

.alert-error {
    background-color: #FDEDEC;
    color: var(--color-error);
    border: 1px solid #F5C6CB;
}

.alert-info {
    background-color: #F0E8F7;
    color: var(--color-primary-dark);
    border: 1px solid var(--color-border);
}

/* QR Code Display */
.qr-container {
    text-align: center;
    padding: 24px;
}

.qr-container canvas,
.qr-container img {
    max-width: 280px;
    width: 100%;
    height: auto;
    border-radius: 12px;
}

.qr-timer {
    margin-top: 12px;
    font-size: var(--font-size-body);
    color: var(--color-text-light);
}

.qr-timer .countdown {
    font-weight: 700;
    color: var(--color-primary);
    font-size: var(--font-size-heading);
}

/* Student cards */
.student-card {
    display: flex;
    align-items: center;
    padding: 12px;
    background: var(--color-bg);
    border-radius: 8px;
    margin-bottom: 8px;
}

.student-card .student-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--color-primary-light);
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: var(--font-size-body);
    margin-right: 12px;
    flex-shrink: 0;
}

.student-card .student-info h3 {
    font-size: var(--font-size-body);
    margin-bottom: 2px;
}

.student-card .student-info p {
    font-size: var(--font-size-label);
    color: var(--color-text-light);
}

/* Admin tables */
.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--font-size-body);
}

.data-table-fixed {
    table-layout: fixed;
}

.data-table th,
.data-table td {
    padding: 10px 8px;
    text-align: left;
    border-bottom: 1px solid var(--color-border);
}
.data-table th:last-child,
.data-table td:last-child {
    padding-right: 16px;
}

.data-table .btn.btn-small {
    padding: 3px 10px;
    font-size: var(--font-size-label);
}

.data-table th {
    font-weight: 600;
    color: var(--color-primary-dark);
    background: var(--color-bg);
    position: sticky;
    top: 0;
}

.data-table tr:hover td {
    background-color: rgba(232, 105, 30, 0.03);
}

/* Vendor Tagging (admin-manage-enrichments) */
.vt-list { border-top: 1px solid var(--color-border); }
.vt-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 0;
    border-bottom: 1px solid var(--color-border);
}
.vt-row-label { min-width: 180px; font-weight: 500; }
.vt-row-vendors { flex: 1; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.vt-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 8px 3px 12px;
    border-radius: 999px;
    background: var(--color-primary);
    color: #fff;
    font-size: var(--font-size-small);
}
.vt-pill-remove {
    background: none;
    border: none;
    color: rgba(255,255,255,0.75);
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    padding: 0;
    display: flex;
    align-items: center;
}
.vt-pill-remove:hover { color: #fff; }
.vt-add-select {
    height: 28px;
    font-size: var(--font-size-small);
    padding: 0 8px;
    border-radius: 999px;
    border: 1.5px dashed var(--color-border);
    background: transparent;
    color: var(--color-text-light);
    cursor: pointer;
}

/* Sortable / filterable table headers */
.th-sortable {
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
}
.th-sortable:hover { color: var(--color-primary); }

.th-sort-arrow {
    font-size: var(--font-size-label);
    margin-left: 3px;
    color: var(--color-text-light);
    vertical-align: middle;
}
.th-sort-arrow.active { color: var(--color-primary); }

.th-filterable {
    cursor: pointer;
    user-select: none;
    font-size: var(--font-size-body);
    margin-left: 4px;
    color: var(--color-text-light);
    padding: 1px 3px;
    border-radius: 3px;
    vertical-align: middle;
}
.th-filterable:hover { color: var(--color-primary); }
.th-filterable.active {
    color: var(--color-primary);
    font-weight: 700;
    background: rgba(232, 105, 30, 0.1);
}

/* Filter dropdown */
.table-filter-dropdown {
    position: fixed;
    background: var(--color-card, #fff);
    border: 1px solid var(--color-border);
    border-radius: 8px;
    box-shadow: 0 4px 16px rgba(0,0,0,0.12);
    z-index: 2000;
    min-width: 130px;
    padding: 4px 0;
}
.table-filter-option {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 14px;
    cursor: pointer;
    font-size: var(--font-size-body);
    color: var(--color-text);
}
.table-filter-option:hover { background: var(--color-bg); }
.table-filter-option.selected { color: var(--color-primary); font-weight: 600; }
.filter-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    border: 2px solid var(--color-text-light);
    flex-shrink: 0;
}
.table-filter-option.selected .filter-dot {
    border-color: var(--color-primary);
    background: var(--color-primary);
}

.table-container {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}
@media (min-width: 769px) {
    .table-container {
        overflow-x: hidden;
    }
}

/* Search bar */
.search-bar {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
}

.search-bar input {
    flex: 1;
    padding: 10px 14px;
    border: 1px solid var(--color-border);
    border-radius: 8px;
    font-size: var(--font-size-body);
}

/* Tabs */
.tabs {
    display: none;
    flex-wrap: wrap;
    border-bottom: 2px solid var(--color-border);
    margin-bottom: 16px;
    gap: 0;
}

.tabs .tab {
    padding: 8px 14px;
    font-size: var(--font-size-body);
    font-weight: 600;
    color: var(--color-text-light);
    cursor: pointer;
    border-bottom: 2px solid transparent;
    margin-bottom: -2px;
    white-space: nowrap;
    background: none;
    border-top: none;
    border-left: none;
    border-right: none;
}

.tabs .tab.active {
    color: var(--color-primary);
    border-bottom-color: var(--color-primary);
}

.tab-content {
    display: none;
}

.tab-content.active {
    display: block;
}

/* A lazily-loaded tab is marked .module-loading from when its DOM is shown until
   its feature scripts finish loading (js/module-loader.js). Suppress pointer
   events so a button in the freshly-shown markup can't fire a handler that the
   still-loading module has not defined yet. Removed once the module's init runs. */
.tab-content.module-loading {
    pointer-events: none;
    cursor: progress;
}

/* Badge */
.badge {
    display: inline-flex;
    align-items: center;
    padding: 3px 8px 2px; /* asymmetric: +1px top compensates for cap-height sitting high in em square */
    border-radius: 12px;
    font-size: var(--font-size-small);
    font-weight: 600;
    line-height: 1;
    text-transform: uppercase;
}

.badge-success {
    background: rgba(30, 107, 61, 0.12);
    color: var(--color-success);
}

.badge-warning {
    background: var(--color-warning-bg);
    color: var(--color-warning);
}

.badge-error {
    background: rgba(192, 57, 43, 0.1);
    color: var(--color-error);
}

.badge-muted {
    background: rgba(107, 107, 107, 0.12);
    color: var(--color-text-light);
}

.badge-trial {
    background: rgba(222, 197, 216, 0.35);
    color: #6B3F62;
}

[data-theme="dark"] .badge-trial {
    background: rgba(222, 197, 216, 0.18);
    color: #E8C4DF;
}

.status-edit-btn {
    background: none;
    border: none;
    padding: 2px 4px;
    color: var(--color-text-light);
    cursor: pointer;
    font-size: var(--font-size-small);
    line-height: 1;
    border-radius: 4px;
}
.status-edit-btn:hover { color: var(--color-primary); background: var(--color-primary-soft); }

/* Modal */
.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    padding: 16px;
    /* Reserve the soft-keyboard height at the bottom so the flex-centred modal
       re-centres in the band ABOVE the keyboard (any field near the modal's
       bottom would otherwise sit under the keyboard on the messenger page,
       where keyboard resize mode is "none", and in mobile web Safari, where
       the layout viewport never shrinks). --kb-inset is 0 at rest / on desktop,
       so this is a no-op there. */
    padding-bottom: calc(16px + var(--kb-inset, 0px));
    transition: padding-bottom 0.25s var(--ease-keyboard);
}

.modal-overlay.show {
    display: flex;
}

/* Feedback modals must float above all other modals */
#genericSuccessModal,
#customErrorModal,
#customConfirmModal {
    z-index: 1100;
}

.modal {
    background: var(--color-card);
    border-radius: 12px;
    padding: 24px;
    width: 100%;
    max-width: 700px;
    /* Cap at 90vh, then also subtract the safe-area insets so a tall modal stays
       clear of the status bar / Dynamic Island (top) and home indicator (bottom)
       on edge-to-edge iOS: it shrinks and scrolls its own content (overflow-y)
       and re-centres inside the safe band rather than poking under the notch.
       env()=0 on desktop / Android / web / cap-inset-always native (the OS
       already insets there), so it stays a plain 90vh. --kb-inset (0 at rest)
       lifts it above the soft keyboard. See css/CLAUDE.md "Safe-Area Insets". */
    max-height: calc(90vh - max(env(safe-area-inset-top, 0px), var(--sat-floor, 0px)) - max(env(safe-area-inset-bottom, 0px), var(--sab-floor, 0px)) - var(--kb-inset, 0px));
    overflow-y: auto;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
    transition: max-height 0.25s var(--ease-keyboard);
}

/* Hero-image modal - image bleeds edge-to-edge at top */
.modal-hero {
    padding: 0;
    position: relative;
}

.modal-hero .modal-close {
    position: absolute;
    top: 10px;
    right: 10px;
    background: rgba(255,255,255,0.88);
    border-radius: 50%;
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 20;
    font-size: 18px; /* fixed - circle is fixed-size, must not scale with font preset */
    line-height: 1;
    float: none;
}

.modal-hero-content {
    padding: 20px 24px 24px;
}

/* Clip image at modal top corners; remove side rounding & bottom margin */
.modal-hero .enrich-carousel {
    border-radius: 12px 12px 0 0;
    margin-bottom: 0;
    overflow: hidden;
}

.modal-hero .enrich-carousel-single {
    border-radius: 12px 12px 0 0;
    margin-bottom: 0;
}

.modal h2 {
    font-size: var(--font-size-heading);
    margin-bottom: 16px;
    color: var(--color-primary-dark);
}

.modal-close {
    float: right;
    background: none;
    border: none;
    font-size: var(--font-size-heading);
    cursor: pointer;
    color: var(--color-text-light);
    line-height: 1;
}

/* Simple user card (buildSimpleUserCardHtml): the avatar is centred via
   margin:auto at the very top of the modal, in the SAME band as the global
   float:right close button. A floated button narrows that band on the right and
   pulls the avatar ~6px left of the text-align:centred name below it (reported as
   the profile photo "skewed to the left"). Take the close button out of flow
   wherever this card is hosted so the avatar centres truly, with no vertical
   cost. Travels with the card via the marker class, so it covers every modal
   that shows it (shared user card, DM profile, child detail, group member). */
.modal:has(.simple-user-card) { position: relative; }
.modal:has(.simple-user-card) > .modal-close {
    float: none;
    position: absolute;
    top: 20px;
    right: 22px;
    z-index: 2;
}

/* Loading spinner. linear (not ease) is correct here: a continuously rotating
   indicator must spin at constant speed, or it visibly pulses each revolution. */
.spinner {
    display: inline-block;
    width: 20px;
    height: 20px;
    border: 3px solid rgba(255, 255, 255, 0.3);
    border-radius: 50%;
    border-top-color: white;
    animation: spin 0.8s linear infinite;
}

.spinner-dark {
    border-color: rgba(232, 105, 30, 0.2);
    border-top-color: var(--color-primary);
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* ── Ambient loaders ──────────────────────────────────────────────────────
   A waiting state should breathe, never freeze. These are AMBIENT (declarative,
   looping) indicators - the counterpart to the played Motion Library primitives
   in Section 5. The global prefers-reduced-motion block freezes them to a
   single static frame, so reduced-motion users get a calm placeholder.

   .loading-text  - the canonical breathing "Loading…" placeholder. Replaces the
                    dead grey <p class="text-muted">Loading...</p> pattern.
   .skeleton      - shows the SHAPE of content that is still loading; compose
                    with a sized box or the .skeleton-circle / .skeleton-text
                    shape helpers. */
.loading-text {
    color: var(--color-text-light);
    animation: loadingPulse 1.4s var(--ease-out) infinite;
}
@keyframes loadingPulse {
    0%, 100% { opacity: 0.45; }
    50%      { opacity: 1; }
}

.skeleton {
    background: linear-gradient(90deg,
        var(--color-border) 25%,
        var(--color-card) 50%,
        var(--color-border) 75%);
    background-size: 200% 100%;
    animation: skeletonShimmer 1.4s ease-in-out infinite;
    border-radius: 6px;
}
@keyframes skeletonShimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}
.skeleton-circle { border-radius: 50%; }
.skeleton-text   { height: 0.72em; border-radius: 4px; }

/* View As banner - sticky, in normal document flow, full page width.
   Sits above .app-container so it pushes the entire page down naturally.
   Pins below the safe-area inset when scrolling so the Exit button always
   stays clear of the status bar / notch. (It used to pin at top:0 = behind the
   status bar, sliding the bar out of reach on native edge-to-edge builds.) */

/* Sticky thead inside a scrollable modal */
.sticky-thead th {
    position: sticky;
    top: 0;
    background: var(--color-card);
    z-index: 1;
}

.view-as-bar {
    position: sticky;
    top: max(env(safe-area-inset-top, 0px), var(--sat-floor, 0px));
    z-index: 975; /* above nav menu (950) but below modals (1000+) */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 10px 20px;
    background: #fff3cd;
    /* Yellow rule frames the bar top AND bottom (reporter: line was only below). */
    border-top: 2px solid #ffc107;
    border-bottom: 2px solid #ffc107;
    gap: 12px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.12);
}
.view-as-label {
    font-size: var(--font-size-body);
    color: #856404;
}
[data-theme="dark"] .view-as-bar {
    background: #3a2e00;
    border-top-color: #ffc107;
    border-bottom-color: #ffc107;
}
[data-theme="dark"] .view-as-label { color: #ffd966; }
/* contentInset:"always" builds: the OS already insets the webview below the
   notch (body padding-top is suppressed to 0), so pin at the webview top.
   env() would push the bar down by a phantom inset on those builds. */
html.cap-inset-always .view-as-bar { top: 0; }
/* CSS-floor fallback: restore the floor value dropped by the zeroing rule
   above (see the css/style-03.css three-rule pattern comment). Ported from
   canopy 2026-08-11. */
html.cap-inset-always.cap-inset-floor .view-as-bar { top: var(--sat-floor, 0px); }

/* Greeting emoji spin animation */
.greeting-emoji {
    display: inline-block;
    margin-left: 2px;
}

@keyframes greetingEmojiSpin {
    0%   { transform: rotate(0deg); }
    100% { transform: rotate(720deg); }
}

.greeting-emoji--spin {
    animation: greetingEmojiSpin 1.1s cubic-bezier(0.34, 1.3, 0.64, 1);
}

/* Event banner - Announcements tab */
.event-banner {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    border-radius: 8px;
    margin-bottom: 10px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-left-width: 3px;
    cursor: pointer;
    transition: background 0.15s;
}
.event-banner:hover        { background: var(--color-border); }
.event-banner-live         { border-left-color: var(--color-error); }
.event-banner-upcoming     { border-left-color: var(--color-primary); }
.event-banner-badge {
    font-size: var(--font-size-small);
    font-weight: 700;
    letter-spacing: 0.07em;
    padding: 3px 9px;
    border-radius: 20px;
    flex-shrink: 0;
    white-space: nowrap;
}
.event-banner-live .event-banner-badge {
    background: #fdecea;
    color: var(--color-error);
}
.event-banner-upcoming .event-banner-badge {
    background: #ede6f5;
    color: var(--color-primary);
}
.event-banner-badge-live { animation: livePulse 1.5s ease-in-out infinite; }
.event-banner-name {
    font-weight: 600;
    font-size: var(--font-size-body);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--color-text);
}
.event-banner-sub {
    font-size: var(--font-size-label);
    color: var(--color-text-light);
    margin-top: 2px;
}
@keyframes livePulse {
    0%, 100% { opacity: 1; }
    50%       { opacity: 0.45; }
}

