/* =====================================================================
   Job Factory Portal — design system
   Color scheme matched to the Naandi Foundation "Jobutsav" portal's
   Liquid Glass theme (same warm-orange/charcoal palette, same glass
   surfaces + light/dark tokens) so the two portals read as one family.
   No external CSS/JS framework dependency — keeps the portal fast and
   working even on restrictive hosting or offline localhost setups.
   Mobile-first; the sidebar collapses to a slide-in panel on small
   screens.
   ===================================================================== */

:root {
    /* ---- Brand (warm orange, from Jobutsav's brand-500/600/700) ---- */
    --brand: #f2660f;
    --brand-hover: #d4550a;
    --brand-light: #ffe1cc;
    --brand-gradient: linear-gradient(135deg, #c2410c 0%, #ea580c 55%, #fb923c 100%);

    /* ---- Ink scale (warm cream/charcoal, not flat grey) ---- */
    --ink: #211d19;
    --ink-muted: #7a7166;
    --border: rgba(122, 113, 102, 0.22);

    --bg-solid: #faf6f0;
    --bg-gradient:
        radial-gradient(circle at 8% 0%, rgba(194, 65, 12, 0.10) 0%, transparent 42%),
        radial-gradient(circle at 92% 100%, rgba(87, 83, 78, 0.09) 0%, transparent 48%),
        var(--bg-solid);

    /* ---- Liquid-glass surfaces ---- */
    --glass-bg: rgba(255, 255, 255, 0.6);
    --glass-bg-strong: rgba(255, 255, 255, 0.78);
    --glass-border: rgba(255, 255, 255, 0.55);
    --glass-blur: 20px;

    /* Delivery 43 fix: these four used to be fixed values (dark panel,
       light/cream text) no matter which theme was active, which is why
       the plain Light theme showed a dark sidebar with light text sitting
       right next to a light page — jarring, and reported as an "issue" in
       its own right, separate from the contrast bugs below. --sidebar-bg
       already had a per-theme override for dark/glass-dark/glass-light
       (only this base/Light value was ever wrong); --sidebar-border/
       --sidebar-ink/--sidebar-ink-strong never had ANY per-theme override
       at all. Rather than add three new override blocks per theme, these
       now simply point at --border/--ink-muted/--ink — tokens that
       already flip correctly for every theme this file has — so the
       sidebar's text/border colour automatically tracks whichever theme
       is active, including glass-light (whose sidebar-bg this file
       already made light, which is exactly the "glass-light sidebar text
       unreadable" bug fixed here too). The one exception is the profile
       card's name/role text, which sits on its own permanently-dark photo
       scrim (templates/layout/sidebar.php) regardless of theme — that is
       NOT wired to these tokens, see .sidebar__profile-name/-role below. */
    --sidebar-bg: linear-gradient(180deg, #fffbf7 0%, #fbf0e4 45%, #f3e2cc 100%);
    --sidebar-border: var(--border);
    /* Delivery 61: darkened from var(--ink-muted) (#7a7166) on direct
       request ("make font color and icon color darker in the sidebar
       menus" — light themes only). This is now a fixed colour rather
       than a forwarded var(), specifically for the light-theme case.
       glass-light needs no override of its own: it never redefines
       --ink/--ink-muted or --sidebar-ink, so it inherits this same
       darker value automatically. Each dark theme below (and the
       prefers-color-scheme dark block) now carries its own fixed
       --sidebar-ink too — Delivery 62 lightened those independently, on
       a separate direct request — so none of them fall through to this
       light-theme value. */
    --sidebar-ink: #49433c;
    --sidebar-ink-strong: var(--ink);
    /* Delivery 62 — "give one more theme option to add cover page,
       selected will be the background for all pages": each page now
       sets this custom property inline on <body> (or <html>, same
       cascade) to the actor's own chosen cover photo — the exact URL
       already computed by coverImageForCurrentActor(), the same helper
       the sidebar profile card and My Profile use — wrapped as
       url('...'). It defaults to `none` here so a page that has not
       been updated to set it (or the "Cover" theme simply not being
       chosen) never breaks the `background` declaration that reads it
       below; see the new :root[data-theme="cover"] body rule. */
    --page-cover-url: none;

    --surface: var(--glass-bg);
    /* One step up from --surface, for a hovered or currently-selected row.
       Real bug found while fixing the dropdowns: this variable was USED in
       four places (.ss__option:hover / .is-current, .theme-menu__item:hover
       / .is-current, .panel__head:hover) but never defined anywhere, so
       `background: var(--surface-2)` was an invalid declaration the browser
       threw away — those rows had no highlight at all, in any theme. It is
       deliberately a translucent ink wash rather than a fixed colour, so the
       one value darkens a light surface and the dark themes' own override
       lightens a dark one. */
    --surface-2: rgba(122, 113, 102, 0.13);
    --success: #12b76a;
    --success-bg: #e7fbf1;
    --success-text: #12b76a;
    --danger: #dc2626;
    --danger-bg: #fdeceb;
    --danger-text: #dc2626;
    --warning: #ca8a04;
    --warning-bg: #fdf5db;
    --info: #0ba5ec;
    --info-bg: #e9f8ff;

    --radius: 14px;
    --shadow: 0 12px 32px rgba(120, 53, 15, 0.16), 0 1px 2px rgba(120, 53, 15, 0.06);

    --font: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --font-display: 'Outfit', var(--font);
}

/* Dark theme: applied when the user picks it explicitly, OR when the
   browser is set to dark mode and the user hasn't chosen "light". */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        color-scheme: dark;
        --ink: #f3ede4;
        --ink-muted: #a3978a;
        --border: rgba(255, 255, 255, 0.10);
        --surface-2: rgba(255, 255, 255, 0.09);

        --bg-solid: #131110;
        --bg-gradient:
            radial-gradient(circle at 8% 0%, rgba(194, 65, 12, 0.16) 0%, transparent 42%),
            radial-gradient(circle at 92% 100%, rgba(87, 83, 78, 0.20) 0%, transparent 48%),
            var(--bg-solid);

        --glass-bg: rgba(38, 32, 25, 0.55);
        --glass-bg-strong: rgba(38, 32, 25, 0.82);
        --glass-border: rgba(255, 255, 255, 0.09);

        --sidebar-bg: linear-gradient(180deg, #171310 0%, #1d1712 45%, #0e0c0a 100%);
        /* Delivery 61 hardcoded --sidebar-ink for the light themes
           instead of forwarding var(--ink-muted), so dark contexts each
           need their own explicit value to stay independent of that.
           Delivery 62: lightened further on direct request ("in the dark
           themes, make sidebar menu font and icons lighter") — this was
           var(--ink-muted) (#a3978a) until now. */
        --sidebar-ink: #cfc6bc;

        --success-bg: rgba(18, 183, 106, 0.16);
        --success-text: #4ade80;
        --danger-bg: rgba(220, 38, 38, 0.16);
        --danger-text: #f87171;
        --warning-bg: rgba(202, 138, 4, 0.16);
        --info-bg: rgba(11, 165, 236, 0.16);

        --shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
    }
}
:root[data-theme="dark"] {
    color-scheme: dark;
    --ink: #f3ede4;
    --ink-muted: #a3978a;
    --border: rgba(255, 255, 255, 0.10);
    --surface-2: rgba(255, 255, 255, 0.09);

    --bg-solid: #131110;
    --bg-gradient:
        radial-gradient(circle at 8% 0%, rgba(194, 65, 12, 0.16) 0%, transparent 42%),
        radial-gradient(circle at 92% 100%, rgba(87, 83, 78, 0.20) 0%, transparent 48%),
        var(--bg-solid);

    --glass-bg: rgba(38, 32, 25, 0.55);
    --glass-bg-strong: rgba(38, 32, 25, 0.82);
    --glass-border: rgba(255, 255, 255, 0.09);

    --sidebar-bg: linear-gradient(180deg, #171310 0%, #1d1712 45%, #0e0c0a 100%);
    /* Delivery 62: lightened on direct request — see the matching
       comment in the prefers-color-scheme dark block above. */
    --sidebar-ink: #cfc6bc;

    --success-bg: rgba(18, 183, 106, 0.16);
    --success-text: #4ade80;
    --danger-bg: rgba(220, 38, 38, 0.16);
    --danger-text: #f87171;
    --warning-bg: rgba(202, 138, 4, 0.16);
    --info-bg: rgba(11, 165, 236, 0.16);

    --shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
}


/* =====================================================================
   Two extra themes: glossy "liquid glass" in light and dark.
   Same tokens as the base themes — only the surfaces change — so every
   component picks them up without any component-level styling.
   The gloss is a stronger blur, a brighter translucent surface, a
   highlight along the top edge of each panel and a deeper shadow, which
   together read as thick glass rather than flat translucency.
   ===================================================================== */
:root[data-theme="glass-light"] {
    color-scheme: light;
    --glass-blur: 26px;
    --glass-bg: rgba(255, 255, 255, 0.55);
    --glass-bg-strong: rgba(255, 255, 255, 0.74);
    --glass-border: rgba(255, 255, 255, 0.85);
    --bg-solid: #eef1f7;
    --bg-gradient:
        radial-gradient(circle at 12% 4%, rgba(242, 102, 15, 0.20) 0%, transparent 46%),
        radial-gradient(circle at 88% 12%, rgba(11, 165, 236, 0.20) 0%, transparent 44%),
        radial-gradient(circle at 50% 100%, rgba(139, 92, 246, 0.16) 0%, transparent 52%),
        var(--bg-solid);
    --shadow: 0 16px 40px rgba(31, 41, 55, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.75);
    --sidebar-bg: linear-gradient(180deg, rgba(255,255,255,.72) 0%, rgba(255,255,255,.58) 100%);
}
:root[data-theme="glass-dark"] {
    color-scheme: dark;
    --ink: #f4f1ec;
    --ink-muted: #a9a29a;
    --border: rgba(255, 255, 255, 0.14);
    --surface-2: rgba(255, 255, 255, 0.10);
    --glass-blur: 26px;
    --glass-bg: rgba(30, 34, 44, 0.52);
    --glass-bg-strong: rgba(30, 34, 44, 0.76);
    --glass-border: rgba(255, 255, 255, 0.16);
    --bg-solid: #0b0f16;
    --bg-gradient:
        radial-gradient(circle at 10% 0%, rgba(242, 102, 15, 0.22) 0%, transparent 44%),
        radial-gradient(circle at 90% 8%, rgba(11, 165, 236, 0.20) 0%, transparent 46%),
        radial-gradient(circle at 50% 100%, rgba(139, 92, 246, 0.18) 0%, transparent 54%),
        var(--bg-solid);
    --shadow: 0 18px 44px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.14);
    --sidebar-bg: linear-gradient(180deg, rgba(18,22,30,.82) 0%, rgba(12,15,21,.72) 100%);
    /* Delivery 62: lightened on direct request — see the matching
       comment in the prefers-color-scheme dark block above. This was
       var(--ink-muted) (#a9a29a) until now. */
    --sidebar-ink: #d2cdc7;
    --success-bg: rgba(18, 183, 106, 0.18);
    --success-text: #4ade80;
    --danger-bg: rgba(220, 38, 38, 0.18);
    --danger-text: #f87171;
    --warning-bg: rgba(202, 138, 4, 0.18);
    --info-bg: rgba(11, 165, 236, 0.18);
}
/* The gloss itself: a bright hairline along the top of every glass panel. */
:root[data-theme="glass-light"] .card,
:root[data-theme="glass-light"] .kpi,
:root[data-theme="glass-dark"] .card,
:root[data-theme="glass-dark"] .kpi {
    position: relative;
    background-image: linear-gradient(180deg, rgba(255,255,255,.28) 0%, rgba(255,255,255,0) 38%);
}
:root[data-theme="glass-light"] .topbar,
:root[data-theme="glass-dark"] .topbar {
    -webkit-backdrop-filter: blur(20px) saturate(170%);
    backdrop-filter: blur(20px) saturate(170%);
}

/* =====================================================================
   Delivery 103 — "Frosted Cover": one more theme, requested as "add one
   more theme with this effect...only for the cover page..if added the
   transparency will be like this everywhere..on all tables, info card..
   charts etc..and add the same in profile theme section". Built from
   two pieces that already exist in this file, turned up together rather
   than as new machinery:
     - the Cover theme's own page background (the actor's own chosen
       cover photo, full-bleed, fixed — see coverImageForCurrentActor(),
       core/helpers.php, and the :root[data-theme="cover"] body rule
       above) — reused below with a much thinner tint, so the photo
       itself stays fully visible behind the frosted surfaces, the way
       the requested screenshot showed pill badges floating over a
       clearly visible snowy photo rather than a tinted-over one.
     - the glass-light/glass-dark gloss just above, turned up well past
       either of them. --glass-blur, --glass-bg, --glass-bg-strong and
       --glass-border are the four tokens every frosted surface in this
       app already reads from directly — .card, .kpi (gloss highlight
       below), .table (line ~1174), .topbar, .sidebar (its own
       backdrop-filter already uses var(--glass-blur)), .btn-secondary,
       dropdown/select menus and more — so turning those four up in one
       place is what makes cards, roster tables, KPI/info tiles AND every
       chart (each one drawn inside a .card — see
       modules/dashboard/_charts.php) all read as the same frosted glass
       over the photo, everywhere, without a single new selector for any
       of them individually.
   One variant, not the usual light/dark/cover trio sharp-* and squircle-*
   got — the request asked for this effect "only for the cover page".
   ===================================================================== */
:root[data-theme="frost-cover"] {
    color-scheme: light;
    --glass-blur: 34px;
    --glass-bg: rgba(255, 255, 255, 0.32);
    --glass-bg-strong: rgba(255, 255, 255, 0.5);
    --glass-border: rgba(255, 255, 255, 0.7);
    --shadow: 0 16px 44px rgba(31, 41, 55, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.6);
    --sidebar-bg: linear-gradient(180deg, rgba(255,255,255,.4) 0%, rgba(255,255,255,.24) 100%);
}
/* Same photo-background mechanic as the Cover theme, deliberately with a
   much thinner tint on top of the photo than that theme uses, so more
   of the photo shows through behind the frosted surfaces above. */
:root[data-theme="frost-cover"] body {
    background:
        linear-gradient(180deg, rgba(255, 255, 255, .10) 0%, rgba(255, 255, 255, .22) 100%) no-repeat,
        var(--page-cover-url) center / cover no-repeat fixed var(--bg-solid);
}
:root[data-theme="frost-cover"] .card,
:root[data-theme="frost-cover"] .kpi {
    position: relative;
    background-image: linear-gradient(180deg, rgba(255,255,255,.32) 0%, rgba(255,255,255,0) 42%);
}
:root[data-theme="frost-cover"] .topbar {
    -webkit-backdrop-filter: blur(24px) saturate(180%);
    backdrop-filter: blur(24px) saturate(180%);
}

/* =====================================================================
   Delivery 107/108 — "Dynamic Backgrounds": Morning, Afternoon and
   Evening Themes that switch automatically, plus a Festival Theme. This
   one theme option (data-theme="dynamic") carries no fixed look of its
   own — templates/layout/app.php also writes a data-daypart attribute
   (morning/afternoon/evening — see currentDaypart(), core/helpers.php)
   that these rules key off, corrected client-side for the VISITOR's own
   local time before first paint (templates/partials/theme_init.php), the
   same reasoning this app's existing time-of-day greeting and birthday
   check already use for reading the visitor's own clock rather than the
   server's.

   Delivery 108 FIX — "font is not visible": the first version of this
   block (Delivery 107) left ink/border/card tokens alone, on the
   assumption that the base (Light) values would simply apply. They
   don't, always: this app ALSO auto-switches to dark tokens whenever the
   VISITOR's OWN operating system is set to dark mode, for every theme
   except one literally named "light" — see the
   `@media (prefers-color-scheme: dark) { :root:not([data-theme="light"])
   {...} }` block far above. "dynamic" is not literally "light", so that
   rule matched it too — meaning a visitor whose OS is in dark mode got
   Dynamic's light morning/afternoon PAGE background with that block's
   light-on-dark TEXT colour on top of it (invisible), and conversely a
   visitor whose OS is in light mode got Dynamic's dark evening
   background with default light-theme text and card colours (also
   invisible). Each daypart below now explicitly pins its own COMPLETE
   set of ink/border/card/sidebar/status tokens — mirroring exactly what
   :root[data-theme="dark"] already does for the plain Dark theme,
   instead of leaving them to whatever else happens to cascade in — so
   the visitor's OS colour-scheme setting can never override what this
   theme intends for the time of day. */
:root[data-theme="dynamic"][data-daypart="morning"],
:root[data-theme="dynamic"][data-daypart="afternoon"] {
    color-scheme: light;
    --ink: #211d19;
    --ink-muted: #7a7166;
    --border: rgba(122, 113, 102, 0.22);
    --surface-2: rgba(122, 113, 102, 0.13);
    --glass-bg: rgba(255, 255, 255, 0.6);
    --glass-bg-strong: rgba(255, 255, 255, 0.78);
    --glass-border: rgba(255, 255, 255, 0.55);
    --sidebar-bg: linear-gradient(180deg, #fffbf7 0%, #fbf0e4 45%, #f3e2cc 100%);
    --sidebar-ink: #49433c;
    --success-bg: #e7fbf1;
    --success-text: #12b76a;
    --danger-bg: #fdeceb;
    --danger-text: #dc2626;
    --warning-bg: #fdf5db;
    --info-bg: #e9f8ff;
    --shadow: 0 12px 32px rgba(120, 53, 15, 0.16), 0 1px 2px rgba(120, 53, 15, 0.06);
    /* Delivery 110 fix — "combobox text / chart text / popup theme not
       visible": the same OS-dark-mode cascade bug the ink/border tokens
       above were fixed for (Delivery 108) also hit --bg-solid (every
       <select> option list, the searchable-select panel, the message
       picker/modal, the notification bell popup and the chatbot panel
       all paint on var(--bg-solid) + var(--ink)) and the whole --viz-*
       token set (chart text/grid colours) — neither was pinned here, so
       both kept falling through to the OS's prefers-color-scheme media
       query instead of this daypart's own explicit value. Pinned now,
       same as every other token above. */
    --bg-solid: #fdf8f2;
    --bg-gradient:
        radial-gradient(circle at 8% 0%, rgba(194, 65, 12, 0.10) 0%, transparent 42%),
        radial-gradient(circle at 92% 100%, rgba(87, 83, 78, 0.09) 0%, transparent 48%),
        var(--bg-solid);
    --viz-1: #2a78d6;
    --viz-2: #eb6834;
    --viz-3: #1baf7a;
    --viz-seq:   #2a78d6;
    --viz-seq-1: #86b6ef;
    --viz-seq-2: #5598e7;
    --viz-seq-3: #2a78d6;
    --viz-seq-4: #1c5cab;
    --viz-seq-5: #104281;
    --viz-surface: #fdfbf8;
    --viz-ink: #2b2521;
    --viz-ink-muted: #857b71;
    --viz-grid: rgba(43, 37, 33, 0.10);
    --viz-track: rgba(43, 37, 33, 0.06);
}
:root[data-theme="dynamic"][data-daypart="morning"] body {
    background:
        radial-gradient(circle at 15% 0%, rgba(255, 214, 153, .5) 0%, transparent 55%),
        linear-gradient(160deg, #fff6e8 0%, #ffe9d2 45%, #fef4ea 100%);
}
:root[data-theme="dynamic"][data-daypart="afternoon"] body {
    background:
        radial-gradient(circle at 85% 0%, rgba(255, 190, 130, .35) 0%, transparent 50%),
        linear-gradient(160deg, #fff8f2 0%, #fdeee0 50%, #fbf6ef 100%);
}
:root[data-theme="dynamic"][data-daypart="evening"] {
    color-scheme: dark;
    --ink: #f4ece6;
    --ink-muted: #cbb9c4;
    --border: rgba(244, 236, 230, 0.18);
    --surface-2: rgba(255, 255, 255, 0.09);
    --glass-bg: rgba(38, 32, 25, 0.55);
    --glass-bg-strong: rgba(38, 32, 25, 0.82);
    --glass-border: rgba(255, 255, 255, 0.09);
    --sidebar-bg: linear-gradient(180deg, #171310 0%, #1d1712 45%, #0e0c0a 100%);
    --sidebar-ink: #cfc6bc;
    --success-bg: rgba(18, 183, 106, 0.16);
    --success-text: #4ade80;
    --danger-bg: rgba(220, 38, 38, 0.16);
    --danger-text: #f87171;
    --warning-bg: rgba(202, 138, 4, 0.16);
    --info-bg: rgba(11, 165, 236, 0.16);
    --shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
    /* Delivery 110 fix — see the matching comment on the morning/afternoon
       block above: --bg-solid and the --viz-* chart tokens were never
       pinned per-daypart, so selects/popups/the chatbot/notif bell and
       chart text all fell back to whatever the OS light/dark setting
       last resolved instead of this (dark) daypart's own colours. */
    --bg-solid: #1e1713;
    --bg-gradient:
        radial-gradient(circle at 8% 0%, rgba(194, 65, 12, 0.16) 0%, transparent 42%),
        radial-gradient(circle at 92% 100%, rgba(87, 83, 78, 0.20) 0%, transparent 48%),
        var(--bg-solid);
    --viz-1: #3987e5;
    --viz-2: #d95926;
    --viz-3: #199e70;
    --viz-seq:   #3987e5;
    --viz-seq-1: #184f95;
    --viz-seq-2: #256abf;
    --viz-seq-3: #3987e5;
    --viz-seq-4: #6da7ec;
    --viz-seq-5: #9ec5f4;
    --viz-surface: #1e1a15;
    --viz-ink: #ece5db;
    --viz-ink-muted: #a49a8d;
    --viz-grid: rgba(255, 255, 255, 0.12);
    --viz-track: rgba(255, 255, 255, 0.07);
}
:root[data-theme="dynamic"][data-daypart="evening"] body {
    background:
        radial-gradient(circle at 20% 100%, rgba(124, 58, 237, .16) 0%, transparent 55%),
        linear-gradient(160deg, #2f2438 0%, #4a2f3c 45%, #6b3b3a 100%);
}
/* A festival day gets a warm gold accent wash layered ON TOP of whichever
   daypart background is already showing, rather than replacing it — the
   time of day doesn't stop just because it's also a festival. Each combo
   below repeats its own daypart's two gradients rather than trying to
   reuse them through a CSS variable — `background` is a shorthand, and a
   plain `background-image` override here would have silently discarded
   the daypart layers underneath instead of stacking on top of them. */
:root[data-theme="dynamic"][data-daypart="morning"][data-festival] body {
    background:
        radial-gradient(circle at 50% 0%, rgba(255, 196, 92, .28) 0%, transparent 60%),
        radial-gradient(circle at 15% 0%, rgba(255, 214, 153, .5) 0%, transparent 55%),
        linear-gradient(160deg, #fff6e8 0%, #ffe9d2 45%, #fef4ea 100%);
}
:root[data-theme="dynamic"][data-daypart="afternoon"][data-festival] body {
    background:
        radial-gradient(circle at 50% 0%, rgba(255, 196, 92, .28) 0%, transparent 60%),
        radial-gradient(circle at 85% 0%, rgba(255, 190, 130, .35) 0%, transparent 50%),
        linear-gradient(160deg, #fff8f2 0%, #fdeee0 50%, #fbf6ef 100%);
}
:root[data-theme="dynamic"][data-daypart="evening"][data-festival] body {
    background:
        radial-gradient(circle at 50% 0%, rgba(255, 196, 92, .18) 0%, transparent 60%),
        radial-gradient(circle at 20% 100%, rgba(124, 58, 237, .16) 0%, transparent 55%),
        linear-gradient(160deg, #2f2438 0%, #4a2f3c 45%, #6b3b3a 100%);
}

/*
 * Theme-aware logo (see logoSwapHtml() in core/helpers.php): the colour mark
 * shows on light backgrounds, the black-and-white mark on dark ones. Follows
 * the same "explicit dark/glass-dark wins, otherwise fall back to the OS
 * preference unless the person explicitly chose a light variant" pattern
 * every other themed rule in this file already uses.
 */
.logo-mono { display: none; }
:root[data-theme="dark"] .logo-color,
:root[data-theme="glass-dark"] .logo-color { display: none; }
:root[data-theme="dark"] .logo-mono,
:root[data-theme="glass-dark"] .logo-mono { display: inline-block; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]):not([data-theme="glass-light"]) .logo-color { display: none; }
    :root:not([data-theme="light"]):not([data-theme="glass-light"]) .logo-mono { display: inline-block; }
}


/* Typography, unified. Body copy is one family everywhere and headings the
   display face; nothing else sets its own family. Sizes are fluid — they
   shrink on a small screen and grow a little on a large one rather than
   staying fixed — so a dense screen stays readable without anything being
   oversized. clamp() keeps each within a deliberate floor and ceiling. */
html { font-size: clamp(13.5px, 0.5vw + 12.2px, 16px); }
body, input, select, textarea, button, .form-control, .table, .btn {
    font-family: var(--font);
}
h1 { font-size: clamp(1.35rem, 1.1vw + 1rem, 1.8rem); }
h2 { font-size: clamp(1.15rem, 0.7vw + 0.95rem, 1.45rem); }
h3 { font-size: clamp(1rem, 0.35vw + 0.9rem, 1.15rem); }
h4 { font-size: clamp(.92rem, 0.2vw + 0.86rem, 1rem); }
h1, h2, h3, h4 { line-height: 1.25; }
.table { font-size: clamp(.78rem, 0.18vw + .74rem, .88rem); }
.kpi__value { font-size: clamp(1.15rem, 0.5vw + 1rem, 1.5rem); }

* { box-sizing: border-box; }

html, body { height: 100%; }
body {
    margin: 0;
    padding: 0;
    background: var(--bg-gradient) fixed;
    color: var(--ink);
    font-family: var(--font);
    /* Follows the root clamp above rather than pinning body copy at a
       fixed size — that is what makes the whole page scale together. On a
       normal desktop this still resolves to 16px, so nothing grew. */
    font-size: 1rem;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    transition: background-color .2s ease, color .2s ease;
}
/* Delivery 62 — the "Cover" theme: the actor's own chosen cover photo
   (the same one behind the sidebar profile card and My Profile — see
   coverImageForCurrentActor(), core/helpers.php) becomes the background
   for every page instead of the usual gradient. A soft tint sits on top
   of the photo (same purpose as the darker scrim the profile/sidebar
   cover already uses, just much lighter here — this is a page
   background behind ordinary dark-on-light text and cards, not a photo
   caption) so the photo reads as a backdrop rather than competing with
   the page's own content. Falls back to --bg-solid wherever the photo
   fails to load or --page-cover-url is still `none` (an anonymous
   visitor on a public page with no cover ever chosen). Everything else
   about this theme — ink, borders, card/glass surfaces, the sidebar
   itself — is exactly the Light theme's, since only the page background
   was asked for. */
:root[data-theme="cover"] body {
    background:
        linear-gradient(180deg, rgba(250, 246, 240, .55) 0%, rgba(250, 246, 240, .85) 100%) no-repeat,
        var(--page-cover-url) center / cover no-repeat fixed var(--bg-solid);
}

h1, h2, h3, .auth-box__head h1 { font-family: var(--font-display); }

a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }
img { max-width: 100%; }

button, input, select, textarea { font: inherit; }

/* ---------- Glass helper ---------- */
.glass {
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
}

/* ---------- Theme toggle ---------- */
.theme-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 38px; height: 38px; border-radius: 10px;
    border: 1px solid var(--border); background: var(--glass-bg);
    cursor: pointer; color: var(--ink); font-size: 1.05rem; line-height: 1;
}
.theme-toggle:hover { border-color: var(--brand); }
.theme-toggle__icon { pointer-events: none; }

/* Four themes now, so the control is a small menu rather than a two-state
   switch. The menu is anchored to the button and closes on outside click. */
.theme-picker { position: relative; display: inline-block; }
.theme-menu {
    position: absolute; right: 0; top: calc(100% + .4rem); z-index: 60;
    min-width: 12.5rem; padding: .3rem;
    /* Opaque, not a glass surface: a menu that floats over a form has to
       hide what is under it, or the labels behind show through and both
       become unreadable. */
    background: var(--bg-solid); color: var(--ink);
    border: 1px solid var(--border); border-radius: 12px;
    box-shadow: 0 14px 34px rgba(15, 23, 42, .28);
}
.theme-menu__item {
    display: flex; align-items: center; gap: .55rem; width: 100%;
    padding: .5rem .6rem; border: 0; border-radius: 8px;
    background: transparent; color: inherit; cursor: pointer;
    font: inherit; font-size: .86rem; text-align: left;
}
.theme-menu__item:hover { background: var(--surface-2); }
.theme-menu__icon { font-size: 1rem; line-height: 1; width: 1.25rem; text-align: center; }
.theme-menu__tick { margin-left: auto; color: var(--brand); opacity: 0; font-weight: 700; }
.theme-menu__item.is-current { background: var(--surface-2); font-weight: 600; }
.theme-menu__item.is-current .theme-menu__tick { opacity: 1; }

/* ---------- Layout shell ---------- */

.app-shell { min-height: 100vh; display: flex; }

/* The sidebar is never a permanently-docked column, at any screen size —
   it is always the menu button's own expanded state. Collapsed, it is
   invisible and takes no space; the button (.topbar__menu-btn) is the
   only thing on screen. Clicked, it grows smoothly out from that
   button's top-left corner (transform-origin + scale, not a slide-in),
   to a size that fits the current device — a near-full-screen panel on
   a phone, a comfortably-sized one on a tablet or desktop — and clicking
   it again (or the button, which itself swaps from a menu icon to a
   close icon while open) shrinks it back down to just the icon. */
.sidebar {
    width: min(92vw, 340px);
    height: min(80vh, 640px);
    background: var(--sidebar-bg);
    border: 1px solid var(--sidebar-border);
    border-radius: 20px;
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(0, 0, 0, 0.15);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    color: var(--sidebar-ink);
    position: fixed;
    top: 68px;
    left: 14px;
    display: flex;
    flex-direction: column;
    z-index: 40;
    transform-origin: top left;
    transform: scale(.05);
    opacity: 0;
    visibility: hidden;
    transition: transform .3s cubic-bezier(.22, 1, .36, 1), opacity .2s ease, visibility 0s linear .3s;
}
.sidebar.is-open {
    transform: scale(1);
    opacity: 1;
    visibility: visible;
    transition: transform .32s cubic-bezier(.22, 1, .36, 1), opacity .22s ease;
}

/* The sidebar-bg gradient is opaque by design (it needs to stay legible
   over whatever page content is behind it while it's open), but every
   panel drawn on top of it — profile card, nav tiles, category headers —
   uses translucent/blurred surfaces, which is what actually reads as
   "glass" here. */

@media (min-width: 600px) {
    .sidebar { width: 380px; height: min(82vh, 680px); }
}

/* ---------- Sidebar: top strip (logo + close button) ---------- */
.sidebar__topstrip {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: .85rem 1rem;
    border-bottom: 1px solid var(--sidebar-border);
    flex-shrink: 0;
}
.sidebar__logo.logo-swap { display: inline-flex; align-items: center; }
.sidebar__logo .logo-img { height: 26px; width: auto; }
.sidebar__close {
    display: inline-flex;
    /* Was a fixed white wash (assumed a dark sidebar-bg behind it always).
       --surface-2 is this file's existing "translucent ink wash that
       darkens a light surface and lightens a dark one" token (see its
       definition further up) — reusing it here makes this chip legible
       against the sidebar-bg regardless of which theme lightens/darkens
       it. */
    background: var(--surface-2);
    border: 1px solid var(--sidebar-border);
    border-radius: 8px;
    width: 34px; height: 34px;
    align-items: center; justify-content: center;
    color: var(--sidebar-ink-strong);
    cursor: pointer;
}

/* ---------- Sidebar: profile card (cover banner + avatar + name) ----------
   Reuses the portal's own existing hero image (assets/images/banner.png,
   already used on the public landing page) rather than a new asset — a
   dark gradient is layered over it for legibility, matching the sidebar's
   own dark theme regardless of the app's light/dark mode. */
.sidebar__profile {
    flex-shrink: 0;
    padding: 1.4rem 1rem 1.1rem;
    background-size: cover;
    background-position: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    border-bottom: 1px solid var(--sidebar-border);
}
.sidebar__avatar-link { display: inline-flex; text-decoration: none; border-radius: 50%; }
.sidebar__avatar-link:hover .sidebar__avatar { filter: brightness(1.06); }
/* 60px -> 76px, and it now shows the person's photo when they have one
   (actorAvatarHtml(), core/helpers.php) rather than always their
   initials. The photo was already being uploaded and shown on My
   Profile; the menu simply never read it. */
.sidebar__avatar {
    width: 76px; height: 76px;
    border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    background: var(--brand-gradient);
    color: #fff;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 1.5rem;
    border: 3px solid rgba(255, 255, 255, 0.88);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
    letter-spacing: .02em;
}
.sidebar__avatar.is-photo { object-fit: cover; background: var(--ink); }
.sidebar__profile-name {
    margin-top: .7rem;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 1.06rem;
    line-height: 1.25;
    /* Fixed to white, NOT var(--sidebar-ink-strong): this sits on the
       profile card's own always-dark photo scrim (see the inline
       background-image on .sidebar__profile in
       templates/layout/sidebar.php), which doesn't change with the
       theme, so the text on it shouldn't either — --sidebar-ink-strong
       now flips per theme for the rest of the menu, which would make
       this invisible in Light/Glass Light if it followed along. */
    color: #fff;
    text-shadow: 0 1px 3px rgba(0, 0, 0, .5);
    /* Two lines then an ellipsis — a four-word name used to push the
       role label and the first nav category down the panel. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    max-width: 100%;
}
.sidebar__profile-role {
    margin-top: .15rem;
    font-size: .78rem;
    /* Same reasoning as .sidebar__profile-name above: this also sits on
       the profile card's fixed-dark photo scrim, not the flipping
       sidebar-bg, so it correctly stays a constant light cream — left
       exactly as it was. */
    color: rgba(228, 216, 200, .85);
    text-transform: uppercase;
    letter-spacing: .06em;
}

/* ---------- Sidebar: menu search (delivery 45) ----------
   Sits between the profile card and <nav>, not inside it, so it stays
   put (flex-shrink: 0, same as .sidebar__topstrip/.sidebar__profile
   above it) while the nav list underneath it scrolls. Every colour here
   is one of the tokens the rest of the sidebar already uses, so it
   tracks whichever theme is active the same way they do — no separate
   per-theme rules needed. */
.sidebar__search {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-shrink: 0;
    margin: .8rem 1.1rem 0;
    padding: .5rem .7rem;
    border-radius: 10px;
    border: 1px solid var(--sidebar-border);
    background: var(--surface-2);
}
.sidebar__search-icon { display: inline-flex; flex: none; color: var(--sidebar-ink); }
.sidebar__search-input {
    flex: 1;
    min-width: 0;
    background: none;
    border: none;
    outline: none;
    font-family: var(--font);
    font-size: .85rem;
    color: var(--sidebar-ink-strong);
}
.sidebar__search-input::placeholder { color: var(--sidebar-ink); opacity: .85; }
/* Same show/hide convention as [data-conditional] elsewhere in this
   file: a plain `hidden` attribute would not actually have hidden this
   button, because its own `display` declaration (needed to lay the icon
   out) is author-origin and always wins over the browser's user-agent
   [hidden]{display:none} default, regardless of source order. */
.sidebar__search-clear {
    display: none;
    align-items: center;
    justify-content: center;
    flex: none;
    padding: 0;
    background: none;
    border: none;
    cursor: pointer;
    color: var(--sidebar-ink);
}
.sidebar__search-clear.is-shown { display: inline-flex; }
.sidebar__search-clear:hover { color: var(--sidebar-ink-strong); }
.sidebar__search-empty {
    display: none;
    padding: .6rem .8rem 0;
    margin: 0;
    font-size: .82rem;
    text-align: center;
    color: var(--sidebar-ink);
}
.sidebar__search-empty.is-shown { display: block; }

/* ---------- Sidebar: nav (categories + icon-tile grids) ---------- */
.sidebar__nav { flex: 1; overflow-y: auto; padding: .9rem .8rem 1.2rem; }

.navgrid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: .55rem;
}

.navtile {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .45rem;
    padding: .8rem .4rem;
    border-radius: 14px;
    /* Same reasoning as .sidebar__close above. */
    background: var(--surface-2);
    border: 1px solid var(--sidebar-border);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    color: var(--sidebar-ink);
    font-size: .78rem;
    text-align: center;
    line-height: 1.2;
    transition: background .15s ease, border-color .15s ease, transform .15s ease;
}
.navtile:hover {
    background: rgba(242, 102, 15, 0.18);
    border-color: rgba(242, 102, 15, 0.45);
    color: var(--sidebar-ink-strong);
    text-decoration: none;
    transform: translateY(-1px);
}
.navtile.is-active {
    background: var(--brand-gradient);
    border-color: transparent;
    color: #fff;
    box-shadow: 0 6px 16px rgba(234, 88, 12, 0.35);
}
.navtile__icon {
    display: inline-flex; align-items: center; justify-content: center;
    width: 34px; height: 34px;
    border-radius: 10px;
    /* Same reasoning as .sidebar__close above. */
    background: var(--surface-2);
}
.navtile.is-active .navtile__icon { background: rgba(255, 255, 255, 0.2); }
.navtile__label { font-weight: 500; }

.navcat { margin-bottom: .5rem; }
.navcat__head {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    background: none;
    border: none;
    padding: .6rem .3rem;
    /* Was a hardcoded cream (rgba(228,216,200,.75)) — fine while the
       sidebar was always dark, wrong once --sidebar-ink started flipping
       per theme (Light/Glass Light now have a light sidebar). This sits
       directly on --sidebar-bg, not the profile card's dark scrim, so it
       should track --sidebar-ink like the rest of the nav does. */
    color: var(--sidebar-ink);
    font-family: var(--font-display);
    font-size: .74rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .07em;
    cursor: pointer;
}
.navcat__chevron { display: inline-flex; transition: transform .15s ease; color: var(--sidebar-ink); }
.navcat.is-collapsed .navcat__chevron { transform: rotate(-90deg); }
.navcat.is-collapsed .navgrid { display: none; }

/* ---------- Sidebar: alternate designs (delivery 34) ----------
   Same markup every time (templates/layout/sidebar.php never branches on
   this) — .sidebar--tiles is the design above and needs no rules of its
   own; .sidebar--list and .sidebar--compact are pure CSS re-layouts of
   the same .navcat/.navgrid/.navtile structure, switched with one class
   on the <aside class="sidebar sidebar--*"> element (see sidebarDesign()
   in core/helpers.php and the picker on My Profile). */

/* List: one full-width row per link, icon + label side by side, instead
   of a 2-3 column tile grid — closer to a conventional sidebar. */
.sidebar--list .navgrid {
    display: flex;
    flex-direction: column;
    gap: .2rem;
}
.sidebar--list .navtile {
    flex-direction: row;
    justify-content: flex-start;
    text-align: left;
    padding: .55rem .6rem;
    border-radius: 10px;
    gap: .65rem;
}
.sidebar--list .navtile__icon { width: 28px; height: 28px; flex-shrink: 0; }
.sidebar--list .navtile:hover { transform: none; }

/* Compact: dense, icon-free text list — every category always expanded
   (no accordion), smallest footprint for someone who just wants to read
   down a list of links. */
.sidebar--compact .navgrid {
    display: flex;
    flex-direction: column;
    gap: 0;
}
.sidebar--compact .navtile {
    flex-direction: row;
    justify-content: flex-start;
    text-align: left;
    padding: .4rem .5rem;
    border-radius: 6px;
    background: none;
    border: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    font-size: .82rem;
}
.sidebar--compact .navtile__icon { display: none; }
.sidebar--compact .navtile:hover { transform: none; background: rgba(242, 102, 15, 0.14); }
.sidebar--compact .navtile.is-active { box-shadow: none; }
.sidebar--compact .navcat__chevron { display: none; }
.sidebar--compact .navcat.is-collapsed .navgrid { display: flex; } /* always expanded in this design */
.sidebar--compact .navcat__head { cursor: default; padding: .4rem .3rem; }

.sidebar__foot { padding: .8rem 1.1rem 1rem; border-top: 1px solid var(--sidebar-border); flex-shrink: 0; }
.sidebar__logout {
    display: flex; align-items: center; gap: .55rem;
    padding: .6rem .7rem;
    border-radius: 10px;
    font-size: .88rem;
    font-weight: 500;
    /* Same fix as .navcat__head/.navcat__chevron above — this sits on the
       flipping --sidebar-bg (the footer), not the profile scrim. */
    color: var(--sidebar-ink);
}
.sidebar__logout:hover { background: var(--surface-2); color: var(--sidebar-ink-strong); text-decoration: none; }

@media (min-width: 600px) {
    .navgrid { grid-template-columns: repeat(3, 1fr); }
}

.sidebar-backdrop {
    position: fixed; inset: 0; background: rgba(10,10,12,.45);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
    z-index: 30; display: none;
}
.sidebar-backdrop.is-open { display: block; }

/* min-width: 0 matters here: .main is a flex item, and a flex item's
   default min-width is auto (its content's min-content size), not 0.
   A content-heavy page — batches' 10-tile KPI row plus a 14-column
   table — has a wide min-content size that this would otherwise use to
   push .main past the viewport rather than let its own grids and
   table-wrap do their own wrapping/scrolling. */
.main { display: flex; flex-direction: column; min-height: 100vh; width: 100%; min-width: 0; }

.topbar {
    background: var(--glass-bg-strong);
    border-bottom: 1px solid var(--border);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    display: flex;
    align-items: center;
    gap: .9rem;
    padding: .75rem 1.1rem;
    position: sticky;
    top: 0;
    z-index: 20;
}
/* The menu button is always visible, on every device — it's the only
   way to open the menu now that there's no permanently-docked sidebar.
   It swaps between a menu icon and a close icon (same pattern as the
   light/dark theme toggle's sun/moon swap) so it visually "becomes" the
   panel's close control while open, and reverts to a plain menu icon
   the moment the panel is closed. */
.topbar__menu-btn {
    /* Delivery 154 — "keep the change in home button, but revert the
       side menu button color as earlier". The brand-gradient background
       a previous delivery gave this button (to match the Home button
       next to it) is reverted; the Home button itself (.topbar__icon-btn,
       further down this file) keeps its gradient background unchanged. */
    background: var(--glass-bg); border: 1px solid var(--border); border-radius: 10px;
    width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer; color: var(--ink);
    -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
    transition: border-color .15s ease;
}
.topbar__menu-btn:hover { border-color: var(--brand); }
.topbar__menu-btn .icon-menu, .topbar__menu-btn .icon-close { display: inline-flex; }
.topbar__menu-btn .icon-close { display: none; }
.topbar__menu-btn.is-open .icon-menu { display: none; }
.topbar__menu-btn.is-open .icon-close { display: inline-flex; }
.topbar__title {
    font-family: var(--font-display); font-weight: 600; font-size: 1.05rem;
    /* Delivery 68 — the birthday badge sitting right after this on a
       narrow phone left too little room for a longer page title, and a
       flex item's default min-width (its own content's width, not 0)
       is what was forcing it onto a second line instead of shrinking.
       Letting it truncate with an ellipsis instead keeps the top bar to
       one tidy row at any width — it was never guaranteed to show a
       page title in full on a phone screen anyway. */
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.topbar__spacer { flex: 1; }
/* Delivery 79 — a light/dark-aware logo centred in the top bar. Absolute
   positioning (rather than another flex slot) keeps it visually dead-
   centre of the whole bar regardless of how wide the page title on the
   left or the theme-toggle/bell/avatar/logout cluster on the right happen
   to be — matching both sides' widths to get a true flex/grid centre
   would be impractical with a variable-length title and the birthday
   badge. .topbar is already a "positioned" element (position: sticky
   counts, per the CSS spec) so this needs no extra wrapper to position
   against. It's decorative only — no link, no click target — hence
   pointer-events: none, so it never intercepts a click meant for the
   title bar or steals focus order. */
.topbar__logo-center.logo-swap {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    z-index: 1;
}
/* Delivery 80 bugfix — this rule used to also set `display: block` here,
   which (being two classes deep: `.topbar__logo-center .logo-img`) is
   MORE specific than the single-class `.logo-mono { display: none; }`
   toggle rule above, so it was silently overriding that toggle and
   showing BOTH the colour and mono logo at once in light theme. Sizing
   only, no display override, matches how .sidebar__logo/.auth-box__head
   already size their own .logo-img without touching display — the
   .logo-color/.logo-mono pair above is the only thing that should ever
   decide which one is visible. */
.topbar__logo-center .logo-img { height: 30px; width: auto; }
/* Delivery 82 — "in mobile view and tab view the logo overlaps the bell
   icon". The rule below USED to just be `.topbar__logo-center { display:
   none; }`, meant to hide the logo below tablet width entirely — but that
   selector (one class) is LESS specific than `.topbar__logo-center.logo-
   swap` above (two classes), which already sets `display: inline-flex`,
   and a more specific selector wins regardless of which one comes later
   in the file or which media query it's inside. So the "hide it below
   tablet width" rule never actually took effect at any width — the logo
   stayed absolutely centred and full-size everywhere, which is exactly
   what was overlapping the bell/avatar/logout cluster on a narrow bar.
   (The exact same specificity mistake as delivery 80's topbar bugfix,
   just the other direction — that one was an unwanted override, this one
   is an override that was silently skipped.)

   Fixed properly this time: the selector matches `.logo-swap` too, so its
   specificity is no lower than the rule it needs to override, and instead
   of hiding the logo outright it now drops OUT of absolute centring and
   becomes a normal (smaller) flex item that sits in the header's own
   row — flexbox itself is what then keeps it clear of the title on one
   side and the theme/bell/avatar/logout cluster on the other, at any
   width, rather than a fixed pixel position trying to guess how wide a
   variable-length title or name will be. That is the "tab view the logo
   can adjust itself" behaviour asked for. */
@media (max-width: 899px) {
    .topbar__logo-center.logo-swap {
        position: static;
        transform: none;
        pointer-events: auto;
        flex: none;
        margin: 0 .3rem;
    }
    .topbar__logo-center .logo-img { height: 22px; }
}
/* The signed-in person, with their own photo — "whereever the user image
   is there ..it should be visible...like on menus". It is a link to My
   Profile, which is where the photo is set, so it needs the link
   underline removed and a hover state of its own. On a narrow screen the
   name is dropped and the avatar alone stands for it; the avatar is
   never dropped, because it is the thing that was asked for. */
.topbar__user {
    display: flex; align-items: center; gap: .55rem;
    font-size: .92rem; color: var(--ink-muted);
    text-decoration: none;
    padding: .18rem .5rem .18rem .18rem;
    border-radius: 999px;
    border: 1px solid transparent;
}
.topbar__user:hover { border-color: var(--border); background: var(--glass-bg); }
.topbar__user strong { color: var(--ink); }
.topbar__avatar {
    width: 34px; height: 34px; flex: none;
    border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--brand-gradient);
    color: #fff;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: .8rem;
    letter-spacing: .02em;
    border: 2px solid var(--glass-bg-strong);
}
/* is-photo is added by actorAvatarHtml() when there is a real image, so
   one class name covers both the <img> and the initials <span>. */
.topbar__avatar.is-photo { object-fit: cover; background: var(--ink); }
.topbar__uname { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Delivery 66 — "a logout button for all users on the top bar after
   their name": same icon-chip treatment as .topbar__menu-btn, sitting
   right after .topbar__user. A subtle red hint on hover (this app's own
   --danger-text, already used for other cautionary/leave-type actions)
   is the only thing that marks it apart from a normal link, matching
   the tone of the sidebar's own "Log out" link without copying its
   layout wholesale. */
.topbar__logout {
    display: flex; align-items: center; gap: .4rem; flex: none;
    background: var(--glass-bg); border: 1px solid var(--border); border-radius: 999px;
    padding: .4rem .7rem;
    color: var(--ink-muted);
    font-size: .86rem;
    font-weight: 600;
    text-decoration: none;
    -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
    transition: border-color .15s ease, color .15s ease;
}
.topbar__logout:hover { border-color: var(--danger-text); color: var(--danger-text); text-decoration: none; }

/* Delivery 68 — "make the topbar beautiful...with some asthetic cakes
   and baloons and ribbons...in a very suttle colors...decent...for that
   particular whole day". A wash so light it reads as "warmer", not
   "colourful" — layered behind the topbar's own existing background so
   every theme's blur/tint is untouched, just tinted a shade warmer for
   the day. */
.topbar--birthday {
    background: linear-gradient(90deg, rgba(255, 173, 199, .16), rgba(255, 214, 153, .16) 48%, rgba(173, 205, 255, .16) 100%),
                var(--glass-bg-strong);
}
.topbar__birthday-badge {
    display: flex; align-items: center; gap: .35rem; flex: none;
    background: linear-gradient(90deg, rgba(255, 214, 224, .55), rgba(255, 236, 204, .55));
    border: 1px solid rgba(242, 102, 15, .25);
    border-radius: 999px;
    padding: .28rem .7rem .28rem .45rem;
    font-size: .82rem;
    font-weight: 600;
    color: var(--ink);
    white-space: nowrap;
}
.topbar__bday-deco { display: inline-flex; align-items: center; gap: 1px; flex: none; }
.bday-ic { display: block; flex: none; }
/* A small, slow float on the balloon only -- the cake and ribbon stay
   still, so the whole badge reads as gently alive rather than busy. */
.bday-ic--balloon { animation: bdayFloat 3.2s ease-in-out infinite; }
@keyframes bdayFloat {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-3px); }
}
@media (prefers-reduced-motion: reduce) {
    .bday-ic--balloon { animation: none; }
}
/* Delivery 107 — "Festival Themes" badge, same shape as .topbar--birthday/
   .topbar__birthday-badge just above (a soft gradient highlight on the
   bar itself, plus a small pill with an icon and text) rather than new
   hand-drawn SVG art per festival — the emoji from festivalCalendar()
   (core/helpers.php) is the icon. */
.topbar--festival {
    background: linear-gradient(90deg, rgba(255, 214, 153, .18), rgba(255, 173, 199, .12) 55%, rgba(173, 205, 255, .16) 100%),
                var(--glass-bg-strong);
}
.topbar__festival-badge {
    display: flex; align-items: center; gap: .35rem; flex: none;
    background: linear-gradient(90deg, rgba(255, 224, 178, .55), rgba(255, 214, 224, .4));
    border: 1px solid rgba(242, 102, 15, .25);
    border-radius: 999px;
    padding: .28rem .7rem;
    font-size: .82rem;
    font-weight: 600;
    color: var(--ink);
    white-space: nowrap;
}
.topbar__festival-badge > span[aria-hidden] { font-size: 1.1rem; line-height: 1; }
@media (max-width: 599px) {
    .topbar__uname { display: none; }
    .topbar__user { padding: .18rem; }
    .topbar__logout-label { display: none; }
    .topbar__logout { padding: .4rem; }
    .topbar__birthday-text { display: none; }
    .topbar__birthday-badge { padding: .28rem; }
    .topbar__festival-text { display: none; }
    .topbar__festival-badge { padding: .28rem; }
    /* Just the cake on a narrow phone -- the badge is already reduced to
       an icon-only pill by dropping the text above; trimming it to one
       icon instead of three keeps it from crowding the title and the
       theme/user/logout controls on the other end of the same row. */
    .bday-ic--balloon, .bday-ic--ribbon { display: none; }
}

.content { padding: 1.25rem; max-width: 1180px; width: 100%; margin: 0 auto; }

/* ---------- Cards / components ---------- */

.card {
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 1.25rem;
}
.card + .card { margin-top: 1rem; }

.card-grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }

.stat-card { display: flex; flex-direction: column; gap: .35rem; }
.stat-card__label { font-size: .85rem; color: var(--ink-muted); }
.stat-card__value { font-family: var(--font-display); font-size: 1.7rem; font-weight: 700; }

.btn {
    display: inline-flex; align-items: center; justify-content: center;
    gap: .5rem; padding: .68rem 1.1rem; border-radius: 10px; border: 1px solid transparent;
    font-weight: 600; font-size: .95rem; cursor: pointer; width: 100%;
}
.btn-primary { background: var(--brand-gradient); color: #fff; box-shadow: 0 8px 20px rgba(234, 88, 12, 0.28); }
.btn-primary:hover { filter: brightness(1.05); text-decoration: none; }
.btn-block { width: 100%; }

.form-group { margin-bottom: 1rem; }
.form-label { display: block; margin-bottom: .35rem; font-size: .9rem; font-weight: 600; color: var(--ink); }
.form-control {
    width: 100%; padding: .68rem .8rem; border-radius: 9px; border: 1px solid var(--border);
    background: rgba(255,255,255,.5); color: var(--ink); font-size: 1rem;
}
:root[data-theme="dark"] .form-control { background: rgba(255,255,255,.06); }
/* Delivery 43 fix: this had a dark-theme override but no glass-dark one,
   so picking Glass Dark left it on the LIGHT default above (rgba(255,255,255,.5))
   with glass-dark's light --ink text on top — light-on-light, invisible. */
:root[data-theme="glass-dark"] .form-control { background: rgba(255,255,255,.06); }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .form-control { background: rgba(255,255,255,.06); } }
.form-control:focus { outline: 2px solid var(--brand); outline-offset: 1px; border-color: var(--brand); }
.form-control[readonly] { background: rgba(122, 113, 102, 0.08); color: var(--ink-muted); cursor: default; }

/* Delivery 44 fix: a <select>'s OPEN option list is drawn by the browser
   as its own separate surface, not as part of the page — so a
   translucent background like .form-control's own `rgba(255,255,255,.06)`
   (which only ever looked dark because it composited over this page's
   own dark background behind it) has nothing to composite against there,
   and the browser fell back to a plain near-white list with our
   dark-theme text colour (light cream) still applied on top of it —
   exactly the barely-readable "Gates Foundation / Loreal / Mahindra..."
   dropdown reported. Every <select> in this app uses .form-control, so
   giving its <option>s their own OPAQUE background fixes every one of
   them at once; --bg-solid and --ink already flip correctly for all
   four themes, so this needs no extra per-theme rule. */
select.form-control option {
    background-color: var(--bg-solid);
    color: var(--ink);
}

/* A slider control (e.g. Bulk Add Batches' "how many") — native
   rendering with the app's own accent color rather than the browser
   default blue, and an <output> instead of a live number field. */
.form-range { width: 100%; accent-color: var(--brand); cursor: pointer; }
.form-label output { font-weight: 700; color: var(--brand); }

.alert { padding: .8rem 1rem; border-radius: 9px; font-size: .92rem; margin-bottom: 1rem; }
.alert-danger { background: var(--danger-bg); color: var(--danger-text); border: 1px solid rgba(220, 38, 38, 0.3); }
.alert-success { background: var(--success-bg); color: var(--success-text); border: 1px solid rgba(18, 183, 106, 0.3); }

.badge { display: inline-block; padding: .2rem .55rem; border-radius: 999px; font-size: .78rem; font-weight: 600; background: var(--brand-light); color: var(--brand-hover); }
a.badge { text-decoration: none; }
a.badge:hover { text-decoration: none; filter: brightness(.95); }

/* ---------- Auth / login page ---------- */

.auth-page {
    min-height: 100vh; display: flex; align-items: center; justify-content: center;
    padding: 1.5rem; background: var(--bg-gradient) fixed;
}
.auth-box {
    width: 100%; max-width: 420px;
    background: var(--glass-bg-strong);
    border: 1px solid var(--glass-border);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    border-radius: 20px; box-shadow: var(--shadow); overflow: hidden;
    position: relative;
}
.auth-box__head { text-align: center; padding: 1.75rem 1.5rem 1rem; position: relative; }
.auth-box__theme-toggle { position: absolute; top: 1rem; right: 1rem; }
.auth-box__head .logo-swap { display: inline-flex; justify-content: center; }
.auth-box__head .logo-img { height: 46px; margin-bottom: .6rem; }
.auth-box__head h1 { font-size: 1.2rem; margin: 0; }
.auth-box__head p { margin: .25rem 0 0; color: var(--ink-muted); font-size: .88rem; }

.tabs { display: flex; border-bottom: 1px solid var(--border); padding: 0 1rem; overflow-x: auto; }
.tab {
    flex: none; display: inline-flex; align-items: center; gap: .35rem;
    padding: .7rem .9rem; font-size: .85rem; font-weight: 600; color: var(--ink-muted);
    text-decoration: none;
    border-bottom: 2px solid transparent; cursor: pointer; background: none; border-top: none; border-left: none; border-right: none;
}
.tab.is-active { color: var(--brand); border-bottom-color: var(--brand); }

.tab-panel { padding: 1.5rem; display: none; }
.tab-panel.is-active { display: block; }

.auth-box__foot { text-align: center; padding: 0 1.5rem 1.75rem; font-size: .8rem; color: var(--ink-muted); }

.empty-state { text-align: center; padding: 2.5rem 1rem; color: var(--ink-muted); }

/* ---------- Registration form ---------- */

.reg-page { min-height: 100vh; background: var(--bg-gradient) fixed; padding: 1.5rem .75rem 4rem; }
.reg-wrap { max-width: 760px; margin: 0 auto; }
.reg-head { text-align: center; padding: 1rem 0 1.5rem; position: relative; }
.reg-head img { height: 44px; margin-bottom: .5rem; }
.reg-head h1 { font-size: 1.35rem; margin: 0; }
.reg-head p { margin: .3rem 0 0; color: var(--ink-muted); font-size: .88rem; }
.reg-theme-toggle { position: absolute; top: .25rem; right: 0; }

.reg-section { margin-bottom: 1.25rem; }
.reg-section__title { font-family: var(--font-display); font-size: 1.05rem; font-weight: 700; margin: 0 0 1rem; padding-bottom: .6rem; border-bottom: 1px solid var(--border); }

/* Required-field marker. Most forms in this app are entirely required and
   just use the `required` attribute, but where a single form mixes required
   and optional fields (the trainer's bank/KYC card), saying which is which
   up front beats finding out on submit. */
.form-req { color: var(--danger-text); font-weight: 700; }
/* The other half of the same idea, added with the registration form's
   required-field fix (delivery 29): on a long form where most fields are
   optional, marking the optional ones is what stops a student treating
   forty questions as forty obligations. Deliberately quiet — it is
   permission to skip, not an instruction. */
.form-optional { font-weight: 400; font-size: .82em; color: var(--ink-muted); }
.form-hint { display: block; margin-top: .3rem; font-size: .8rem; color: var(--ink-muted); }
.form-hint.is-error { color: var(--danger-text); }
.form-hint.is-ok { color: var(--success-text); }

/* The field a validator has just objected to. Outline plus a tint, so it
   is findable after a scrollIntoView() on a form several screens long,
   and readable in all four themes. */
/* The ring is a literal rgba rather than color-mix(): this stylesheet
   supports whatever browser a student's phone happens to have, and an
   unsupported color-mix() drops the whole declaration silently. */
.form-control.is-error,
.form-control.is-error:focus {
    border-color: var(--danger-text);
    box-shadow: 0 0 0 3px rgba(220, 38, 38, .22);
}
.radio-group input.is-error + *,
.checkbox-item input.is-error + * { color: var(--danger-text); }

.form-row { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }

.radio-group { display: flex; flex-wrap: wrap; gap: .6rem; }
.radio-pill {
    display: inline-flex; align-items: center; gap: .4rem;
    padding: .55rem .95rem; border-radius: 999px; border: 1px solid var(--border);
    background: rgba(255,255,255,.4); cursor: pointer; font-size: .9rem; user-select: none;
}
:root[data-theme="dark"] .radio-pill { background: rgba(255,255,255,.05); }
/* Delivery 43 fix: same missing-glass-dark-override bug as .form-control above. */
:root[data-theme="glass-dark"] .radio-pill { background: rgba(255,255,255,.05); }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .radio-pill { background: rgba(255,255,255,.05); } }
.radio-pill input { margin: 0; accent-color: var(--brand); }
.radio-pill:has(input:checked) { border-color: var(--brand); background: var(--brand-light); color: var(--brand-hover); font-weight: 600; }

.checkbox-grid { display: grid; gap: .55rem; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.checkbox-item {
    display: flex; align-items: flex-start; gap: .55rem;
    padding: .6rem .8rem; border-radius: 10px; border: 1px solid var(--border);
    background: rgba(255,255,255,.35); font-size: .88rem; cursor: pointer;
}
:root[data-theme="dark"] .checkbox-item { background: rgba(255,255,255,.04); }
/* Delivery 43 fix: same missing-glass-dark-override bug as .form-control above. */
:root[data-theme="glass-dark"] .checkbox-item { background: rgba(255,255,255,.04); }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .checkbox-item { background: rgba(255,255,255,.04); } }
.checkbox-item input { margin-top: .15rem; accent-color: var(--brand); flex: none; }
/* Delivery 44 fix: --brand-light is a fixed pale tint (never darkens for
   dark themes — see the .dash-chip comment above for the same lesson
   learned there), so a checked item painted with it while its text stays
   the theme's flipping --ink went light-cream-on-pale-peach in Dark/Glass
   Dark, i.e. invisible the moment you actually checked it. Its sibling
   .radio-pill/.rating-pill already pair --brand-light with
   var(--brand-hover) correctly; this just brings .checkbox-item in
   line with them. */
.checkbox-item:has(input:checked) { border-color: var(--brand); background: var(--brand-light); color: var(--brand-hover); }

.reg-step-foot { display: flex; justify-content: center; margin-top: 1.5rem; }
.reg-step-foot .btn { max-width: 320px; }

[data-conditional] { display: none; }
[data-conditional].is-shown { display: block; }
.empty-state h2 { color: var(--ink); font-size: 1.1rem; }

/* ---------- Buttons (additional variants) ---------- */

.btn-secondary { background: var(--glass-bg-strong); color: var(--ink); border-color: var(--border); box-shadow: none; }
.btn-secondary:hover { border-color: var(--brand); text-decoration: none; }
.btn-outline { background: transparent; color: var(--brand); border-color: var(--brand); box-shadow: none; }
.btn-outline:hover { background: var(--brand-light); text-decoration: none; }
.btn-danger { background: var(--danger); color: #fff; box-shadow: 0 8px 20px rgba(220, 38, 38, 0.22); }
.btn-danger:hover { filter: brightness(1.05); text-decoration: none; }
.btn-sm { width: auto; padding: .4rem .8rem; font-size: .85rem; }
.btn-inline { width: auto; display: inline-flex; }
.btn:disabled { opacity: .4; cursor: not-allowed; }
.btn-row { display: flex; flex-wrap: wrap; gap: .6rem; }

/* ---------- Data tables (admin lists) ---------- */

.table-wrap { overflow-x: auto; border-radius: var(--radius); border: 1px solid var(--border); }
.table { width: 100%; border-collapse: collapse; font-size: .9rem; background: var(--glass-bg-strong); }
.table th, .table td { padding: .65rem .8rem; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
.table th { font-family: var(--font-display); font-weight: 600; color: var(--ink-muted); font-size: .8rem; text-transform: uppercase; letter-spacing: .03em; }
.table td.wrap { white-space: normal; }
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: rgba(242, 102, 15, 0.06); }

/* Delivery 141 — "check the alignment in the trainers student roster
   page." That table (modules/trainer/roster.php) grew from 6 columns to
   11 across deliveries 138/139 (gender, category, father's name, mobile,
   email added), and three of them wrap (`.wrap`: name, father's name,
   email). `.table td`'s browser-default vertical-align is `baseline`, so
   a row with a long name/guardian-name wrapping to 2-3 lines left every
   short single-line cell in that same row (Gender, Mobile, the result
   badges...) sitting at the TOP of a much taller row with empty space
   below — every row a different height depending on whose name was
   longest, and nothing in it lining up with anything else in the same
   row. Scoped to this one table (not every `.table` site-wide) since
   this is the one with that many wrapping columns at once. */
.roster-table td { vertical-align: middle; }

.badge-success { background: var(--success-bg); color: var(--success-text); }
.badge-danger { background: var(--danger-bg); color: var(--danger-text); }
.badge-warning { background: var(--warning-bg); color: var(--warning-text, var(--warning)); }
.badge-info { background: var(--info-bg); color: var(--info); }

.filter-bar { display: flex; flex-wrap: wrap; gap: .8rem; align-items: flex-end; margin-bottom: 1rem; }
.filter-bar .form-group { margin-bottom: 0; min-width: 180px; }

.thumb { width: 64px; height: 64px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); }
.thumb-lg { max-width: 260px; max-height: 260px; border-radius: 10px; border: 1px solid var(--border); }

.section-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; }
.section-head h2 { margin: 0; }

/* ---------- Assessment (take / result / question bank) ---------- */

.question-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 1.1rem; background: rgba(255,255,255,.35); }
:root[data-theme="dark"] .question-card { background: rgba(255,255,255,.04); }
/* Delivery 43 fix: same missing-glass-dark-override bug as .form-control above. */
:root[data-theme="glass-dark"] .question-card { background: rgba(255,255,255,.04); }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .question-card { background: rgba(255,255,255,.04); } }
.question-card + .question-card { margin-top: 1rem; }
.question-card__title { font-weight: 700; margin: 0 0 .8rem; }

.option-list { display: flex; flex-direction: column; gap: .55rem; }
.option-item {
    display: flex; align-items: center; gap: .6rem;
    padding: .65rem .9rem; border-radius: 10px; border: 1px solid var(--border);
    background: rgba(255,255,255,.5); cursor: pointer; font-size: .95rem;
}
:root[data-theme="dark"] .option-item { background: rgba(255,255,255,.06); }
/* Delivery 43 fix: same missing-glass-dark-override bug as .form-control above. */
:root[data-theme="glass-dark"] .option-item { background: rgba(255,255,255,.06); }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .option-item { background: rgba(255,255,255,.06); } }
.option-item input { accent-color: var(--brand); flex: none; }
/* Delivery 44 fix: same missing --brand-hover pairing as .checkbox-item
   above — a selected quiz option went invisible in Dark/Glass Dark. */
.option-item:has(input:checked) { border-color: var(--brand); background: var(--brand-light); color: var(--brand-hover); font-weight: 600; }
.option-item.is-correct { border-color: var(--success); background: var(--success-bg); }
.option-item img { max-height: 90px; border-radius: 6px; margin-top: .4rem; }

.rating-group { display: flex; flex-wrap: wrap; gap: .5rem; }
.rating-pill {
    display: inline-flex; align-items: center; justify-content: center; min-width: 2.6rem;
    padding: .5rem .7rem; border-radius: 10px; border: 1px solid var(--border);
    background: rgba(255,255,255,.4); cursor: pointer; font-size: 1rem; user-select: none;
}
:root[data-theme="dark"] .rating-pill { background: rgba(255,255,255,.05); }
/* Delivery 43 fix: same missing-glass-dark-override bug as .form-control above. */
:root[data-theme="glass-dark"] .rating-pill { background: rgba(255,255,255,.05); }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .rating-pill { background: rgba(255,255,255,.05); } }
.rating-pill input { position: absolute; opacity: 0; pointer-events: none; }
.rating-pill:has(input:checked) { border-color: var(--brand); background: var(--brand-light); font-weight: 700; color: var(--brand-hover); }

/* ---------------------------------------------------------------------
   Post-training feedback survey redesign (delivery 86).
   "there was a survey form which come when the student give their post
   assessment..check that too if its working..redesign it in an asthetic
   manner" / "redesign it more beautifully". The survey itself already
   worked correctly (verified against the real database); this is a pure
   visual redesign — every input keeps its original name/value, so the
   controller (modules/assessment/feedback.php) needed no changes at all.
   --------------------------------------------------------------------- */
.survey-head { display: flex; align-items: center; gap: .9rem; margin-bottom: .3rem; }
.survey-head__icon {
    flex: none; width: 46px; height: 46px; border-radius: 14px; display: grid; place-items: center;
    background: var(--brand-gradient); color: #fff; box-shadow: 0 8px 20px rgba(234, 88, 12, 0.28);
}
.survey-progress { margin: 1.1rem 0 1.6rem; }
.survey-progress__row { display: flex; justify-content: space-between; align-items: baseline; font-size: .82rem; color: var(--ink-muted); margin-bottom: .35rem; }
.survey-progress__row strong { color: var(--ink); font-weight: 700; }

.survey-section { margin: 1.9rem 0 1.2rem; }
.survey-section__head { display: flex; align-items: center; gap: .55rem; margin-bottom: .2rem; }
.survey-section__head .num {
    flex: none; width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
    background: var(--brand-light); color: var(--brand-hover); font-size: .8rem; font-weight: 800;
}
.survey-section__head h3 { margin: 0; }
.survey-section__note { margin: .15rem 0 1rem 2.1rem; font-size: .84rem; color: var(--ink-muted); }

.survey-grid { display: grid; gap: .85rem; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.survey-q {
    border: 1px solid var(--border); border-radius: var(--radius); padding: .95rem 1.05rem;
    background: var(--surface-2); transition: border-color .2s, box-shadow .2s;
}
.survey-q:focus-within, .survey-q:has(input:checked) { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-light); }
.survey-q .form-label { margin-bottom: .55rem; }

.survey-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 1.1rem 1.2rem; background: var(--surface-2); margin-bottom: 1rem; }

/* Days-attended: same rating-pill trick, laid out as a tidy wrapping row of round chips. */
.day-picker { display: flex; flex-wrap: wrap; gap: .5rem; }
.day-picker .rating-pill { min-width: 2.4rem; height: 2.4rem; border-radius: 50%; padding: 0; }

/* Star rating — 5 real stars, CSS-only fill via the classic reversed-DOM-order trick:
   inputs render 5..1 in source order so ":checked ~ label" / ":hover ~ label" (siblings
   AFTER the acted-on element) light up that star and everything lower, while
   `flex-direction: row-reverse` displays them the normal 1..5 left-to-right way. */
.star-picker { display: inline-flex; flex-direction: row-reverse; gap: .15rem; }
.star-picker input { position: absolute; opacity: 0; pointer-events: none; }
.star-picker label {
    font-size: 2.2rem; line-height: 1; color: var(--border); cursor: pointer;
    transition: color .15s, transform .15s ease;
}
.star-picker label:hover { transform: scale(1.14); }
.star-picker input:checked ~ label,
.star-picker:not(:has(input:checked)) label:hover,
.star-picker:not(:has(input:checked)) label:hover ~ label {
    color: var(--warning);
}
.star-picker__value { margin-left: .6rem; font-weight: 700; color: var(--ink); vertical-align: middle; }

.survey-submit { margin-top: 1.6rem; }

.score-hero { text-align: center; padding: 1rem 0; }
.score-hero__value { font-family: var(--font-display); font-size: 3rem; font-weight: 700; line-height: 1; }
.score-hero__value.is-pass { color: var(--success-text); }
.score-hero__value.is-fail { color: var(--danger-text); }
.score-hero__label { color: var(--ink-muted); font-size: .95rem; margin-top: .3rem; }

.textarea { min-height: 110px; resize: vertical; }

/* ==================================================================
   STAFF DASHBOARD
   Grid-based so it reflows on its own at any width, with no JS and no
   chart library — the bars are divs with a percentage width, so the
   page paints as soon as the HTML lands.
   ================================================================== */

.dash-head { margin-bottom: 1.1rem; }
.dash-head h2 { margin: 0 0 .3rem; font-size: 1.5rem; letter-spacing: -.01em; display: inline-flex; align-items: center; gap: .55rem; }
.dash-head h2 svg { color: var(--brand); flex: none; }
.dash-head p { margin: 0; color: var(--ink-muted); font-size: .92rem; }

/* ---- filter bar ---- */
.dash-filterbar { margin-bottom: 1.35rem; padding: 1.05rem 1.15rem; }
.dash-filterbar__head {
    display: flex; align-items: center; gap: .85rem; flex-wrap: wrap;
    padding-bottom: .85rem; margin-bottom: .95rem; border-bottom: 1px solid var(--border);
}
.dash-filterbar__head h3 {
    margin: 0; font-size: .95rem; display: inline-flex; align-items: center; gap: .45rem;
    color: var(--ink);
}
.dash-filterbar__head h3 svg { color: var(--brand); }
.dash-chips { display: flex; flex-wrap: wrap; gap: .4rem; }
.dash-chip {
    display: inline-flex; align-items: center; gap: .3rem;
    font-size: .78rem; padding: .26rem .6rem; border-radius: 999px;
    /* Was a hardcoded #9a3412 — a light-theme brand ink that sat on
       --brand-light in the dark themes too, where the surface flips and
       the pair loses its contrast. --brand-hover is the token the app
       already pairs with --brand-light (see .pf-role), and it is defined
       per theme. */
    background: var(--brand-light); color: var(--brand-hover);
    white-space: nowrap;
}
.dash-chip b { font-weight: 700; }
/* A filter that is always on and cannot be changed — the cohort year for
   roles pinned to it. Outlined rather than filled, so it reads as a
   statement of what is shown rather than as something to click off. */
.dash-chip--fixed {
    background: transparent;
    border: 1px dashed var(--border-strong, var(--border));
    color: var(--ink-muted);
}
.dash-chip__clear {
    border: 0; background: none; padding: 0 0 0 .1rem; margin: 0;
    font-size: 1rem; line-height: 1; cursor: pointer;
    color: inherit; opacity: .6;
}
.dash-chip__clear:hover { opacity: 1; }
.dash-chips__none { font-size: .82rem; color: var(--ink-muted); }

/* The same chip row on every other screen's `.filter-bar`, built from
   the form's own controls by initFilterChips() (assets/js/app.js). It
   spans the whole filter grid so it reads as a heading for it, not as
   another field. */
.filter-chips {
    /* `.filter-bar` is a flex row globally and a grid inside the report
       shell, so the row has to claim the full width in both — hence both
       properties, each inert in the layout that is not in force. */
    flex: 1 0 100%;
    grid-column: 1 / -1;
    display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
    margin-bottom: .2rem;
}
.filter-chips__head {
    font-size: .74rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: .06em; color: var(--ink-muted); margin-right: .15rem;
}
.filter-chips:empty { display: none; }

.dash-filterbar__grid {
    display: grid; gap: .7rem .8rem;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    align-items: end;
}
.dash-filter .form-label { font-size: .75rem; margin-bottom: .25rem; }
.dash-filter .form-control { padding: .5rem .6rem; font-size: .87rem; }
/* Delivery 156 — "align to the right most..the filter button". Apply/
   Reset/All years used to just be one more auto-fit grid cell (same as
   every dropdown above it), so it landed wherever the wrap happened to
   leave it — usually a half-empty row with a lot of dead space to its
   right, as in the request's own screenshot. Spanning every column of
   its own row and right-aligning its contents puts the button group on
   the actual right edge of the filter bar instead. */
.dash-filter--actions { display: flex; gap: .45rem; flex-wrap: wrap; grid-column: 1 / -1; justify-content: flex-end; }
.dash-filter--actions .btn { padding: .5rem .9rem; font-size: .85rem; }

/* ---- KPI tiles ---- */
.kpi-row {
    display: grid; gap: .8rem; margin-bottom: 1.35rem;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.kpi-row--tight { gap: .6rem; margin-bottom: 0; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); }

.kpi {
    position: relative; display: flex; flex-direction: column; gap: .28rem;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    padding: .95rem 1rem; overflow: hidden;
    transition: transform .18s ease, box-shadow .18s ease;
}
.kpi:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.kpi__icon {
    display: inline-grid; place-items: center; width: 34px; height: 34px; border-radius: 10px;
    background: var(--brand-light); color: var(--brand-hover); margin-bottom: .2rem;
}
.kpi__value { font-size: 1.6rem; font-weight: 800; line-height: 1.1; letter-spacing: -.02em; }
.kpi__label { font-size: .76rem; color: var(--ink-muted); text-transform: uppercase; letter-spacing: .05em; }
.kpi--ok   .kpi__icon { background: rgba(18,183,106,.14); color: var(--success-text); }
.kpi--warn .kpi__icon { background: var(--warning-bg); color: var(--warning); }
.kpi--info .kpi__icon { background: var(--info-bg); color: var(--info); }
.kpi--bad  .kpi__icon { background: rgba(220,38,38,.12); color: var(--danger-text); }
.kpi-row--tight .kpi { padding: .75rem .8rem; }
.kpi-row--tight .kpi__value { font-size: 1.3rem; }
a.kpi, .kpi[href] { text-decoration: none; color: inherit; cursor: pointer; }
.daytile__status { display: flex; gap: .35rem; flex-wrap: wrap; margin-top: .1rem; }

/* ---- quick-action link cards (icon + label, e.g. trainer dashboard) ---- */
.quick-actions { display: grid; gap: .75rem; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.quick-action {
    display: flex; align-items: center; gap: .75rem;
    padding: .85rem 1rem; border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--surface); text-decoration: none; color: inherit;
    transition: transform .15s ease, box-shadow .15s ease;
}
.quick-action:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.quick-action__icon {
    display: inline-grid; place-items: center; width: 40px; height: 40px; border-radius: 10px;
    background: var(--brand-light); color: var(--brand-hover); flex: none;
}
.quick-action__label { font-weight: 600; font-size: .92rem; }

/* ---- section headers ---- */
.card-head {
    display: flex; align-items: baseline; justify-content: space-between; gap: .8rem;
    flex-wrap: wrap; margin-bottom: 1rem;
}
.card-head h3 { margin: 0; font-size: 1.02rem; display: inline-flex; align-items: center; gap: .5rem; }
.card-head h3 svg { color: var(--brand); flex: none; }
.card-head__note { font-size: .78rem; color: var(--ink-muted); }

/* ---- trainer batch-context header (templates/partials/trainer-batch-header.php) ---- */
.batch-banner { margin-bottom: 1.25rem; }
.batch-banner__head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.batch-banner__head h3 {
    margin: 0; font-size: 1.08rem; font-family: var(--font-display);
    display: inline-flex; align-items: center; gap: .5rem;
}
.batch-banner__head h3 svg { color: var(--brand); flex: none; }
.meta-row { display: flex; flex-wrap: wrap; gap: .5rem 1.1rem; margin-top: .65rem; }
.meta-item { display: inline-flex; align-items: center; gap: .4rem; font-size: .85rem; color: var(--ink-muted); }
.meta-item svg { color: var(--brand); flex: none; }

/* ---- trainer attendance report: screen spacing + the printable sheet ----
   On screen the summary tiles sat flush against the table card below them
   (.kpi-row--tight deliberately zeroes its own margin so it can nest inside
   a card), which read as broken padding on this page where the row is a
   top-level block. .att-summary restores the same 1.25rem gap the batch
   header above it uses, so the three blocks are evenly spaced.
   Everything prefixed .att-print-* is the printed sheet: hidden on screen,
   revealed by the body.print-sheet print rules further down. */
.att-summary { margin-bottom: 1.25rem; }
.att-table .att-day { text-align: center; }
.att-table .att-day__short { display: none; }
.att-print-head, .att-print-foot { display: none; }

/* ---- feedback report: per-question Yes/Neutral/No bars ----
   One stacked bar per question rather than three numbers, so a screenful of
   questions can be read at a glance. Colours come from the same success /
   warning / danger tokens the badges use, so "yes" reads the same here as
   everywhere else in the app. */
.fb-qlist { list-style: none; margin: 0; padding: 0; }
.fb-q { padding: .55rem 0; border-bottom: 1px solid var(--border); }
.fb-q:last-child { border-bottom: none; }
.fb-q__head { display: flex; justify-content: space-between; align-items: baseline; gap: .8rem; margin-bottom: .35rem; }
.fb-q__label { font-weight: 600; font-size: .9rem; color: var(--ink); }
.fb-q__count { font-size: .75rem; color: var(--ink-muted); white-space: nowrap; }
.fb-bar { display: flex; height: 10px; border-radius: 999px; overflow: hidden; background: var(--surface); }
.fb-bar__seg { display: block; height: 100%; }
.fb-bar__seg--yes { background: var(--success-text); }
.fb-bar__seg--neutral { background: var(--warning); }
.fb-bar__seg--no { background: var(--danger-text); }
.fb-q__legend { display: flex; flex-wrap: wrap; gap: .8rem; margin-top: .3rem; font-size: .75rem; color: var(--ink-muted); }
.fb-key { display: inline-flex; align-items: center; gap: .3rem; }
.fb-key::before { content: ''; width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
.fb-key--yes::before { background: var(--success-text); }
.fb-key--neutral::before { background: var(--warning); }
.fb-key--no::before { background: var(--danger-text); }


/* ---- batch roster: active/inactive bar and bulk controls ---- */
.roster-bar {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: .8rem; margin-bottom: .8rem;
}
.roster-bar__counts { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.roster-bulk {
    display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
    padding: .7rem .85rem; margin-bottom: .9rem;
    border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface);
}
.roster-bulk__label { font-size: .85rem; color: var(--ink-muted); }
.roster-bulk .form-control { width: auto; padding: .4rem .55rem; font-size: .85rem; }
.roster-inline { margin: 0; }
.roster-inline .form-control { width: auto; padding: .3rem .45rem; font-size: .82rem; }

/* ---- column layouts ---- */
.dash-cols { display: grid; gap: 1.1rem; margin-bottom: 1.1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); }
.dash-cols--3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr)); }

/* ---- bar lists ---- */
.barlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .55rem; }
.barlist__row { display: grid; grid-template-columns: minmax(70px, 1fr) minmax(60px, 2fr) auto; align-items: center; gap: .6rem; }
.barlist__label {
    font-size: .84rem; color: var(--ink); overflow: hidden;
    text-overflow: ellipsis; white-space: nowrap;
}
.barlist__track { height: 8px; border-radius: 999px; background: var(--border); overflow: hidden; }
.barlist__fill {
    display: block; height: 100%; border-radius: 999px; background: var(--brand-gradient);
    /* grows in on load; the width itself is already correct without it */
    animation: barGrow .5s cubic-bezier(.16,1,.3,1) both;
    transform-origin: left center;
}
@keyframes barGrow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.barlist__value { font-size: .84rem; font-weight: 700; font-variant-numeric: tabular-nums; }

/* ---- impact block ---- */
.impact { display: flex; gap: 1.1rem; align-items: stretch; flex-wrap: wrap; }
.impact__pair { display: grid; gap: .8rem; flex: 1 1 220px; }
.impact__one { display: grid; gap: .3rem; }
.impact__lab { font-size: .8rem; color: var(--ink-muted); display: inline-flex; align-items: center; gap: .35rem; }
.impact__pct { font-size: 1.15rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.meter { display: block; height: 8px; border-radius: 999px; background: var(--border); overflow: hidden; }
.meter__fill { display: block; height: 100%; border-radius: 999px; background: var(--brand-gradient);
    animation: barGrow .55s cubic-bezier(.16,1,.3,1) both; transform-origin: left center; }
.meter__fill--ok { background: linear-gradient(135deg, #059669 0%, #12b76a 100%); }
.impact__delta {
    flex: 0 0 auto; display: grid; place-content: center; text-align: center; min-width: 118px;
    padding: .8rem 1rem; border-radius: var(--radius);
    background: rgba(18,183,106,.10); color: var(--success-text);
}
.impact__delta.is-down { background: rgba(220,38,38,.10); color: var(--danger-text); }
.impact__delta-val { font-size: 1.65rem; font-weight: 800; line-height: 1; font-variant-numeric: tabular-nums; }
.impact__delta-lab { font-size: .73rem; text-transform: uppercase; letter-spacing: .05em; opacity: .85; }

.muted-sm { display: block; color: var(--ink-muted); font-size: .8rem; }

/* ---- global "please wait" overlay ----
   One copy per page, injected by templates/layout/app.php; shown/hidden by
   initLoadingOverlay() in assets/js/app.js on every form submit and normal
   link navigation. Sits above everything else in the app (sidebar panel,
   searchable-select dropdown panel, etc.), so it always reads as "the
   whole screen is waiting," not just one card of it. */
.loading-overlay {
    position: fixed; inset: 0; z-index: 2000;
    display: flex; align-items: center; justify-content: center;
    background: rgba(20, 16, 12, 0.32);
    -webkit-backdrop-filter: blur(6px) saturate(140%);
    backdrop-filter: blur(6px) saturate(140%);
    opacity: 0; visibility: hidden;
    transition: opacity .18s ease, visibility 0s linear .18s;
}
.loading-overlay.is-active {
    opacity: 1; visibility: visible;
    transition: opacity .12s ease, visibility 0s linear 0s;
}
.loading-overlay__card {
    /* No card any more — "just the image and the circle around it", no
       white box behind them. Purely a centring wrapper now: transparent,
       no background/border/shadow/blur of its own (the dim+blur behind
       it is .loading-overlay's, on the whole screen, not this box). */
    display: flex; align-items: center; justify-content: center;
    background: transparent; border: none; box-shadow: none;
    -webkit-backdrop-filter: none; backdrop-filter: none;
    padding: 0;
    /* a little pop-in so it feels like it arrived, not just appeared */
    transform: scale(.92); opacity: 0;
    animation: loadingCardIn .22s cubic-bezier(.16,1,.3,1) forwards;
}
@keyframes loadingCardIn { to { transform: scale(1); opacity: 1; } }
.loading-overlay__spinner {
    position: relative;
    width: 84px; height: 84px;
    border-radius: 50%;
}
.loading-overlay__spinner::before {
    content: ''; position: absolute; inset: 0; border-radius: 50%;
    /* the rotating brand-gradient ring — this is the "circle gradient
       moving around" the icon */
    background: conic-gradient(from 0deg, transparent 0deg, var(--brand-hover) 40deg, var(--brand) 190deg, transparent 360deg);
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 6px), #000 calc(100% - 6px));
            mask: radial-gradient(farthest-side, transparent calc(100% - 6px), #000 calc(100% - 6px));
    animation: loadingSpin .9s linear infinite;
}
/* The uploaded icon itself — centred inside the rotating ring, and
   breathing (growing/shrinking) independently of the ring's own
   rotation, so the two motions read as two separate, deliberate things
   happening rather than one busy blur. Now the only two things in this
   overlay (no card, no text behind them), so the drop-shadow below is
   what keeps the icon readable against whatever page is blurred behind
   it, light or dark. */
.loading-overlay__icon {
    position: absolute;
    top: 50%; left: 50%;
    width: 42px; height: 42px;
    object-fit: contain;
    /* Only reaches an SVG (the loadingIconMarkup() fallback glyph, when
       the uploaded PNG is missing on this server) — currentColor there
       has nothing else to inherit from inside this overlay. */
    color: var(--brand);
    transform: translate(-50%, -50%) scale(1);
    animation: loadingIconPulse 1.3s ease-in-out infinite;
    filter: drop-shadow(0 2px 6px rgba(0, 0, 0, .35));
}
@keyframes loadingSpin { to { transform: rotate(360deg); } }
@keyframes loadingIconPulse {
    0%, 100% { transform: translate(-50%, -50%) scale(.78); }
    50% { transform: translate(-50%, -50%) scale(1.18); }
}

@media (prefers-reduced-motion: reduce) {
    .barlist__fill, .meter__fill { animation: none; }
    .kpi { transition: none; }
    /* Still needs to visibly indicate "working," so the spin stays but
       slows down considerably rather than vanishing outright; the pop-in
       and icon pulse are pure decoration and can safely stop. */
    .loading-overlay__card { animation: none; transform: none; opacity: 1; }
    .loading-overlay__spinner::before { animation-duration: 2.2s; }
    .loading-overlay__icon { animation: none; transform: translate(-50%, -50%) scale(1); }
}

/* ---- Trainer info popup (Delivery 47) ----
   The "Info" button next to a trainer's name on the batch report
   (templates/pages/reports/batch-detail.php) opens this one shared panel;
   see initTrainerInfoModal() in assets/js/app.js. Same glass-card tokens
   as .card, so it already reads correctly in all four themes with no
   extra per-theme rules. Shown/hidden with a class, not the `hidden`
   attribute/`display` alone — an element that already has an authored
   `display` (this one needs `display: flex` to center its card) never
   goes away just from toggling `hidden`, since author CSS always wins
   over the browser's own `[hidden]{display:none}` default. */
.trainer-info-modal {
    display: none;
    position: fixed; inset: 0; z-index: 1900;
    align-items: center; justify-content: center;
    padding: 1rem;
    background: rgba(20, 16, 12, 0.32);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
}
.trainer-info-modal.is-shown { display: flex; }
.trainer-info-modal__card {
    position: relative;
    width: 100%; max-width: 380px;
    max-height: calc(100vh - 2rem);
    overflow-y: auto;
    padding: 1.5rem 1.5rem 1.25rem;
    border-radius: var(--radius);
    background: var(--glass-bg-strong);
    border: 1px solid var(--glass-border);
    box-shadow: var(--shadow);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
}
.trainer-info-modal__close {
    position: absolute; top: .6rem; right: .6rem;
    width: 30px; height: 30px;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; border-radius: 50%;
    background: var(--surface-2);
    color: var(--ink);
    font-size: 1.3rem; line-height: 1; cursor: pointer;
}
.trainer-info-modal__close:hover { color: var(--brand-hover); }
.trainer-info-modal__photo-wrap { display: flex; justify-content: center; margin-bottom: .6rem; }
.trainer-info-modal__photo {
    width: 84px; height: 84px; border-radius: 50%;
    object-fit: cover;
    border: 1px solid var(--glass-border);
}
.trainer-info-modal__name {
    margin: 0 0 .9rem; padding-right: 1.6rem;
    font-family: var(--font-display); font-size: 1.15rem; text-align: center;
    color: var(--ink);
}
.trainer-info-modal__grid { margin: 0; display: flex; flex-direction: column; gap: .55rem; }
.trainer-info-modal__grid > div {
    display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
    padding-bottom: .55rem;
    border-bottom: 1px solid var(--border);
}
.trainer-info-modal__grid > div:last-child { border-bottom: none; padding-bottom: 0; }
/* A row with nothing to show (e.g. no WhatsApp on file) is hidden via
   this class, not the `hidden` attribute alone — this row already has an
   authored `display: flex` above, and author CSS always beats the
   browser's own `[hidden]{display:none}` default, so toggling `hidden`
   by itself would not have actually hidden it. Same convention as
   .navtile/.navcat elsewhere in this file. */
.trainer-info-modal__grid > div.is-empty-row { display: none; }
.trainer-info-modal__grid dt { margin: 0; font-size: .82rem; color: var(--ink-muted); }
.trainer-info-modal__grid dd { margin: 0; font-size: .9rem; font-weight: 600; color: var(--ink); text-align: right; }

/* Delivery 146 — the Trainer Report's clickable trainer name (opens
   #trainerBatchesModal below). A plain button made to read as a text
   link, not a button — same brand-color-on-hover convention as every
   other in-content link in this app. */
.trainer-name-link {
    background: none; border: none; padding: 0; margin: 0;
    font: inherit; color: var(--brand); cursor: pointer; text-align: left;
}
.trainer-name-link:hover, .trainer-name-link:focus-visible { color: var(--brand-hover); text-decoration: underline; }

/* Delivery 146 — "click the trainer name...a popup...a table with batch
   id, batch name, college name, start/end date, status and a view
   button...for all the batches that trainer has taken". Same overlay/
   glass-card mechanics as .trainer-info-modal just above (see that
   block's own comment for why a class, not `hidden` alone, drives
   show/hide) — just its own, wider card, since this one holds a table
   rather than a handful of label/value rows. See
   initTrainerBatchesModal() in assets/js/app.js. */
.trainer-batches-modal {
    display: none;
    position: fixed; inset: 0; z-index: 1900;
    align-items: center; justify-content: center;
    padding: 1rem;
    background: rgba(20, 16, 12, 0.32);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
}
.trainer-batches-modal.is-shown { display: flex; }
.trainer-batches-modal__card {
    position: relative;
    width: 100%; max-width: 880px;
    max-height: calc(100vh - 2rem);
    overflow-y: auto;
    padding: 1.5rem 1.5rem 1.25rem;
    border-radius: var(--radius);
    background: var(--glass-bg-strong);
    border: 1px solid var(--glass-border);
    box-shadow: var(--shadow);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
}
.trainer-batches-modal__close {
    position: absolute; top: .6rem; right: .6rem;
    width: 30px; height: 30px;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; border-radius: 50%;
    background: var(--surface-2);
    color: var(--ink);
    font-size: 1.3rem; line-height: 1; cursor: pointer;
}
.trainer-batches-modal__close:hover { color: var(--brand-hover); }
.trainer-batches-modal__name { margin: 0 1.6rem .15rem 0; font-family: var(--font-display); font-size: 1.15rem; color: var(--ink); }
.trainer-batches-modal__count { margin: 0 0 .8rem; font-size: .85rem; color: var(--ink-muted); }

@media (max-width: 640px) {
    .kpi__value { font-size: 1.4rem; }
    .impact__delta { width: 100%; }
}

/* ==================================================================
   REPORT PAGES (Batch / Student / Trainer / Impact)
   ================================================================== */

/* The default .content caps at 1180px, which is comfortable for forms
   and dashboards but starves a 14-column report table — it either
   scrolls sideways inside a narrow column while the rest of a wide
   screen sits empty, or wraps awkwardly.
   This used to cap the wide layout at 1720px, on the theory that an
   ultra-wide monitor should still read as columns rather than one endless
   row. In practice that left ~420px of empty page down each side of a
   2560px screen, which is exactly what it was meant to avoid, so the cap
   is gone: the page now fills whatever width the window actually has. */
.content--wide { max-width: none; }

/* Filter bar: grid instead of flex, so every field lines up on a
   shared baseline no matter how many are shown on a given page —
   batches/impact/trainers show 9, students shows 8, and the columns
   stay aligned either way instead of the ragged wrap flex produced. */
.filter-bar { display: grid; gap: .7rem .9rem; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); align-items: end; margin-bottom: 0; }
.filter-bar .form-group { margin-bottom: 0; min-width: 0; }
.filter-bar .form-group:last-child { grid-column: -2 / -1; }

.report-head {
    display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
    margin: 0 0 .35rem;
}
.report-head svg { color: var(--brand); flex: none; }

/* Report stat tiles reuse the dashboard's .kpi look (icon + big number +
   label) instead of the plain .stat-card, so every screen in the app
   presents its headline numbers the same way. */
.kpi-row--reports { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }

/* ==================================================================
   BATCH LIFECYCLE / SHARE / SORTABLE TABLES / PAGER
   ================================================================== */

/* Sortable table headers (assets/js/app.js: initSortableTables()). The
   arrow is a single ::after glyph that only shows direction once a
   column is actively sorted, so unsorted headers don't get cluttered
   with a stack of up/down hints on every single column. */
.table th.is-sortable { cursor: pointer; user-select: none; }
.table th.is-sortable:hover { color: var(--brand); }
.table th.is-sortable::after { content: '\2195'; margin-left: .35em; opacity: .35; font-size: .9em; }
.table th.is-sortable[data-sort-dir="asc"]::after { content: '\2191'; opacity: 1; color: var(--brand); }
.table th.is-sortable[data-sort-dir="desc"]::after { content: '\2193'; opacity: 1; color: var(--brand); }

/* Shared pager (templates/partials/pager.php) */
.pager { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .8rem; }
.pager__summary { color: var(--ink-muted); font-size: .88rem; }
.pager__controls { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; }
.pager__perpage { display: flex; align-items: center; gap: .5rem; font-size: .85rem; color: var(--ink-muted); white-space: nowrap; }
.pager__perpage .form-control { width: auto; padding: .35rem .5rem; font-size: .85rem; }

/* Registration link "share box" (batch-status.php) — the link/copy/open
   controls on the left, the QR code on the right, wrapping to stack on
   narrow screens instead of squeezing a 150px QR image sideways. */
.share-box { display: flex; gap: 1.5rem; align-items: flex-start; flex-wrap: wrap; }
.share-box__link { flex: 1 1 320px; min-width: 0; display: flex; flex-direction: column; gap: .6rem; }
.share-box__link .form-control { font-family: var(--font-mono, monospace); font-size: .85rem; }
.share-box__qr { flex: none; text-align: center; display: flex; flex-direction: column; gap: .4rem; align-items: center; }
/* Both shapes of QR get the same frame: an <img> pointing at
   batch-qr.php, and the inline <svg> the page now draws itself (delivery
   30). The white background is not decoration — a QR on a translucent
   glass card is a QR a phone camera struggles to read, and on the dark
   theme it would be light-on-dark, which many scanners reject outright. */
.share-box__qr img,
.share-box__qr svg {
    border: 1px solid var(--border); border-radius: 8px;
    background: #fff; padding: .5rem;
    width: 150px; height: 150px;
}

/* =====================================================================
   THE APP'S TYPE AND CONTROL SCALE
   =====================================================================

   Delivery 29, and the reason this block reads the way it does.

   The Batches list was built tight on purpose (37 legacy columns), so it
   opted into `.table--dense` and `.filter-bar--compact` while every other
   screen used the roomier defaults above. Asked to make the whole app
   read like that page, the fix is not to paste those classes onto sixty
   templates — it is to make those values THE DEFAULT, once, here.

   SCOPED TO `.content`, which is the signed-in app shell
   (templates/layout/app.php). The pages that skip that layout are the
   public ones — login, and the five student self-registration steps —
   and they keep the roomier scale deliberately: they are filled in once,
   by someone who has never seen this app before, often on a phone, and a
   dense admin grid is the wrong shape for that. Everything a staff
   member, trainer or college SPOC sees is inside `.content`, so
   "everywhere" in the request is covered.

   The two modifier classes are kept below and now change (almost)
   nothing, so existing markup carries on working and no template needed
   touching for this.
   --------------------------------------------------------------------- */

/* ---- Tables ---- */
.content .table { font-size: .78rem; }
.content .table th,
.content .table td { padding: .38rem .55rem; font-size: .78rem; }
.content .table th { font-size: .68rem; }
.content .table .badge { padding: .12rem .45rem; font-size: .7rem; }
.content .table .btn-sm { padding: .22rem .5rem; font-size: .74rem; }
.content .table .form-hint { font-size: .7rem; }

/* ---- Forms ---- */
.content .form-label { font-size: .74rem; margin-bottom: .2rem; }
.content .form-control { padding: .4rem .55rem; font-size: .84rem; }
.content .form-hint { font-size: .72rem; }
.content .form-group { margin-bottom: .8rem; }

/* One deliberate exception to the whole block. A font-size under 16px in
   a focused text field makes iOS Safari zoom the page in, and it does not
   zoom back out — so on phones the fields stay at 16px while everything
   around them stays compact. Trainers and college SPOCs work on phones,
   and a form that jumps on every field is worse than a form that is a
   little roomier than the desktop one. */
@media (max-width: 640px) {
    .content .form-control { font-size: 1rem; padding: .5rem .65rem; }
}

/* ---- Buttons ---- */
.content .btn { padding: .4rem .8rem; font-size: .84rem; }
.content .btn-sm { padding: .3rem .6rem; font-size: .78rem; }

/* ---- Cards and section headings ---- */
.content .card { padding: 1rem; }
.content .card + .card { margin-top: .8rem; }
.content .section-head { margin-bottom: .8rem; }
.content .badge { font-size: .74rem; padding: .18rem .5rem; }
.content .alert { font-size: .86rem; padding: .65rem .85rem; }

/* ---------------------------------------------------------------------
   The two original modifier classes. Their sizing is the app default
   now, so what is left here is only what is genuinely specific to those
   two components: the dense table's even tighter small buttons, and the
   compact filter bar's four-column grid.
   --------------------------------------------------------------------- */
.table--dense th, .table--dense td { padding: .38rem .55rem; font-size: .78rem; }
.table--dense th { font-size: .68rem; }
.table--dense .badge { padding: .12rem .45rem; font-size: .7rem; }
.table--dense .btn-sm { padding: .22rem .5rem; font-size: .74rem; }
.table--dense .form-hint { font-size: .7rem; }

/* Keeps every combobox/textbox on one row instead of wrapping — narrow
   viewports get a horizontal scrollbar on the bar itself rather than
   the fields dropping to new lines. */
/* Four fields per row, so 5-7 filter fields settle into two clean rows
   instead of either wrapping raggedly (one field per leftover row) or
   forcing a single scrolling row (tried first — made Search/Filter/Reset
   scroll out of view, which is worse, not better, for visibility). */
.filter-bar--compact { display: grid; grid-template-columns: repeat(4, minmax(150px, 1fr)); gap: .7rem .9rem; align-items: end; margin-bottom: 0; }
.filter-bar--compact .form-group { min-width: 0; flex: unset; margin-bottom: 0; }
.filter-bar--compact .form-label { font-size: .74rem; margin-bottom: .2rem; white-space: nowrap; }
.filter-bar--compact .form-control { padding: .4rem .55rem; font-size: .84rem; }
.filter-bar--compact .btn { padding: .4rem .8rem; font-size: .84rem; }
@media (max-width: 900px) {
    .filter-bar--compact { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
}

/* Compact KPI tiles (Batch Trainers, and any other admin batch screen
   that wants the same denser look): one consistent value size for
   every tile in the row, whether it holds a number or a line of text —
   the plain .kpi__value (1.6rem, built for a big dashboard count) reads
   fine for one or two numeric tiles but looks oversized and inconsistent
   next to text tiles like a college name or a date range. */
.kpi-row--compact .kpi { padding: .7rem .85rem; }
.kpi-row--compact .kpi__value { font-size: 1rem; font-weight: 700; }
.kpi-row--compact .kpi__label { font-size: .68rem; }

/* ---------- My Profile — Facebook-style cover + avatar header ----------
   Same cover photo + gradient + initials-avatar idea as the menu's own
   profile card (sidebar__profile), just full-size: a wide banner with the
   avatar overlapping its bottom edge and the name/role sitting beside it,
   in the app's own glass-card surface rather than a plain white one. */
.pf-hero { padding: 0; overflow: hidden; position: relative; }

/* THE BANNER. 240px, up from 160 — "header little more in length". At
   160 a 112px avatar overlapping it by 52 covered a third of the image
   and the cover a person had chosen was never really seen. The identity
   block now sits inside the banner too, which is what the extra height
   is actually for: it is not empty space, it is where the name lives. */
.pf-cover {
    height: 240px;
    background-color: var(--ink);
    background-size: cover;
    background-position: center;
}

/* The header overlays the bottom of the banner rather than sitting under
   it, so the name is on the image and the whole hero reads as one block.
   `position:absolute` + `inset-inline:0` rather than a negative margin:
   a negative margin on a flex row whose tallest item is the avatar makes
   the card's own height depend on the avatar size, which is exactly what
   made the old 112/-52 pair so fiddly to change. */
.pf-header {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    display: flex;
    align-items: flex-end;
    gap: 1.3rem;
    /* The bottom padding has to clear the avatar's ring and shadow, or
       the circle reads as clipped by the banner's edge — which is what
       1.3rem did at 148px. */
    padding: 1.4rem 1.6rem 1.6rem;
}
.pf-avatar-wrap { position: relative; flex-shrink: 0; width: 132px; height: 132px; }
.pf-avatar {
    width: 132px; height: 132px;
    border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    background: var(--brand-gradient);
    color: #fff;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 2.6rem;
    letter-spacing: .02em;
    /* A white ring, not the card surface: the avatar now sits on the
       photograph, where a glass-surface border disappears into whatever
       the cover happens to be. */
    border: 5px solid rgba(255, 255, 255, .92);
    box-shadow: 0 10px 28px rgba(0, 0, 0, .38);
}
.pf-avatar--photo { object-fit: cover; background: var(--ink); }

/* The change-photo control. Was a bare 34px circle with an icon in it,
   which is discoverable only by hovering; it now carries the word
   "Change" on a screen wide enough for it, and stays a circle below
   that. Either way the whole thing is one <label> for the file input. */
/* Bottom-CENTRE of the avatar, not bottom-right.
   At bottom-right the pill extended past the avatar's own circle and
   landed on top of the role chip beside it — visible immediately in a
   screenshot and invisible in the code. Centred and translated back by
   half its width, it can never leave the avatar's footprint however wide
   the label gets. */
.pf-avatar-edit {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    /* Hanging further below the circle than in it: at bottom:-6px the
       pill covered a third of the person's face. */
    bottom: -12px;
    min-width: 34px; height: 30px;
    padding: 0 .65rem;
    border-radius: 999px;
    display: inline-flex; align-items: center; justify-content: center; gap: .3rem;
    background: var(--brand-gradient);
    color: #fff;
    font-size: .72rem;
    font-weight: 600;
    border: 3px solid rgba(255, 255, 255, .92);
    box-shadow: 0 6px 16px rgba(0, 0, 0, .35);
    cursor: pointer;
    white-space: nowrap;
}
.pf-avatar-edit:hover { filter: brightness(1.08); }
.pf-avatar-edit__text { display: none; }

.pf-identity { min-width: 0; padding-bottom: .5rem; }

/* THE NAME. "names should be visible beautifully."
   The display face, fluid between 1.7rem and 2.4rem so a long Indian
   full name (which is the common case in this data — three and four
   words) fills the available width on a laptop without wrapping to
   three lines on a phone. White with a shadow because it now sits on
   the cover photo; the gradient above guarantees the contrast, the
   shadow carries the edges over a busy image. Two lines maximum, then
   an ellipsis — the full name is in the title attribute. */
.pf-name {
    margin: 0;
    font-family: var(--font-display);
    font-size: clamp(1.7rem, 3.4vw, 2.4rem);
    line-height: 1.12;
    font-weight: 700;
    letter-spacing: -.015em;
    color: #fff;
    text-shadow: 0 2px 10px rgba(0, 0, 0, .55), 0 1px 2px rgba(0, 0, 0, .5);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.pf-identity__row {
    display: flex; flex-wrap: wrap; align-items: center; gap: .45rem;
    margin-top: .55rem;
}
.pf-role {
    display: inline-block;
    padding: .22rem .7rem;
    border-radius: 999px;
    /* On the photo, so the brand-light/brand-hover pair the rest of the
       app uses on a card surface does not apply — this is a light chip
       on a dark scrim, which is legible over any cover. */
    background: rgba(255, 255, 255, .92);
    color: #7c2d12;
    font-size: .78rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
}
/* The identifying facts, from the field list already loaded. Glass
   chips, so they read as secondary to the role without needing a
   second colour. */
.pf-meta {
    display: inline-flex; align-items: center; gap: .3rem;
    padding: .22rem .6rem;
    border-radius: 999px;
    background: rgba(255, 255, 255, .16);
    border: 1px solid rgba(255, 255, 255, .28);
    color: #fff;
    font-size: .78rem;
    font-weight: 500;
    max-width: 22ch;
}
.pf-meta svg { flex: none; opacity: .85; }
.pf-meta__value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (min-width: 900px) {
    /* Wide enough for the word next to the camera icon. */
    .pf-avatar-edit__text { display: inline; }
}

@media (max-width: 767px) {
    /* Below this the name cannot share a row with a 148px avatar without
       being squeezed to four words a line, so the hero stacks: the
       banner keeps its own height, and the identity block moves out from
       under the image onto the card surface, where it takes the normal
       ink colours. */
    .pf-cover { height: 150px; }
    .pf-header {
        position: static;
        flex-direction: column;
        align-items: center;
        text-align: center;
        gap: .7rem;
        padding: 0 1.1rem 1.2rem;
        margin-top: -58px;
    }
    .pf-avatar-wrap, .pf-avatar { width: 112px; height: 112px; }
    .pf-avatar { font-size: 2.1rem; }
    .pf-identity { padding-bottom: 0; }
    .pf-name {
        color: var(--ink);
        text-shadow: none;
        font-size: clamp(1.35rem, 6vw, 1.75rem);
    }
    .pf-identity__row { justify-content: center; }
    .pf-role { background: var(--brand-light); color: var(--brand-hover); }
    .pf-meta {
        background: var(--surface-muted, rgba(0, 0, 0, .05));
        border-color: var(--border);
        color: var(--ink-muted);
    }
}

/* Profile body. Was a fixed 1.4fr/1fr split with About locked into the left
   column and everything else in the right — which left a tall empty column
   beside a short field list (a trainer's About is five or six tiles). Now
   About spans the full width and its own tiles flow across it, and the
   remaining cards are direct items in a flowing auto-fit grid, so one card
   (student, college SPOC) or two (staff, trainer) both fill the row instead
   of leaving a hole. align-items:start keeps a short card its own height
   rather than stretching it to match a tall neighbour. */
.pf-about { margin-top: 1.25rem; }
.pf-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
    gap: 1.25rem; align-items: start; margin-top: 1.25rem;
}
/* `.card + .card { margin-top: 1rem }` is meant for cards stacked in normal
   flow; as grid items it pushed every card after the first down by 16px
   inside its own row, so a row of cards sat visibly out of line with each
   other. The grid's own `gap` is the spacing here. */
.pf-grid > .card + .card { margin-top: 0; }
.pf-form-narrow { max-width: 460px; }
.pf-bank { margin-top: 1.25rem; }
/* Fields flow across the card instead of stacking, so a six-field form is
   two short rows on a desktop and one field per row on a phone — the grid's
   own gap does the spacing, so the stacked-form margin is dropped. */
.pf-field-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: 1rem; }
.pf-field-grid .form-group { margin-bottom: 0; }

.pf-card-title { margin: 0 0 1rem; display: flex; align-items: center; gap: .5rem; }
.pf-card-title .ic { color: var(--brand); }

/*
 * Real gap found: these tiles (the student/trainer/staff/college "About"
 * card on My Profile — req: "the design with icons in info cards, alignment
 * should be correct") had no icon at all, unlike every other card in the app
 * (each already gets one via .pf-card-title), and were a plain
 * label-over-value block. Rebuilt as icon + two-line text, icon vertically
 * centered against the whole two-line block via align-items:center on the
 * flex row (not against the label line alone, which is what a naive
 * icon-beside-label markup would do and is exactly the kind of misalignment
 * this was reported for) — matching the icon-tile pattern already used for
 * .kpi elsewhere in the app rather than inventing a second one.
 */
.pf-info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: .8rem; }
.pf-info-tile {
    display: flex;
    align-items: center;
    gap: .7rem;
    padding: .75rem .9rem;
    border-radius: 12px;
    background: var(--surface);
    border: 1px solid var(--border);
}
.pf-info-tile__icon {
    display: inline-grid;
    place-items: center;
    flex-shrink: 0;
    width: 34px;
    height: 34px;
    border-radius: 10px;
    background: var(--brand-light);
    color: var(--brand-hover);
}
.pf-info-tile__text { min-width: 0; }
.pf-info-tile__label { font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-muted); line-height: 1.3; }
.pf-info-tile__value { margin-top: .2rem; font-weight: 600; color: var(--ink); word-break: break-word; line-height: 1.3; }

.pf-assign-group { margin-bottom: 1rem; }
.pf-assign-group:last-child { margin-bottom: 0; }
.pf-assign-label { font-size: .8rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-muted); margin-bottom: .4rem; }
.pf-assign-count { font-weight: 500; text-transform: none; letter-spacing: 0; }
.pf-chip-list { display: flex; flex-wrap: wrap; gap: .4rem; }

/* ---------- Manage Users — assignment tabs ---------- */
.assign-form__toolbar { display: flex; align-items: center; gap: .5rem; margin-bottom: .7rem; }
.checkbox-grid--dense { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: .4rem; }
.checkbox-grid--dense .checkbox-item { padding: .4rem .6rem; font-size: .78rem; }
.btn-row--nowrap { flex-wrap: nowrap; white-space: nowrap; }

/* ---------- Manage Users — photo thumbnails ---------- */
.user-row-avatar {
    width: 32px; height: 32px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    background: var(--brand-gradient); color: #fff;
    font-size: .68rem; font-weight: 700; font-family: var(--font-display);
}
.user-row-avatar--photo { object-fit: cover; background: var(--ink); }
.user-photo-row { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.1rem; }
.user-photo-row__avatar {
    width: 56px; height: 56px; border-radius: 50%; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    background: var(--brand-gradient); color: #fff;
    font-size: 1.1rem; font-weight: 700; font-family: var(--font-display);
    border: 2px solid var(--glass-border);
}
.user-photo-row__avatar--photo { object-fit: cover; background: var(--ink); }
.user-photo-row__form { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.user-photo-row__form .form-control { width: auto; padding: .4rem .55rem; font-size: .82rem; }
.user-photo-row__form .btn { width: auto; }

/* ---------- Course Content — trainer viewer (sidebar + right-hand frame) ----------
   Delivery 65 redesign, on direct request ("make it more professional
   ... font size as everywhere used throughout the project ... don't
   remove any restrictions"): the whole two-pane layout now sits inside
   one real `.card` (the same glass-panel treatment every other screen
   uses), with the sidebar and viewer as solid inset panels inside it
   rather than two flat, unrelated boxes floating side by side — and
   every font size here already sat inside this file's normal small-text
   range (.74rem–.92rem, the same scale .form-hint/.badge/.meta-item use
   elsewhere), so nothing needed inventing, only tidying up two spots
   that had drifted (the pane title's font-size was never actually set,
   and the topic/item text sizes are confirmed against that existing
   scale below). None of the restrictions below this comment changed:
   same watermark, same tab-blur cover, same disabled right-click/print/
   devtools-shortcut/clipboard behaviour, same class names and element
   ids the inline <script> on the template depends on — this section is
   appearance only.
   Every topic (lesson plan) is listed in a left sidebar; clicking a
   content item loads it into a frame on the right. Expand/collapse and
   the item-click-swap need a little JS (a <template> per item, cloned
   into the viewer pane — the same cloning pattern Bulk Add Batches' "+
   Add Row" already uses), so it lives in a small inline <script> on the
   template itself, matching the "small, page-specific behaviour stays
   local, not in assets/js/app.js" rule already followed elsewhere. */
.lms-card { padding: 1rem; }
.lms-shell {
    display: flex;
    gap: 1.1rem;
    align-items: flex-start;
}
.lms-sidebar {
    flex: 0 0 300px;
    max-width: 300px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface-2);
    overflow-y: auto;
    max-height: 80vh;
    padding: .5rem;
}
.lms-sidebar__head {
    display: flex; align-items: center; justify-content: space-between; gap: .5rem;
    padding: .5rem .6rem .65rem;
    margin-bottom: .3rem;
    border-bottom: 1px solid var(--border);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: .86rem;
    color: var(--ink);
}
.lms-sidebar__head > span:first-child { display: inline-flex; align-items: center; gap: .4rem; }
.lms-sidebar__count { font-family: var(--font); font-weight: 400; font-size: .74rem; color: var(--ink-muted); }
.lms-topic { border-bottom: 1px solid var(--border); }
.lms-topic:last-child { border-bottom: none; }
.lms-topic__head {
    width: 100%;
    display: flex; align-items: center; justify-content: space-between; gap: .5rem;
    padding: .75rem .6rem;
    background: none;
    border: 0;
    border-radius: 8px;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: .92rem;
    color: var(--ink);
    text-align: left;
    cursor: pointer;
    transition: background-color .15s ease;
}
.lms-topic__head:hover { background: var(--surface); }
.lms-topic__head-label { display: flex; align-items: center; gap: .5rem; min-width: 0; }
.lms-topic__head-label span:last-child { word-break: break-word; }
.lms-topic__chevron {
    display: inline-flex;
    flex-shrink: 0;
    color: var(--ink-muted);
    transform: rotate(-90deg);
    transition: transform .15s ease, color .15s ease;
}
.lms-topic.is-open .lms-topic__chevron { transform: rotate(0deg); color: var(--brand); }
.lms-topic__items { display: none; padding: 0 .3rem .5rem 1.6rem; }
.lms-topic.is-open .lms-topic__items { display: block; }
.lms-item-link {
    display: flex; align-items: center; gap: .5rem;
    width: 100%;
    background: none;
    border: 0;
    padding: .5rem .5rem;
    border-radius: 8px;
    text-align: left;
    color: var(--ink-muted);
    font-size: .86rem;
    cursor: pointer;
    transition: background-color .15s ease, color .15s ease;
}
.lms-item-link:hover { background: var(--surface); color: var(--ink); }
.lms-item-link.is-active { background: var(--brand); color: #fff; }
.lms-item-link.is-active .lms-item-link__icon { color: #fff; }
.lms-item-link__icon { flex-shrink: 0; color: var(--brand); }

.lms-viewer {
    position: relative;
    flex: 1;
    min-width: 0;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
    padding: 1.1rem 1.25rem;
    min-height: 80vh;
    user-select: none;
    -webkit-user-select: none;
    overflow: hidden;
}
.lms-viewer__body { position: relative; z-index: 1; height: 100%; }
.lms-viewer__placeholder {
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .6rem;
    height: 75vh;
    color: var(--ink-muted);
    text-align: center;
    padding: 2rem;
}
.lms-viewer__placeholder svg { color: var(--border); }
.lms-viewer__placeholder p { margin: 0; font-size: .92rem; }
.lms-pane-titleblock { padding-bottom: .75rem; margin-bottom: 1rem; border-bottom: 1px solid var(--border); }
.lms-pane-head { display: flex; align-items: center; gap: .5rem; font-family: var(--font-display); font-weight: 700; font-size: 1.02rem; color: var(--ink); }
.lms-pane-meta { font-size: .8rem; color: var(--ink-muted); margin: .3rem 0 0; }
.lms-frame { width: 100%; height: 75vh; border: 0; border-radius: 10px; background: #fff; }
.lms-image-wrap { text-align: center; }
.lms-frame-img {
    max-width: 100%;
    max-height: 75vh;
    border-radius: 10px;
    border: 1px solid var(--border);
    -webkit-user-drag: none;
    user-select: none;
}
.lms-video-embed { position: relative; padding-top: 56.25%; height: 0; overflow: hidden; border-radius: 10px; background: var(--ink); }
.lms-video-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.lms-external-note {
    display: flex; flex-direction: column; align-items: center; gap: .5rem;
    padding: 2rem 1.2rem;
    text-align: center;
    color: var(--ink-muted);
}

/* Identifying watermark tiled across the viewer (see watermarkSvgDataUri())
   — pointer-events:none so it never blocks clicking through to whatever
   real content is underneath, positioned above it purely visually. Not a
   screenshot blocker (nothing can be); a traceability mark on any copy
   that does leak out. Unchanged by the Delivery 65 redesign. */
.lms-watermark {
    position: absolute;
    inset: 0;
    z-index: 2;
    background-repeat: repeat;
    pointer-events: none;
}
/* Shown only while the tab is out of focus / hidden (see the blur/
   visibilitychange listeners) — content is blurred and interaction
   disabled underneath, so switching away to another window or app doesn't
   leave the material sitting there in the background the whole time.
   Delivery 65: this used to be a hardcoded white panel regardless of
   theme, which looked out of place — and unreadably bright — on either
   dark theme; it now uses this app's own themed background token instead
   (color-mix over --bg-solid, the same technique the public landing
   page's header already uses), so it reads correctly in all four themes.
   Still the same full, interaction-blocking cover it always was. */
.lms-blur-notice {
    display: none;
    position: absolute;
    inset: 0;
    z-index: 3;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .6rem;
    background: color-mix(in srgb, var(--bg-solid) 92%, transparent);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: .92rem;
    color: var(--ink-muted);
    text-align: center;
    padding: 2rem;
}
.lms-viewer.lms-blurred .lms-viewer__body { filter: blur(22px); pointer-events: none; }
.lms-viewer.lms-blurred .lms-blur-notice { display: flex; }

/* Printing this page is disabled outright (not just Ctrl+P) — the shell
   itself is hidden in print media and this note is the only thing left
   to print. */
.lms-print-note { display: none; }
@media print {
    .lms-shell { display: none !important; }
    .lms-print-note {
        display: block !important;
        text-align: center;
        padding: 4rem 2rem;
        font-family: var(--font-display);
        font-weight: 700;
        color: var(--ink-muted);
    }
}

/* =====================================================================
   Printable sheet (body.print-sheet — currently the trainer's Attendance
   Report). Scoped to that one body class so no other screen's printing
   changes. Designed to survive the browser's default "don't print
   background graphics" setting: everything that has to be visible on
   paper is text or a border, never a background colour.
   ===================================================================== */
@media print {
    body.print-sheet { background: #fff; color: #000; }

    /* The app's own furniture has no business on a printed sheet. This
       used to miss the floating widgets — the clock (.jfclock), the DIA
       chat launcher (.jfchat), the to-do widget (.jftodo), and the
       scroll-to-top button (.scroll-top-btn), all rendered once in
       templates/layout/app.php so they float over every signed-in
       screen, this one included. .app-shell just below is only ever
       reset to a plain block layout for print, never hidden outright (it
       still holds the actual sheet content), so none of THESE were ever
       actually caught by it — they needed their own line, same as the
       sidebar/topbar/etc. above them. "when i click on print...it is
       also showing the widgets...clock and chatbox icon". */
    body.print-sheet .sidebar,
    body.print-sheet .sidebar-backdrop,
    body.print-sheet .topbar,
    body.print-sheet .alert,
    body.print-sheet .section-head,
    body.print-sheet .batch-banner,
    body.print-sheet .att-summary,
    body.print-sheet .jfclock,
    body.print-sheet .jfchat,
    body.print-sheet .jfchat__panel,
    body.print-sheet .jftodo,
    body.print-sheet .scroll-top-btn { display: none !important; }

    body.print-sheet .app-shell,
    body.print-sheet .main { display: block; margin: 0; padding: 0; min-height: 0; }
    body.print-sheet .content { max-width: none; width: auto; margin: 0; padding: 0; }

    /* The card is a screen affordance — on paper it's just the page. */
    body.print-sheet .att-card {
        background: none; border: 0; box-shadow: none; padding: 0;
        -webkit-backdrop-filter: none; backdrop-filter: none;
    }

    /* ---- sheet header: Naandi one side, the project's logo the other ---- */
    body.print-sheet .att-print-head { display: block; margin-bottom: 8px; }
    body.print-sheet .att-print-head__row {
        display: flex; align-items: center; justify-content: space-between; gap: 12px;
        padding-bottom: 6px; border-bottom: 2px solid #000;
    }
    body.print-sheet .att-print-logo { flex: 0 0 auto; width: 22%; }
    body.print-sheet .att-print-logo--right { text-align: right; }
    body.print-sheet .att-print-logo img { max-height: 42px; max-width: 100%; width: auto; }
    body.print-sheet .att-print-title { flex: 1 1 auto; text-align: center; }
    body.print-sheet .att-print-title h1 {
        margin: 0; font-size: 15pt; letter-spacing: .04em; text-transform: uppercase; color: #000;
    }
    body.print-sheet .att-print-title p { margin: 2px 0 0; font-size: 8.5pt; color: #333; }

    /* ---- batch + college details ---- */
    body.print-sheet .att-print-details {
        display: flex; gap: 14px; margin-top: 7px;
        padding-bottom: 7px; border-bottom: 1px solid #000;
    }
    body.print-sheet .att-print-details__col { flex: 1 1 0; min-width: 0; }
    body.print-sheet .att-print-field { display: flex; gap: 5px; font-size: 7.6pt; line-height: 1.5; }
    body.print-sheet .att-print-field span { flex: 0 0 40%; color: #444; }
    body.print-sheet .att-print-field b { flex: 1 1 auto; color: #000; word-break: break-word; }

    /* ---- the grid itself ---- */
    body.print-sheet .att-table-wrap { overflow: visible; border: 0; border-radius: 0; }
    body.print-sheet .att-table {
        width: 100%; font-size: 6.6pt; background: none; table-layout: auto;
    }
    body.print-sheet .att-table th,
    body.print-sheet .att-table td {
        border: 1px solid #444; padding: 2px 3px; color: #000; background: none;
        text-transform: none; letter-spacing: 0; font-size: 6.6pt;
    }
    body.print-sheet .att-table th { font-weight: 700; }
    body.print-sheet .att-table .att-day { padding: 2px 1px; }
    body.print-sheet .att-table .att-day__full { display: none; }
    body.print-sheet .att-table .att-day__short { display: inline; }
    body.print-sheet .att-table td.wrap { white-space: normal; }
    /* Badges become plain letters: a background colour may not print at
       all, but a bold P/A always does. */
    body.print-sheet .att-table .badge {
        background: none !important; color: #000 !important;
        padding: 0; border-radius: 0; font-size: 6.6pt; font-weight: 700;
    }
    body.print-sheet .att-table thead { display: table-header-group; }
    body.print-sheet .att-table tr { page-break-inside: avoid; }
    body.print-sheet .att-table tbody tr:hover { background: none; }

    /* ---- signatures: trainer one side, college authority the other ---- */
    /* The gap above the rules is the actual signing space — sign above the
       line, label underneath, the way a signed sheet is normally laid out.
       The block flows after the table (so it lands under the last student
       on the last page) rather than being pinned to the page bottom: a
       print-fixed footer repeats on every page, which is wrong for a roster
       long enough to run over one sheet. */
    body.print-sheet .att-print-foot {
        display: flex; justify-content: space-between; gap: 40px;
        margin-top: 58px; page-break-inside: avoid;
    }
    body.print-sheet .att-print-sign { flex: 0 0 38%; text-align: center; }
    body.print-sheet .att-print-sign__line { border-top: 1px solid #000; margin-bottom: 4px; }
    body.print-sheet .att-print-sign__role { font-size: 8pt; font-weight: 700; color: #000; }
    body.print-sheet .att-print-sign__name { font-size: 7.5pt; color: #333; margin-top: 1px; }
    body.print-sheet .att-print-sign__meta { font-size: 7.5pt; color: #333; margin-top: 7px; }
}

@media (max-width: 860px) {
    .lms-shell { flex-direction: column; }
    .lms-sidebar { flex: 1 1 auto; max-width: none; width: 100%; max-height: 40vh; }
    .lms-viewer { min-height: 45vh; }
    .lms-frame, .lms-frame-img { height: 50vh; max-height: 50vh; }
}

/* ---------- Searchable dropdowns ---------- */
/* The real <select> stays in the DOM and stays the thing that submits; it
   is only taken out of the visual flow. Without JavaScript none of this
   applies and the plain dropdown is what a person sees. */
.ss { position: relative; }
.ss__native {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.ss__button {
    display: flex; align-items: center; justify-content: space-between; gap: .5rem;
    width: 100%; text-align: left; cursor: pointer;
}
.ss__button::after {
    content: ""; flex: 0 0 auto;
    width: .5rem; height: .5rem; margin-right: .1rem;
    border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
    transform: translateY(-2px) rotate(45deg); opacity: .5;
}
.ss__button--placeholder { color: var(--muted); }
.ss__button:disabled { cursor: not-allowed; opacity: .6; }
/* The panel is a child of <body>, positioned in viewport coordinates by
   JS (see place() in app.js) rather than sitting inside its own field.
   That is deliberate and load-bearing: every .card here carries a
   backdrop-filter, which makes the card both its own stacking context and
   the containing block for anything positioned inside it, and .table-wrap
   sets overflow-x, which per CSS turns the other axis into a scroll
   container too. A panel drawn inside the field was therefore painted
   under the following card, or clipped at the edge of a table — options
   you could see but not click. Out here, nothing can clip or cover it.
   left/top/min-width/max-height are all set inline while it is open. */
.ss__panel {
    position: fixed; z-index: 200; left: 0; top: 0;
    /* At least as wide as its own field, but free to grow for long names
       (colleges, projects, courses) rather than clipping them. */
    width: max-content;
    max-width: min(34rem, 92vw);
    display: flex; flex-direction: column;
    padding: .35rem; background: var(--bg-solid); color: var(--ink);
    border: 1px solid var(--border); border-radius: 10px;
    box-shadow: 0 14px 34px rgba(15, 23, 42, .28);
}
.ss__panel[hidden] { display: none; }
.ss__panel--wide { min-width: min(30rem, 92vw); }
.ss__search { margin-bottom: .35rem; flex: none; }
.ss__list { max-height: 15rem; overflow-y: auto; overscroll-behavior: contain; }
/* Shown when a search matches more rows than the panel draws at once. */
.ss__more {
    flex: none; padding: .4rem .55rem .2rem; font-size: .74rem;
    color: var(--ink-muted); border-top: 1px solid var(--border); margin-top: .3rem;
}
.ss__option {
    display: block; width: 100%; padding: .42rem .55rem;
    border: 0; border-radius: 7px; background: transparent; color: inherit;
    font: inherit; font-size: .85rem; text-align: left; cursor: pointer;
    /* Wrap rather than truncate: the point of opening the list is to read
       the whole name. */
    white-space: normal; overflow-wrap: anywhere;
}
/* A class rule with `display: block` outranks the browser's own style for
   the hidden attribute, so filtering has to hide these explicitly — without
   this line typing in the search box changes nothing on screen. */
.ss__option[hidden] { display: none; }
.ss__option:hover { background: var(--surface-2); }
/* Keyboard highlight (arrow keys) — deliberately stronger than :hover so
   the two can't be confused while typing. */
.ss__option.is-current { background: var(--surface-2); font-weight: 600; }
/* --brand-light is one pale tint shared by every theme, so the text colour
   has to be set with it rather than inherited — inheriting left light ink on
   a light tint in the dark themes, which a screenshot caught as unreadable.
   Same background/ink pairing .badge and .radio-pill:has(:checked) use. */
.ss__option.is-active { background: var(--brand-light); color: var(--brand-hover); box-shadow: inset 0 0 0 1px var(--brand); }
.ss__option.is-disabled { color: var(--muted); cursor: default; }
.ss__empty { padding: .5rem .55rem; color: var(--muted); font-size: .82rem; }

/* A dropdown inside a toolbar (rows-per-page, the filter bars) sizes to its
   own content rather than stretching the row. The panel's own width comes
   from JS, which floors it at a readable size. */
.table-tools .ss, .pager .ss { min-width: 6rem; }

/* A dependent list the current selection has narrowed to nothing — the
   placeholder already says which selection did it (data-empty-label), this
   just stops it reading as an ordinary empty dropdown. */
select.is-cascade-empty, .is-cascade-empty + .ss__button {
    border-color: var(--warning, #b45309);
}

/* ---------- Section head with a supporting line ---------- */
.section-head h2 { display: inline-flex; align-items: center; gap: .55rem; }
.section-head h2 svg { color: var(--brand); flex: none; }
.section-head__note { margin: .25rem 0 0; font-size: .82rem; color: var(--ink-muted); }

/* ---------- Collapsible panel (a card that folds away) ----------
   Used where a screen is mostly a list but also carries a form: the form
   is out of the way until it is wanted, and opens by itself when there is
   something in it. Built on <details> so it still works with no
   JavaScript at all. */
.panel { padding: 0; }
.panel__head {
    display: flex; align-items: center; gap: .6rem;
    padding: .95rem 1.15rem; cursor: pointer; list-style: none;
    font-weight: 600; color: var(--ink); border-radius: var(--radius);
}
.panel__head::-webkit-details-marker { display: none; }
.panel__head:hover { background: var(--surface-2); }
.panel__title { display: inline-flex; align-items: center; gap: .5rem; }
.panel__title svg { color: var(--brand); flex: none; }
.panel__hint {
    color: var(--ink-muted); font-weight: 400; font-size: .82rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.panel__chev { margin-left: auto; display: inline-flex; color: var(--ink-muted); transition: transform .15s ease; }
.panel[open] .panel__chev { transform: rotate(180deg); }
.panel[open] .panel__head { border-bottom: 1px solid var(--border); border-radius: var(--radius) var(--radius) 0 0; }
.panel__body { padding: 1.15rem; }

/* ---------- Courses ---------- */
.crs-name { display: block; font-weight: 600; }
.crs-sub { display: block; font-size: .72rem; color: var(--ink-muted); margin-top: .1rem; }
.crs-assess { display: flex; align-items: flex-start; gap: .4rem; }
.crs-assess + .crs-assess { margin-top: .25rem; }
.crs-assess__tag {
    flex: none; min-width: 2.4rem; padding: .05rem .35rem;
    border: 1px solid var(--border); border-radius: 5px;
    font-size: .66rem; text-transform: uppercase; letter-spacing: .04em;
    color: var(--ink-muted); text-align: center;
}
/* A deactivated row is still readable, just visibly stood down. */
tr.is-muted-row td { opacity: .68; }
tr.is-muted-row .badge { opacity: 1; }
/* A tile whose number is a problem to fix, not a fact to note. */
.kpi--warn .kpi__icon { color: var(--warning, #b45309); }

/* A destructive action that appears on every row of a table. Solid red on
   each of thirty rows reads as thirty warnings; the outline keeps the
   meaning without turning the list into a wall of alarm. */
.btn-outline-danger {
    background: transparent; color: var(--danger);
    border-color: var(--danger); box-shadow: none;
}
.btn-outline-danger:hover { background: var(--danger); color: #fff; text-decoration: none; }

/* (The old .has-open-select lift is gone: the dropdown panel is now a
   child of <body>, so there is no ancestor card left to lift. See the
   .ss__panel block above.) */

/* A dropdown whose own values are long asks the filter grid for two
   columns, so the chosen college/project/course is readable without
   opening the list. */
.filter-bar .form-group--wide,
.filter-bar--compact .form-group--wide,
.form-row .form-group--wide { grid-column: span 2; }
@media (max-width: 700px) {
    .filter-bar .form-group--wide,
    .filter-bar--compact .form-group--wide,
    .form-row .form-group--wide { grid-column: auto; }
}

/* ---------- Live table search ---------- */
/* Sits directly above the table it filters, so it can't be mistaken for a
   filter that reloads the page — this one narrows the rows as you type. */
.table-search {
    display: flex; align-items: center; gap: .5rem;
    flex-wrap: wrap; margin: 0 0 .6rem;
}
.table-search__label {
    font-size: .78rem; font-weight: 600; color: var(--ink-muted);
    text-transform: uppercase; letter-spacing: .04em;
}
.table-search__input { width: auto; min-width: min(22rem, 100%); flex: 0 1 22rem; }
.table-search__count { font-size: .78rem; color: var(--ink-muted); }
.table-search__empty td { color: var(--ink-muted); font-style: italic; }

/* A warning that is information, not a failure — used where an action is
   allowed but has a consequence worth reading first (a transfer that
   carries attendance with it, a batch going over its planned seats). */
.alert-warning {
    background: var(--warning-bg); color: var(--warning-text, var(--warning));
    border: 1px solid rgba(202, 138, 4, 0.35);
}

/* Numbered step marker on a multi-step screen (Batch Transfer). */
.step-num {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.5rem; height: 1.5rem; margin-right: .15rem;
    border-radius: 50%; background: var(--brand); color: #fff;
    font-size: .8rem; font-weight: 700; flex: none;
}

/* Visible to a screen reader, not on screen — a checkbox whose meaning
   comes from its row rather than from a visible label beside it. */
.sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* A pager control that leads nowhere from this page: shown, so the set of
   four never changes shape between pages, but plainly not clickable. */
.btn.is-disabled {
    opacity: .4; cursor: not-allowed; pointer-events: none;
    text-decoration: none;
}

/* Inline gender picker in the roster's own toolbar. */
.roster-gender { display: inline-flex; align-items: center; gap: .4rem; }
.roster-gender > span { font-size: .78rem; color: var(--ink-muted); white-space: nowrap; }
.roster-gender .form-control { width: auto; padding: .35rem .5rem; font-size: .82rem; }

/* ---------- Cover image picker ---------- */
.cover-grid { display: grid; gap: .7rem; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.cover-tile { position: relative; cursor: pointer; display: block; }
.cover-tile input { position: absolute; opacity: 0; width: 0; height: 0; }
.cover-tile__img {
    display: block; height: 84px; border-radius: 10px;
    border: 2px solid var(--border); background: var(--surface-2);
    background-size: cover; background-position: center;
}
.cover-tile__label {
    display: block; margin-top: .3rem; font-size: .72rem; color: var(--ink-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cover-tile:hover .cover-tile__img { border-color: var(--brand); }
.cover-tile.is-current .cover-tile__img { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-light); }
.cover-tile input:focus-visible + .cover-tile__img { outline: 2px solid var(--brand); outline-offset: 2px; }
/* An image whose file has not been copied to the server yet: still
   selectable (the choice is a row, not a file), but honest about it. */
.cover-tile.is-missing .cover-tile__img {
    background-image: repeating-linear-gradient(45deg, var(--surface-2) 0 8px, var(--surface) 8px 16px);
}

/* Menu Design picker (My Profile) — reuses .cover-tile/.cover-grid, with
   a tiny schematic instead of a photo so each option previews its own
   layout rather than a generic placeholder. */
.menu-design-preview {
    display: flex; flex-direction: column; gap: 5px;
    padding: 10px 12px; justify-content: center;
    background: var(--ink);
}
.menu-design-preview span { display: block; border-radius: 3px; background: rgba(242, 102, 15, .55); height: 8px; }
.menu-design-preview--tiles { flex-direction: row; align-items: stretch; }
.menu-design-preview--tiles span { flex: 1; height: auto; border-radius: 4px; }
.menu-design-preview--list span { width: 100%; height: 10px; }
.menu-design-preview--compact span { width: 70%; height: 6px; background: rgba(242, 102, 15, .4); }

/* Login Voice Greeting on/off picker (My Profile) — reuses
   .cover-tile/.cover-grid same as the two pickers above, but centres a
   plain icon instead of a photo or a layout schematic, since there is
   only an on/off state to show rather than something visual to preview. */
.voice-pref-preview { display: flex; align-items: center; justify-content: center; color: var(--ink-muted); }
.cover-tile.is-current .voice-pref-preview { color: var(--brand); }

/* Delivery 83 — Login Voice Greeting, Appearance and Menu Design (My
   Profile) were all big .cover-tile picture-cards with a separate "Save"
   button below — "make that a small toggles kind of buttons...just make
   that if selected it changes". These three now render as a row of small
   pill buttons instead (same idea as .radio-pill elsewhere on the app,
   just with the real radio input hidden so the pill itself is the whole
   control, closer to a segmented toggle), and each one submits its form
   the instant it's picked (onchange="this.form.submit()", the same
   pattern already used for the filter dropdowns on Master Data/SMS/Batch
   Detail) — there is nothing left to press Save on. The account-level
   save itself is unchanged; only the picker's shape and the missing
   round trip are new. */
.toggle-pill-row { display: flex; flex-wrap: wrap; gap: .5rem; }
.toggle-pill {
    position: relative; display: inline-flex; align-items: center; gap: .4rem;
    padding: .5rem 1rem; border-radius: 999px; border: 1px solid var(--border);
    background: var(--surface-2); cursor: pointer; user-select: none;
    font-size: .86rem; font-weight: 600; color: var(--ink-muted);
    transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.toggle-pill input { position: absolute; opacity: 0; width: 0; height: 0; }
.toggle-pill:hover { border-color: var(--brand); }
.toggle-pill.is-current { border-color: var(--brand); background: var(--brand-light); color: var(--brand-hover); }
.toggle-pill:has(input:focus-visible) { outline: 2px solid var(--brand); outline-offset: 2px; }
.toggle-pill__icon { font-size: 1rem; line-height: 1; }

/* Delivery 130 — the Chatbot Face tiles (My Profile): a small round
   swatch matching DIA's own launcher button (same brand-gradient
   background, same white face), so the tile IS the preview rather than
   a description of one. */
.toggle-pill--avatar { padding-left: .6rem; }
.jfchat-avatar-preview {
    display: inline-flex; align-items: center; justify-content: center;
    width: 32px; height: 32px; border-radius: 50%; flex: none;
    background: var(--brand-gradient); color: #fff;
}

/* ---------- Cover Pages library (admin) ---------- */
.cover-grid--admin { grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
.cover-card {
    border: 1px solid var(--border); border-radius: 12px; overflow: hidden;
    background: var(--surface); display: flex; flex-direction: column;
}
.cover-card.is-off { opacity: .62; }
.cover-card__img { height: 110px; background-size: cover; background-position: center; background-color: var(--surface-2); }
.cover-card__img.is-missing {
    background-image: repeating-linear-gradient(45deg, var(--surface-2) 0 10px, var(--surface) 10px 20px);
}
.cover-card__body { padding: .7rem .8rem; display: flex; flex-direction: column; gap: .4rem; }
.cover-card__name { font-size: .78rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cover-card__meta { display: flex; gap: .3rem; flex-wrap: wrap; }
.cover-card__use { font-size: .72rem; color: var(--ink-muted); }

/* An enquiry's message body: readable, but never so wide that the row
   above it stops being scannable. */
.msg-text { max-width: 46ch; }

/* ---------- Our Team ---------- */
.team-grid {
    display: grid; gap: 1.1rem;
    grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
}
.team-card {
    position: relative; border-radius: var(--radius); overflow: hidden;
    background: var(--glass-bg); border: 1px solid var(--glass-border);
    box-shadow: var(--shadow);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    display: flex; flex-direction: column;
}
.team-card__cover { height: 96px; background-size: cover; background-position: center; }
/* The avatar straddles the cover's bottom edge — the shape people expect
   from a profile card, and the reason the cover reads as a header rather
   than a random band of colour. */
.team-card__avatar-wrap { margin-top: -38px; display: flex; justify-content: center; }
.team-card__avatar {
    width: 76px; height: 76px; border-radius: 50%; object-fit: cover;
    border: 3px solid var(--surface); background: var(--surface);
    box-shadow: 0 6px 18px rgba(15, 23, 42, .22);
}
.team-card__avatar--initials {
    display: flex; align-items: center; justify-content: center;
    background: var(--brand); color: #fff; font-weight: 700; font-size: 1.5rem;
    font-family: var(--font-display, inherit);
}
.team-card__body { padding: .6rem 1rem 1.1rem; text-align: center; display: flex; flex-direction: column; gap: .45rem; flex: 1; }
.team-card__name { margin: 0; font-size: 1rem; }
.team-card__role {
    margin: 0; font-size: .76rem; color: var(--ink-muted);
    text-transform: uppercase; letter-spacing: .05em;
}
.team-card__contact { list-style: none; margin: .2rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .3rem; }
.team-card__contact li {
    display: flex; align-items: center; justify-content: center; gap: .35rem;
    font-size: .8rem; min-width: 0;
}
.team-card__contact li svg { color: var(--brand); flex: none; }
.team-card__contact a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.team-card__tags { display: flex; flex-wrap: wrap; gap: .3rem; justify-content: center; margin-top: auto; padding-top: .5rem; }

/* ---------- Shared file cards ---------- */
.file-grid { display: grid; gap: .8rem; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.file-card {
    display: flex; align-items: center; gap: .75rem;
    padding: .8rem; border: 1px solid var(--border); border-radius: 10px;
    background: var(--surface);
}
.file-card__ext {
    flex: none; width: 46px; height: 46px; border-radius: 9px;
    display: flex; align-items: center; justify-content: center;
    background: var(--brand-light); color: var(--brand-hover);
    font-size: .64rem; font-weight: 700; letter-spacing: .03em;
}
.file-card__body { flex: 1; min-width: 0; }
.file-card__name { font-weight: 600; font-size: .86rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-card__desc {
    margin: .15rem 0 0; font-size: .74rem; color: var(--ink-muted);
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* =====================================================================
   ANALYTICS DASHBOARD — chart palette and layout
   =====================================================================

   THE PALETTE WAS VALIDATED, NOT CHOSEN BY EYE. Both modes are selected
   sets of steps — the dark column is not an automatic flip of the light
   one. Run against this app's own card surfaces (light #fbf8f3, dark
   #1e1a15), all pairs:

     light   lightness band PASS · chroma floor PASS
             CVD separation  worst pair aqua↔orange ΔE 9.2 (deutan)
             normal vision   worst pair aqua↔blue   ΔE 24.0
             contrast        aqua 2.66:1 — below 3:1
     dark    every check PASS, all three ≥ 3:1 on the dark surface

   That one light-mode contrast warning is an obligation, not a note:
   every chart using --viz-3 carries visible direct labels AND repeats
   its numbers in a table beside it, so the aqua is never the only thing
   telling the reader a value. That is why _charts.php labels every bar
   rather than labelling selectively as it otherwise would.

   Three categorical slots is the cap here, deliberately: past three,
   this set stops clearing the all-pairs floors, and the honest fix is
   to fold the tail into "Other" (which _analytics.php does) rather than
   to invent a fourth hue.

   Categorical is used ONLY where the series are the subject — this year
   against last, pre against post, one gender against another. Every
   single-series chart uses --viz-seq, one hue, because its job is
   magnitude and a sequential hue is the safe default.
   --------------------------------------------------------------------- */
:root {
    /* Categorical — fixed order, never cycled. */
    --viz-1: #2a78d6;   /* blue   — the current period, the "pre" value */
    --viz-2: #eb6834;   /* orange — the comparison period, the "post" value */
    --viz-3: #1baf7a;   /* aqua   — a third series, always directly labelled */

    /* Sequential: one hue, light to dark, for magnitude. Five discrete
       steps rather than a continuous gradient so neighbouring heatmap
       cells stay distinguishable. Step 1 is the palest that still
       clears 2:1 on this surface, because an ordinal ramp's lightest
       step still has to be visible. */
    --viz-seq:   #2a78d6;
    --viz-seq-1: #86b6ef;
    --viz-seq-2: #5598e7;
    --viz-seq-3: #2a78d6;
    --viz-seq-4: #1c5cab;
    --viz-seq-5: #104281;

    /* Status — reserved, never reused as "series 4", and always shipped
       with a word or an icon beside them. */
    --viz-good: #0ca30c;
    --viz-warn: #fab219;
    --viz-serious: #ec835a;
    --viz-critical: #d03b3b;

    /* Chart chrome. The surface token is what a marker's 2px ring is
       drawn in, so it has to be the colour actually behind the chart. */
    --viz-surface: #fdfbf8;
    --viz-ink: #2b2521;
    --viz-ink-muted: #857b71;
    --viz-grid: rgba(43, 37, 33, 0.10);
    --viz-track: rgba(43, 37, 33, 0.06);
}

:root[data-theme="dark"],
:root[data-theme="glass-dark"],
:root[data-theme="sharp-dark"],
:root[data-theme="squircle-dark"] {
    /* sharp-dark/squircle-dark added by delivery 96/97: without being
       listed here too, these dark themes fell back to this block's
       sibling above (the light-mode --viz-1/2/3 etc.), which fails
       every one of the contrast checks noted above once actually on a
       dark background — caught before shipping, not a regression in
       anything already delivered. */
    --viz-1: #3987e5;
    --viz-2: #d95926;
    --viz-3: #199e70;

    --viz-seq:   #3987e5;
    --viz-seq-1: #184f95;
    --viz-seq-2: #256abf;
    --viz-seq-3: #3987e5;
    --viz-seq-4: #6da7ec;
    --viz-seq-5: #9ec5f4;

    --viz-surface: #1e1a15;
    --viz-ink: #ece5db;
    --viz-ink-muted: #a49a8d;
    --viz-grid: rgba(255, 255, 255, 0.12);
    --viz-track: rgba(255, 255, 255, 0.07);
}
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]):not([data-theme="glass-light"]) {
        --viz-1: #3987e5;
        --viz-2: #d95926;
        --viz-3: #199e70;

        --viz-seq:   #3987e5;
        --viz-seq-1: #184f95;
        --viz-seq-2: #256abf;
        --viz-seq-3: #3987e5;
        --viz-seq-4: #6da7ec;
        --viz-seq-5: #9ec5f4;

        --viz-surface: #1e1a15;
        --viz-ink: #ece5db;
        --viz-ink-muted: #a49a8d;
        --viz-grid: rgba(255, 255, 255, 0.12);
        --viz-track: rgba(255, 255, 255, 0.07);
    }
}

/* ---------- Chart internals ---------- */
.viz { display: block; overflow: visible; }
/* Text in a chart wears an ink token, never a series colour: a light
   categorical hue is illegible as text. Identity comes from the
   coloured mark beside the words. */
.viz__cat { fill: var(--viz-ink); font-size: 11px; font-weight: 500; }
.viz__val { fill: var(--viz-ink); font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; }
.viz__note { fill: var(--viz-ink-muted); font-weight: 500; }
.viz__tick { fill: var(--viz-ink-muted); font-size: 10px; font-variant-numeric: tabular-nums; }
.viz__cellab { fill: var(--viz-ink); font-size: 10px; font-weight: 600; font-variant-numeric: tabular-nums; }
/* The one place text sits on a filled mark: white on the darker steps,
   which is the exception the label rule allows. */
.viz__inlab { fill: #fff; font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; }
.viz__grid { stroke: var(--viz-grid); stroke-width: 1; }
.viz__track { fill: var(--viz-track); }
.viz-empty {
    margin: 0; padding: 1.1rem .2rem; color: var(--ink-muted);
    font-size: .82rem; text-align: center;
}

.viz-legend {
    display: flex; flex-wrap: wrap; gap: .35rem .9rem;
    margin: .5rem 0 0; padding: 0; list-style: none;
    font-size: .74rem; color: var(--ink-muted);
}
.viz-legend li { display: inline-flex; align-items: center; gap: .35rem; }
.viz-legend__dot { width: 9px; height: 9px; border-radius: 3px; flex: none; }

/* ---------- Delta cues ---------- */
/* An arrow glyph and a word, so the meaning survives colour blindness,
   printing and forced-colors — the colour is the third channel, not the
   only one. */
.delta { font-size: .74rem; font-weight: 700; white-space: nowrap; font-variant-numeric: tabular-nums; }
.delta--good { color: var(--success-text); }
.delta--bad { color: var(--danger-text); }
.delta--flat { color: var(--ink-muted); font-weight: 600; }

/* ---------- Dashboard layout ---------- */
/* One grid, so every card lines up on the same columns whatever mix of
   charts a role's dashboard ends up showing. */
.dash-grid { display: grid; gap: .8rem; grid-template-columns: repeat(12, minmax(0, 1fr)); align-items: start; }
.dash-grid > * { min-width: 0; }               /* or a wide table blows the column out */
/* Equal-height rows, for a page whose cards are lists rather than charts.
   `align-items: start` is right on Analytics — a chart card is as tall as
   its chart and stretching it would just add space under the plot. It is
   wrong on Home, where a seven-item action list beside a one-line stat
   tile left a 380px hole in the page; measured on the rendered page,
   which is the only way that kind of thing shows up. Stretching moves the
   empty space inside the short card, where it reads as breathing room
   instead of a gap. */
.dash-grid--stretch { align-items: stretch; }
.dash-grid--stretch > .card { display: flex; flex-direction: column; }
.dash-grid--stretch > .card > .viz-card__body { flex: 1; }
/* A stretched stat tile: label at the top, figure in the middle, note at
   the bottom, rather than everything crammed against the top edge with
   200px of nothing under it — which is what stretching a two-line tile
   to match a six-row card does by default. */
.dash-grid--stretch > .stat-tile { justify-content: space-between; }
/* Spans of 3, 6 and 12 ONLY, all divisors of 12, so every card edge
   falls on column 1, 4, 7 or 10 and the rows line up down the page.
   There is deliberately no span-4: mixing it with span-3 put edges on
   columns 5 and 9 as well, and six different edges over eighteen cards
   reads as drift rather than as a grid. Caught by measuring the rendered
   card positions, which is the only way to see it. */
.dash-col-3 { grid-column: span 3; }
.dash-col-6 { grid-column: span 6; }
.dash-col-12 { grid-column: span 12; }

/* Delivery 92 — visible section headings inside .dash-grid (Analytics),
   grouping the cards that already existed into named categories without
   moving or removing any of them. A plain heading, not a .card: it sits
   between rows rather than being one, and the top border plus margin is
   what actually separates one group's cards from the next visually
   (the grid's own `gap` alone reads the same between a header and a
   card as it does between two cards). The first section has no border
   since there is nothing above it to separate from. */
.dash-section-title {
    grid-column: span 12;
    margin: .4rem 0 -.2rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--border);
    font-size: .95rem;
    font-weight: 700;
    letter-spacing: .02em;
    text-transform: uppercase;
    color: var(--ink-muted);
    display: flex;
    align-items: center;
    gap: .45rem;
}
.dash-section-title:first-child { margin-top: 0; padding-top: 0; border-top: none; }
.dash-section-title .ic { flex: none; }
@media (max-width: 1100px) {
    .dash-col-3 { grid-column: span 6; }
}
@media (max-width: 760px) {
    .dash-grid { grid-template-columns: 1fr; }
    .dash-col-3, .dash-col-6, .dash-col-12 { grid-column: 1 / -1; }
}

/* A chart card: title, optional note, then the plot. Fixed internal
   rhythm so a row of cards has its titles and plots on the same lines. */
.viz-card { display: flex; flex-direction: column; gap: .1rem; }
.viz-card__head { display: flex; align-items: baseline; justify-content: space-between; gap: .6rem; flex-wrap: wrap; }
.viz-card__title { margin: 0; font-family: var(--font-display); font-size: .92rem; font-weight: 600; color: var(--ink); }
.viz-card__note { font-size: .72rem; color: var(--ink-muted); }
.viz-card__body { margin-top: .55rem; }

/* A stat tile: the value leads, the delta and the sparkline support it. */
.stat-tile { display: flex; flex-direction: column; gap: .15rem; }
.stat-tile__label { font-size: .7rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--ink-muted); }
.stat-tile__value { font-family: var(--font-display); font-size: 1.55rem; font-weight: 700; line-height: 1.1; color: var(--ink); }
.stat-tile__foot { display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-top: .15rem; min-height: 34px; }
/* The second (student-side) tile row carries a short note where the
   first row carries a sparkline — "42 of 60 planned", "median 24". Right
   aligned, so it lands where the sparkline does and the two rows read as
   one grid rather than two. */
.stat-tile__note { font-size: .7rem; color: var(--ink-muted); text-align: right; line-height: 1.3; }
/* Nothing outstanding — the good state, said in colour as well as in
   words, on the one tile where zero is the answer you want. */
.stat-tile--clear .stat-tile__value { color: var(--viz-good); }
.stat-tile__spark { flex: none; width: 132px; }
/* The hero figure a dashboard leads with. */
.stat-tile--hero .stat-tile__value { font-size: 2.5rem; }

/* Compact table for the numbers beside a chart — the table view that
   makes every chart's data readable without relying on colour. */
.viz-table { width: 100%; border-collapse: collapse; font-size: .76rem; }
.viz-table th, .viz-table td { padding: .3rem .45rem; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
.viz-table th { font-size: .66rem; text-transform: uppercase; letter-spacing: .03em; color: var(--ink-muted); font-weight: 600; }
.viz-table td.num, .viz-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.viz-table tbody tr:last-child td { border-bottom: none; }
.viz-table__swatch { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: .4rem; vertical-align: baseline; }

/* ---- Delivery 33: the student-side cards ----------------------------

   A caveat under a chart. Used where a number is a real approximation
   and a reader who does not know that would draw the wrong conclusion —
   "students are counted in the month their batch starts", for one,
   because the registration tables carry no date of their own. Small, but
   full ink: a warning in muted grey is a warning nobody reads. */
.viz-note {
    margin: .55rem 0 0;
    font-size: .74rem;
    line-height: 1.45;
    color: var(--ink-muted);
    padding-left: .6rem;
    border-left: 2px solid var(--border-strong, var(--border));
}
.viz-note strong { color: var(--ink); }

/* One big number above its meter — the fill rate. Not a stat tile: it
   lives inside a chart card and is the label for the meter under it,
   so it aligns left with the meter rather than centring in a tile. */
.viz-figure { display: flex; align-items: baseline; gap: .6rem; margin-bottom: .5rem; flex-wrap: wrap; }
.viz-figure__value {
    font-family: var(--font-display);
    font-size: 2rem;
    font-weight: 700;
    line-height: 1;
    letter-spacing: -.02em;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}
.viz-figure__label { font-size: .78rem; color: var(--ink-muted); display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }

/* The demographics card holds four or five independent little tables.
   auto-fit at a 260px minimum gives 4 across on a desktop, 2 on a
   tablet and 1 on a phone, with no breakpoint of its own — and because
   each block is a table rather than a chart, none of it depends on
   colour. */
/* 320px, not 260. At 260 the grid fitted four or five blocks across a
   1180px card and each one was 262px wide — while the four-column table
   inside it needed 408px, so every block was scrolling its own table
   sideways. That is what "not visible properly" was: not the count of
   blocks in the row, the truncation inside each one.
   320px gives three across on a desktop (so five blocks read as 3 + 2,
   two rows), two on a tablet and one on a phone, with the table fitting
   in every case — measured, not assumed. */
.demo-grid {
    display: grid;
    gap: 1.1rem 1.3rem;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
    align-items: start;
}
/* A demographics table is the one place a header may wrap: these sit in
   a narrow column and "Students"/"Share" are the widest thing in them.
   Everywhere else .viz-table keeps its headers on one line. */
.demo-table th { white-space: normal; }
.demo-table td:first-child { white-space: normal; }
.demo-block { min-width: 0; }
.demo-block__title {
    margin: 0 0 .4rem;
    font-size: .8rem;
    font-weight: 700;
    color: var(--ink);
    display: flex; align-items: baseline; justify-content: space-between; gap: .5rem;
}
.demo-block__n { font-size: .68rem; font-weight: 500; color: var(--ink-muted); white-space: nowrap; }
/* These tables sit in a narrow column, so their first cell is the one
   thing allowed to wrap. */
.demo-block .viz-table td.wrap, .viz-table td.wrap { white-space: normal; }

/* Delivery 113 — the Analytics "Insights" section's auto-generated
   plain-language callouts (analyticsInsights(), modules/dashboard/
   _analytics.php). Same --brand-light/--brand-hover pairing every other
   highlighted chip in this app already uses (.badge, .radio-pill:checked,
   etc.) — --brand-light is a fixed pale tint that never darkens for a
   dark theme (see the Delivery 44 fix note above .pf-role in this file),
   so pairing it with --ink like a normal card would repeat that exact
   invisible-text bug; --brand-hover is the token already proven safe
   against it. */
.insight-notes { display: flex; flex-direction: column; gap: .55rem; }
.insight-note {
    display: flex; align-items: flex-start; gap: .55rem;
    background: var(--brand-light); color: var(--brand-hover);
    border-left: 3px solid var(--brand); border-radius: 8px;
    padding: .6rem .85rem; font-size: .87rem; line-height: 1.45;
}
.insight-note .ic { flex: none; margin-top: .15rem; }

/* The year notice a non-admin sees, and the year picker an admin gets. */
.dash-yearbar { display: flex; align-items: center; justify-content: space-between; gap: .8rem; flex-wrap: wrap; }
.dash-yearbar__note { font-size: .76rem; color: var(--ink-muted); margin: 0; }
/* An inline label on a LIGHT fill (the status warning yellow, a muted
   track) — dark ink, since white on those is less readable than nothing.
   The counterpart of .viz__inlab, which is for the dark fills. */
.viz__onlight { fill: #2b2521; font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; }

/* A heatmap label on one of the ramp's TOP steps. The ramp runs
   light->dark in the light themes and dark->light in the dark ones, so
   the top step is nearly black in one and pale blue in the other — which
   means the readable text colour flips with the theme. The server draws
   one class; these three rules resolve it. */
.viz__heatlab { fill: #fff; font-size: 10px; font-weight: 700; font-variant-numeric: tabular-nums; }
:root[data-theme="dark"] .viz__heatlab,
:root[data-theme="glass-dark"] .viz__heatlab { fill: #14100c; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]):not([data-theme="glass-light"]) .viz__heatlab { fill: #14100c; }
}

/* =====================================================================
   THE STAFF HOME PAGE (delivery 33)
   =====================================================================
   Uses the analytics dashboard's own grid (.dash-grid, spans 3/6/12) and
   card shells (.card, .viz-card, .stat-tile), so the two screens line up
   with each other and there is one set of card internals to maintain.
   Everything below is only what Home needs on top of that. */

/* ---- The welcome card ---- */
.home-hello { display: flex; flex-direction: column; gap: .7rem; }
.home-hello__who { display: flex; align-items: center; gap: .9rem; min-width: 0; }
.home-hello__avatar {
    width: 58px; height: 58px; flex: none;
    border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--brand-gradient);
    color: #fff;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: 1.15rem;
    border: 3px solid var(--glass-bg-strong);
    box-shadow: var(--shadow);
}
.home-hello__avatar.is-photo { object-fit: cover; background: var(--ink); }
.home-hello__text { min-width: 0; }
.home-hello__name {
    margin: 0;
    font-family: var(--font-display);
    font-size: clamp(1.1rem, 2vw, 1.4rem);
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -.01em;
    color: var(--ink);
}
.home-hello__role { margin: .2rem 0 0; font-size: .8rem; color: var(--ink-muted); }
/* The scope line. Every number on the page is narrowed to it, so it is
   given its own tinted strip rather than being another muted line — a
   reader who misses it misreads everything else. */
.home-hello__scope {
    margin: 0;
    display: flex; align-items: flex-start; gap: .4rem;
    font-size: .8rem;
    line-height: 1.45;
    /* Delivery 44 fix: was var(--ink), which flips light in Dark/Glass
       Dark — but this banner's own background (--brand-light) never
       darkens for those themes, so the text went near-white on pale
       peach. var(--brand-hover) is the token this file already pairs
       with --brand-light everywhere else (.badge, .kpi__icon, .pf-role,
       etc.) — this was the one place that didn't. */
    color: var(--brand-hover);
    background: var(--brand-light);
    border-radius: 10px;
    padding: .5rem .65rem;
}
.home-hello__scope svg { flex: none; color: var(--brand); margin-top: .1rem; }
/* "+ Maharashtra state-wide" — the second half of a territory holder's
   reach. Quieter than the territory name it qualifies, because the
   territory is the identity and this is the qualification. */
.scope-plus { color: var(--ink-muted); font-weight: 400; }
/* Delivery 44 fix: dashScopeSentence() (core/helpers.php) prints this
   span both here (on the fixed-pale-peach .home-hello__scope banner)
   and on the Analytics header (on the plain page background, where the
   flipping var(--ink-muted) above is correct and unchanged). Only this
   banner needs its own override, for the same reason .home-hello__scope
   itself just got one. */
.home-hello__scope .scope-plus { color: var(--brand-hover); }
.home-hello__stats {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: .5rem;
    margin-top: auto;
}
.home-mini {
    display: flex; flex-direction: column; gap: .1rem;
    padding: .5rem .3rem;
    border-radius: 10px;
    background: var(--glass-bg);
    border: 1px solid var(--border);
    text-align: center;
}
.home-mini__value {
    font-family: var(--font-display);
    font-size: 1.15rem; font-weight: 700; line-height: 1.1;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}
.home-mini__label { font-size: .68rem; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-muted); }
.home-hello__note { margin: 0; font-size: .74rem; color: var(--ink-muted); }

/* ---- Quick actions ----
   A full-width strip rather than a stacked list in a narrow card: ten
   buttons in a span-3 column made that row 500px tall. auto-fit at a
   170px minimum gives six or seven across on a desktop and two on a
   phone, with no breakpoint of its own. */
.home-actions {
    display: grid;
    gap: .45rem;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr));
}
.home-action {
    display: flex; align-items: center; gap: .5rem;
    padding: .45rem .55rem;
    border-radius: 9px;
    border: 1px solid var(--border);
    background: var(--glass-bg);
    color: var(--ink);
    font-size: .82rem;
    font-weight: 500;
    text-decoration: none;
}
.home-action:hover { border-color: var(--brand); color: var(--brand-hover); }
.home-action__icon { display: inline-flex; color: var(--brand); flex: none; }
.home-action__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- Needs attention ----
   A count, what it means, and the button that fixes it. The left border
   carries the tone; the words carry the meaning, so the colour is never
   the only channel — the same rule the chart palette follows. */
.attn-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .4rem; }
.attn {
    display: flex; align-items: center; gap: .7rem;
    padding: .5rem .6rem;
    border-radius: 10px;
    background: var(--glass-bg);
    border: 1px solid var(--border);
    border-left: 3px solid var(--viz-track);
}
.attn__count {
    flex: none;
    min-width: 2.4rem;
    font-family: var(--font-display);
    font-size: 1.25rem; font-weight: 700; line-height: 1;
    text-align: center;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
}
.attn__text { min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.attn__label { font-size: .84rem; font-weight: 600; color: var(--ink); }
.attn__detail { font-size: .72rem; line-height: 1.35; color: var(--ink-muted); }
.attn__go { flex: none; margin-left: auto; }
.attn--ok   { border-left-color: var(--viz-1); }
.attn--warn { border-left-color: var(--viz-warn); }
.attn--bad  { border-left-color: var(--viz-critical); }
.attn--bad .attn__count { color: var(--viz-critical); }
/* Cleared: kept in the list, not dropped, so "nothing outstanding" is
   something the reader can actually see rather than infer from an
   absence they cannot distinguish from a card that failed to load. */
.attn--clear { border-left-color: var(--viz-good); opacity: .72; }
.attn--clear .attn__count { color: var(--viz-good); font-size: 1.05rem; }

/* The elapsed-days cell on "Running now": the meter and its percentage
   on one line. Stacked (the default, a block <svg> followed by a
   <span>) they read as two separate things and the number floats away
   from the bar it belongs to. */
.home-elapsed { display: flex; align-items: center; gap: .45rem; }
/* An explicit width, because vizOpen() emits width="100%" on every
   chart (that is what makes the charts responsive inside their cards).
   In a flex row that made the meter eat the whole cell and push the
   percentage out of sight entirely — the bar rendered and the number
   silently did not, which a screenshot showed and the markup did not. */
.home-elapsed .viz--meter { flex: none; width: 80px; }

/* ---- The activity feed ---- */
.home-feed { list-style: none; margin: 0; padding: 0; display: grid; gap: .55rem; }
.home-feed__item { display: flex; gap: .55rem; align-items: flex-start; }
.home-feed__dot {
    flex: none;
    width: 8px; height: 8px;
    margin-top: .4rem;
    border-radius: 50%;
    background: var(--viz-1);
}
.home-feed__body { min-width: 0; }
.home-feed__what {
    font-size: .8rem; line-height: 1.35; color: var(--ink);
    /* Two lines, then an ellipsis: `batchupdates.updates` is up to 1000
       characters and a single verbose row would otherwise own the card. */
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.home-feed__meta { font-size: .71rem; color: var(--ink-muted); margin-top: .1rem; }

/* ---- My data access ---- */
.assign-chips { display: flex; flex-wrap: wrap; gap: .45rem; }
.assign-chip {
    display: inline-flex; align-items: baseline; gap: .4rem; flex-wrap: wrap;
    padding: .35rem .65rem;
    border-radius: 999px;
    background: var(--glass-bg);
    border: 1px solid var(--border);
    font-size: .76rem;
}
.assign-chip__state { font-weight: 700; color: var(--ink); }
.assign-chip__part { color: var(--ink-muted); }
.assign-chip__part--all { font-style: italic; }

@media (max-width: 599px) {
    .home-hello__stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .attn { flex-wrap: wrap; }
    .attn__go { margin-left: 0; }
}

/* =====================================================================
 * Delivery 69 — internal messaging + the topbar notification bell
 * ===================================================================== */

/* Delivery 121 — "remove the bell icon..make dia notify instead". The
   topbar notification bell (.notif, .notif-bell, .notif-badge,
   .notif-panel-prefixed rules, .notif-item-prefixed rules, previously
   here) is gone — DIA (the chatbot widget) is now the one place unread
   messages, plus to-do/calendar reminders, surface (see .jfchat__alert
   below and chatbotProactiveAlerts(), modules/chatbot/_shared.php).
   modules/messages/bell-data.php is left in place but nothing calls it
   any more.
   Delivery 110 — topbar Home button, unaffected by the above; kept its
   same size/shape.
   FOUND WHILE FIXING DELIVERY 152 — this comment's own previous wording
   used to list ".notif-panel" and ".notif-item" back to back with a
   star-then-slash landing right between them, which is the two
   characters that end a CSS comment. Written out like that, in the
   middle of a sentence, it closed this comment seven lines early by
   accident. Everything after that point, up to the point where this
   comment actually intended to end (garbled mid-sentence text), was
   therefore parsed as CSS, not a comment — an invalid, silently-dropped
   "rule" whose body swallowed the very next real rule below
   (.topbar__icon-btn), which is exactly why every earlier delivery's
   changes to that rule (Home button background/visibility) kept
   verifying correctly in isolation but never actually rendered live.
   Reworded above so this comment no longer spells out that same
   star-slash pair. */
.topbar__icon-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 38px; height: 38px; border-radius: 10px;
    border: none;
    /* "make the home button background as attached image background
       which will change as per the theme" — the attached image is this
       app's own .btn-primary "+" tile (Notes/Quick Links, see
       templates/partials/home_widgets.php), so this is that exact
       background/colour/shadow, not a lookalike — same --brand-gradient
       variable, so it already follows whichever theme/accent colour is
       active, the same way every other .btn-primary on the site does. */
    background: var(--brand-gradient); color: #fff; text-decoration: none; flex: none;
    box-shadow: 0 8px 20px rgba(234, 88, 12, 0.28);
}
.topbar__icon-btn:hover { filter: brightness(1.05); text-decoration: none; }
/* "make the home button proper visible" — this used to disappear below
   600px (the rule that hid it stood right here), the one topbar icon
   that vanished on a phone instead of collapsing to icon-only like the
   avatar and Log out chips already do. It's already icon-only by design
   at every width, so nothing else here needs to shrink for it to stay
   visible on a phone screen. */

/* Delivery 143 — "add one filter and search option in message page..to
   search and filter messages." Shared between Inbox (padding-less card,
   so it gets its own top/side padding here) and Sent (the .table
   already has padding — that page sets its own inline padding on this
   element instead, see templates/pages/messages/sent.php). */
.msg-filter-bar {
    display: flex;
    flex-wrap: wrap;
    gap: .6rem;
    padding: 1rem 1.1rem;
    border-bottom: 1px solid var(--border);
}
.msg-filter-bar [data-msg-search] { flex: 1 1 220px; min-width: 0; }
.msg-filter-bar [data-msg-read-filter] { flex: 0 1 170px; min-width: 140px; }

/* ---- Inbox list ---- */
.msg-list-card { padding: 0; }
.msg-list { display: flex; flex-direction: column; }
.msg-row {
    display: flex; align-items: flex-start; gap: .7rem;
    padding: .85rem 1.1rem; text-decoration: none; color: inherit;
    border-bottom: 1px solid var(--border);
}
.msg-row:last-child { border-bottom: none; }
.msg-row:hover { background: var(--surface-2); text-decoration: none; }
/* Delivery 158 — found while live-verifying the DIA/Messages tabs' search
   filter: .msg-row's own `display: flex` above has the same specificity as
   the browser's default `[hidden]{display:none}`, so it always wins and
   the search/unread filter's `row.hidden = true` (assets/js/app.js,
   initMessageFilters()) never actually hid a filtered-out row — same
   pattern already handled explicitly elsewhere in this file (e.g.
   .msg-modal[hidden], .jfchat__panel[hidden]) but missed here. */
.msg-row[hidden] { display: none; }
.msg-row__dot {
    flex: none; width: 8px; height: 8px; border-radius: 50%; margin-top: .5rem;
    background: transparent;
}
.msg-row--unread .msg-row__dot { background: var(--brand); }
.msg-row--unread .msg-row__sender, .msg-row--unread .msg-row__subject { font-weight: 700; }
.msg-row__main { min-width: 0; flex: 1 1 auto; }
.msg-row__top { display: flex; align-items: baseline; justify-content: space-between; gap: .6rem; }
.msg-row__sender { font-size: .88rem; color: var(--ink); }
.msg-row__time { font-size: .74rem; color: var(--ink-muted); flex: none; }
.msg-row__subject { font-size: .84rem; margin-top: .12rem; }
.msg-row__excerpt {
    font-size: .78rem; color: var(--ink-muted); margin-top: .12rem;
    display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;
}
.msg-row__att {
    flex: none; display: inline-flex; align-items: center; gap: .25rem;
    font-size: .74rem; color: var(--ink-muted); margin-top: .15rem;
}

/* Delivery 158 — "make a different section for DIA automated messages
   and other messages" (Inbox) → "make it two different tabs". Reuses the
   app's existing .tabs/.tab/.tab-panel pattern (see the batch roster tabs
   for the shared rules) inside one .msg-list-card. The shared .tab-panel
   rule pads its content (1.5rem) for the batch-roster's use — here the
   panel holds a .msg-list of .msg-row rows that already carry their own
   .85rem/1.1rem padding, so that shared padding is zeroed just for this
   card to keep rows flush with the card edges exactly as before tabs. */
.msg-list-card .tab-panel { padding: 0; }
.msg-tab-count {
    display: inline-flex; align-items: center; justify-content: center;
    margin-left: .35rem; font-weight: 600;
    background: var(--surface); border: 1px solid var(--border); border-radius: 999px;
    padding: .05rem .5rem; font-size: .72rem; color: var(--ink-muted);
}
.tab.is-active .msg-tab-count { color: var(--brand); }

/* ---- One message ---- */
.msg-view__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
.msg-view__subject { margin: 0 0 .3rem; font-size: 1.2rem; }
.msg-view__meta { font-size: .82rem; color: var(--ink-muted); display: flex; flex-wrap: wrap; gap: .4rem; }
.msg-view__meta-sep { opacity: .6; }
.msg-view__body { font-size: .95rem; line-height: 1.6; white-space: pre-line; }
.msg-view__attachments { display: flex; flex-wrap: wrap; gap: .7rem; margin-top: 1.2rem; padding-top: 1rem; border-top: 1px solid var(--border); }
.msg-att--image {
    display: block; width: 140px; height: 140px; border-radius: 10px; overflow: hidden;
    border: 1px solid var(--border);
}
.msg-att--image img { width: 100%; height: 100%; object-fit: cover; display: block; }
.msg-att--video {
    display: inline-flex; align-items: center; gap: .4rem;
    padding: .55rem .9rem; border-radius: 10px; border: 1px solid var(--border);
    background: var(--glass-bg); font-size: .84rem; font-weight: 600; color: var(--brand);
    text-decoration: none; height: 140px; width: 140px; justify-content: center; text-align: center;
}
.msg-att--video:hover { background: var(--brand-light); text-decoration: none; }

/* ---- Compose: mode tabs ---- */
.msg-mode-tabs { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1rem; }
.msg-mode-tab {
    display: inline-flex; align-items: center; gap: .4rem;
    padding: .45rem .8rem; border-radius: 999px; border: 1px solid var(--border);
    background: var(--glass-bg); font-size: .84rem; cursor: pointer;
}
.msg-mode-tab input { accent-color: var(--brand); }
.msg-mode-tab.is-active { border-color: var(--brand); background: var(--brand-light); color: var(--brand-hover); font-weight: 600; }

/* ---- Compose: the individual add/search picker ---- */
.msg-picker { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: .8rem; }
.msg-picker__type { flex: 0 0 auto; width: auto; min-width: 9.5rem; }
.msg-picker__search-wrap { position: relative; flex: 1 1 240px; min-width: 200px; }
.msg-picker__results {
    position: absolute; left: 0; right: 0; top: calc(100% + .3rem); z-index: 40;
    max-height: 260px; overflow-y: auto;
    background: var(--bg-solid); border: 1px solid var(--border); border-radius: 10px;
    box-shadow: 0 14px 34px rgba(15, 23, 42, .22);
}
/* Tabular row layout — name and detail sit in their own aligned column
   instead of stacking, and the type sits far right, so a long results
   list reads like a table rather than a wall of names. */
.msg-picker__result {
    display: flex; flex-direction: row; align-items: baseline; gap: .6rem;
    width: 100%; text-align: left;
    padding: .4rem .7rem; border: 0; border-bottom: 1px solid var(--border);
    background: transparent; color: inherit; cursor: pointer; font: inherit;
}
.msg-picker__result:nth-child(even) { background: var(--surface-2); }
.msg-picker__result:last-child { border-bottom: none; }
.msg-picker__result:hover, .msg-picker__result:focus { background: var(--brand-light); }
.msg-picker__result-name {
    font-size: .8rem; font-weight: 600; flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.msg-picker__result-sub {
    font-size: .68rem; color: var(--ink-muted); flex: 0 0 auto;
    max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    text-align: right;
}
.msg-picker__result-empty { padding: .7rem; font-size: .82rem; color: var(--ink-muted); }
.msg-picker__chips { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .6rem; }
.msg-chip {
    display: inline-flex; align-items: center; gap: .4rem;
    padding: .35rem .5rem .35rem .7rem; border-radius: 999px;
    background: var(--brand-light); color: var(--brand-hover); font-size: .8rem; font-weight: 600;
}
.msg-chip__type { opacity: .75; font-weight: 500; }
.msg-chip__remove {
    display: inline-flex; align-items: center; justify-content: center;
    width: 18px; height: 18px; border-radius: 50%; border: 0; cursor: pointer;
    background: rgba(0,0,0,.08); color: inherit; font-size: .8rem; line-height: 1;
}
.msg-chip__remove:hover { background: rgba(0,0,0,.18); }

/* ---- Compose: bulk broadcast's 4 tabs (Delivery 74) — each tab's
   own "All ..." checkbox sits above its optional filter groups. ---- */
.msg-bulk-all {
    background: var(--surface-2); border-radius: 10px; padding: .6rem .8rem;
    margin-bottom: .9rem; font-weight: 600;
}
.msg-bulk-all .form-hint { font-weight: 400; }
.msg-bulk-filters { transition: opacity .15s ease; }
.msg-bulk-filters--disabled { opacity: .45; pointer-events: none; }

/* ---- Compose: staff "browse and tick" fallback ---- */
.msg-browse { margin-top: .7rem; border: 1px solid var(--border); border-radius: 10px; padding: .6rem .8rem; }
.msg-browse summary { cursor: pointer; font-size: .84rem; font-weight: 600; color: var(--brand); }
.msg-browse__group { margin-top: .7rem; }
.msg-browse__label { font-size: .78rem; font-weight: 700; color: var(--ink-muted); text-transform: uppercase; letter-spacing: .03em; margin-bottom: .4rem; }
.msg-browse__count { color: var(--ink-muted); font-weight: 400; }

/* Staff / College SPOC "tick from the full list" — a real two-column
   table (Name, Role/College) instead of a wrapped grid of checkboxes, so
   a long list of names stays scannable. Scrolls internally with a
   sticky header once it grows past a few screens' worth of rows. */
.msg-table-wrap {
    max-height: 260px; overflow-y: auto;
    border: 1px solid var(--border); border-radius: 10px;
}
.msg-table { width: 100%; border-collapse: collapse; font-size: .78rem; }
.msg-table thead th {
    position: sticky; top: 0; z-index: 1;
    text-align: left; padding: .45rem .7rem;
    background: var(--surface-2); color: var(--ink-muted);
    font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
    border-bottom: 1px solid var(--border);
}
.msg-table tbody tr:nth-child(even) { background: var(--surface-2); }
.msg-table tbody tr:hover { background: var(--brand-light); }
.msg-table tbody tr { cursor: pointer; } /* the whole row toggles its checkbox — see app.js */
.msg-table td { padding: .4rem .7rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
.msg-table tbody tr:last-child td { border-bottom: none; }
.msg-table__checkcol { width: 2.1rem; text-align: center; }
.msg-table__checkcol input { cursor: pointer; }
.msg-table__name { font-weight: 600; }
.msg-table__sub { color: var(--ink-muted); white-space: nowrap; }

/* =====================================================================
 * Delivery 72 — the "browse and pick" popup (Staff/College recipients,
 * and the Student bulk filter's batch picker): a centred modal dialog
 * with its own search box, quick-filter dropdowns and Select All/Clear
 * All, instead of an inline list competing for space on the page.
 * ===================================================================== */
.msg-modal {
    position: fixed; top: 0; left: 0; right: 0; bottom: 0;
    width: 100vw; height: 100vh; z-index: 200;
    display: flex; justify-content: center; padding: 4vh 1rem; overflow-y: auto;
    box-sizing: border-box;
}
.msg-modal[hidden] { display: none; }
.msg-modal__backdrop { position: fixed; inset: 0; background: rgba(15, 23, 42, .5); }
.msg-modal__dialog {
    position: relative; z-index: 1; align-self: flex-start;
    width: min(94vw, 760px); max-height: 100%;
    display: flex; flex-direction: column;
    background: var(--bg-solid); color: var(--ink);
    border: 1px solid var(--border); border-radius: 14px;
    box-shadow: 0 24px 60px rgba(15, 23, 42, .35);
    overflow: hidden;
}
.msg-modal__head {
    display: flex; align-items: center; justify-content: space-between;
    padding: .9rem 1.1rem; border-bottom: 1px solid var(--border); font-size: .95rem;
}
.msg-modal__x {
    border: 0; background: transparent; cursor: pointer; font-size: 1.3rem;
    line-height: 1; color: var(--ink-muted); padding: .1rem .5rem; border-radius: 6px;
}
.msg-modal__x:hover { background: var(--surface-2); color: var(--ink); }
.msg-modal .msg-mode-tabs { margin: .8rem 1.1rem 0; }
.msg-modal__panel { padding: .9rem 1.1rem; overflow-y: auto; }
.msg-modal__toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: .7rem; }
.msg-modal__toolbar input[type="text"] { flex: 1 1 200px; min-width: 160px; }
.msg-modal__toolbar select { flex: 0 1 9.5rem; min-width: 8rem; }
.msg-modal__toolbar .btn-row { flex: 0 0 auto; margin-left: auto; }
.msg-modal__foot {
    display: flex; align-items: center; justify-content: space-between; gap: .8rem;
    padding: .8rem 1.1rem; border-top: 1px solid var(--border);
}
.msg-modal__foot .form-hint { margin: 0; }
.msg-modal__selcount { font-size: .76rem; font-weight: 600; color: var(--brand-hover); margin-left: .3rem; }
body.msg-modal-open { overflow: hidden; }

@media (max-width: 599px) {
    .msg-modal { padding: 0; }
    .msg-modal__dialog { width: 100%; max-width: 100%; height: 100%; max-height: 100%; border-radius: 0; }
    .msg-modal__toolbar .btn-row { margin-left: 0; width: 100%; }
}

/* ---- Compose: video link rows ---- */
.msg-link-rows { display: flex; flex-direction: column; gap: .5rem; }
.msg-link-row { display: flex; gap: .5rem; align-items: center; }
.msg-link-row .form-control { flex: 1 1 auto; }
.msg-link-row button { flex: none; }

@media (max-width: 599px) {
    .msg-picker { flex-direction: column; }
    .msg-picker__type { width: 100%; }
    .msg-att--image, .msg-att--video { width: 110px; height: 110px; }
}

/* =====================================================================
   Delivery 75 — Portal Assistant floating chatbot widget
   =====================================================================
   Deliberately placed as a sibling of .topbar/.main's <main> in the
   markup (templates/layout/app.php), never nested inside a .card — this
   app's Glass theme puts backdrop-filter on every .card AND on .topbar
   itself, and (see the long comment above .ss__panel elsewhere in this
   file, and the Delivery 72 messaging popups) any backdrop-filter makes
   its element the containing block for a position:fixed descendant
   instead of the viewport. Kept out of both, this always centres/anchors
   against the real viewport regardless of theme. */
.jfchat { position: fixed; right: 1.1rem; bottom: 1.1rem; z-index: 1200; }
.jfchat__launcher {
    display: inline-flex; align-items: center; justify-content: center;
    width: 58px; height: 58px; padding: 0; border-radius: 50%; border: none; cursor: pointer;
    background: var(--brand-gradient); color: #fff;
    box-shadow: 0 10px 26px rgba(0, 0, 0, .22);
    /* Delivery 162 — "make dia icon and floating..user can move any
       place". touch-action: none stops a touch-drag from also scrolling
       the page underneath (assets/js/app.js's drag handling calls
       preventDefault() on pointerdown too, but that alone isn't reliable
       on every touch browser without this); user-select: none stops a
       fast drag from selecting page text around the icon the way a plain
       click-drag normally would. */
    touch-action: none; user-select: none;
}
.jfchat__launcher:hover { filter: brightness(1.06); }
/* Same dragging-cursor convention as the floating clock/to-do widget
   (.jfclock.is-dragging / .jftodo.is-dragging elsewhere in this file). */
.jfchat.is-dragging .jfchat__launcher { cursor: grabbing; }

/* Delivery 143 — "add one floating button at the right bottom near the
   chatbot icon..which when clicked will scroll page to the top."
   Stacked directly above .jfchat's own launcher (58px, right/bottom
   1.1rem — see just above), same right edge, with a fixed gap between
   them via calc() so the two never overlap regardless of viewport
   height. Hidden by default (the `hidden` attribute in the markup);
   assets/js/app.js's initScrollTopButton() toggles it. */
.scroll-top-btn {
    position: fixed;
    right: 1.1rem;
    bottom: calc(1.1rem + 58px + .6rem);
    z-index: 1190;
    width: 46px;
    height: 46px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--glass-bg-strong);
    color: var(--ink-muted);
    cursor: pointer;
    box-shadow: 0 8px 20px rgba(0, 0, 0, .16);
    transition: opacity .2s, transform .2s, color .2s, background .2s;
}
.scroll-top-btn:hover { color: var(--brand); background: var(--brand-light); transform: translateY(-2px); }
.scroll-top-btn[hidden] { display: none; }
@media (max-width: 640px) {
    .scroll-top-btn {
        right: .7rem;
        bottom: calc(.7rem + 52px + .5rem);
        width: 40px;
        height: 40px;
    }
}

/* Delivery 79 — "remove the help word and keep the face only..with lots
   of animation..should look its a living creature". The text label is
   gone from the markup entirely, so the launcher is now a plain round
   icon button sized and centred around the face alone, and everything
   below layers several small, slow, out-of-sync animations onto it:
   breathing (the whole button), blinking + a side-glance (eyes), a soft
   mouth wobble, an occasional curious head-tilt, a periodic jump with a
   squash-and-stretch landing, and a gentle pulsing glow. Each one is slow
   with a long hold between repeats — something idling and alive at rest,
   not a distraction — and every one pauses on hover/focus so it never
   fights with someone actually about to click it. prefers-reduced-motion
   turns all of them off outright. The panel header's own small face
   (.jfchat__face--static) is deliberately excluded from all of this,
   same as before this delivery. */
.jfchat__face { color: #fff; flex: none; }
.jfchat__face--static { animation: none !important; }
.jfchat__face--static .jfchat__eye { animation: none !important; }
.jfchat__eye { fill: currentColor; transform-origin: center; transform-box: fill-box; }
.jfchat__mouth { stroke: currentColor; transform-origin: center; transform-box: fill-box; }
/* Delivery 130 — the 'girl' face's two extra pieces (chatbotFaceInnerSvg(),
   core/helpers.php): a bow (two triangles + a small knot) and a short set
   of lashes on each eye, both in currentColor same as the rest of the
   face so they stay legible in every theme and on any launcher colour. */
.jfchat__bow polygon, .jfchat__bow circle { fill: currentColor; }
.jfchat__lash path { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; }

.jfchat__launcher .jfchat__eye { animation: jfchat-blink 4.2s ease-in-out infinite; }
.jfchat__launcher .jfchat__eye--r { animation-delay: .08s; }
.jfchat__launcher .jfchat__mouth { animation: jfchat-mouth 3.6s ease-in-out infinite; }
.jfchat__launcher .jfchat__face { animation: jfchat-tilt 6.4s ease-in-out infinite; }
.jfchat__launcher {
    animation: jfchat-breathe 3.6s ease-in-out infinite, jfchat-jump 5.6s ease-in-out infinite, jfchat-glow 3.6s ease-in-out infinite;
}
.jfchat__launcher:hover, .jfchat__launcher:focus-visible,
/* Delivery 162 — same pause-while-hovered rule, extended to "while being
   dragged": :hover alone can flicker on/off mid-drag if the pointer
   briefly outruns the icon, since pointer capture keeps the DRAG going
   without necessarily keeping the mouse visually over the button. */
.jfchat.is-dragging .jfchat__launcher {
    animation-play-state: paused;
}
.jfchat__launcher:hover .jfchat__eye, .jfchat__launcher:hover .jfchat__mouth, .jfchat__launcher:hover .jfchat__face,
.jfchat__launcher:focus-visible .jfchat__eye, .jfchat__launcher:focus-visible .jfchat__mouth, .jfchat__launcher:focus-visible .jfchat__face,
.jfchat.is-dragging .jfchat__launcher .jfchat__eye, .jfchat.is-dragging .jfchat__launcher .jfchat__mouth, .jfchat.is-dragging .jfchat__launcher .jfchat__face {
    animation-play-state: paused;
}
@keyframes jfchat-blink {
    0%, 88%, 100% { transform: translateX(0) scaleY(1); }
    /* A little glance to the side... */
    40% { transform: translateX(1.2px) scaleY(1); }
    46% { transform: translateX(-1.2px) scaleY(1); }
    52% { transform: translateX(0) scaleY(1); }
    /* ...then a blink. */
    92% { transform: scaleY(.15); }
    96% { transform: scaleY(1); }
}
@keyframes jfchat-mouth {
    /* A gentle, slow "chatter" — nothing like actual speech, just enough
       wobble on the smile that the face doesn't look frozen. */
    0%, 60%, 100% { transform: scaleX(1) scaleY(1); }
    75% { transform: scaleX(1.05) scaleY(.85); }
    88% { transform: scaleX(.95) scaleY(1.08); }
}
@keyframes jfchat-tilt {
    /* An occasional curious head-tilt, held for the vast majority of the
       cycle at rest. */
    0%, 82%, 100% { transform: rotate(0deg); }
    87% { transform: rotate(-4deg); }
    93% { transform: rotate(3deg); }
    97% { transform: rotate(0deg); }
}
@keyframes jfchat-breathe {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.045); }
}
@keyframes jfchat-jump {
    /* Same timing as before, with a squash-and-stretch at take-off/landing
       (rather than a plain vertical hop) so it reads as a springy little
       creature rather than a UI element sliding up and down. */
    0%, 80%, 100% { transform: translateY(0) scale(1, 1); }
    84% { transform: translateY(-9px) scale(.94, 1.08); }
    88% { transform: translateY(0) scale(1.08, .92); }
    91% { transform: translateY(-3px) scale(.98, 1.03); }
    94% { transform: translateY(0) scale(1, 1); }
}
@keyframes jfchat-glow {
    0%, 100% { box-shadow: 0 10px 26px rgba(0, 0, 0, .22), 0 0 0 0 rgba(255, 255, 255, 0); }
    50% { box-shadow: 0 10px 26px rgba(0, 0, 0, .22), 0 0 0 8px rgba(255, 255, 255, .12); }
}
@media (prefers-reduced-motion: reduce) {
    .jfchat__launcher,
    .jfchat__launcher .jfchat__eye,
    .jfchat__launcher .jfchat__mouth,
    .jfchat__launcher .jfchat__face {
        animation: none !important;
    }
}
@media (max-width: 480px) {
    .jfchat { right: .7rem; bottom: .7rem; }
    .jfchat__launcher { width: 52px; height: 52px; }
}

/* ===================================================================
   Delivery 116 — "add waving hand and other hand gestures to the
   chatbox icon...it should be lively...every 5-10 random seconds...it
   should show some hand gestures...positive ones". A small round badge
   that pops up beside the launcher and back down again; assets/js/app.js's
   initChatGestures() decides WHICH gesture (writes its markup into
   #jfChatGestureIcon) and WHEN (a random 5-10s timer, paused whenever the
   panel is open or the tab isn't visible) — everything below only
   decides how the pop-in/out and each gesture's own movement look.

   Delivery 119 — "add real hands rather than emojis...means moving hands
   and making different gestures". Each gesture is now several small
   pieces (a thumb, individual fingers, or a whole hand — see the
   .jfg-* class names JS gives them) instead of one static shape, so each
   piece below gets its own small animation: a thumb that flicks up from
   a fist, two fingers that spread apart into a peace sign, a wave's
   fingers fluttering on top of the wrist's own rotate, two hands that
   actually clap together and apart. transform-box: fill-box makes each
   piece rotate/slide around its OWN shape's centre — the SVG default
   would pivot everything around the whole 24x24 icon's corner instead,
   which is wrong for a finger that should hinge at its own base. */
.jfchat__gesture {
    position: absolute; top: -4px; right: -2px; z-index: 2;
    width: 34px; height: 34px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    background: var(--bg-solid, #fff);
    box-shadow: 0 6px 16px rgba(0, 0, 0, .22), 0 0 0 2px var(--brand-light, #ede9fe);
    opacity: 0; transform: scale(.4); pointer-events: none;
}
.jfchat__gesture.is-active { animation: jfchat-gesture-pop 1.5s ease-in-out; }
@keyframes jfchat-gesture-pop {
    0% { opacity: 0; transform: scale(.3) translateY(6px); }
    16% { opacity: 1; transform: scale(1.15) translateY(0); }
    26% { transform: scale(1) translateY(0); }
    84% { opacity: 1; transform: scale(1) translateY(0); }
    100% { opacity: 0; transform: scale(.5) translateY(4px); }
}

/* ---- Wave: the wrist rotate (on the icon itself, same as before) plus
   each finger flickering a few degrees on its own, alternating left and
   right so they read as four separate fingers moving, not one paddle. ---- */
.jfchat__gesture--wave.is-active .jfchat__gesture-icon {
    animation: jfchat-gesture-wave 1.5s ease-in-out;
    transform-box: fill-box; transform-origin: 65% 90%;
}
@keyframes jfchat-gesture-wave {
    0%, 22% { transform: rotate(0deg); }
    36% { transform: rotate(-22deg); }
    48% { transform: rotate(18deg); }
    60% { transform: rotate(-16deg); }
    72% { transform: rotate(10deg); }
    84%, 100% { transform: rotate(0deg); }
}
.jfchat__gesture--wave.is-active .jfg-f1,
.jfchat__gesture--wave.is-active .jfg-f3 {
    transform-box: fill-box; transform-origin: 50% 100%;
    animation: jfchat-gesture-finger-a .5s ease-in-out 3;
}
.jfchat__gesture--wave.is-active .jfg-f2,
.jfchat__gesture--wave.is-active .jfg-f4 {
    transform-box: fill-box; transform-origin: 50% 100%;
    animation: jfchat-gesture-finger-b .5s ease-in-out 3;
}
.jfchat__gesture--wave.is-active .jfg-f2 { animation-delay: .08s; }
.jfchat__gesture--wave.is-active .jfg-f3 { animation-delay: .16s; }
.jfchat__gesture--wave.is-active .jfg-f4 { animation-delay: .24s; }
@keyframes jfchat-gesture-finger-a { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(10deg); } }
@keyframes jfchat-gesture-finger-b { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(-10deg); } }

/* ---- Thumbs up: the thumb starts folded down against the fist and
   flicks up into place with a little bounce — a real "thumbs up"
   happening, not a thumb that was simply always up. ---- */
.jfchat__gesture--thumbsup.is-active .jfg-thumb {
    transform-box: fill-box; transform-origin: 50% 100%;
    animation: jfchat-gesture-thumb-flick 1.5s cubic-bezier(.34, 1.56, .64, 1);
}
@keyframes jfchat-gesture-thumb-flick {
    0% { transform: rotate(66deg); }
    60% { transform: rotate(-8deg); }
    78% { transform: rotate(2deg); }
    100% { transform: rotate(0deg); }
}

/* ---- Peace: the two fingers start together (straight up) and spread
   apart into the final V shape. ---- */
.jfchat__gesture--peace.is-active .jfg-f1 {
    transform-box: fill-box; transform-origin: 50% 100%;
    animation: jfchat-gesture-peace-l 1.5s ease-out;
}
.jfchat__gesture--peace.is-active .jfg-f2 {
    transform-box: fill-box; transform-origin: 50% 100%;
    animation: jfchat-gesture-peace-r 1.5s ease-out;
}
@keyframes jfchat-gesture-peace-l { 0%, 15% { transform: rotate(9deg); } 100% { transform: rotate(0deg); } }
@keyframes jfchat-gesture-peace-r { 0%, 15% { transform: rotate(-9deg); } 100% { transform: rotate(0deg); } }

/* ---- Clap: the two whole hands actually slide together and apart a
   couple of times, instead of one merged shape being squeezed. ---- */
.jfchat__gesture--clap.is-active .jfg-clap-left { animation: jfchat-gesture-clap-l 1.5s ease-in-out; }
.jfchat__gesture--clap.is-active .jfg-clap-right { animation: jfchat-gesture-clap-r 1.5s ease-in-out; }
@keyframes jfchat-gesture-clap-l {
    0%, 100% { transform: translateX(0); }
    20%, 56% { transform: translateX(3px); }
    38%, 74% { transform: translateX(0); }
}
@keyframes jfchat-gesture-clap-r {
    0%, 100% { transform: translateX(0); }
    20%, 56% { transform: translateX(-3px); }
    38%, 74% { transform: translateX(0); }
}

@media (prefers-reduced-motion: reduce) {
    .jfchat__gesture { display: none !important; }
}
@media (max-width: 480px) {
    .jfchat__gesture { width: 28px; height: 28px; top: -2px; right: -1px; }
    .jfchat__gesture-icon { width: 18px; height: 18px; }
}

/* ---- Hover "talking bubble" — "if mouse is hover....then it should
   show a talking bubble...greeting the user and telling the user that
   she is ready to help him". Pure CSS: shown on :hover/:focus-visible of
   the launcher immediately before it in the markup, hidden again the
   moment the panel is actually open (aria-expanded="true") since the
   real conversation on screen already says all of this. ---- */
.jfchat__hint {
    position: absolute; right: 0; bottom: 70px; z-index: 2;
    /* An explicit width, not just max-width: .jfchat (this element's
       containing block) is only as wide as the 58px launcher button
       itself — everything else inside it is position:absolute/fixed and
       out of flow — so with width left to shrink-to-fit, the browser sized
       this bubble against that tiny 58px box instead of its own text,
       which is what was actually wrapping it to one word per line. */
    width: 220px; max-width: calc(100vw - 2.5rem);
    background: var(--bg-solid, #fff); color: var(--ink); border: 1px solid var(--border);
    border-radius: 14px; border-bottom-right-radius: 4px; padding: .65rem .85rem;
    font-size: .82rem; line-height: 1.4; box-shadow: 0 10px 24px rgba(0, 0, 0, .2);
    opacity: 0; visibility: hidden; transform: translateY(6px);
    transition: opacity .18s ease, transform .18s ease;
    pointer-events: none;
}
.jfchat__launcher:hover + .jfchat__gesture + .jfchat__hint,
.jfchat__launcher:focus-visible + .jfchat__gesture + .jfchat__hint {
    opacity: 1; visibility: visible; transform: translateY(0);
}
.jfchat__launcher[aria-expanded="true"] + .jfchat__gesture + .jfchat__hint {
    display: none !important;
}

/* Delivery 121 — "the chatbox should tell the user...in a talking bubble
   with proper link to visit that place". A second speech bubble, shown
   and hidden by JS (assets/js/app.js's initDiaProactiveAlerts()) rather
   than CSS :hover like .jfchat__hint above — this one pops up on its own
   when DIA has something to say: an overdue/due-today to-do, a calendar
   event today/tomorrow, or a new message (see chatbotProactiveAlerts(),
   modules/chatbot/_shared.php — this is also what replaced the old
   topbar notification bell). Same bubble shape as .jfchat__hint, just
   toggled via [hidden] instead of :hover/:focus-visible, and with its own
   close button + a real link rather than plain status text. */
.jfchat__alert {
    position: absolute; right: 0; bottom: 70px; z-index: 3;
    width: 240px; max-width: calc(100vw - 2.5rem);
    background: var(--bg-solid, #fff); color: var(--ink); border: 1px solid var(--border);
    border-radius: 14px; border-bottom-right-radius: 4px; padding: .7rem 1.7rem .75rem .9rem;
    font-size: .82rem; line-height: 1.4; box-shadow: 0 10px 28px rgba(0, 0, 0, .25);
    animation: jfchat-alert-pop .3s ease;
}
.jfchat__alert[hidden] { display: none; }
.jfchat__alert-close {
    position: absolute; top: .35rem; right: .4rem; width: 20px; height: 20px;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; background: transparent; color: var(--ink-muted); cursor: pointer;
    border-radius: 50%; padding: 0;
}
.jfchat__alert-close:hover { background: var(--surface-2); color: var(--ink); }
.jfchat__alert-text { margin: 0 0 .45rem; }
.jfchat__alert-link {
    display: inline-flex; align-items: center; gap: .3rem;
    font-size: .78rem; font-weight: 700; color: var(--brand);
}
.jfchat__alert-link:hover { text-decoration: underline; }
@keyframes jfchat-alert-pop {
    0% { opacity: 0; transform: translateY(10px) scale(.94); }
    100% { opacity: 1; transform: translateY(0) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
    .jfchat__alert { animation: none; }
}
@media (max-width: 480px) {
    .jfchat__alert { width: 200px; bottom: 64px; }
}

.jfchat__panel {
    position: fixed; right: 1.1rem; bottom: 5rem; z-index: 1200;
    width: min(92vw, 360px); height: min(70vh, 560px);
    display: flex; flex-direction: column;
    background: var(--bg-solid); color: var(--ink);
    border: 1px solid var(--border); border-radius: 16px;
    box-shadow: 0 20px 50px rgba(0, 0, 0, .3);
}
/* Same equal-specificity gotcha the notification panel already documents
   (.notif-panel[hidden] above) — the base rule sets `display: flex`
   unconditionally, so it must be beaten explicitly rather than left to
   the browser's own [hidden] default. */
.jfchat__panel[hidden] { display: none; }
@media (max-width: 480px) {
    .jfchat__panel { right: .6rem; left: .6rem; bottom: 4.6rem; width: auto; height: min(72vh, 560px); }
}

.jfchat__head {
    display: flex; align-items: center; justify-content: space-between;
    padding: .75rem .9rem; border-bottom: 1px solid var(--border);
    border-radius: 16px 16px 0 0; background: var(--surface-2);
}
.jfchat__head-title { display: flex; align-items: center; gap: .55rem; color: var(--brand); }
.jfchat__head-title > div { display: flex; flex-direction: column; line-height: 1.25; color: var(--ink); }
.jfchat__head-sub { font-size: .7rem; color: var(--ink-muted); font-weight: 400; }
.jfchat__head-actions { display: flex; align-items: center; gap: .3rem; }
.jfchat__textbtn {
    border: 1px solid var(--border); background: transparent; color: var(--ink-muted);
    border-radius: 8px; padding: .3rem .55rem; font-size: .72rem; cursor: pointer;
}
.jfchat__textbtn:hover { color: var(--ink); border-color: var(--brand); }
.jfchat__iconbtn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; border-radius: 8px; border: 1px solid transparent;
    background: transparent; color: var(--ink-muted); cursor: pointer;
}
.jfchat__iconbtn:hover { color: var(--ink); border-color: var(--border); }

.jfchat__body {
    flex: 1 1 auto; overflow-y: auto; padding: .8rem .7rem;
    display: flex; flex-direction: column; gap: .55rem;
}
.jfchat__msg { display: flex; flex-direction: column; max-width: 88%; }
.jfchat__msg--bot { align-self: flex-start; }
.jfchat__msg--user { align-self: flex-end; align-items: flex-end; }
.jfchat__bubble {
    padding: .55rem .75rem; border-radius: 12px; font-size: .84rem; line-height: 1.4;
    white-space: pre-wrap; overflow-wrap: anywhere;
}
.jfchat__msg--bot .jfchat__bubble { background: var(--surface-2); border-bottom-left-radius: 4px; }
.jfchat__msg--user .jfchat__bubble { background: var(--brand); color: #fff; border-bottom-right-radius: 4px; }
.jfchat__links { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .35rem; }
.jfchat__link {
    display: inline-flex; align-items: center; font-size: .76rem; font-weight: 600;
    padding: .3rem .6rem; border-radius: 8px; border: 1px solid var(--brand);
    color: var(--brand); text-decoration: none; background: transparent;
}
.jfchat__link:hover { background: var(--brand); color: #fff; }
.jfchat__typing { align-self: flex-start; display: flex; gap: .25rem; padding: .55rem .75rem; background: var(--surface-2); border-radius: 12px; border-bottom-left-radius: 4px; }
.jfchat__typing span {
    width: 6px; height: 6px; border-radius: 50%; background: var(--ink-muted);
    animation: jfchat-bounce 1.1s infinite ease-in-out;
}
.jfchat__typing span:nth-child(2) { animation-delay: .15s; }
.jfchat__typing span:nth-child(3) { animation-delay: .3s; }
@keyframes jfchat-bounce { 0%, 60%, 100% { transform: translateY(0); opacity: .5; } 30% { transform: translateY(-4px); opacity: 1; } }

.jfchat__quickreplies { display: flex; flex-wrap: wrap; gap: .4rem; align-self: flex-start; max-width: 100%; }
.jfchat__chip {
    border: 1px solid var(--brand); background: transparent; color: var(--brand);
    border-radius: 999px; padding: .32rem .7rem; font-size: .74rem; font-weight: 600; cursor: pointer;
}
.jfchat__chip:hover { background: var(--brand); color: #fff; }
.jfchat__chip:disabled { opacity: .5; cursor: not-allowed; }

.jfchat__inputrow {
    display: flex; align-items: center; gap: .5rem;
    padding: .65rem .7rem; border-top: 1px solid var(--border);
}
.jfchat__input {
    flex: 1 1 auto; border: 1px solid var(--border); border-radius: 999px;
    padding: .55rem .9rem; font-size: .84rem; background: var(--bg-solid); color: var(--ink);
}
.jfchat__input:focus { outline: none; border-color: var(--brand); }
.jfchat__send {
    display: inline-flex; align-items: center; justify-content: center;
    width: 36px; height: 36px; border-radius: 50%; border: none; cursor: pointer;
    background: var(--brand); color: #fff; flex: none;
}
.jfchat__send:disabled { opacity: .5; cursor: not-allowed; }

/* ===================================================================
   Delivery 112 — the floating clock. Opposite corner from the chatbot
   bubble above (bottom-left vs. bottom-right) so the two never overlap.
   One shared structure (.jfclock__face--analog / --digital, both always
   in the DOM); [data-face="..."] on the root .jfclock is the only thing
   that decides which face shows and how it's drawn — see
   templates/partials/floating_clock.php and assets/js/app.js's
   initFloatingClock() for the two halves of this (markup+ticking JS)
   that never change per face, and everything below for the part that
   does. 'analog-classic'/'digital-mono' deliberately read the app's own
   theme tokens (var(--ink)/--glass-bg-strong/etc.) so those two faces
   match whichever theme is active; 'analog-neon'/'digital-led'/
   'digital-flip' are deliberately fixed, distinct looks that don't
   change with the theme, same as a real desk clock wouldn't.
   =================================================================== */
.jfclock {
    position: fixed; left: 1.1rem; bottom: 1.1rem; z-index: 1150;
    display: flex; align-items: center; justify-content: center;
    /* Delivery 114 — "make the clock movable to any part of the screen".
       cursor:grab/grabbing is the only visual hint it's draggable (no
       extra handle icon — the whole widget IS the handle, except its own
       close button, see initClockDrag()/app.js); touch-action:none stops
       a touch-drag from also scrolling the page underneath it. */
    cursor: grab; touch-action: none; user-select: none;
}
.jfclock.is-dragging { cursor: grabbing; }
@media (max-width: 480px) {
    .jfclock { left: .7rem; bottom: .7rem; transform: scale(.82); transform-origin: bottom left; }
}
.jfclock__close {
    position: absolute; top: -8px; right: -8px; z-index: 1;
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; border-radius: 50%; border: 1px solid var(--border);
    background: var(--bg-solid); color: var(--ink-muted); cursor: pointer; padding: 0;
    box-shadow: 0 4px 10px rgba(0, 0, 0, .18); opacity: 0; transition: opacity .15s;
}
.jfclock:hover .jfclock__close, .jfclock__close:focus-visible { opacity: 1; }
.jfclock__close:hover { color: var(--danger, #dc2626); border-color: var(--danger, #dc2626); }
.jfclock__face { display: none; }
.jfclock[data-face^="analog"] .jfclock__face--analog { display: flex; }
.jfclock[data-face^="digital"] .jfclock__face--digital { display: flex; }

/* ---- Analog: shared geometry for every analog face ---- */
.jfclock__dial {
    position: relative; width: 108px; height: 108px; border-radius: 50%;
    flex: none;
}
.jfclock__tick {
    position: absolute; top: 6px; left: 50%; width: 2px; height: 8px;
    margin-left: -1px; transform-origin: 1px 48px; border-radius: 1px;
}
.jfclock__tick--major { height: 11px; width: 2.5px; margin-left: -1.25px; transform-origin: 1.25px 45px; }
.jfclock__hand { position: absolute; left: 50%; bottom: 50%; transform-origin: 50% 100%; border-radius: 3px; }
.jfclock__hand--hour { width: 4px; height: 27px; margin-left: -2px; }
.jfclock__hand--min { width: 3px; height: 38px; margin-left: -1.5px; }
.jfclock__hand--sec { width: 1.5px; height: 42px; margin-left: -.75px; }
.jfclock__center { position: absolute; top: 50%; left: 50%; width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px; border-radius: 50%; z-index: 2; }

/* ---- Analog: Classic — reads the app's own theme tokens ---- */
.jfclock[data-face="analog-classic"] .jfclock__dial {
    background: var(--glass-bg-strong); border: 3px solid var(--border); box-shadow: var(--shadow);
    -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur));
}
.jfclock[data-face="analog-classic"] .jfclock__tick { background: var(--ink-muted); opacity: .6; }
.jfclock[data-face="analog-classic"] .jfclock__hand--hour,
.jfclock[data-face="analog-classic"] .jfclock__hand--min { background: var(--ink); }
.jfclock[data-face="analog-classic"] .jfclock__hand--sec,
.jfclock[data-face="analog-classic"] .jfclock__center { background: var(--brand); }

/* ---- Analog: Minimal — thin lines, near-transparent, theme-aware ---- */
.jfclock[data-face="analog-minimal"] .jfclock__dial { background: transparent; border: 1px solid var(--border); }
.jfclock[data-face="analog-minimal"] .jfclock__tick { background: var(--ink-muted); opacity: .4; height: 5px; }
.jfclock[data-face="analog-minimal"] .jfclock__tick--major { height: 7px; opacity: .6; }
.jfclock[data-face="analog-minimal"] .jfclock__hand--hour,
.jfclock[data-face="analog-minimal"] .jfclock__hand--min { background: var(--ink); width: 2px; margin-left: -1px; }
.jfclock[data-face="analog-minimal"] .jfclock__hand--sec { background: var(--ink-muted); width: 1px; margin-left: -.5px; }
.jfclock[data-face="analog-minimal"] .jfclock__center { background: var(--ink); width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px; }

/* ---- Analog: Neon — a fixed, deliberately dark/glowing look ---- */
.jfclock[data-face="analog-neon"] .jfclock__dial {
    background: #0b0b16; border: 2px solid #221f3d;
    box-shadow: 0 0 26px rgba(124, 58, 237, .38), inset 0 0 22px rgba(124, 58, 237, .16);
}
.jfclock[data-face="analog-neon"] .jfclock__tick { background: #7c3aed; box-shadow: 0 0 5px #7c3aed; }
.jfclock[data-face="analog-neon"] .jfclock__hand--hour,
.jfclock[data-face="analog-neon"] .jfclock__hand--min { background: #e6ddff; box-shadow: 0 0 6px #a78bfa; }
.jfclock[data-face="analog-neon"] .jfclock__hand--sec,
.jfclock[data-face="analog-neon"] .jfclock__center { background: #22d3ee; box-shadow: 0 0 7px #22d3ee; }

/* ---- Digital: shared layout ---- */
.jfclock__face--digital { flex-direction: column; align-items: center; justify-content: center; gap: .1rem; min-width: 128px; }
.jfclock__time { line-height: 1.1; }
.jfclock__date { line-height: 1.1; }

/* ---- Digital: LED — a fixed retro-alarm-clock look ---- */
.jfclock[data-face="digital-led"] .jfclock__face--digital {
    background: #0a0a0a; border: 2px solid #161616; border-radius: 12px; padding: .75rem 1.1rem;
    box-shadow: 0 10px 26px rgba(0, 0, 0, .35), 0 0 20px rgba(255, 40, 40, .18);
}
.jfclock[data-face="digital-led"] .jfclock__time {
    font-family: 'Courier New', Courier, monospace; font-size: 1.55rem; font-weight: 700;
    color: #ff2d2d; text-shadow: 0 0 9px rgba(255, 45, 45, .85); letter-spacing: .04em;
}
.jfclock[data-face="digital-led"] .jfclock__date { font-family: 'Courier New', Courier, monospace; font-size: .68rem; color: #8a2323; }

/* ---- Digital: Flip — a fixed dark flip-clock look ---- */
.jfclock[data-face="digital-flip"] .jfclock__face--digital {
    position: relative; background: linear-gradient(180deg, #2c2c2c 0%, #161616 100%);
    border-radius: 10px; padding: .7rem 1.15rem;
    box-shadow: 0 10px 24px rgba(0, 0, 0, .4), inset 0 1px 0 rgba(255, 255, 255, .08);
}
.jfclock[data-face="digital-flip"] .jfclock__face--digital::after {
    content: ''; position: absolute; left: 6px; right: 6px; top: 50%; height: 1px; background: rgba(0, 0, 0, .55);
}
.jfclock[data-face="digital-flip"] .jfclock__time { font-family: var(--font-display); font-weight: 700; font-size: 1.6rem; color: #f2f2f2; letter-spacing: .03em; }
.jfclock[data-face="digital-flip"] .jfclock__date { color: #9c9c9c; font-size: .68rem; }

/* ---- Digital: Mono — clean, theme-aware ---- */
.jfclock[data-face="digital-mono"] .jfclock__face--digital {
    background: var(--glass-bg-strong); border: 1px solid var(--border); border-radius: 10px; padding: .7rem 1.2rem;
    -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur)); box-shadow: var(--shadow);
}
.jfclock[data-face="digital-mono"] .jfclock__time { font-family: var(--font-display); font-weight: 500; font-size: 1.4rem; color: var(--ink); letter-spacing: .02em; }
.jfclock[data-face="digital-mono"] .jfclock__date { color: var(--ink-muted); font-size: .7rem; }

/* ---- Analog: Rose Gold — a fixed, dressy watch-face look ---- */
.jfclock[data-face="analog-rosegold"] .jfclock__dial {
    background: radial-gradient(circle at 35% 30%, #fbeee4 0%, #f3ddd0 55%, #e9c9b8 100%);
    border: 3px solid #d8a996; box-shadow: 0 10px 22px rgba(120, 72, 58, .28), inset 0 0 14px rgba(255, 255, 255, .5);
}
.jfclock[data-face="analog-rosegold"] .jfclock__tick { background: #b76e79; }
.jfclock[data-face="analog-rosegold"] .jfclock__tick--major { background: #9c4f5c; }
.jfclock[data-face="analog-rosegold"] .jfclock__hand--hour,
.jfclock[data-face="analog-rosegold"] .jfclock__hand--min { background: #8a4a55; }
.jfclock[data-face="analog-rosegold"] .jfclock__hand--sec,
.jfclock[data-face="analog-rosegold"] .jfclock__center { background: #b76e79; }

/* ---- Analog: Wood — a fixed, warm wood-tone look ---- */
.jfclock[data-face="analog-wood"] .jfclock__dial {
    background: radial-gradient(circle at 32% 28%, #a9764a 0%, #7c4f2c 55%, #5c3a1e 100%);
    border: 4px solid #4a2f18; box-shadow: 0 10px 22px rgba(0, 0, 0, .35), inset 0 0 16px rgba(0, 0, 0, .3);
}
.jfclock[data-face="analog-wood"] .jfclock__tick { background: #f0d9b5; opacity: .85; }
.jfclock[data-face="analog-wood"] .jfclock__tick--major { background: #fff1d8; }
.jfclock[data-face="analog-wood"] .jfclock__hand--hour,
.jfclock[data-face="analog-wood"] .jfclock__hand--min { background: #2b1a0d; }
.jfclock[data-face="analog-wood"] .jfclock__hand--sec,
.jfclock[data-face="analog-wood"] .jfclock__center { background: #e0a458; }

/* ---- Digital: Cyberpunk — a fixed, neon-on-black look ---- */
.jfclock[data-face="digital-cyberpunk"] .jfclock__face--digital {
    background: #0a0014; border: 2px solid #2a0f3d; border-radius: 10px; padding: .75rem 1.1rem;
    box-shadow: 0 10px 26px rgba(0, 0, 0, .4), 0 0 22px rgba(236, 72, 153, .25), inset 0 0 14px rgba(34, 211, 238, .12);
}
.jfclock[data-face="digital-cyberpunk"] .jfclock__time {
    font-family: var(--font-display); font-weight: 700; font-size: 1.5rem; letter-spacing: .05em;
    color: #f0abfc; text-shadow: 0 0 8px #ec4899, 0 0 16px rgba(236, 72, 153, .6);
}
.jfclock[data-face="digital-cyberpunk"] .jfclock__date {
    font-family: var(--font-display); font-size: .68rem; color: #22d3ee; text-shadow: 0 0 6px rgba(34, 211, 238, .7);
}

/* ---- Digital: Sunset — a fixed, warm gradient look ---- */
.jfclock[data-face="digital-sunset"] .jfclock__face--digital {
    background: linear-gradient(155deg, #ff7e5f 0%, #ff477e 55%, #8a2be2 100%);
    border-radius: 12px; padding: .75rem 1.15rem;
    box-shadow: 0 10px 26px rgba(138, 43, 226, .3);
}
.jfclock[data-face="digital-sunset"] .jfclock__time {
    font-family: var(--font-display); font-weight: 700; font-size: 1.5rem; letter-spacing: .03em;
    color: #fff; text-shadow: 0 2px 6px rgba(0, 0, 0, .25);
}
.jfclock[data-face="digital-sunset"] .jfclock__date { color: rgba(255, 255, 255, .85); font-size: .7rem; }

/* ---- Delivery 145 — 10 more clock faces ("atleast 10 more...analog
   and digital both"): 5 analog, 5 digital, same fixed-look contract as
   every face above. ---- */

/* ---- Analog: Ocean — a fixed, cool blue-teal look ---- */
.jfclock[data-face="analog-ocean"] .jfclock__dial {
    background: radial-gradient(circle at 32% 28%, #0ea5c9 0%, #0b6e91 55%, #073d52 100%);
    border: 3px solid #05303f; box-shadow: 0 10px 22px rgba(3, 40, 54, .4), inset 0 0 14px rgba(255, 255, 255, .15);
}
.jfclock[data-face="analog-ocean"] .jfclock__tick { background: #bdf1ff; opacity: .75; }
.jfclock[data-face="analog-ocean"] .jfclock__tick--major { background: #eafcff; }
.jfclock[data-face="analog-ocean"] .jfclock__hand--hour,
.jfclock[data-face="analog-ocean"] .jfclock__hand--min { background: #eafcff; }
.jfclock[data-face="analog-ocean"] .jfclock__hand--sec,
.jfclock[data-face="analog-ocean"] .jfclock__center { background: #fbbf24; }

/* ---- Analog: Mint — a fixed, fresh light-green look ---- */
.jfclock[data-face="analog-mint"] .jfclock__dial {
    background: radial-gradient(circle at 34% 30%, #e6fbf3 0%, #b9f3dc 55%, #86e3bf 100%);
    border: 3px solid #4fbf94; box-shadow: 0 10px 20px rgba(79, 191, 148, .28), inset 0 0 12px rgba(255, 255, 255, .6);
}
.jfclock[data-face="analog-mint"] .jfclock__tick { background: #2f8f6c; opacity: .7; }
.jfclock[data-face="analog-mint"] .jfclock__tick--major { background: #1f6e51; }
.jfclock[data-face="analog-mint"] .jfclock__hand--hour,
.jfclock[data-face="analog-mint"] .jfclock__hand--min { background: #1f6e51; }
.jfclock[data-face="analog-mint"] .jfclock__hand--sec,
.jfclock[data-face="analog-mint"] .jfclock__center { background: #f97316; }

/* ---- Analog: Midnight — a fixed, deep-navy starry look ---- */
.jfclock[data-face="analog-midnight"] .jfclock__dial {
    background: radial-gradient(circle at 30% 25%, #1b2a4a 0%, #0d162e 60%, #060a18 100%);
    border: 3px solid #2a3a63; box-shadow: 0 10px 24px rgba(0, 0, 0, .5), inset 0 0 16px rgba(255, 255, 255, .06);
}
.jfclock[data-face="analog-midnight"] .jfclock__tick { background: #c7d2fe; opacity: .55; }
.jfclock[data-face="analog-midnight"] .jfclock__tick--major { background: #e0e7ff; }
.jfclock[data-face="analog-midnight"] .jfclock__hand--hour,
.jfclock[data-face="analog-midnight"] .jfclock__hand--min { background: #e0e7ff; }
.jfclock[data-face="analog-midnight"] .jfclock__hand--sec,
.jfclock[data-face="analog-midnight"] .jfclock__center { background: #fbbf24; }

/* ---- Analog: Copper — a fixed, warm burnished-metal look ---- */
.jfclock[data-face="analog-copper"] .jfclock__dial {
    background: radial-gradient(circle at 33% 28%, #d98a5f 0%, #a9552c 55%, #7a3618 100%);
    border: 4px solid #5c2810; box-shadow: 0 10px 22px rgba(0, 0, 0, .35), inset 0 0 14px rgba(255, 220, 190, .3);
}
.jfclock[data-face="analog-copper"] .jfclock__tick { background: #ffe3c7; opacity: .8; }
.jfclock[data-face="analog-copper"] .jfclock__tick--major { background: #fff3e6; }
.jfclock[data-face="analog-copper"] .jfclock__hand--hour,
.jfclock[data-face="analog-copper"] .jfclock__hand--min { background: #3a1a0a; }
.jfclock[data-face="analog-copper"] .jfclock__hand--sec,
.jfclock[data-face="analog-copper"] .jfclock__center { background: #ffe3c7; }

/* ---- Analog: Pastel — a fixed, soft candy-color look ---- */
.jfclock[data-face="analog-pastel"] .jfclock__dial {
    background: linear-gradient(150deg, #ffd6e8 0%, #d6e8ff 55%, #e0d6ff 100%);
    border: 3px solid #f3c6dd; box-shadow: 0 10px 20px rgba(214, 178, 219, .3), inset 0 0 12px rgba(255, 255, 255, .7);
}
.jfclock[data-face="analog-pastel"] .jfclock__tick { background: #9b7fae; opacity: .55; }
.jfclock[data-face="analog-pastel"] .jfclock__tick--major { background: #7a5c8e; }
.jfclock[data-face="analog-pastel"] .jfclock__hand--hour,
.jfclock[data-face="analog-pastel"] .jfclock__hand--min { background: #6b4f7e; }
.jfclock[data-face="analog-pastel"] .jfclock__hand--sec,
.jfclock[data-face="analog-pastel"] .jfclock__center { background: #ff8fb1; }

/* ---- Digital: Matrix — a fixed, green-on-black terminal-rain look ---- */
.jfclock[data-face="digital-matrix"] .jfclock__face--digital {
    background: #000905; border: 2px solid #063d1f; border-radius: 8px; padding: .75rem 1.1rem;
    box-shadow: 0 10px 26px rgba(0, 0, 0, .4), 0 0 18px rgba(34, 197, 94, .22);
}
.jfclock[data-face="digital-matrix"] .jfclock__time {
    font-family: 'Courier New', Courier, monospace; font-size: 1.5rem; font-weight: 700;
    color: #22ff6e; text-shadow: 0 0 8px rgba(34, 255, 110, .8); letter-spacing: .04em;
}
.jfclock[data-face="digital-matrix"] .jfclock__date { font-family: 'Courier New', Courier, monospace; font-size: .68rem; color: #16803c; }

/* ---- Digital: Terminal — a fixed amber CRT-terminal look ---- */
.jfclock[data-face="digital-terminal"] .jfclock__face--digital {
    background: #120c00; border: 2px solid #3a2a00; border-radius: 6px; padding: .75rem 1.1rem;
    box-shadow: 0 10px 26px rgba(0, 0, 0, .4), inset 0 0 12px rgba(255, 176, 0, .1);
}
.jfclock[data-face="digital-terminal"] .jfclock__time {
    font-family: 'Courier New', Courier, monospace; font-size: 1.5rem; font-weight: 700;
    color: #ffb000; text-shadow: 0 0 8px rgba(255, 176, 0, .8); letter-spacing: .03em;
}
.jfclock[data-face="digital-terminal"] .jfclock__date { font-family: 'Courier New', Courier, monospace; font-size: .68rem; color: #a86f00; }

/* ---- Digital: Ocean — a fixed, cool blue-teal gradient look ---- */
.jfclock[data-face="digital-ocean"] .jfclock__face--digital {
    background: linear-gradient(155deg, #0ea5c9 0%, #0b6e91 55%, #073d52 100%);
    border-radius: 12px; padding: .75rem 1.15rem;
    box-shadow: 0 10px 24px rgba(7, 61, 82, .35);
}
.jfclock[data-face="digital-ocean"] .jfclock__time {
    font-family: var(--font-display); font-weight: 700; font-size: 1.5rem; letter-spacing: .03em;
    color: #eafcff; text-shadow: 0 2px 6px rgba(0, 0, 0, .3);
}
.jfclock[data-face="digital-ocean"] .jfclock__date { color: rgba(234, 252, 255, .85); font-size: .7rem; }

/* ---- Digital: Gold — a fixed, dressy black-and-gold look ---- */
.jfclock[data-face="digital-gold"] .jfclock__face--digital {
    background: linear-gradient(160deg, #1a1a1a 0%, #0a0a0a 100%);
    border: 1px solid #b8860b; border-radius: 10px; padding: .75rem 1.2rem;
    box-shadow: 0 10px 24px rgba(0, 0, 0, .4), inset 0 0 10px rgba(212, 175, 55, .12);
}
.jfclock[data-face="digital-gold"] .jfclock__time {
    font-family: var(--font-display); font-weight: 600; font-size: 1.45rem; letter-spacing: .04em;
    color: #f0d38a; text-shadow: 0 0 8px rgba(212, 175, 55, .5);
}
.jfclock[data-face="digital-gold"] .jfclock__date { color: #b8860b; font-size: .7rem; }

/* ---- Digital: Pastel — a fixed, soft candy-color gradient look ---- */
.jfclock[data-face="digital-pastel"] .jfclock__face--digital {
    background: linear-gradient(150deg, #ffd6e8 0%, #d6e8ff 55%, #e0d6ff 100%);
    border-radius: 12px; padding: .75rem 1.15rem;
    box-shadow: 0 10px 20px rgba(214, 178, 219, .3);
}
.jfclock[data-face="digital-pastel"] .jfclock__time {
    font-family: var(--font-display); font-weight: 700; font-size: 1.5rem; letter-spacing: .03em; color: #6b4f7e;
}
.jfclock[data-face="digital-pastel"] .jfclock__date { color: #9b7fae; font-size: .7rem; }

/* ===================================================================
   Delivery 133 — the floating, movable, resizable To-Do widget ("add to
   do task as a floatable widget...movable to anyplace...resizable...
   give option to change color of the widget"). Defaults to the
   top-right corner (the clock is bottom-left, the chatbot launcher is
   bottom-right, so this starts somewhere neither already occupies) —
   templates/partials/todo_widget.php, assets/js/app.js's
   initTodoWidget()/initTodoWidgetDrag()/initTodoWidgetResize().

   --jftodo-color is this account's one chosen colour (My Profile → To-Do
   Widget — same palette/validator as Accent Color), written inline on
   the root element itself rather than needing a data-attribute + a rule
   per preset the way the clock's faces or the cursor styles do: a colour
   is a single CSS value, not a whole alternate drawing, so a plain custom
   property is the simpler fit here.
   =================================================================== */
.jftodo {
    position: fixed; top: 5.5rem; right: 1.1rem; z-index: 1140;
    width: 320px; height: 380px;
    min-width: 260px; min-height: 260px;
    max-width: min(520px, 92vw); max-height: min(620px, 80vh);
    background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
    box-shadow: 0 10px 34px rgba(0,0,0,.18);
    display: flex; flex-direction: column; overflow: hidden;
}
.jftodo__header {
    display: flex; align-items: center; gap: .4rem;
    padding: .55rem .6rem; background: var(--jftodo-color); color: #fff;
    cursor: grab; touch-action: none; user-select: none; flex: none;
}
.jftodo.is-dragging .jftodo__header { cursor: grabbing; }
.jftodo__drag-icon { opacity: .85; display: flex; }
.jftodo__title { display: flex; align-items: center; gap: .3rem; font-weight: 700; font-size: .85rem; flex: 1; }
.jftodo__overdue-badge {
    font-size: .68rem; font-weight: 700; background: rgba(0,0,0,.22); color: #fff;
    padding: .15rem .5rem; border-radius: 999px; white-space: nowrap;
}
.jftodo__close {
    border: none; background: rgba(255,255,255,.22); color: #fff; border-radius: 50%;
    width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
    cursor: pointer; flex: none;
}
.jftodo__close:hover { background: rgba(255,255,255,.35); }
.jftodo__body { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: .65rem .7rem; gap: .5rem; }
.jftodo__list { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: .15rem; }
.jftodo__empty { color: var(--ink-muted); font-size: .82rem; margin: .4rem 0; }
.jftodo__row {
    display: flex; align-items: flex-start; gap: .45rem; padding: .35rem .2rem;
    border-radius: 8px; cursor: pointer; transition: opacity .15s, background .15s;
}
.jftodo__row:hover { background: var(--bg); }
.jftodo__check { margin-top: .2rem; accent-color: var(--jftodo-color); cursor: pointer; flex: none; }
.jftodo__flag { flex: none; width: 8px; height: 8px; border-radius: 50%; background: currentColor; margin-top: .35rem; }
.jftodo__row-text { font-size: .82rem; line-height: 1.35; word-break: break-word; }
.jftodo__due { font-size: .72rem; color: var(--ink-muted); white-space: nowrap; }
.jftodo__due--over { color: var(--danger, #dc2626); font-weight: 600; }
.jftodo__more-note { font-size: .72rem; color: var(--ink-muted); margin: .2rem 0 0; }
.jftodo__more {
    flex: none; font-size: .78rem; font-weight: 600; color: var(--jftodo-color);
    text-decoration: none; padding-top: .1rem; border-top: 1px solid var(--border);
}
.jftodo__more:hover { text-decoration: underline; }
.jftodo__resize {
    position: absolute; right: 2px; bottom: 2px; width: 16px; height: 16px;
    cursor: nwse-resize; touch-action: none;
    background:
        linear-gradient(135deg, transparent 0 40%, var(--border) 40% 46%, transparent 46% 60%, var(--border) 60% 66%, transparent 66% 80%, var(--border) 80% 86%, transparent 86%);
    opacity: .8;
}
.jftodo.is-resizing { user-select: none; }
@media (max-width: 480px) {
    .jftodo { top: auto; bottom: .7rem; right: .7rem; left: .7rem; width: auto !important; max-width: none; }
}

/* ===================================================================
   Delivery 77 — live-translate toggle (registration form + pre/post
   assessment form). See assets/js/livetranslate.js for the mechanics:
   an English line (.jf-original) always stays; a duplicate line
   (.jf-translated) is what Google's page-translator widget is allowed
   to rewrite, and it's shown only once a non-English language is on.
   =================================================================== */
.jf-translate-toggle {
    display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
    justify-content: center; margin: 0 0 1rem;
}
.jf-translate-toggle__label { font-size: .8rem; color: var(--ink-muted); font-weight: 600; margin-right: .15rem; }
.jf-translate-btn {
    border: 1px solid var(--border); background: var(--surface-2); color: var(--ink);
    border-radius: 999px; padding: .32rem .85rem; font-size: .78rem; font-weight: 600; cursor: pointer;
}
.jf-translate-btn:hover { border-color: var(--brand); color: var(--brand); }
.jf-translate-btn--active { background: var(--brand); border-color: var(--brand); color: #fff; }

/* Hidden until a non-English language is actually chosen. */
.jf-translated { display: none; }
html[data-jf-lang]:not([data-jf-lang="en"]) .jf-translated {
    display: block; margin-top: .2rem; font-size: .87em; font-weight: 500;
    color: var(--ink-muted); font-style: italic;
}
html[data-jf-lang]:not([data-jf-lang="en"]) .jf-translated.jf-translated--inline { display: inline; margin-top: 0; margin-left: .35rem; }

/* Google's widget injects its own top banner/iframe and nudges <body>
   down to make room for it — this app supplies its own toggle instead,
   so that default chrome is suppressed everywhere it could appear.
   IMPORTANT: #google_translate_element itself must NOT be display:none —
   Google's TranslateElement silently fails to attach its language <select>
   (goog-te-combo) into a container that's display:none when its script
   runs, which was a real bug here: it made every language button a
   no-op. assets/js/livetranslate.js already keeps that container visually
   invisible (off-screen + clipped to 1x1px, but still laid out), so only
   the banner/tooltip/highlight chrome Google adds elsewhere needs hiding. */
.goog-te-banner-frame.skiptranslate { display: none !important; }
body { top: 0 !important; }
.goog-tooltip, .goog-tooltip:hover { display: none !important; }
.goog-text-highlight { background: none !important; box-shadow: none !important; }

/* =====================================================================
   Delivery 96 — two more theme families. PURELY ADDITIVE: nothing above
   this point was touched, so every existing theme (Light, Dark, Glass
   Light, Glass Dark, Cover) renders exactly as it did before this
   delivery, pixel for pixel.

   1. "Sharp" (Sharp Light / Sharp Dark / Sharp Cover) — request: "make
      one more theme... make the theme where shapes take edges all sharp
      rectangle. use the same color scheme." These three reuse the
      IDENTICAL colour tokens as Light/Dark/Cover (copied here rather than
      shared, so the original blocks above can still be edited later
      without silently reshaping this theme too) — the only difference is
      shape: every corner in the app is forced to a hard right angle
      while one of these three is active.

   2. "Colorful" (Colorful Light / Colorful Dark / Colorful Cover) —
      request: "shapes are squaricle... use complete colorful approach...
      all colors different... table borders, infographics colors are
      different red blue green yellow and many more... everywhere
      colorful." A new, deliberately vivid multi-hue palette (not the
      app's warm-orange brand colour) plus a uniform rounded-square
      ("squircle") radius everywhere, PLUS a 6-colour rainbow cycle on
      KPI tiles and table rows so "many more" colours actually show up on
      screen rather than only in the palette's own definitions.

   Both are switched on exactly the way every existing theme already is
   — pick one from the Appearance card on My Profile, or the public
   pages' own theme picker. Both read the same jfThemeChoices() list
   (core/helpers.php), which now has these six more entries, so neither
   picker needed any template change to show them.
   ===================================================================== */

/* ---- 1. Sharp: same colours as Light/Dark/Cover, zero radius everywhere ---- */

:root[data-theme="sharp-light"] {
    color-scheme: light;
    --brand: #f2660f; --brand-hover: #d4550a; --brand-light: #ffe1cc;
    --brand-gradient: linear-gradient(135deg, #c2410c 0%, #ea580c 55%, #fb923c 100%);
    --ink: #211d19; --ink-muted: #7a7166; --border: rgba(122, 113, 102, 0.22);
    --bg-solid: #faf6f0;
    --bg-gradient:
        radial-gradient(circle at 8% 0%, rgba(194, 65, 12, 0.10) 0%, transparent 42%),
        radial-gradient(circle at 92% 100%, rgba(87, 83, 78, 0.09) 0%, transparent 48%),
        var(--bg-solid);
    --glass-bg: rgba(255, 255, 255, 0.6); --glass-bg-strong: rgba(255, 255, 255, 0.78);
    --glass-border: rgba(255, 255, 255, 0.55); --glass-blur: 20px;
    --sidebar-bg: linear-gradient(180deg, #fffbf7 0%, #fbf0e4 45%, #f3e2cc 100%);
    --sidebar-border: var(--border); --sidebar-ink: #49433c; --sidebar-ink-strong: var(--ink);
    --surface: var(--glass-bg); --surface-2: rgba(122, 113, 102, 0.13);
    --success: #12b76a; --success-bg: #e7fbf1; --success-text: #12b76a;
    --danger: #dc2626; --danger-bg: #fdeceb; --danger-text: #dc2626;
    --warning: #ca8a04; --warning-bg: #fdf5db; --info: #0ba5ec; --info-bg: #e9f8ff;
    --shadow: 0 12px 32px rgba(120, 53, 15, 0.16), 0 1px 2px rgba(120, 53, 15, 0.06);
}
:root[data-theme="sharp-dark"] {
    color-scheme: dark;
    --brand: #f2660f; --brand-hover: #d4550a; --brand-light: #ffe1cc;
    --brand-gradient: linear-gradient(135deg, #c2410c 0%, #ea580c 55%, #fb923c 100%);
    --ink: #f3ede4; --ink-muted: #a3978a; --border: rgba(255, 255, 255, 0.10);
    --surface-2: rgba(255, 255, 255, 0.09);
    --bg-solid: #131110;
    --bg-gradient:
        radial-gradient(circle at 8% 0%, rgba(194, 65, 12, 0.16) 0%, transparent 42%),
        radial-gradient(circle at 92% 100%, rgba(87, 83, 78, 0.20) 0%, transparent 48%),
        var(--bg-solid);
    --glass-bg: rgba(38, 32, 25, 0.55); --glass-bg-strong: rgba(38, 32, 25, 0.82);
    --glass-border: rgba(255, 255, 255, 0.09); --glass-blur: 20px;
    --sidebar-bg: linear-gradient(180deg, #171310 0%, #1d1712 45%, #0e0c0a 100%);
    --sidebar-border: var(--border); --sidebar-ink: #cfc6bc; --sidebar-ink-strong: var(--ink);
    --surface: var(--glass-bg);
    --success: #12b76a; --success-bg: rgba(18, 183, 106, 0.16); --success-text: #4ade80;
    --danger: #dc2626; --danger-bg: rgba(220, 38, 38, 0.16); --danger-text: #f87171;
    --warning: #ca8a04; --warning-bg: rgba(202, 138, 4, 0.16);
    --info: #0ba5ec; --info-bg: rgba(11, 165, 236, 0.16);
    --shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
}
/* Sharp Cover: exactly Sharp Light's colours (the existing Cover theme is
   likewise exactly Light's), plus the same actor-chosen photo background
   the existing Cover theme uses — reusing --page-cover-url, which
   templates/layout/app.php already sets on <body> for every page and
   every theme, whether or not a photo-background theme is active. */
:root[data-theme="sharp-cover"] {
    color-scheme: light;
    --brand: #f2660f; --brand-hover: #d4550a; --brand-light: #ffe1cc;
    --brand-gradient: linear-gradient(135deg, #c2410c 0%, #ea580c 55%, #fb923c 100%);
    --ink: #211d19; --ink-muted: #7a7166; --border: rgba(122, 113, 102, 0.22);
    --bg-solid: #faf6f0;
    --glass-bg: rgba(255, 255, 255, 0.6); --glass-bg-strong: rgba(255, 255, 255, 0.78);
    --glass-border: rgba(255, 255, 255, 0.55); --glass-blur: 20px;
    --sidebar-bg: linear-gradient(180deg, #fffbf7 0%, #fbf0e4 45%, #f3e2cc 100%);
    --sidebar-border: var(--border); --sidebar-ink: #49433c; --sidebar-ink-strong: var(--ink);
    --surface: var(--glass-bg); --surface-2: rgba(122, 113, 102, 0.13);
    --success: #12b76a; --success-bg: #e7fbf1; --success-text: #12b76a;
    --danger: #dc2626; --danger-bg: #fdeceb; --danger-text: #dc2626;
    --warning: #ca8a04; --warning-bg: #fdf5db; --info: #0ba5ec; --info-bg: #e9f8ff;
    --shadow: 0 12px 32px rgba(120, 53, 15, 0.16), 0 1px 2px rgba(120, 53, 15, 0.06);
}
:root[data-theme="sharp-cover"] body {
    background:
        linear-gradient(180deg, rgba(250, 246, 240, .55) 0%, rgba(250, 246, 240, .85) 100%) no-repeat,
        var(--page-cover-url) center / cover no-repeat fixed var(--bg-solid);
}

/* The shape itself: every corner in the app becomes a hard right angle
   while any Sharp theme is active — cards, buttons, badges, pills,
   inputs, avatars, dropdown panels, everything. Deliberately a wildcard
   rather than a hand-picked class list: this stylesheet has 129+
   separate border-radius declarations and only 14 of them go through the
   shared --radius token, so a hand-picked list would silently miss some
   and leave a rounded corner behind somewhere. !important is intentional
   here — the one deliberate, narrow case of forcing a whole theme's
   shape language uniformly, scoped only to these three data-theme
   values, and unable to win any other way against dozens of more
   specific literal border-radius rules elsewhere in this file. */
:root[data-theme^="sharp-"] *,
:root[data-theme^="sharp-"] *::before,
:root[data-theme^="sharp-"] *::after {
    border-radius: 0 !important;
}

/* ---- 2. Colorful: squircle radius everywhere, ONE user-chosen accent
   colour (delivery 97 replaces delivery 96's fixed five-hue rainbow —
   see core/helpers.php's JF_ACCENT_PRESETS/jfValidAccentColor() and My
   Profile's new "Accent Color" card, templates/pages/profile/index.php).
   "don't make it so colorful.. add options to choose color..whatever the
   user chooses it should use that": --user-accent is set as an inline
   style on <html> from that saved choice (templates/layout/app.php) and
   is the ONLY thing that changes here when someone picks a different
   colour — every --brand-* token below reads it via var(--user-accent,
   <this theme's own default>), so with nothing picked yet it still looks
   intentional out of the box. Status colours (success/danger/warning/
   info) and the viz-* chart tokens are deliberately NOT tied to the
   accent and are not redefined in these three blocks at all — they fall
   through to the ordinary, order-preserving palette every other theme
   uses (the bare :root block, or its dark-mode sibling just above for
   squircle-dark), because a green "post-assessment done" tile has to
   stay green no matter which accent colour someone picked, and a chart
   has to keep the same three colours in the same order no matter the
   theme. color-mix() (Chrome/Edge 111+, Firefox 113+, Safari 16.4+ —
   all long-shipped) derives the hover/tint/gradient shades from that one
   colour instead of hand-picking a second and third hex per theme. */

:root[data-theme="squircle-light"] {
    color-scheme: light;
    --brand: var(--user-accent, #7c3aed);
    --brand-hover: color-mix(in srgb, var(--user-accent, #7c3aed) 80%, black);
    --brand-light: color-mix(in srgb, var(--user-accent, #7c3aed) 14%, white);
    --brand-gradient: linear-gradient(135deg, var(--user-accent, #7c3aed) 0%, color-mix(in srgb, var(--user-accent, #7c3aed) 65%, black) 100%);
    --ink: #211334; --ink-muted: #776a92;
    --border: color-mix(in srgb, var(--user-accent, #7c3aed) 35%, transparent);
    --bg-solid: #fbfaff;
    --bg-gradient:
        radial-gradient(circle at 6% 0%, color-mix(in srgb, var(--user-accent, #7c3aed) 10%, transparent) 0%, transparent 42%),
        var(--bg-solid);
    --glass-bg: rgba(255, 255, 255, 0.62); --glass-bg-strong: rgba(255, 255, 255, 0.80);
    --glass-border: color-mix(in srgb, var(--user-accent, #7c3aed) 18%, transparent); --glass-blur: 20px;
    --sidebar-bg: linear-gradient(180deg, color-mix(in srgb, var(--user-accent, #7c3aed) 10%, #fdfaff) 0%, #fdfaff 100%);
    --sidebar-border: var(--border); --sidebar-ink: #3b2f5e; --sidebar-ink-strong: var(--ink);
    --surface: var(--glass-bg); --surface-2: color-mix(in srgb, var(--user-accent, #7c3aed) 10%, transparent);
    --success: #16a34a; --success-bg: #e8fbef; --success-text: #16a34a;
    --danger: #dc2626; --danger-bg: #fdeceb; --danger-text: #dc2626;
    --warning: #d97706; --warning-bg: #fdf1db; --info: #2563eb; --info-bg: #e8f0ff;
    --shadow: 0 12px 32px color-mix(in srgb, var(--user-accent, #7c3aed) 20%, transparent), 0 1px 2px rgba(0, 0, 0, 0.06);
}
:root[data-theme="squircle-dark"] {
    color-scheme: dark;
    --brand: var(--user-accent, #a78bfa);
    --brand-hover: color-mix(in srgb, var(--user-accent, #a78bfa) 80%, white);
    --brand-light: color-mix(in srgb, var(--user-accent, #a78bfa) 22%, transparent);
    --brand-gradient: linear-gradient(135deg, var(--user-accent, #a78bfa) 0%, color-mix(in srgb, var(--user-accent, #a78bfa) 60%, white) 100%);
    --ink: #f3edff; --ink-muted: #c3b6e8;
    --border: color-mix(in srgb, var(--user-accent, #a78bfa) 30%, transparent);
    --surface-2: color-mix(in srgb, var(--user-accent, #a78bfa) 14%, transparent);
    --bg-solid: #150f28;
    --bg-gradient:
        radial-gradient(circle at 6% 0%, color-mix(in srgb, var(--user-accent, #a78bfa) 14%, transparent) 0%, transparent 42%),
        var(--bg-solid);
    --glass-bg: rgba(45, 32, 74, 0.55); --glass-bg-strong: rgba(45, 32, 74, 0.82);
    --glass-border: color-mix(in srgb, var(--user-accent, #a78bfa) 20%, transparent); --glass-blur: 20px;
    --sidebar-bg: linear-gradient(180deg, #1e1433 0%, #170f28 45%, #0d0818 100%);
    --sidebar-border: var(--border); --sidebar-ink: #d8cef0; --sidebar-ink-strong: var(--ink);
    --surface: var(--glass-bg);
    --success: #4ade80; --success-bg: rgba(74, 222, 128, 0.18); --success-text: #4ade80;
    --danger: #f87171; --danger-bg: rgba(248, 113, 113, 0.18); --danger-text: #f87171;
    --warning: #fbbf24; --warning-bg: rgba(251, 191, 36, 0.18);
    --info: #60a5fa; --info-bg: rgba(96, 165, 250, 0.18);
    --shadow: 0 14px 34px rgba(0, 0, 0, 0.55);
}
:root[data-theme="squircle-cover"] {
    color-scheme: light;
    --brand: var(--user-accent, #7c3aed);
    --brand-hover: color-mix(in srgb, var(--user-accent, #7c3aed) 80%, black);
    --brand-light: color-mix(in srgb, var(--user-accent, #7c3aed) 14%, white);
    --brand-gradient: linear-gradient(135deg, var(--user-accent, #7c3aed) 0%, color-mix(in srgb, var(--user-accent, #7c3aed) 65%, black) 100%);
    --ink: #211334; --ink-muted: #776a92;
    --border: color-mix(in srgb, var(--user-accent, #7c3aed) 35%, transparent);
    --bg-solid: #fbfaff;
    --glass-bg: rgba(255, 255, 255, 0.62); --glass-bg-strong: rgba(255, 255, 255, 0.80);
    --glass-border: color-mix(in srgb, var(--user-accent, #7c3aed) 18%, transparent); --glass-blur: 20px;
    --sidebar-bg: linear-gradient(180deg, color-mix(in srgb, var(--user-accent, #7c3aed) 10%, #fdfaff) 0%, #fdfaff 100%);
    --sidebar-border: var(--border); --sidebar-ink: #3b2f5e; --sidebar-ink-strong: var(--ink);
    --surface: var(--glass-bg); --surface-2: color-mix(in srgb, var(--user-accent, #7c3aed) 10%, transparent);
    --success: #16a34a; --success-bg: #e8fbef; --success-text: #16a34a;
    --danger: #dc2626; --danger-bg: #fdeceb; --danger-text: #dc2626;
    --warning: #d97706; --warning-bg: #fdf1db; --info: #2563eb; --info-bg: #e8f0ff;
    --shadow: 0 12px 32px color-mix(in srgb, var(--user-accent, #7c3aed) 20%, transparent), 0 1px 2px rgba(0, 0, 0, 0.06);
}
:root[data-theme="squircle-cover"] body {
    background:
        linear-gradient(180deg, rgba(251, 250, 255, .55) 0%, rgba(251, 250, 255, .85) 100%) no-repeat,
        var(--page-cover-url) center / cover no-repeat fixed var(--bg-solid);
}

/* The shape itself: a uniform rounded-square ("squircle") radius on
   every element while any Colorful theme is active — same wildcard
   technique and the same reason as the Sharp themes above (129+
   scattered border-radius declarations, only 14 through --radius). A
   small element (a status dot, a checkbox) simply stays fully round at
   this radius, same as it would at any radius at or above half its own
   size — only larger elements (cards, buttons, avatars, table corners)
   actually take on the visible rounded-square look. */
:root[data-theme^="squircle-"] *,
:root[data-theme^="squircle-"] *::before,
:root[data-theme^="squircle-"] *::after {
    border-radius: 20px !important;
}

/* ---------- Delivery 107 — "My Dashboard" (Interactive Widgets) ----------
   A CSS-grid of draggable cards. Reordering/hiding is plain HTML5
   drag-and-drop (see templates/pages/widgets/index.php's inline script) —
   this section is only the card chrome, the grab handle, and the
   dragging/drop visual feedback. */

.widget-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1rem;
    align-items: start;
}

.widget-card {
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 1rem;
    transition: opacity .15s ease, transform .15s ease;
}

.widget-card--dragging { opacity: .4; }

.widget-card__head {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-bottom: .7rem;
}
.widget-card__head h3 {
    display: flex;
    align-items: center;
    gap: .4rem;
    margin: 0;
    font-size: 1rem;
    flex: 1 1 auto;
}
.widget-card__drag {
    flex: none;
    cursor: grab;
    color: var(--ink-muted);
    display: flex;
    align-items: center;
    touch-action: none;
}
.widget-card__drag:active { cursor: grabbing; }

.widget-card__toggle {
    flex: none;
    display: flex;
    align-items: center;
    gap: .3rem;
    font-size: .78rem;
    color: var(--ink-muted);
    font-weight: 400;
    cursor: pointer;
}

.widget-card__body { font-size: .9rem; }

.widget-empty { color: var(--ink-muted); font-size: .85rem; margin: 0 0 .5rem; }

.widget-list { list-style: none; margin: 0; padding: 0; max-height: 220px; overflow-y: auto; }
.widget-list li {
    display: flex;
    align-items: flex-start;
    gap: .5rem;
    padding: .45rem 0;
    border-bottom: 1px solid var(--glass-border);
}
.widget-list li:last-child { border-bottom: none; }
.widget-list__icon { flex: none; color: var(--ink-muted); margin-top: .1rem; }
.widget-list__text { flex: 1 1 auto; word-break: break-word; }
.widget-list__delete {
    flex: none;
    background: none;
    border: none;
    color: var(--ink-muted);
    cursor: pointer;
    padding: .1rem;
    display: flex;
}
.widget-list__delete:hover { color: var(--danger); }
.widget-list--notes li { align-items: center; }

.widget-inline-form { display: flex; gap: .4rem; }
.widget-inline-form .form-control { flex: 1 1 auto; }
.widget-inline-form--stack { flex-wrap: wrap; }
.widget-inline-form--stack .form-control { flex: 1 1 120px; }

.widget-more-link {
    display: inline-block;
    margin-top: .5rem;
    font-size: .82rem;
    color: var(--user-accent, #7c3aed);
    text-decoration: none;
}
.widget-more-link:hover { text-decoration: underline; }

.widget-weather { display: flex; align-items: center; gap: .9rem; }
.widget-weather__emoji { font-size: 2rem; line-height: 1; flex: none; }
.widget-weather__temp { font-family: var(--font-display); font-size: 2.1rem; font-weight: 700; }

@media (max-width: 640px) {
    .widget-grid { grid-template-columns: 1fr; }
}

/* ===================================================================
   Delivery 142 — "make the calendar section a real calendar type...show
   the things in calendar itself." A real Sunday-first month grid, used
   in two places: the full Calendar page (templates/pages/calendar/
   index.php, 7 columns, roomy) and the Home "My Workspace" widget's
   mini version (templates/partials/home_widgets.php, same markup inside
   a `.cal-grid--mini` wrapper, cramped into one widget-card width).
   Built mobile-first from the start given delivery 141's lesson (the
   analytics tables that overflowed on 390px screens): grid cells use
   `minmax(0, 1fr)` (never a fixed px minmax that forces horizontal
   scroll on a phone), and every chip/number truncates with ellipsis
   rather than pushing its cell wider than the column.
   =================================================================== */
.cal-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    margin-bottom: .85rem;
}
.cal-nav__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1rem;
    font-weight: 700;
    text-align: center;
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .5rem;
    min-width: 0;
}
.cal-nav__today {
    font-size: .74rem;
    font-weight: 600;
    color: var(--brand);
    text-decoration: none;
    padding: .1rem .5rem;
    border-radius: 999px;
    background: var(--brand-light);
    white-space: nowrap;
}
.cal-nav__today:hover { text-decoration: none; filter: brightness(.95); }

.cal-grid { width: 100%; }
.cal-grid__weekdays,
.cal-grid__week {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 3px;
}
.cal-grid__weekdays {
    margin-bottom: 4px;
}
.cal-grid__weekdays span {
    text-align: center;
    font-size: .72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--ink-muted);
    padding-bottom: .3rem;
}
.cal-grid__week { margin-bottom: 3px; }

.cal-day {
    min-width: 0;
    min-height: 68px;
    padding: .3rem .35rem;
    border-radius: 8px;
    background: var(--glass-bg-strong, transparent);
    border: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    gap: .2rem;
    overflow: hidden;
}
.cal-day--outside { opacity: .4; }
.cal-day--today { border-color: var(--brand); background: var(--brand-light); }

.cal-day__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .2rem;
}
.cal-day__num {
    font-size: .78rem;
    font-weight: 700;
    color: var(--ink);
}
.cal-day--today .cal-day__num { color: var(--brand-hover); }
.cal-day__add {
    flex: none;
    width: 18px;
    height: 18px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: 5px;
    background: transparent;
    color: var(--ink-muted);
    cursor: pointer;
    line-height: 0;
}
.cal-day__add:hover { background: var(--brand-light); color: var(--brand-hover); }

.cal-day__events {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}
.cal-chip {
    display: flex;
    align-items: center;
    gap: .25rem;
    font-size: .68rem;
    font-weight: 600;
    line-height: 1.3;
    padding: .05rem .3rem;
    border-radius: 5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}
.cal-chip svg { flex: none; }
.cal-chip--meeting { background: var(--info-bg); color: var(--info); }
.cal-chip--event { background: var(--brand-light); color: var(--brand-hover); }
.cal-chip--deadline { background: var(--danger-bg); color: var(--danger-text); }
.cal-chip--interview { background: var(--success-bg); color: var(--success-text); }
.cal-chip--more { background: transparent; color: var(--ink-muted); font-weight: 700; }

.cal-legend {
    display: flex;
    flex-wrap: wrap;
    gap: .9rem;
    margin: .85rem 0 0;
    font-size: .78rem;
    color: var(--ink-muted);
}
.cal-legend__item { display: inline-flex; align-items: center; gap: .35rem; }
.cal-legend__swatch {
    width: .65rem;
    height: .65rem;
    border-radius: 3px;
    display: inline-block;
}

/* The Home widget's compact version: same grid, tighter cells, no add
   button (adding stays on the full Calendar page — a widget card is
   too narrow for a comfortable inline form) and only a single dot per
   day rather than full text chips. */
.cal-grid--mini .cal-day {
    min-height: 34px;
    padding: .15rem;
    gap: .1rem;
}
.cal-grid--mini .cal-day__num { font-size: .72rem; }
.cal-grid--mini .cal-day__dots {
    display: flex;
    flex-wrap: wrap;
    gap: 2px;
    justify-content: center;
}
.cal-grid--mini .cal-day__dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    flex: none;
}
.cal-grid--mini .cal-day__dot--meeting { background: var(--info); }
.cal-grid--mini .cal-day__dot--event { background: var(--brand); }
.cal-grid--mini .cal-day__dot--deadline { background: var(--danger); }
.cal-grid--mini .cal-day__dot--interview { background: var(--success); }

@media (max-width: 640px) {
    .cal-day { min-height: 54px; }
    .cal-chip { font-size: .64rem; }
    .cal-nav__title { font-size: .88rem; }
}

/* ===================================================================
   Delivery 114 — custom cursors ("add a section to choose cursors...add
   10 cursors to choose..all toggle...may have smileys, may have some
   different arrows etc."). One [data-cursor="key"] attribute on <html>
   (templates/layout/app.php, from this account's saved preference —
   JF_CURSOR_STYLES/currentCursorStyle(), core/helpers.php) selects
   exactly one of these ten rules; no attribute at all (never chosen, or
   reset from My Profile) leaves the browser's own plain default cursor
   completely untouched. Applied broadly (every element) so the pointer
   is replaced everywhere on the page — but NOT inside anything a person
   actually types into, which keeps its own normal text (I-beam) cursor,
   since a novelty pointer there would make it hard to see where text
   will land. Each url() is the exact same small SVG (a fixed pointer
   shape, not theme-aware) baked as a data: URI, with a hotspot — the
   two numbers right after the url — placed at the visual "tip" of an
   arrow, or the centre of a round face/shape.
   =================================================================== */
html[data-cursor] *:not(input):not(textarea):not([contenteditable="true"]):not([style*="cursor"]) {
    /* !important because plenty of existing rules (.btn, .toggle-pill,
       .navtile, etc.) set their own `cursor: pointer` at a higher
       specificity than a plain `*` selector — without it, this custom
       cursor would only ever show over empty space, never over any
       button or link, which is the opposite of "for the whole portal".
       :not([style*="cursor"]) leaves My Profile's own Cursor Style tiles
       alone (templates/pages/profile/index.php sets its own inline
       `cursor` per tile, on purpose, so hovering one previews THAT
       cursor rather than whichever one is already active). */
    cursor: inherit !important;
}
html[data-cursor="arrow-classic"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZD0iTTUgMyBMNSAyNSBMMTAuNSAyMC4yIEwxNC4zIDI3LjggTDE4LjIgMjUuOSBMMTQuNCAxOC40IEwyMS41IDE4LjQgWiIgZmlsbD0iIzFjMWMxYyIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjEuNCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4=) 4 2, auto;
}
html[data-cursor="arrow-bold"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZD0iTTQgMiBMNCAyNyBMMTAuOCAyMS41IEwxNS4yIDMwIEwxOS44IDI3LjcgTDE1LjQgMTkuMiBMMjMuNSAxOS4yIFoiIGZpbGw9IiMwZjE3MmEiIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIyLjQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+) 4 2, auto;
}
html[data-cursor="arrow-brand"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZD0iTTUgMyBMNSAyNSBMMTAuNSAyMC4yIEwxNC4zIDI3LjggTDE4LjIgMjUuOSBMMTQuNCAxOC40IEwyMS41IDE4LjQgWiIgZmlsbD0iIzdjM2FlZCIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjEuNiIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4=) 4 2, auto;
}
html[data-cursor="arrow-neon"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPGRlZnM+PGZpbHRlciBpZD0iZyIgeD0iLTYwJSIgeT0iLTYwJSIgd2lkdGg9IjIyMCUiIGhlaWdodD0iMjIwJSI+PGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMS40IiByZXN1bHQ9ImIiLz48ZmVNZXJnZT48ZmVNZXJnZU5vZGUgaW49ImIiLz48ZmVNZXJnZU5vZGUgaW49IlNvdXJjZUdyYXBoaWMiLz48L2ZlTWVyZ2U+PC9maWx0ZXI+PC9kZWZzPgo8cGF0aCBkPSJNNSAzIEw1IDI1IEwxMC41IDIwLjIgTDE0LjMgMjcuOCBMMTguMiAyNS45IEwxNC40IDE4LjQgTDIxLjUgMTguNCBaIiBmaWxsPSIjMjJkM2VlIiBzdHJva2U9IiMwODMzNDQiIHN0cm9rZS13aWR0aD0iMS4yIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWx0ZXI9InVybCgjZykiLz4KPC9zdmc+) 4 2, auto;
}
html[data-cursor="smiley-sunny"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPGNpcmNsZSBjeD0iMTYiIGN5PSIxNiIgcj0iMTMiIGZpbGw9IiNmZmQ0M2IiIHN0cm9rZT0iIzhhNWEwMCIgc3Ryb2tlLXdpZHRoPSIxLjQiLz4KPGNpcmNsZSBjeD0iMTEuMiIgY3k9IjEzLjUiIHI9IjEuNyIgZmlsbD0iIzNhMjYwMCIvPgo8Y2lyY2xlIGN4PSIyMC44IiBjeT0iMTMuNSIgcj0iMS43IiBmaWxsPSIjM2EyNjAwIi8+CjxwYXRoIGQ9Ik05LjUgMTkgUTE2IDI1LjUgMjIuNSAxOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjM2EyNjAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4=) 16 16, auto;
}
html[data-cursor="smiley-wink"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPGNpcmNsZSBjeD0iMTYiIGN5PSIxNiIgcj0iMTMiIGZpbGw9IiNmZmQ0M2IiIHN0cm9rZT0iIzhhNWEwMCIgc3Ryb2tlLXdpZHRoPSIxLjQiLz4KPHBhdGggZD0iTTkgMTMuNSBRMTEuMiAxMS43IDEzLjQgMTMuNSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjM2EyNjAwIiBzdHJva2Utd2lkdGg9IjEuOCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjxjaXJjbGUgY3g9IjIwLjgiIGN5PSIxMy41IiByPSIxLjciIGZpbGw9IiMzYTI2MDAiLz4KPHBhdGggZD0iTTkuNSAxOC41IFExNiAyNi41IDIzIDE3LjgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzNhMjYwMCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+) 16 16, auto;
}
html[data-cursor="smiley-cool"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPGNpcmNsZSBjeD0iMTYiIGN5PSIxNiIgcj0iMTMiIGZpbGw9IiNmZmQ0M2IiIHN0cm9rZT0iIzhhNWEwMCIgc3Ryb2tlLXdpZHRoPSIxLjQiLz4KPHJlY3QgeD0iNy41IiB5PSIxMS41IiB3aWR0aD0iNy4yIiBoZWlnaHQ9IjQuNiIgcng9IjEuNiIgZmlsbD0iIzExMTgyNyIvPgo8cmVjdCB4PSIxNy4zIiB5PSIxMS41IiB3aWR0aD0iNy4yIiBoZWlnaHQ9IjQuNiIgcng9IjEuNiIgZmlsbD0iIzExMTgyNyIvPgo8cmVjdCB4PSIxNC43IiB5PSIxMi44IiB3aWR0aD0iMi42IiBoZWlnaHQ9IjEuNiIgZmlsbD0iIzExMTgyNyIvPgo8cGF0aCBkPSJNMTAgMjAgUTE2IDI0LjUgMjIgMjAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzNhMjYwMCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+) 16 16, auto;
}
html[data-cursor="heart-point"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZD0iTTE2IDI2LjUgQzE2IDI2LjUgNC41IDE4LjggNC41IDExLjIgQzQuNSA2LjkgNy45IDQuMiAxMS40IDQuMiBDMTMuNiA0LjIgMTUuMyA1LjQgMTYgNy4yIEMxNi43IDUuNCAxOC40IDQuMiAyMC42IDQuMiBDMjQuMSA0LjIgMjcuNSA2LjkgMjcuNSAxMS4yIEMyNy41IDE4LjggMTYgMjYuNSAxNiAyNi41IFoiIGZpbGw9IiNlZjQ0NDQiIHN0cm9rZT0iIzdmMWQxZCIgc3Ryb2tlLXdpZHRoPSIxLjIiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+) 16 14, auto;
}
html[data-cursor="star-point"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZD0iTTE2IDMuNSBMMTkuNiAxMi4zIEwyOSAxMy4xIEwyMS44IDE5LjIgTDI0IDI4LjUgTDE2IDIzLjQgTDggMjguNSBMMTAuMiAxOS4yIEwzIDEzLjEgTDEyLjQgMTIuMyBaIiBmaWxsPSIjZmJiZjI0IiBzdHJva2U9IiM5MjYwMGEiIHN0cm9rZS13aWR0aD0iMS4yIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPg==) 16 16, auto;
}
html[data-cursor="target-cross"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPGNpcmNsZSBjeD0iMTYiIGN5PSIxNiIgcj0iMTAuNSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZWY0NDQ0IiBzdHJva2Utd2lkdGg9IjIiLz4KPGNpcmNsZSBjeD0iMTYiIGN5PSIxNiIgcj0iNC41IiBmaWxsPSJub25lIiBzdHJva2U9IiNlZjQ0NDQiIHN0cm9rZS13aWR0aD0iMS42Ii8+CjxsaW5lIHgxPSIxNiIgeTE9IjEuNSIgeDI9IjE2IiB5Mj0iOCIgc3Ryb2tlPSIjZWY0NDQ0IiBzdHJva2Utd2lkdGg9IjIiLz4KPGxpbmUgeDE9IjE2IiB5MT0iMjQiIHgyPSIxNiIgeTI9IjMwLjUiIHN0cm9rZT0iI2VmNDQ0NCIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxsaW5lIHgxPSIxLjUiIHkxPSIxNiIgeDI9IjgiIHkyPSIxNiIgc3Ryb2tlPSIjZWY0NDQ0IiBzdHJva2Utd2lkdGg9IjIiLz4KPGxpbmUgeDE9IjI0IiB5MT0iMTYiIHgyPSIzMC41IiB5Mj0iMTYiIHN0cm9rZT0iI2VmNDQ0NCIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPg==) 16 16, auto;
}
html[data-cursor="naandi-flame"] {
    /* Delivery 131 fix — this site-wide rule was missing entirely, so
       picking "Naandi Flame" saved fine but the portal cursor never
       actually changed anywhere outside the My Profile preview tile.
       Same PNG data URI as core/helpers.php's
       JF_CURSOR_STYLES['naandi-flame']. */
    cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAA4CAYAAADqxUiJAAAMFklEQVR4nLVYa3SV1Zl+9v5u535yTk5uXAyQAJIJlIt1RKcGZkrptGiXs3qYVaVYISTgDOrYy+panfGQOqudpaNUrYIZnYrgLelqV2eotVOYoCjiQJWCoAghJORycj85t+++3/mRBJGCXIzPn299397ffvb77ne/+3k3cBUggAHA4XuqS3Z/b1EFABCNfrtc8KshPhqHAgBlTs9X5tDwzpZEPAD28YQ+F2IisOpmWADg2kZPmZu6tmjwUBUDCPHLH++KiCkBzhijg3VzFzYlqtSsCOlwcsIv9IqmpiYJzRATTtySgMwaIJJ1hf88jZ15MY64E5L0v4YsOHG+bHjXLj7q7ctz92URUxzS0gY47RunfaPAb22SuX2cNTwowpRbY9pSNi2HPHP9h+Uria5LEhPA0ATx5vdvDFqQVhq62yVM3niotnyW6hHTdMdj+plhhUJpVyTAR3+ZAOLmODhjII859OWAPfyXsi3Cf3IXvlUu9f+DEMK2uCfJXfyyuuGYhWMTGNXxZoiDdYuUgKItLZDtsMTo+NJtb6Y01/q64WitvYGSP+qR2D4C2IQFFxEYA0hiSjE3TJU7OlxOHcfWFdV4NFaRlwNlArSrumH/UHMc48H12YmbV462h3jnJL+VvEn1uTFdUg6WOZm7BhDq7FZjz//64dteoAT4yiuw9pLE8bFnVvX+BXl8up33uK7Br/Vq9h1CiC7iU/91Expo02i3y7YWAORPa2wee+pCbdfEyHQOmMKnLshydTdLFn9zfvMfsgSwhoYrs/aSxPGqUStkEx1MmG+ThhVDweIfVv/bB68BA6MxwK7M0ssGxSEBDN0boqtpo18cvb20nADWFIf0Wca9dOaqAgGEPim2NxMsZcUF5jcZQPGqKzsGz8enuhoAWAPEqEs/ajty/7xbvNB1YBjAla/rVeHcs5YARglw+ozuvnzyC5B92uHfFId0pcrkQmAtidGlqUGN3P+PhZs/qp9yx/nkFwq8K1EmZ3+gBDgR2PhIb60qKR6+O/x7+kGE9Hqv/f7aKZUAQDWQKfFxsHbVlflaf1C97PnvrioG/lyT/dlMCKP5+TxjgTtPeQ6HvnZrpdv1oDdgzcoNQ9eZdDqF0KqZjT3vjvfM1XsnJ1lkAUmBSp/XuVVxs2o/ReNzfvZhclPi42RzQRcQwD78/uyAJ20UqshPTpJ/Spj0FTGWu0XmImAKvgOMXs1Y7P1pv8geoyZIPbsKF+hMviErBadHMMJj0OcpjrVA9jiRYTO4Jfp0+m6Kk8Sa4QLnbCcG4Kd1Xw4vVnor3iU54NFH/BzCMXigt9we/KJMxm0p+M44UB6qbOz/BQD0fLfE37NeXtG6O7BYKLzCp6Ki2B2Y5LfSk5giAAcCsgrHVmYBGMsJo5DPsRIRDOd7Vf8HN3fut8Znllrne0AWzj0D3PO+Tdr9Mxt79wxsKJ2TdZ2brKw7z+WB0gKfuFYx02UhIxsDEwDBhQ2OcIQPOL6XDvHy7yUS+zgaPia+6Bo/vGqZf73vzVdIuEsGhbdFVZWHuBUaHoF1syKL2WFNVMhkz5FsozQg5X0QBIC5IOLgjI3I0cEzyqQfzn30/WcAQlMc0njuZw0QnyA+WAflukbYZ74zY2ZM6/6V7lBkiPwHM1rkOY9ruX5hLw5J2Xk+yf6S4mchO+V2Z6G2BxXzOtl1JTDitlDEGbX0nZOi9O7lTx04RHVQWCPsCxg39jIqX52etYVLwjy7I+3wEzpXj7oe/wFVuKFCkfmWjxuLoUjQDf6eAfnRIfjyZTz7I59kLRQOc7Pw9JzwTG85osx49K5//82h8bGbEvFAONVXWcG618Gyflv5VPur7Fz3dtcXrPYLY7PlitN50vZ5ZOqOqsa3ZQ1zzDw3bJJeGnADj09/ZvhQ14aCe0uQ2ix5FTaY1falvQWU9pec7FCKHri1YWfHq49t1Erb9y5iRDd5Reb6IntoQVTKVpiG1PeemDmPUQKcgZDsjfyoRMn/eNjWkips1S+7UUhA3mJtOVd7vM+Mvly9rT35xl3zixYpH273BWh5zlKzSeF/xeEepnmV1sHC8ifttkxEk/M1CrNmg0NThGlOcftX+3luMhwAXgVtVuxeBgBd6yKbJoXSCWRcAb+XOxbBsPBaljxP3jOy7H+am5stAOheV/jtQj7yMzXIon35guMDrn+nB6ZPUVhbylt2lFnG9eS4voBrZHwihQC3ZnvJqOGMphA40uTtSMpFrxhM2cIAoHNNcHGJpu8joowp+IsjjvL0lP/U3xtPYO/VT5k8g4YeDqnmt2x4MEDhlmGhHC1k+WUD5P1fQ4t1Rik1I2wM54MsVyKRO58xzGKyCzAVOXjzfRR+o8MtfmrJlgM7AUZsfH1bv1N4i+lqx6u293wE0GiB1gDRUxtbUSDlfu7xivKc7RkacAMHSaFMUOhRi7ynXKHrYRhzvMKukJmYBo0AhwBisCUvpRE82YHCJpOr2xY/fuhEUxxSvGlsO52rnWgjNPYEzEfiN3jvih7+SUQx7wMjmAg4OdKSsmMxR+Kt5NgjIWbMVVRnGogBLkYzFYigyJLO/fkuVrA7aUf/wzv/i69dV9/4iS11NqqRgIIeEGuE/UHdjLnX8K7nfJq90NFhZOE9FeLGbIMpWVkIn8ptBT4G6AxZRz6gSFSpkVUAMJaBT9cV38lOt+DXfcLz4t9uPXIcAPrrww+CeUS/0LbOebojKY+xExpgARw99dG1Ud65WZWsIEzmCMGFAtvDuZB8ihEGOCxb6jVz2vYh17c/KBm3eZH/gsU11+a+TJ8d+O2A5f3lVMr8YVHjifzp+muqVCf1Ew6xNOrPhKQRfRJjWMfi8bgax1H1pujA3/hh1oaV3Ao4DkDMAoMKhQCZwcxJGRtqiwll24bhNb97KPrS0snS4BMKFzOGXP9AhntaMyL4Wgq+57+05cgpgOHMhsjqYso94ZAUUmCmFMn1WUIZ2SemV8m3T20r+St74PcxGpwDEIQlbC4xBSpXHRPCsvl+3VKbOkXgv+Y/09t2550Jz7OxR54NasbteZ33JZ3IWyb3tLmu+uLcrad+BzC0rS9fEGap9RGeroPjCBUANBSASzB1Wc9Jmiungv5+b2/WB8m14IC4xjXTlFp1h780JLzNFc+OHAZsADkk64KrIvynTwIi2JMLvD0CX6ulaO86csFun5wP/Gnj9PsiZK8opoGbNUlXQASoEjctPmyayusm5ztP2LFdX3/2SIoBQLI2sr4kYmxBzsWQJW8+aFT+y/Idh3PjEfjOneWlVf6+xwOyEx/JyXt7ReiopsohXfb02uBtXvAvcOTLFMcsmCr33wgwGLbcaXF5r0naf/dJZS3VT32QPFfYjKXMBM6kn3sAer516tb+7QBAcah7qmpEZc/R5UVyulEwkgcN3698Cm50IWWHhPa2jyPgYXax5hoRlayYKosKl5g0KLz378XMHSsb/zhyziHE9+wB37MEouH8Y/FsB4AvbYDTvTb6d1Ett50cMexCyhtQP3IFemXOZImJyQrsGYKUDomMqd6APMMweLIvrawsfyG1d2zyoxqxCeL8GuusKqQ4JIpDYg0Q/cdGOwluJwUxs1sU7FVlt1zlbrGfW9d7uD1f5rwvK6sbMq70TFp483268s5pM7yk/IXUXkpAJoCxZrisGe6FCruL6l1KgONYnA0VvNrk18QtwhKnbc7fsIT8mwNayetfe+Jkuqu24BtcOD92OT9l2+yfpm8bOU01kNnrcC427iUxLsL331EZGtgQ/cpjG7+qndveUxf7+641ocPta4M7uurKrvmEaycatBEaAHTVFi1P1kb+70xt9OW2+pJpV0N6ydKCADZ+R8ma4XbUl1xPrri3x1+2exLPvXHN5taTFMdZvTyhIIARwI7dFyvrqi98+a26Wdei7qACjMXC54VxN3bUxbaerostHCeciErwohiv/NrrSje01xetHiO9ZEH/mTDuxpPrp1R31Jc8AgAtNRNDesn1SSTAOXPX2OR/jAC2Z8nnfAUxbm1r/dRlp9eX1QITu08v7rYGECXA23rZ/GTG//SYLpsway9IPC7+Dg3Om+ZI7uEbXjiapkpwTOBNzwWJx5P6O1bxkGrarxPA2FVcG34a/h8Rl9Pu/yB97gAAAABJRU5ErkJggg==) 15 4, auto;
}
html[data-cursor="dia-footprint"] {
    /* Delivery 131 fix — same missing-rule bug as above, for "DIA
       Footprint". Same PNG data URI as core/helpers.php's
       JF_CURSOR_STYLES['dia-footprint']. */
    cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAiCAYAAABIiGl0AAAGuElEQVR4nMWXfWxWVx3Hv+ftvj33KX36AlvpykuQOcaEDQwryAppNpwdw2R0Sgzx5Q8bp2SJcbps2ZpGXaJbNBqmZkQ3k7mZdVswwWSBGQfBMCeIIRUxG2156wstfdo+L/fl3HOOf/R5oDwUBlri759777nnnM/5/l7OuRe4eUbLN+TydlLZcdahrel07R2C7ZzvWSdXeN5bbcDiMpxeY/B/Ze0AA6DXCfo1HSdPaEo2eJTvJoTXfWTbf74XqLm4slk00g2oFojlCeGZPBM5auhrdVIpHck3Y4omMGsdAMNnEUoB6I1wFkwI9Vy1sEZPE70t1nov5dSNBRYsUubRmJJboIDZBJt7AVfb5sG51Hs5iiPHgZysEW6CKDk2wKLnpeFygUrOAqCzlWEEgNnm+3VjSt2XU0lrEaQWnA8wQ5K8DJsURHVoZHqtUh3DwInZUmyWAVY2iDfU+u6f8rmI1/O0GTXJygmNRoti+RylnzaM0Q+BJ48qtX02kosAQBXg56HW9CTBHsmQHqeJVVDhxpjE99UqvTe29Hxj1D0U6AZg/mdw5xSY1LruEkrYkJDkrA9BLKIDh7kHhSbZO1R1F9HsNmPIeJOlBoAbKyfSeak/LSvtmno2sTTOBGefjFU8vljyQ+e1eWpEB5vuidnmvziFDh7JnoXE3jcWoO+im64GAmBKED2tvfIZOwD7uJ169hxRDSKKztxqV78zbMI2JoMBxX0/YaZFGDXsKnXokJQvtQPsWopN6aoJgC/VoOrzaSwFoDfZ9sL7gVR5gX0Ao0b+o4hkTcKcxnEWPBmocP4R3fgbS6u7SSL7GHQxJ+UfMLXJ6KsqfgJI92cymTEZPpSKySnF9CNG8L8qGQxFzF5h4jjYHyfPA8DDlrWsSEhLKOW4ZfsnVZK/s0iF0YZWMWAwluGnfcNfPQh5rBOgXYC+THE5hp/1nc+ccFiXzudrXTCVUJaFZq+RRL8PQ87HRJyWnHsAyCpAKKUahEY4arHtJ1TwkzOSnPYS9Geht46Y6Kf1lL5zEPJYC8C7SmGaqY6JyIdZw/DHt5Q6CimPloMNAC1Oapulyfp60KcBmOUA6wegjayV1G4OKK9OSPTmKS3fcxO87lGwglInSkrVRch0IABDQfBQTd3WyaS4I5H4ICGMWgyP2JS9Ui/17t5i+t9pe+SWfVHU94AQq0YNnhGGvlitop5TwmuT1DwnNUZrCd9lqWL9HKVe2AuM4VKyArhUHgQAvg6IdY77816Z/0WWIqWFyGtq1seWVR9Q883EZt9eWFP81L4o6msHXKF1k8XFMAR77ALlX4Us9rRGzicyRv4ukxSCRcraWYLS6dBKsOn1/SWDnOwwEINraua13j/5+Pfv9FNtt5rC7b5Ba4HQYs6YAgAyxPmXzxMzT+nwzFzBfpRntNGjovOAndu+Lsn8DEx/MIigrPSy8psOBgCMJUldopPzlvC++FJv70QXuvTLQ0MjGeJNcODhOAzqh7JOrwGgGYtrOBtLG5ztieIXhgi2+Dbf5RGs/Du58HatxPB+IDSVxJJxAGifqi1QbrZUETrcpGRDYyZDo2IxShGyekwpLpQKiwSPHcFg8W5gLaTa0qST7/zLs58lcOa5USFnG5OpIfTHOWru6lYYBEBIhYsrk4u0AAxp/xXJ3chS6mSaqGEaBBeI1lmZSh3LT0zkNgC6C9DrmdXmMyYGgS8UIJvHCY/8RB5qZOSfjjKprUr9sANIKuNaqZgAMFYqVRNQnrN03KdUdM4phL/vLqf/xAQAYH9poRkVH/iQ0W8IZps5xvtenBRfdJn3dsiiLZKo1zsUElRk8RUxLp0uGCmEKyaLQV1THL+3oBDu7gZUO8A6px0IZe98BERUsUcNxTmbyLoGwr5rQbblEpN/P1Lvts+wn18BPl6alDGkXEv0Zyn1XwUK7QDrBlRppymv3HQC5DgQL6XWV0wkl44b8lSeYnM6UQeqpHwGU+OuCQUAUgY0W/xx13a5Z8xvV+XzY11TsKu5igAwq4HbOHiTDXZuP6L+j4NdYcsAa2XaO9zspn5wA8Nm2uev+xuOAoANtjmU0tVCRC2p9K82etbnZpq8wjRKHgNAKkJyfeAC020OtU4u8b03Csz69aQh33pg6otff4wKU8r86wZeBHcCdK5lHfYoDwbGJucdnrzwN0aYFdlzqm50shsC7wHY9mD5rkgn4+MEOzdlGpqrbPtdC+HN/atbDOgOHJGC6GyBs7tOy9zBKC4sXhRFZzHDqTKrcABkLeerb3fEnpUp55ctQGPp3U1V/X+x/wBmsQ2V6NyWcAAAAABJRU5ErkJggg==) 15 17, auto;
}

/* ---- Delivery 145 — 10 more cursors ("some professional , some
   funny"): same html[data-cursor="key"] contract as every cursor
   above, same artwork as core/helpers.php's matching JF_CURSOR_STYLES
   entries. ---- */
html[data-cursor="pen-nib"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZD0iTTYgNCBMMjcgOSBMMTQgMTUgTDkgMjcgWiIgZmlsbD0iIzFlM2E1ZiIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjEuMyIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8bGluZSB4MT0iMTUiIHkxPSIxMCIgeDI9IjExIiB5Mj0iMjIiIHN0cm9rZT0iI2Q0YWYzNyIgc3Ryb2tlLXdpZHRoPSIxLjYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4=) 5 4, auto;
}
html[data-cursor="briefcase"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHJlY3QgeD0iNSIgeT0iMTMiIHdpZHRoPSIyMiIgaGVpZ2h0PSIxNCIgcng9IjIiIGZpbGw9IiM0YTM3MjgiIHN0cm9rZT0iIzI0MWExMiIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KPHJlY3QgeD0iMTIiIHk9IjgiIHdpZHRoPSI4IiBoZWlnaHQ9IjYiIHJ4PSIxLjUiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzRhMzcyOCIgc3Ryb2tlLXdpZHRoPSIxLjgiLz4KPHJlY3QgeD0iMTQuNSIgeT0iMTgiIHdpZHRoPSIzIiBoZWlnaHQ9IjMiIHJ4PSIuNiIgZmlsbD0iI2Q0YWYzNyIvPgo8L3N2Zz4=) 16 16, auto;
}
html[data-cursor="magnifier"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPGNpcmNsZSBjeD0iMTMiIGN5PSIxMyIgcj0iOC41IiBmaWxsPSJub25lIiBzdHJva2U9IiMxZjI5MzciIHN0cm9rZS13aWR0aD0iMi42Ii8+CjxsaW5lIHgxPSIxOSIgeTE9IjE5IiB4Mj0iMjcuNSIgeTI9IjI3LjUiIHN0cm9rZT0iIzFmMjkzNyIgc3Ryb2tlLXdpZHRoPSIzLjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4=) 13 13, auto;
}
html[data-cursor="checkmark"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPGNpcmNsZSBjeD0iMTYiIGN5PSIxNiIgcj0iMTMiIGZpbGw9IiMwZjdhNGMiIHN0cm9rZT0iIzBhNGQzMCIgc3Ryb2tlLXdpZHRoPSIxLjQiLz4KPHBhdGggZD0iTTkuNSAxNi41IEwxNCAyMSBMMjMgMTAuNSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjIuNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPg==) 16 16, auto;
}
html[data-cursor="graduation-cap"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZD0iTTE2IDUgTDMwIDExIEwxNiAxNyBMMiAxMSBaIiBmaWxsPSIjMWMxYzJlIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMSIvPgo8cGF0aCBkPSJNOSAxNCBMOSAyMSBRMTYgMjUgMjMgMjEgTDIzIDE0IiBmaWxsPSJub25lIiBzdHJva2U9IiMxYzFjMmUiIHN0cm9rZS13aWR0aD0iMS42Ii8+CjxsaW5lIHgxPSIyNyIgeTE9IjEyIiB4Mj0iMjciIHkyPSIyMSIgc3Ryb2tlPSIjZDRhZjM3IiBzdHJva2Utd2lkdGg9IjEuNiIvPgo8Y2lyY2xlIGN4PSIyNyIgY3k9IjIyIiByPSIxLjYiIGZpbGw9IiNkNGFmMzciLz4KPC9zdmc+) 16 14, auto;
}
html[data-cursor="banana"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZD0iTTcgMjYgQzQgMjAgNiAxMCAxNCA1IEMxNSA0LjMgMTYuMyA1IDE2IDYuMiBDMTMgOSAxMCAxNSAxMSAyMiBDMTEuNCAyNC41IDkuMyAyNyA3IDI2IFoiIGZpbGw9IiNmZmQ5M2QiIHN0cm9rZT0iI2E4NzcwYSIgc3Ryb2tlLXdpZHRoPSIxLjIiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTE0IDUuNSBMMTYuNSA0IiBzdHJva2U9IiM3YTViMDAiIHN0cm9rZS13aWR0aD0iMS4zIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+) 16 16, auto;
}
html[data-cursor="ghost"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZD0iTTggMjcgTDggMTQgUTggNSAxNiA1IFEyNCA1IDI0IDE0IEwyNCAyNyBMMjAuNSAyMy41IEwxNyAyNyBMMTMuNSAyMy41IFoiIGZpbGw9IiNmNWY1ZjciIHN0cm9rZT0iI2M3YzdjZiIgc3Ryb2tlLXdpZHRoPSIxLjIiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPGNpcmNsZSBjeD0iMTIuNSIgY3k9IjE0IiByPSIxLjYiIGZpbGw9IiMyYjJiMzMiLz4KPGNpcmNsZSBjeD0iMTkuNSIgY3k9IjE0IiByPSIxLjYiIGZpbGw9IiMyYjJiMzMiLz4KPC9zdmc+) 16 14, auto;
}
html[data-cursor="alien"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZD0iTTE2IDQgQzI0IDQgMjcgMTIgMjQgMjAgQzIyIDI2IDEwIDI2IDggMjAgQzUgMTIgOCA0IDE2IDQgWiIgZmlsbD0iIzdlZTA4YyIgc3Ryb2tlPSIjM2Y4YTRjIiBzdHJva2Utd2lkdGg9IjEuMiIvPgo8ZWxsaXBzZSBjeD0iMTEuNSIgY3k9IjE1IiByeD0iMi42IiByeT0iMy42IiBmaWxsPSIjMTExODI3IiB0cmFuc2Zvcm09InJvdGF0ZSgtMTUgMTEuNSAxNSkiLz4KPGVsbGlwc2UgY3g9IjIwLjUiIGN5PSIxNSIgcng9IjIuNiIgcnk9IjMuNiIgZmlsbD0iIzExMTgyNyIgdHJhbnNmb3JtPSJyb3RhdGUoMTUgMjAuNSAxNSkiLz4KPC9zdmc+) 16 15, auto;
}
html[data-cursor="paw-print"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPGVsbGlwc2UgY3g9IjE2IiBjeT0iMjEiIHJ4PSI3LjUiIHJ5PSI2IiBmaWxsPSIjNmI0YTM2Ii8+CjxlbGxpcHNlIGN4PSI4IiBjeT0iMTIiIHJ4PSIzIiByeT0iMy44IiBmaWxsPSIjNmI0YTM2Ii8+CjxlbGxpcHNlIGN4PSIxNSIgY3k9IjgiIHJ4PSIzIiByeT0iMy44IiBmaWxsPSIjNmI0YTM2Ii8+CjxlbGxpcHNlIGN4PSIyMiIgY3k9IjkiIHJ4PSIzIiByeT0iMy44IiBmaWxsPSIjNmI0YTM2Ii8+CjxlbGxpcHNlIGN4PSIyNiIgY3k9IjE1IiByeD0iMi42IiByeT0iMy40IiBmaWxsPSIjNmI0YTM2Ii8+Cjwvc3ZnPg==) 16 16, auto;
}
html[data-cursor="rocket"] {
    cursor: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KPHBhdGggZD0iTTE2IDMgQzIxIDcgMjIgMTQgMjAgMjAgTDEyIDIwIEMxMCAxNCAxMSA3IDE2IDMgWiIgZmlsbD0iI2U1ZTdlYiIgc3Ryb2tlPSIjNmI3MjgwIiBzdHJva2Utd2lkdGg9IjEuMSIvPgo8Y2lyY2xlIGN4PSIxNiIgY3k9IjEyIiByPSIyLjYiIGZpbGw9IiMzYjgyZjYiLz4KPHBhdGggZD0iTTEyIDE4IEw3IDI0IEwxMiAyMiBaIiBmaWxsPSIjZWY0NDQ0Ii8+CjxwYXRoIGQ9Ik0yMCAxOCBMMjUgMjQgTDIwIDIyIFoiIGZpbGw9IiNlZjQ0NDQiLz4KPHBhdGggZD0iTTE0IDIwIEwxMyAyNyBMMTYgMjQgTDE5IDI3IEwxOCAyMCBaIiBmaWxsPSIjZjk3MzE2Ii8+Cjwvc3ZnPg==) 16 3, auto;
}

/* ---------------------------------------------------------------------
 * Delivery 125 — India map cards (Analytics: "State" / "District" viz)
 * and the matching full-screen page. Leaflet itself ships its own CSS
 * (assets/vendor/leaflet/leaflet.css, loaded alongside this file); the
 * rules below are only this app's own chrome around the map: the
 * segmented Political/Satellite and State/District toggles, the
 * legend, and sizing.
 * --------------------------------------------------------------------- */
.indiamap-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; justify-content: space-between; margin-bottom: .55rem; }
.indiamap-seg { display: inline-flex; border: 1px solid var(--border); border-radius: 999px; overflow: hidden; background: var(--glass-bg); }
.indiamap-seg__btn {
    border: none; background: transparent; color: var(--ink-muted);
    font-size: .78rem; font-weight: 600; padding: .38rem .85rem; cursor: pointer;
    font-family: inherit; line-height: 1.2;
}
.indiamap-seg__btn + .indiamap-seg__btn { border-left: 1px solid var(--border); }
.indiamap-seg__btn.is-active { background: var(--brand-gradient); color: #fff; }
.indiamap-seg__btn:hover:not(.is-active) { background: var(--surface-2); color: var(--ink); }
.indiamap-toolbar__group { display: inline-flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
.indiamap-fullscreen-link {
    display: inline-flex; align-items: center; gap: .3rem;
    font-size: .78rem; font-weight: 600; color: var(--brand);
    text-decoration: none; border: 1px solid var(--border); border-radius: 999px;
    padding: .38rem .8rem; background: var(--glass-bg);
}
.indiamap-fullscreen-link:hover { border-color: var(--brand); text-decoration: none; }
.indiamap-fullscreen-link svg { width: 14px; height: 14px; }

.indiamap { width: 100%; height: 440px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--surface-2); }
.indiamap .leaflet-tile-pane { filter: saturate(0.96); }
.indiamap .leaflet-tooltip { font-family: inherit; font-size: .78rem; border-radius: 8px; }

.indiamap-legend { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem; font-size: .72rem; color: var(--ink-muted); margin-top: .5rem; }
.indiamap-legend__label { font-size: .7rem; }
.indiamap-legend__swatch { width: 16px; height: 10px; border-radius: 2px; display: inline-block; border: 1px solid var(--border); }
.indiamap-legend__none { display: inline-flex; align-items: center; gap: .3rem; margin-left: .4rem; }
.indiamap-legend__swatch--none { background: rgba(150,150,150,0.18); }

/* Full-screen map page (modules/dashboard/map-fullscreen.php) — rendered
   bare (View::renderBare(), no sidebar/topbar) so the map itself gets
   almost the whole viewport. */
.indiamap-full-page { min-height: 100vh; display: flex; flex-direction: column; background: var(--bg-solid); color: var(--ink); padding: 1rem; box-sizing: border-box; }
.indiamap-full-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .6rem; margin-bottom: .7rem; }
.indiamap-full-head h1 { font-family: var(--font-display); font-size: 1.15rem; margin: 0; display: flex; align-items: center; gap: .5rem; }
.indiamap-full-back { font-size: .82rem; color: var(--ink-muted); text-decoration: none; }
.indiamap-full-back:hover { color: var(--brand); text-decoration: none; }
.indiamap-full-body { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.indiamap--full { flex: 1; height: auto; min-height: 420px; }

@media (max-width: 720px) {
    .indiamap { height: 340px; }
    .indiamap-toolbar { flex-direction: column; align-items: stretch; }
    .indiamap-toolbar__group { justify-content: center; }
}

/* =====================================================================
   Delivery 164 — Appreciation Wall, redesigned as Facebook-style post
   cards: avatar+name header, message, image/video BELOW the message,
   a reaction/comment summary line, reaction "pills" + a Comment button,
   the comment thread, and a "write a comment" box pinned to the card's
   own bottom. .wallpost is the full Wall page's own card
   (templates/pages/wall/index.php); .wallmini is the lighter version
   that lives in the Home dashboard's own wall widget
   (templates/partials/home_widgets.php) — both share the same
   .wallpost__avatar / .wallpost__action / .wallpost__react-form classes
   so assets/js/app.js's initAppreciationWall() only needs one set of
   handlers for both.
   ===================================================================== */

.wallfeed { display: flex; flex-direction: column; gap: 1rem; }

.wallpost {
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    backdrop-filter: blur(var(--glass-blur)) saturate(160%);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 1.1rem 1.25rem;
}

.wallpost__head { display: flex; align-items: flex-start; gap: .6rem; margin-bottom: .5rem; }

.wallpost__avatar {
    width: 40px; height: 40px; flex: none;
    border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--brand-gradient);
    color: #fff;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: .85rem;
    letter-spacing: .02em;
}
.wallpost__avatar--sm { width: 30px; height: 30px; font-size: .72rem; }

.wallpost__who { flex: 1 1 auto; min-width: 0; }
.wallpost__name { display: block; font-size: .96rem; }
.wallpost__meta { display: flex; align-items: center; gap: .35rem; font-size: .8rem; color: var(--ink-muted); flex-wrap: wrap; }

.wallpost__delete-form { flex: none; margin: 0; }
.wallpost__iconbtn {
    flex: none;
    background: none; border: none; cursor: pointer;
    color: var(--ink-muted);
    padding: .3rem;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 50%;
}
.wallpost__iconbtn:hover { color: var(--danger); background: rgba(220, 38, 38, 0.1); }

.wallpost__message { margin: 0 0 .6rem; white-space: pre-line; }

.wallpost__media { margin-bottom: .6rem; border-radius: 10px; overflow: hidden; }
.wallpost__media img { display: block; width: 100%; max-height: 420px; object-fit: cover; }

.wallpost__video { position: relative; padding-top: 56.25%; margin-bottom: .6rem; border-radius: 10px; overflow: hidden; }
.wallpost__video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.wallpost__stats {
    display: flex; align-items: center; justify-content: space-between; gap: .5rem;
    padding: .4rem 0;
    border-bottom: 1px solid var(--glass-border);
    margin-bottom: .35rem;
    font-size: .82rem;
    min-height: 1.6rem;
}
.wallpost__stats-reactions { color: var(--ink-muted); letter-spacing: .02em; }
.wallpost__comments-toggle {
    background: none; border: none; cursor: pointer;
    color: var(--ink-muted); font-size: .82rem; padding: 0;
    margin-left: auto;
}
.wallpost__comments-toggle:hover { text-decoration: underline; color: var(--brand); }

.wallpost__actions { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; padding-bottom: .5rem; }

.wallpost__action {
    display: inline-flex; align-items: center; gap: .3rem;
    padding: .4rem .75rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--glass-bg-strong);
    color: var(--ink);
    font-size: .85rem; font-weight: 600;
    cursor: pointer;
}
.wallpost__action:hover { border-color: var(--brand); }
.wallpost__action.is-active { border-color: var(--brand); background: var(--brand-light); color: var(--brand-hover); }
.wallpost__action--comment { margin-left: auto; }
.wallpost__action--sm { padding: .25rem .55rem; font-size: .78rem; }

/* Delivery 165 — "when i hover on the emoji icons...it should pop up a
   window and shows the names of the people who likes that". Each
   .wallpost__react-form (there's one per emoji, wrapping that emoji's
   own button — see templates/pages/wall/index.php and the Home
   dashboard's own wallmini version) is the hover anchor; the tooltip is
   its own absolutely-positioned child, pure CSS to show/hide (no JS
   needed for the hover itself — JS only refreshes its text after a
   reaction changes who's in it, see initAppreciationWall()). A post
   with zero reactions of a given emoji renders the tooltip with
   [hidden] instead, so hovering an empty pill shows nothing. */
.wallpost__react-form { position: relative; display: inline-block; }
.wallpost__react-tooltip {
    position: absolute;
    bottom: calc(100% + 8px);
    left: 50%;
    transform: translateX(-50%) translateY(4px);
    background: var(--ink);
    color: #fff;
    font-size: .78rem;
    line-height: 1.35;
    padding: .45rem .65rem;
    border-radius: 8px;
    max-width: 220px;
    width: max-content;
    text-align: center;
    box-shadow: var(--shadow);
    opacity: 0;
    pointer-events: none;
    transition: opacity .12s ease, transform .12s ease;
    z-index: 5;
}
.wallpost__react-tooltip::after {
    content: '';
    position: absolute; top: 100%; left: 50%;
    transform: translateX(-50%);
    border: 5px solid transparent;
    border-top-color: var(--ink);
}
.wallpost__react-form:hover .wallpost__react-tooltip,
.wallpost__react-form:focus-within .wallpost__react-tooltip {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}
/* Explicit, rather than relying on the browser's own default [hidden]
   rule: the hover rule above never touches `display`, only opacity/
   transform, so there's no actual conflict either way — this is just
   making sure an empty tooltip (no reactors for that emoji) is really
   gone, not just invisible-but-still-there. */
.wallpost__react-tooltip[hidden] { display: none; }

.wallpost__comments-more {
    display: block;
    background: none; border: none; cursor: pointer;
    color: var(--ink-muted); font-size: .82rem;
    padding: .3rem 0; margin-bottom: .2rem;
}
.wallpost__comments-more:hover { text-decoration: underline; color: var(--brand); }

.wallpost__comment { display: flex; align-items: flex-start; gap: .5rem; margin-bottom: .5rem; }
.wallpost__comment-body { flex: 1 1 auto; min-width: 0; }
.wallpost__comment-bubble {
    display: inline-block;
    background: var(--glass-bg-strong);
    border: 1px solid var(--glass-border);
    border-radius: 14px;
    padding: .45rem .7rem;
    font-size: .88rem;
}
.wallpost__comment-bubble strong { display: block; font-size: .82rem; margin-bottom: .1rem; }
.wallpost__comment-bubble span { word-break: break-word; white-space: pre-line; }
.wallpost__comment-foot { display: flex; align-items: center; gap: .6rem; margin: .2rem .2rem 0; font-size: .75rem; color: var(--ink-muted); }
.wallpost__comment-delete-form { margin: 0; }
.wallpost__comment-delete {
    background: none; border: none; cursor: pointer; padding: 0;
    color: var(--ink-muted); font-size: .75rem; font-weight: 600;
}
.wallpost__comment-delete:hover { color: var(--danger); text-decoration: underline; }

.wallpost__comment-form {
    display: flex; align-items: center; gap: .5rem;
    margin-top: .3rem;
}
.wallpost__comment-input {
    flex: 1 1 auto;
    border: 1px solid var(--border);
    background: var(--glass-bg-strong);
    border-radius: 999px;
    padding: .5rem .9rem;
    font-size: .88rem;
    color: var(--ink);
}
.wallpost__comment-input:focus { outline: 2px solid var(--brand); outline-offset: 1px; border-color: var(--brand); }
.wallpost__iconbtn--send { color: var(--brand); }
.wallpost__iconbtn--send:hover { background: var(--brand-light); color: var(--brand-hover); }

/* ---- The lighter Home-dashboard version (templates/partials/home_widgets.php) ---- */
.wallmini { display: flex; flex-direction: column; }
.wallmini__item {
    padding: .7rem 0;
    border-bottom: 1px solid var(--glass-border);
}
.wallmini__item:first-child { padding-top: 0; }
.wallmini__head { display: flex; align-items: center; gap: .5rem; margin-bottom: .3rem; }
.wallmini__who { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.wallmini__who strong { font-size: .86rem; }
.wallmini__meta { font-size: .75rem; color: var(--ink-muted); display: flex; align-items: center; gap: .3rem; }
.wallmini__text { margin: 0 0 .4rem; font-size: .85rem; word-break: break-word; }
.wallmini__media { margin-bottom: .4rem; border-radius: 8px; overflow: hidden; }
.wallmini__media img { display: block; width: 100%; max-height: 160px; object-fit: cover; }
.wallmini__has-video { display: inline-flex; align-items: center; gap: .25rem; font-size: .78rem; color: var(--ink-muted); margin-bottom: .4rem; }
.wallmini__actions { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
.wallmini__comments-link { font-size: .78rem; color: var(--ink-muted); margin-left: auto; text-decoration: none; }
.wallmini__comments-link:hover { text-decoration: underline; color: var(--brand); }

@media (max-width: 480px) {
    .wallpost { padding: .9rem 1rem; }
    .wallpost__action--comment { margin-left: 0; }
}
