/* Mycroft Portal v2 -- Stylesheet
 *
 * Design tokens from Council of Elrond consultation:
 * - Primary: #22A7F0 (Mycroft Blue)
 * - Accent: #FFC300 (Gold)
 * - Typography: Inter (body), Fira Code (code)
 * - Icons: Lucide (inline SVG)
 * - Layout: Bottom nav mobile, icon-rail desktop
 *
 * Docs view uses a mobile-first approach: document list is the
 * default view on mobile, tapping a doc switches to detail view
 * with a back button.
 */

/* === CSS Custom Properties (Design Tokens) === */

:root {
    /* Colors -- Light Mode (default) — Provost/cockpit grey palette */
    --color-primary: #22A7F0;
    --color-primary-dark: #0f7fc4;
    --color-primary-light: #e8f4fb;
    --color-secondary: #005B91;
    --color-accent: #FFC300;
    --color-accent-dark: #E0AB00;

    /* Part A: grey (NOT white) light palette */
    --color-bg: #c3cdd8;          /* backdrop field base — never used directly on body; body uses the CSS gradient */
    --orb-backdrop: #c3cdd8;      /* light mode: matches light body gradient top stop — orb sits on grey field, no box */
    --color-surface: #F1F3F4;
    --color-surface-hover: #E9ECEF;
    --color-border: #D6DADC;
    --color-border-light: #D6DADC;

    --color-text: #1c2733;
    --color-text-secondary: #4a5a6b;
    --color-text-muted: #6C7A89;

    /* Chat bubble colors — on dark glass panel, text must be light */
    --color-bubble-user: #22A7F0;
    --color-bubble-user-text: #FFFFFF;
    --color-bubble-ai: rgba(255,255,255,0.10);
    --color-bubble-ai-text: #e8f0f8;

    --color-success: #22C55E;
    --color-warning: #F59E0B;
    --color-danger: #EF4444;

    /* Typography — Noto Sans per farm brand (fallback chain: Segoe UI → Inter → Helvetica Neue → Arial) */
    --font-body: 'Noto Sans', 'Segoe UI', 'Inter', 'Helvetica Neue', Arial, sans-serif;
    --font-code: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
    --font-size-xs: 0.75rem;
    --font-size-sm: 0.875rem;
    --font-size-base: 1rem;
    --font-size-lg: 1.125rem;
    --font-size-xl: 1.25rem;
    --font-size-2xl: 1.5rem;

    /* Spacing */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.25rem;
    --space-6: 1.5rem;
    --space-8: 2rem;
    --space-10: 2.5rem;

    /* Layout */
    --header-height: 56px;
    --bottomnav-height: 56px;
    --radius-sm: 6px;
    --radius-md: 10px;
    --radius-lg: 16px;
    --radius-full: 9999px;
    --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
    --shadow-md: 0 4px 6px rgba(0,0,0,0.07);
    --shadow-lg: 0 10px 15px rgba(0,0,0,0.1);

    /* Transitions */
    --transition-fast: 150ms ease;
    --transition-normal: 250ms ease;

    /* T32: af-card drop shadow — visible on both light and dark backgrounds */
    --af-card-shadow: 0 8px 24px rgba(0,0,0,0.22), 0 2px 6px rgba(0,0,0,0.14);
}

/* === Dark Mode === */

[data-theme="dark"] {
    --color-primary: #22A7F0;
    --color-primary-dark: #78C3EE;
    --color-primary-light: #1a3a4a;
    --color-secondary: #78C3EE;

    --color-bg: #1c2733;
    --orb-backdrop: #16212e;      /* dark mode: EXACT match to dark body gradient darkest stop — byte-identical to wall */
    --color-surface: #1a2430;
    --color-surface-hover: #243040;
    --color-border: #2d3d4e;
    --color-border-light: #243040;

    --color-text: #E0E0E0;
    --color-text-secondary: #AAAAAA;
    --color-text-muted: #777777;

    /* T32: af-card drop shadow — heavier for dark mode visibility */
    --af-card-shadow: 0 8px 32px rgba(0,0,0,0.55), 0 2px 8px rgba(0,0,0,0.35);

    /* Dark mode chat on dark glass — still readable */
    --color-bubble-user: #1B8BC7;
    --color-bubble-user-text: #FFFFFF;
    --color-bubble-ai: rgba(255,255,255,0.07);
    --color-bubble-ai-text: #c8d8ea;
}

/* === Base Reset & Typography === */

*, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

html {
    font-size: 16px;
    -webkit-text-size-adjust: 100%;
    /* Match the app backdrop so the area revealed when the mobile keyboard
       opens (below <body>) is never the browser's default WHITE. The product owner
       2026-07-23: "white patch appearing from the bottom" on input focus. */
    background: var(--color-bg);
}

body {
    font-family: var(--font-body);
    font-size: var(--font-size-base);
    line-height: 1.6;
    color: var(--color-text);
    /* Part B: 3D depth backdrop field — light mode silver-blue depth field */
    background:
        radial-gradient(1300px 900px at 16% 6%,  rgba(34,167,240,0.22),  transparent 60%),
        radial-gradient(1100px 820px at 88% 94%, rgba(64,219,176,0.18),  transparent 60%),
        radial-gradient(1000px 760px at 60% 48%, rgba(44,62,80,0.10),    transparent 70%),
        linear-gradient(160deg, #c3cdd8 0%, #b6c3d1 45%, #aeb9c6 100%);
    background-attachment: fixed;
    overflow: hidden;
    height: 100vh;      /* fallback */
    height: 100dvh;     /* dynamic viewport — see #app note */
    width: 100vw;
}

/* Part B: Dark mode backdrop field — same radial glows over dark navy base */
[data-theme="dark"] body {
    background:
        radial-gradient(1300px 900px at 16% 6%,  rgba(34,167,240,0.22),  transparent 60%),
        radial-gradient(1100px 820px at 88% 94%, rgba(64,219,176,0.18),  transparent 60%),
        radial-gradient(1000px 760px at 60% 48%, rgba(44,62,80,0.10),    transparent 70%),
        linear-gradient(160deg, #16212e 0%, #10212e 100%);
    background-attachment: fixed;
}

a { color: var(--color-primary); text-decoration: none; }
a:hover { text-decoration: underline; }

code, pre {
    font-family: var(--font-code);
    font-size: var(--font-size-sm);
}

pre {
    background: var(--color-surface);
    border: 1px solid var(--color-border-light);
    border-radius: var(--radius-sm);
    padding: var(--space-4);
    overflow-x: auto;
    margin: var(--space-4) 0;
}

code:not(pre code) {
    background: var(--color-surface);
    padding: 0.15em 0.4em;
    border-radius: 3px;
    font-size: 0.9em;
}

/* === App Layout === */

#app {
    display: flex;
    height: 100vh;      /* fallback */
    height: 100dvh;     /* mobile Chrome: track the DYNAMIC viewport (URL bar +
                           keyboard) so the app is exactly the VISIBLE height —
                           100vh is the URL-bar-HIDDEN (large) height, which made
                           the fixed composer land below the visible bottom (gap)
                           and the header shift off-screen on send. The product owner 2026-07-24. */
    width: 100vw;
    overflow: hidden;
    background: transparent;
}

.theme-toggle {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    background: none;
    border: 1px solid var(--color-border-light);
    color: var(--color-text-secondary);
    cursor: pointer;
    font: inherit;
    font-size: var(--font-size-sm);
    width: 100%;
    transition: all var(--transition-fast);
}

.theme-toggle:hover {
    background: var(--color-surface-hover);
}

.theme-toggle svg {
    width: 16px;
    height: 16px;
}

/* --- Main Content --- */

.main-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    height: 100vh;      /* fallback */
    height: 100dvh;     /* dynamic viewport — see #app note */
    overflow: hidden;
    min-width: 0;
    width: 100%;
    /* Transparent — the body backdrop field shows through */
    background: transparent;
}

/* Portal v2: no sidebar — the chat canvas is a single focused column, centered
   on the page, full screen width up to a comfortable reading max-width. */
.main-content > .view {
    width: 100%;
    margin: 0 auto;
}

.view {
    display: none;
    flex: 1;
    overflow: hidden;
    flex-direction: column;
}

.view.active {
    display: flex;
}

/* Removed views (terminal, tasks folded into chat): never show regardless of .active */
.view--removed,
.view--removed.active {
    display: none !important;
}

/* Terminal overlay — floats over the working area while #view-chat stays active.
   Mirrors the panel-full pattern (position:fixed, z-index between stage and modals).
   Left edge starts at 56px (icon-rail width) so the rail stays visible + clickable.
   Hidden by default via .view--removed; toggled by .terminal-overlay-open class
   (added/removed by openTerminalOverlay / closeTerminalOverlay in app.js). */
#view-terminal.terminal-overlay-open {
    display: flex !important;
    position: fixed !important;
    top: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    left: 56px !important; /* icon-rail width — keep rail visible */
    width: auto !important;
    height: auto !important;
    flex-direction: column !important;
    z-index: 200; /* above orb(2), stage(4), panel-full(50), below modals(1000) */
    background: rgba(14, 20, 30, 0.97);
    border-left: 1px solid rgba(34, 167, 240, 0.40);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    overflow: hidden;
}

/* On mobile (rail hidden), terminal overlay goes edge-to-edge */
@media (max-width: 768px) {
    #view-terminal.terminal-overlay-open {
        left: 0 !important;
    }
}

/* Close button inside the terminal overlay header */
.terminal-overlay-close {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding: var(--space-2) var(--space-4);
    border-bottom: 1px solid rgba(34, 167, 240, 0.20);
    gap: var(--space-3);
    flex-shrink: 0;
}

.terminal-overlay-close-btn {
    background: none;
    border: 1px solid rgba(34, 167, 240, 0.35);
    border-radius: var(--radius-md);
    color: var(--color-text-muted);
    cursor: pointer;
    padding: var(--space-1) var(--space-3);
    font-size: var(--font-size-sm);
    font-family: var(--font-body);
    display: flex;
    align-items: center;
    gap: var(--space-2);
    transition: background 160ms, border-color 160ms, color 160ms;
}

.terminal-overlay-close-btn:hover {
    background: rgba(34, 167, 240, 0.12);
    border-color: rgba(34, 167, 240, 0.60);
    color: var(--color-text);
}

/* --- Mobile Header --- */

.mobile-header {
    display: none;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
    min-height: var(--header-height);
}

.mobile-header .sidebar-portrait {
    width: 32px;
    height: 32px;
}

.mobile-header .sidebar-title {
    font-size: var(--font-size-base);
}

/* --- Bottom Nav (Mobile) --- */

.bottom-nav {
    display: none;
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
    min-height: var(--bottomnav-height);
}

.bottom-nav-inner {
    display: flex;
    justify-content: space-around;
    align-items: center;
    height: 100%;
}

.bottom-nav-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: var(--space-2);
    color: var(--color-text-muted);
    cursor: pointer;
    border: none;
    background: none;
    font: inherit;
    font-size: var(--font-size-xs);
    min-width: 44px;
    min-height: 44px;
    justify-content: center;
    transition: color var(--transition-fast);
}

.bottom-nav-item.active {
    color: var(--color-primary);
}

.bottom-nav-item svg {
    width: 22px;
    height: 22px;
}

/* === Chat View === */

.chat-container {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.chat-header {
    padding: var(--space-2) var(--space-4);
    padding-left: 80px; /* Room for the orb on the left */
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    /* A GRADIENT FADE, NOT A BAR, AND NOT NOTHING EITHER.
       Opaque in the page's own base colour at the very top, through ~70% at
       the midpoint, to fully transparent by the bottom of the header's own
       box. No border, no divider, no hard edge anywhere -- transcript
       scrolling up simply fades out of visibility instead of meeting a line.

       Flat `transparent` (what this was) is the other failure: the icons
       then sit on whatever message happens to be behind them and stop being
       legible. The opaque top of this fade is the ground they stand on.

       The extra bottom padding is load-bearing, not spacing: the fade needs
       vertical room to happen BELOW the icon row, or it compresses into a
       visible band right behind the icons -- which is the "there is
       something there" this replaces. */
    background: none;
    padding-bottom: var(--space-6, 32px);
    color: #e8f0f8;
    min-height: 48px;
    /* An OVERLAY, not a flow item. Absolutely positioned over the scroller
       so the transcript passes underneath and fades, rather than starting
       below a reserved strip. .chat-messages reserves matching padding-top
       so the first message is never born underneath it.
       z-index sits above the composer (151) and the minimap (100) but below
       modals (200): any dropdown opened from a header icon lives inside this
       element, and a child's z-index cannot escape its parent's stacking
       context -- so the header itself has to outrank anything it must appear
       over. */
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 160;
}

/* ---- Option A shell: robot-eyes header ---- */

/* Wrapper: agent name only (name-only after #58 — the eyes/orb live in #orbHero
   and travel into chat via the orb-chat-docked CSS transform, NOT through this
   element). Sits in grid COLUMN 1, LEFT-aligned on every width.
   NOTE (#58 guardrail): the orb target is #orbHero (position:absolute, centered
   in orb-chat-docked) and is unaffected by this element's grid placement — moving
   the NAME left does not move the orb. */
.robot-eyes-header {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: 6px;
    text-align: left;
    /* Keep the header click-through active for tap-to-raise (#89) —
       no pointer-events override; canvas is a plain element, clicks bubble up */
}

/* Dot + name row: inline flex row below (formerly: below the eyes canvas) */
.companion-name-row {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 6px;
    position: relative;  /* stacking context; z-index:1 keeps it above any future siblings */
    z-index: 1;
}

/* Agent name (name-only after #58 — header eyes + orb retired): Noto Sans ≥16px, white — WCAG AA on dark header */
.robot-eyes-header .companion-badge-name {
    font-family: var(--font-body, 'Noto Sans', sans-serif);
    font-size: 1.25rem;
    font-weight: 600;
    /* NOT italic (reverted by request). It used to be, mirrored from the
       plain .companion-badge-name rule below -- but italic's rightward
       glyph slant pushed the trailing letter past this element's
       max-width, and overflow:hidden clipped it (e.g. "Cujo" losing its
       final "o") even on names nowhere near actually needing truncation.
       font-style is set explicitly here (not just left to inherit
       'normal') because this selector is the more specific of the two, so
       it's the one that actually governs the header instance -- an
       italic left set on the plain rule would otherwise silently win here
       via specificity if this block didn't say otherwise. */
    font-style: normal;
    color: #FFFFFF;          /* White — passes AA on dark header */
    white-space: nowrap;
    /* Raised with the font size. This cap plus text-overflow:ellipsis is what
       stops a long agent name pushing the header layout around, so it has to
       grow in step -- left at 200px, a name that fitted at 1rem starts
       truncating at 1.25rem for no reason the reader can see. */
    max-width: 260px;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Belt-and-suspenders: ID-level specificity (1,1,0) overrides any cascade reset */
.robot-eyes-header #companionName { color: #FFFFFF; }

[data-theme="light"] .robot-eyes-header .companion-badge-name {
    color: #22A7F0;          /* Mycroft Blue passes AA on light header too */
}

@media (max-width: 768px) {
    .robot-eyes-header {
        justify-self: start;
        align-items: flex-start;
    }
}

/* ---- / Option A shell ---- */

.chat-header-title {
    font-size: var(--font-size-base);
    font-weight: 600;
    color: #e8f0f8;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ---- Chat header left column: identity badge + companion bar ---- */

/* Identity badge: compact pill — colored ball + agent name (direct child of .chat-header grid, col-1) */
.companion-badge {
    grid-column: 1;
    margin-bottom: 2px;
    display: inline-flex;
    flex-direction: row;
    align-items: center;
    gap: 6px;
    background: rgba(44,62,80,0.70);        /* Navy semi-transparent */
    border: 1px solid rgba(34,167,240,0.25);
    border-radius: 20px;
    padding: 3px 10px 3px 5px;
    max-width: 100%;
    overflow: hidden;
}

[data-theme="light"] .companion-badge {
    background: rgba(44,62,80,0.12);
    border-color: rgba(34,167,240,0.30);
}

/* Colored ball — background-color set dynamically by initAgentName() */
.companion-badge-ball {
    display: inline-block;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    flex-shrink: 0;
    /* Fallback color until JS sets it */
    background-color: #22A7F0;
}

/* Agent name label */
.companion-badge-name {
    font-family: var(--font-body, 'Noto Sans', sans-serif);
    font-size: 0.78rem;
    font-weight: 600;
    /* NOT italic (reverted by request -- see .robot-eyes-header
       .companion-badge-name's own comment for why: italic clipped the
       trailing letter of short names against this element's overflow
       box). The only place this class currently renders is nested inside
       .robot-eyes-header, where that more specific rule already governs
       and wins by specificity -- kept in sync here anyway in case
       anything ever renders this class on its own. */
    font-style: normal;
    color: #e8f0f8;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 120px;
}

[data-theme="light"] .companion-badge-name {
    color: #2C3E50;
}

/* ---- Companion bar (inc1a): 4 app-icon buttons in col-1 of the chat header ---- */

/* Fix B: wrap bar in a grouped rail container so it reads as a deliberate launcher.
   Hidden by default (splash S0 state); revealed in chat via .orb-chat-docked on #view-chat,
   which enterChat() adds and enterSplash() removes — no inline style needed. */
.companion-bar {
    display: none;                          /* splash: hidden; enterChat() reveals via .orb-chat-docked */
    flex-direction: row;
    align-items: center;
    gap: 8px;                               /* Fix B: wider inter-icon spacing */
    /* Grouped container: faint rounded pill */
    background: rgba(34,167,240,0.08);
    border: 1px solid rgba(34,167,240,0.22);
    border-radius: 10px;
    padding: 3px 6px;
    /* Stays within col-1; no overflow into center/right columns */
    overflow: hidden;
    flex-shrink: 0;
}

/* Chat state: orb docked to header band → show the companion bar */
.orb-chat-docked .companion-bar {
    display: flex;
}

[data-theme="dark"] .companion-bar {
    background: rgba(34,167,240,0.10);
    border-color: rgba(34,167,240,0.28);
}

.app-icon-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;                            /* Fix B: enlarged from 32px */
    height: 36px;
    padding: 0;
    border: none;
    border-radius: 7px;
    background: none;
    /* Fix B: lifted resting contrast — was 0.55 opacity muted, now clearly visible */
    color: rgba(232,240,248,0.78);
    cursor: pointer;
    flex-shrink: 0;
    transition: background var(--transition-fast, 120ms ease), color var(--transition-fast, 120ms ease);
    font-family: var(--font-body, 'Noto Sans', sans-serif);
}

/* Fix B: light-mode resting icon contrast */
[data-theme="light"] .app-icon-btn {
    color: rgba(44,62,80,0.72);
}

.app-icon-btn svg {
    width: 20px;                            /* Fix B: enlarged from 18px */
    height: 20px;
    pointer-events: none;
}

.app-icon-btn:hover {
    background: rgba(34,167,240,0.16);
    color: #e8f0f8;
}

[data-theme="dark"] .app-icon-btn:hover {
    background: rgba(34,167,240,0.20);
}

[data-theme="light"] .app-icon-btn:hover {
    background: rgba(34,167,240,0.18);
    color: #1c2733;
}

/* Active state: solid Mycroft Blue pill — unmistakably active (Fix A) */
.app-icon-btn.active {
    background: #22A7F0;
    color: #fff;
    /* Secondary cue: bottom border accent so state never relies on color alone */
    box-shadow: 0 2px 0 0 rgba(255,255,255,0.55), inset 0 0 0 1px rgba(255,255,255,0.20);
}

[data-theme="dark"] .app-icon-btn.active {
    background: #22A7F0;
    color: #fff;
    box-shadow: 0 2px 0 0 rgba(255,255,255,0.40), inset 0 0 0 1px rgba(255,255,255,0.15);
}

/* ---- Media player (T64 + T-player) — adaptive media transport, top-right of the chat header ----
   Absorbed the old Universal Remote (#urControls, formerly in the companion
   bar): same button/icon classes, same show/hide-on-media-activity behavior,
   now with prev/next track transport added alongside play/pause/mute/volume. */

/* Persistent control bar (customer feedback 2026-07-19: "should persist and
   not disappear, always stay") — display:none is the pre-load state only;
   initUniversalRemote() adds .ur-visible at page load and never removes it
   again. Idle (nothing loaded) reads via disabled controls below, not via
   hiding the bar. */
#mediaPlayer {
    display: none;
    flex-direction: row;
    align-items: center;
    gap: 4px;
    flex-shrink: 0;
}

#mediaPlayer.ur-visible {
    display: flex;
}

/* Hidden per the product owner 2026-07-23: the media player has no purpose until the
   audio-emit feature (daemon ui.card.audio, cm-sandbox) is wired — that work
   is deferred — and the bar was overlapping the "Companion" header name on
   mobile + eating space. This supersedes the 2026-07-19 "player always
   visible" directive per the customer's new call. REVERSIBLE: delete this one
   rule to restore the player once audio emit ships. */
#mediaPlayer,
#mediaPlayer.ur-visible {
    display: none !important;
}

/* Vertical divider between app icons and the remote */
.ur-divider {
    width: 1px;
    height: 22px;
    background: rgba(34,167,240,0.35);
    border-radius: 1px;
    margin: 0 2px;
    flex-shrink: 0;
}

/* Remote action buttons — match app-icon-btn sizing/shape */
.ur-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: none;
    border-radius: 7px;
    background: none;
    color: rgba(232,240,248,0.78);
    cursor: pointer;
    flex-shrink: 0;
    transition: background var(--transition-fast, 120ms ease), color var(--transition-fast, 120ms ease);
    font-family: var(--font-body, 'Noto Sans', sans-serif);
}

[data-theme="light"] .ur-btn {
    color: rgba(44,62,80,0.72);
}

.ur-btn:hover {
    background: rgba(34,167,240,0.16);
    color: #e8f0f8;
}

[data-theme="light"] .ur-btn:hover {
    background: rgba(34,167,240,0.18);
    color: #1c2733;
}

.ur-btn svg {
    width: 16px;
    height: 16px;
    pointer-events: none;
}

/* Prev/next: disabled when there's no adjacent track to step to (single/no
   audio track in the transcript) — see _syncTrackButtons() in face-integration.js */
.ur-btn:disabled {
    color: rgba(232,240,248,0.28);
    cursor: default;
    pointer-events: none;
}

[data-theme="light"] .ur-btn:disabled {
    color: rgba(44,62,80,0.25);
}

/* Play/Pause icon toggling: show play when paused, pause when playing */
#urPlayPause .ur-icon-pause { display: none; }
#urPlayPause .ur-icon-play  { display: block; }

#urPlayPause.ur-playing .ur-icon-pause { display: block; }
#urPlayPause.ur-playing .ur-icon-play  { display: none; }

/* Mute icon toggling: show unmuted icon by default, muted icon when muted */
#urMute .ur-icon-muted   { display: none; }
#urMute .ur-icon-unmuted { display: block; }

#urMute.ur-muted .ur-icon-muted   { display: block; }
#urMute.ur-muted .ur-icon-unmuted { display: none; }

/* Volume slider — compact inline range input */
.ur-volume {
    -webkit-appearance: none;
    appearance: none;
    width: 68px;
    height: 4px;
    border-radius: 2px;
    background: rgba(34,167,240,0.30);
    outline: none;
    cursor: pointer;
    flex-shrink: 0;
    accent-color: #22A7F0;
    /* Provide non-color cue (label + border) per brand: never color-only state */
    border: 1px solid rgba(34,167,240,0.22);
}

.ur-volume::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #22A7F0;
    cursor: pointer;
}

.ur-volume::-moz-range-thumb {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #22A7F0;
    border: none;
    cursor: pointer;
}

[data-theme="light"] .ur-volume {
    background: rgba(34,167,240,0.22);
}

/* Idle player (nothing loaded) — greyed to match .ur-btn:disabled instead of
   the whole bar hiding (customer feedback 2026-07-19). */
.ur-volume:disabled {
    opacity: 0.4;
    cursor: default;
}

/* Center column: panel toggle + status stacked vertically.
   grid-column:2 pins this to the centered `auto` grid column so the chevrons sit
   on the SAME row as the name (defect c) and are horizontally CENTERED in the
   header bar (defect b). Previously robot-eyes-header spanned 1/-1 and shoved
   this onto a 2nd row landing ~left. */
.chat-header-center {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 12px;
}

/* T89 (2c): Chat header must sit above the spotlight overlay (z:150) and
   the pinned input (z:151) so the maximize control is NEVER covered.
   position:relative is needed for z-index to take effect. */
#view-chat .chat-container .chat-header {
    /* MUST repeat the overlay positioning from the base .chat-header rule.
       This selector is more specific, so a bare `position: relative` here
       silently defeats the base rule's `position: absolute` -- the header
       goes back to being a flow item, the transcript starts below it
       instead of scrolling under it, and .chat-messages' matching
       padding-top then reserves space for a header that is not overlaying
       anything: a dead strip at the top with the fade painted over it. */
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 200;
}

/* Agent status display — center of chat header */
.agent-status-display {
    font-size: var(--font-size-xs);
    color: rgba(200,215,230,0.70);
    white-space: nowrap;
    text-align: center;
    min-height: 14px;
    letter-spacing: 0.01em;
    transition: opacity var(--transition-normal);
}

.agent-status-display:empty {
    /* idle: invisible AND zero vertical footprint so the empty status span never
       forces the header taller than the single chevron row (defect c — reclaim
       the wasted double-height). When status has text it renders normally below
       the chevrons. */
    opacity: 0;
    min-height: 0;
    height: 0;
    margin: 0;
    overflow: hidden;
}

.session-selector {
    font-size: var(--font-size-sm);
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    cursor: pointer;
}

.chat-messages {
    flex: 1;
    overflow-y: auto;
    padding: var(--space-4) var(--space-5);
    /* T-input-occlusion fix: 140px was a guess at the fixed .chat-input-area's
       real rendered height + the typing-indicator's reserved space. Measured
       live it was NOT enough (input real height + raise-wrapper + glow
       exceeds 140px on real viewports), so the last message's bottom edge
       rendered UNDER the input. --chat-input-clearance is now computed live
       by updateChatLayoutClearance() in app.js from actual getBoundingClientRect()
       numbers (input top vs. chat-messages bottom, plus the typing indicator's
       own height) and kept in sync on resize / panel-level change / show-typing.
       140px stays as the pre-JS fallback so layout isn't broken before first paint. */
    padding-bottom: var(--chat-input-clearance, 140px) !important;
    /* Top reserve for the header, which is now an absolutely-positioned
       overlay rather than a flow item -- without this the first message is
       born underneath it instead of below it. The value is the header's own
       height (48px min + its vertical padding) plus its fade, so a message
       has cleared the gradient entirely before it reaches the icons.
       Deliberately NOT tight: the fade is meant to catch a message on its way
       past, not to be the exact line where content starts. */
    padding-top: 104px;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* Bottom-anchor (the product owner 2026-07-23): short/first message sits at the BOTTOM,
       transcript grows upward (iMessage-style). Implemented via the `::before`
       spacer with margin-top:auto (below) — NOT justify-content:flex-end, which
       BREAKS upward scrolling once content overflows: flex-end pushes the top of
       the content out of the scrollable area and it becomes unreachable (the product owner
       2026-07-24: "unable to go up"). The auto-margin spacer bottom-anchors when
       short and collapses to 0 when content overflows, so scroll-to-top works. */
    gap: var(--space-4);
    background: transparent;
    min-height: 120px;
}

/* Bottom-anchor spacer (replaces justify-content:flex-end — see .chat-messages
   note). margin-top:auto absorbs free space ABOVE the transcript when it's
   shorter than the viewport (pushing messages to the bottom), and collapses to
   0 once content overflows so the user can scroll all the way to the top. */
.chat-messages::before {
    content: '';
    margin-top: auto;
}

.chat-messages::after {
    display: none;
}

/* =========================================================
   Portal v2: Chat Date Minimap (DeepSeek style)
   ========================================================= */

/* Outer: sized to the transcript pane, pointer-events:none ALWAYS -- must
   never block clicks on messages behind it, far from the ticks. Centered
   with top:0;bottom:0;display:flex;align-items:center rather than the
   previous top:50%;transform:translateY(-50%) -- that transform is
   harmless on its own today (nothing here is position:fixed) but is
   exactly the pattern that silently breaks a position:fixed DESCENDANT's
   containing block if one is ever added inside this stack later. Avoiding
   the pattern now means that trap can't recur. */
.chat-minimap-outer {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 8px;
    display: flex;
    align-items: center;
    z-index: 100;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.chat-minimap-outer.minimap-visible {
    opacity: 1;
}

/* Middle: sized to just popup + gap + strip (NOT the full outer height/
   width) -- this is the ONE element with real hover detection. Because the
   gap between the popup and the strip is INSIDE this box, moving the
   cursor across that gap never fires mouseleave on either side, which is
   what caused the popup to flicker shut when this was two separate
   hoverable elements. */
.chat-minimap-hover-region {
    display: flex;
    align-items: center;
    gap: 10px;
    pointer-events: auto;
}

.chat-history-panel {
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* CLOSED STATE IS width:0, NOT just opacity:0 -- and width/padding are
       NOT in the transition list below (they snap instantly, opacity/
       transform still animate for the fade). This is load-bearing, not
       cosmetic: before this, the panel reserved its full 340px of layout
       width even while invisible, which made .chat-minimap-hover-region's
       real hit-testing box (pointer-events:auto) a ~350px-wide invisible
       strip sitting over the message transcript -- hovering anywhere in
       that dead zone (nowhere near the actual tick strip) silently opened
       the popup, AND intercepted clicks meant for messages underneath
       before they ever reached them. That was the "very easy to mistap,
       opens when I'm nowhere near the strip" bug. Collapsing width to 0
       when closed means the hover-region's box IS the tick strip's real
       box -- hovering only opens it when the cursor is actually over the
       strip, and there's no invisible click-eating area left. Width
       snapping (not transitioning) matters too: a slow width transition
       means the hit-box lags behind the mouse while opening, so a fast
       mouse move from strip toward the popup can end up briefly outside
       the (still-growing) box mid-transition and self-cancel via
       mouseleave -- snapping avoids that race entirely. */
    width: 0;
    max-height: 60vh;
    /* Split, not `overflow: hidden` -- that killed vertical scrolling
       inside the day list entirely (found live: "there's no scroll on the
       minimap anymore"). overflow-x stays hidden so content doesn't peek
       out sideways while width is animating/collapsed; overflow-y stays
       auto so the list scrolls once it's open and taller than max-height. */
    overflow-x: hidden;
    overflow-y: auto;
    padding: 0;
    box-sizing: border-box;
    border-radius: 16px;
    /* Same frosted-glass tint as .chat-input-wrapper (the composer) --
       sheen gradient + translucent navy + blur, not a flat dark fill. */
    background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02) 45%, rgba(255,255,255,0) 100%), rgba(20, 30, 44, 0.55);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.14);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.28);
    opacity: 0;
    pointer-events: none;
    transform: translateX(6px);
    transition: opacity 0.15s ease, transform 0.15s cubic-bezier(0.2, 0, 0, 1);
    /* Modern inset scrollbar -- thin, rounded, only visible where it
       overlaps content rather than reserving a hard gutter (Firefox);
       WebKit gets the equivalent via the rules below since it doesn't
       support `scrollbar-width`/`scrollbar-color`. */
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}

.chat-history-panel::-webkit-scrollbar {
    width: 6px;
}

.chat-history-panel::-webkit-scrollbar-track {
    background: transparent;
}

.chat-history-panel::-webkit-scrollbar-thumb {
    background: rgba(255, 255, 255, 0.18);
    border-radius: 3px;
}

.chat-history-panel::-webkit-scrollbar-thumb:hover {
    background: rgba(255, 255, 255, 0.32);
}

[data-theme="light"] .chat-history-panel {
    /* Same tint .chat-input-wrapper uses in light theme -- see its own
       comment: "night has a tint day doesn't", pale cool-blue instead of
       flat white so the sheen gradient actually shows. */
    background: linear-gradient(180deg, rgba(255,255,255,0.55), rgba(255,255,255,0.12) 45%, rgba(255,255,255,0) 100%), rgba(232,240,248,0.72);
    border-color: rgba(44,62,80,0.16);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.12);
    scrollbar-color: rgba(0, 0, 0, 0.18) transparent;
}

[data-theme="light"] .chat-history-panel::-webkit-scrollbar-thumb {
    background: rgba(0, 0, 0, 0.18);
}

[data-theme="light"] .chat-history-panel::-webkit-scrollbar-thumb:hover {
    background: rgba(0, 0, 0, 0.32);
}

/* :hover reveal for mouse users -- pure CSS, no JS state needed since a
   real hover state already exists natively. Touch has no hover at all, so
   ChatMinimap.jsx tracks an explicit tap-opened `popupOpen` boolean and
   applies .popup-open here as the touch equivalent of :hover -- same
   visual result, different trigger. */
.chat-minimap-hover-region:hover .chat-history-panel,
.chat-minimap-hover-region.popup-open .chat-history-panel {
    width: 340px;
    padding: 8px;
    opacity: 1;
    pointer-events: auto;
    transform: translateX(0);
}

/* Explicit close override after a jump. :hover can't be cleared from JS,
   so a jump click adds this class for a moment to force the panel shut
   even while the cursor is still physically resting over the strip --
   otherwise "click a day, it should jump AND close" only half-worked: the
   jump happened but the popup stayed open under an unmoved cursor. Cleared
   again on the next real mouseenter/tap-open so hovering still works
   afterward. */
.chat-minimap-hover-region.force-closed .chat-history-panel {
    width: 0 !important;
    padding: 0 !important;
    opacity: 0 !important;
    pointer-events: none !important;
}

.chat-history-item {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 7px 10px;
    border-radius: 6px;
    cursor: pointer;
}

.chat-history-item:hover {
    background: rgba(255, 255, 255, 0.06);
}

.chat-history-item-preview {
    font-size: 0.82rem;
    font-weight: 500;
    color: rgba(225, 232, 240, 0.92);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chat-history-item-date {
    font-size: 0.68rem;
    font-weight: 500;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: rgba(200, 215, 230, 0.5);
    white-space: nowrap;
}

.chat-history-item-active {
    background: rgba(255, 255, 255, 0.05);
}

.chat-history-item-active .chat-history-item-preview,
.chat-history-item-active .chat-history-item-date {
    color: var(--color-primary);
}

[data-theme="light"] .chat-history-item:hover {
    background: rgba(0, 0, 0, 0.04);
}

[data-theme="light"] .chat-history-item-preview {
    color: rgba(30, 40, 50, 0.88);
}

[data-theme="light"] .chat-history-item-date {
    color: rgba(40, 50, 60, 0.5);
}

[data-theme="light"] .chat-history-item-active {
    background: rgba(0, 0, 0, 0.035);
}

.chat-minimap-strip {
    display: flex;
    flex-direction: column;
    height: 50vh;
    justify-content: space-evenly;
    align-items: flex-end;
}

.minimap-item {
    pointer-events: auto; /* Make item clickable */
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    /* Real click/tap target is bigger than the visible marker -- the
       6x2px marker alone is a mistap magnet, especially on touch. Padding
       widens the hit box without changing how big the marker looks. */
    padding: 8px 4px;
    cursor: pointer;
    /* Each tick is React-keyed by date, so when the sliding window moves
       (scrolling further into history), ticks that leave the window
       unmount and ticks entering it mount as BRAND NEW elements -- there's
       no shared element to transform-animate a slide on. What CAN be
       animated is each new tick settling into place on mount, which is
       what actually reads as "smooth" here: the active highlight staying
       centered while the ticks around it visibly ease in, instead of
       popping in instantly on every window shift. */
    animation: minimapTickIn 0.28s cubic-bezier(0.2, 0, 0, 1) both;
}

@keyframes minimapTickIn {
    from {
        opacity: 0;
        transform: translateX(4px);
    }
    to {
        opacity: 1;
        transform: translateX(0);
    }
}

@media (prefers-reduced-motion: reduce) {
    .minimap-item {
        animation: none;
    }
}

.minimap-marker {
    width: 6px;
    height: 2px;
    background: rgba(255, 255, 255, 0.2);
    border-radius: 1px;
    transition: all 0.2s cubic-bezier(0.2, 0, 0, 1);
}

[data-theme="light"] .minimap-marker {
    background: rgba(0, 0, 0, 0.2);
}

/* Hover effects. The floating per-tick date label (.minimap-label) that
   used to live here was removed by request -- the hover popup
   (.chat-history-panel) already shows the date alongside a real preview,
   so a second floating date bubble next to the strip was redundant. */
.minimap-item:hover .minimap-marker,
.minimap-item.active .minimap-marker {
    width: 12px;
    height: 4px;
    border-radius: 2px;
    background: var(--color-primary);
}

/* Enabled on mobile now (was previously hidden entirely below 600px) --
   the tick strip is a thin column near the edge either way, so at narrow
   widths the real overlap risk is the POPUP, not the strip. Clamp its
   width to the viewport instead of hiding the whole feature. */
@media (max-width: 600px) {
    .chat-minimap-hover-region:hover .chat-history-panel,
    .chat-minimap-hover-region.popup-open .chat-history-panel {
        width: min(280px, calc(100vw - 48px));
    }

    /* .chat-minimap-outer centers itself with top:0;bottom:0 over its
       WHOLE positioning ancestor, which includes the space the fixed
       composer sits over -- on a short mobile viewport that centered box
       is tall enough that its bottom ticks land visually under the
       composer. Insetting bottom by the same live clearance the composer
       itself reserves keeps the whole strip's centered box above it,
       same fix pattern as the jump-to-bottom button and typing indicator
       already use for the same reason. */
    .chat-minimap-outer {
        bottom: var(--chat-input-clearance, 140px);
    }
}

/* Floating jump-to-bottom button. Unlike .chat-minimap this stays visible on
   mobile -- it's a single small control bottom-center, not a label strip
   that needs the width the minimap does, so there's no overlap to avoid. */
.jump-to-bottom-btn {
    /* MUST be position:fixed, not absolute. The previous z-index bump
       (90 -> 110) did nothing, because it was the wrong axis entirely:
       .chat-input-area (the composer) is position:fixed at the PAGE ROOT
       with z-index:151, while this button lived inside .chat-container
       (#view-chat .chat-container: position:relative, z-index:3 -- its
       OWN, separate, lower stacking context). No z-index given to a
       descendant can ever out-rank a sibling stacking context sitting
       higher at the root -- the button was structurally trapped behind
       the composer, at the exact bottom-center screen position they both
       occupy, on every single render, not just when the minimap popup
       happened to be open. Fixed by escaping to position:fixed at the
       root (same tier as the composer) with a z-index that actually
       competes.
       Anchor is --typing-offset, NOT --chat-input-clearance -- these are
       two different live-measured properties (layoutClearance.js) and
       picking the wrong one caused two separate real regressions here.
       --chat-input-clearance = gap + typingHeight + 24: it deliberately
       reserves room for the composer AND the typing indicator floating
       above it, which is why anchoring to it (even halved) left the
       button sitting well above the actual composer edge instead of
       close to it. --typing-offset = gap + 12: exactly "12px above the
       composer's real top edge", the SAME anchor .typing-indicator itself
       uses to sit right above the composer -- which is precisely where
       this button belongs too, and it tracks the composer's real current
       height (draft wrapping, staged attachments, the voice/listening
       bar) the same way the old fallback-140px guess never did. */
    position: fixed;
    left: 50%;
    bottom: calc(var(--typing-offset, 106px) + 8px);
    transform: translateX(-50%) translateY(8px);
    width: 36px;
    height: 36px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(20, 30, 44, 0.95);
    color: rgba(200, 215, 230, 0.9);
    border: 1px solid rgba(255, 255, 255, 0.08);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    cursor: pointer;
    z-index: 155; /* above .chat-input-area (151), the actual competing tier */
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.2, 0, 0, 1), bottom 0.2s ease;
}

.jump-to-bottom-btn.jump-to-bottom-visible {
    opacity: 1;
    pointer-events: auto;
    transform: translateX(-50%) translateY(0);
}

.jump-to-bottom-btn:hover {
    background: rgba(30, 42, 58, 0.98);
}

[data-theme="light"] .jump-to-bottom-btn {
    background: rgba(255, 255, 255, 0.95);
    color: rgba(40, 50, 60, 0.9);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    border-color: rgba(0, 0, 0, 0.05);
}

[data-theme="light"] .jump-to-bottom-btn:hover {
    background: rgba(245, 246, 248, 0.98);
}

/* MUST NOT be `display: flex`. The bubble sits inside an unnamed wrapper div that
   would then be a row-flex item and shrink-wrap to the bubble's max-content width.
   The bubble's percentage max-width is (correctly) ignored while that width is
   computed, then resolved against it afterwards — so the bubble ends up capped at
   70% of a width derived from the bubble itself, and short text wraps for no reason.
   Both passes are spec-correct; the artifact is emergent. Keep this a block box.
   Right-alignment of user bubbles comes from `.message.user .message-bubble`'s
   `margin-left: auto`, NOT from the now-inert `flex-direction: row-reverse` below. */
.message {
    display: block;
    gap: 0;
    width: 100%;
    max-width: 800px;
    animation: fadeIn 0.2s ease;
}

@keyframes fadeIn {
    from { opacity: 0; transform: translateY(4px); }
    to { opacity: 1; transform: translateY(0); }
}

.message.user {
    align-self: flex-end;
}

.message.assistant {
    align-self: flex-start;
}

/* Portal v2: avatars removed — sender differentiation is via layout offset only */

.message-bubble {
    padding: var(--space-3) var(--space-4);
    /* All four corners equal — matches the WhatsApp reference the product owner supplied
       (edge-traced: uniform radius, no tail, no distinct corner). The previous
       4px squared corner on the sender's side was the single thing making these
       not read as WhatsApp. */
    border-radius: 18px;
    line-height: 1.5;
    word-break: break-word;
    width: fit-content;
    position: relative;
}

.message-bubble::after {
    content: '';
    display: table;
    clear: both;
}

.message.user .message-bubble {
    background: var(--color-primary, #22a7f0);
    color: #ffffff;
    /* B: cap user bubbles so they read as chat bubbles, not full-width blocks */
    max-width: 80%;
    margin-left: auto; /* push bubble to right within the message container */
}

.message.assistant .message-bubble {
    background: var(--color-bubble-ai);
    color: var(--color-bubble-ai-text);
    /* Subtle border on the glass panel */
    border: 1px solid rgba(255,255,255,0.08);
    max-width: 80%;
}

.copy-button {
    appearance: none;
    position: relative;
    border: 1px solid transparent;
    background: transparent;
    color: var(--color-text-secondary, #6b7280);
    width: 28px;
    height: 28px;
    padding: 5px;
    border-radius: 7px;
    display: inline-grid;
    place-items: center;
    cursor: pointer;
    opacity: .72;
    box-shadow: none;
    transition: opacity 120ms ease, color 120ms ease, background 120ms ease, border-color 120ms ease;
}
.copy-button svg {
    width: 16px;
    height: 16px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.copy-button:hover,
.copy-button:focus-visible {
    opacity: 1;
    color: var(--color-primary, #22A7F0);
    background: rgba(34, 167, 240, .1);
    border-color: rgba(34, 167, 240, .22);
}
.copy-button:focus-visible {
    outline: 2px solid var(--color-primary, #22A7F0);
    outline-offset: 2px;
}
.copy-button.is-copied { opacity: 1; color: #16835d; background: rgba(22, 131, 93, .1); }
.copy-button.is-failed { opacity: 1; color: var(--color-danger, #dc2626); background: rgba(220, 38, 38, .1); }

.message-bubble.has-message-actions {
    padding-right: 42px;
}
.message-actions {
    position: absolute;
    z-index: 4;
    top: 5px;
    right: 5px;
}
.message-actions__trigger {
    appearance: none;
    width: 28px;
    height: 28px;
    padding: 5px;
    display: grid;
    place-items: center;
    border: 1px solid rgba(255,255,255,.15);
    border-radius: 8px;
    background: rgba(20,30,44,.26);
    color: currentColor;
    cursor: pointer;
    opacity: 0;
    transition: opacity 120ms ease, background 120ms ease, transform 120ms ease;
}
.message-actions__trigger svg,
.message-actions__item svg {
    width: 17px;
    height: 17px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.message:hover .message-actions__trigger,
.message:focus-within .message-actions__trigger,
.message-actions.is-open .message-actions__trigger {
    opacity: 1;
}
.message-actions__trigger:hover,
.message-actions__trigger:focus-visible {
    background: rgba(20,30,44,.42);
}
.message-actions__trigger:focus-visible,
.message-actions__item:focus-visible {
    outline: 2px solid var(--color-primary, #22A7F0);
    outline-offset: 2px;
}
.message-actions__menu {
    position: absolute;
    top: 32px;
    right: 0;
    width: 148px;
    padding: 5px;
    border: 1px solid var(--color-border, rgba(128,128,128,.24));
    border-radius: 10px;
    background: var(--color-surface, #fff);
    color: var(--color-text, #1f2937);
    box-shadow: 0 10px 28px rgba(0,0,0,.2);
}
.message.assistant .message-actions__menu {
    right: auto;
    left: 0;
}
.message-actions.opens-up .message-actions__menu {
    top: auto;
    bottom: 32px;
}
.message-actions__item {
    appearance: none;
    width: 100%;
    min-height: 38px;
    padding: 8px 10px;
    display: flex;
    align-items: center;
    gap: 10px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: var(--font-size-sm, .875rem);
    text-align: left;
    cursor: pointer;
}
.message-actions__item:hover { background: var(--color-surface-hover, #f3f4f6); }
.message-actions__item.is-copied { color: #16835d; }
.message-actions__item.is-failed { color: var(--color-danger, #dc2626); }

.message-code-shell {
    position: relative;
    margin: 10px 0;
}
.message-code-block {
    margin: 0;
    padding-top: 48px;
}
.message-code-copy {
    position: absolute;
    z-index: 1;
    top: 8px;
    right: 8px;
    background: rgba(255, 255, 255, .045);
    border-color: rgba(255, 255, 255, .08);
}

@media (hover: none), (pointer: coarse) {
    .message-bubble.has-message-actions { padding-right: var(--space-4); }
    .message:hover .message-actions__trigger,
    .message:focus-within .message-actions__trigger {
        opacity: 0;
        pointer-events: none;
    }
    .message-actions.is-open .message-actions__trigger,
    .message .message-actions__trigger:focus-visible {
        opacity: 1;
        pointer-events: auto;
    }
}

@media (hover: none), (max-width: 768px) {
    .message-actions__trigger { min-width: 34px; min-height: 34px; }
}

@media (hover: none), (pointer: coarse) {
    .message-code-copy::before,
    .af-markdown__code-copy::before {
        content: '';
        position: absolute;
        inset: -8px;
    }
}

/* Portal v2: System log messages — centered plain-text, no bubble */
.message.system-trigger {
    align-self: center !important;
    max-width: 90%;
    animation: none;
}

.message.system-trigger .message-bubble {
    display: none; /* bubble suppressed — system-log-entry replaces it */
}

/* System log entry: centered, quiet, plain-text */
.system-log-entry {
    text-align: center;
    padding: 6px 0;
}

.system-log-text {
    color: rgba(160, 175, 190, 0.7);
    font-size: 0.85rem;
    font-style: italic;
    line-height: 1.5;
}

.system-log-tag {
    font-size: 0.68rem;
    color: rgba(140, 155, 170, 0.45);
    display: block;
    margin-top: 3px;
    letter-spacing: 0.3px;
}

[data-theme="light"] .system-log-text {
    color: rgba(60, 80, 100, 0.55);
}

[data-theme="light"] .system-log-tag {
    color: rgba(60, 80, 100, 0.4);
}

#chatMessages:not(.show-system-events) .is-system-event,
#chatMessages:not(.show-system-events) .is-system-reply {
    display: none !important;
}

/* Metadata footer inside message bubbles (model info, context, session) */
/* #40 — routing footer hidden for normal users; visible only in observability mode */
.message-footer {
    display: none;
    font-size: var(--font-size-xs);
    color: rgba(200,215,230,0.55);
    margin-top: var(--space-2);
    padding-top: var(--space-2);
    border-top: 1px solid rgba(255,255,255,0.10);
    font-family: var(--font-code);
    line-height: 1.4;
    opacity: 0.85;
}
/* Show footer when observability mode is active (set by ui.debug.firehose beat) */
html[data-observability] .message-footer {
    display: block;
}

[data-theme="dark"] .message-footer {
    border-top-color: rgba(255, 255, 255, 0.10);
}


.date-divider {
    text-align: center;
    font-size: 0.75rem;
    font-weight: 500;
    color: rgba(255, 255, 255, 0.6);
    background: rgba(255, 255, 255, 0.05);
    padding: 4px 12px;
    border-radius: 12px;
    margin: var(--space-2) auto;
    width: fit-content;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* WhatsApp-style timestamp: sits bottom-right INSIDE the bubble, on the same
   line as the last line of text when it fits. Achieved with a paired
   technique — see .message-time-spacer below:
     1. .message-time-spacer is an invisible inline run of the same text,
        placed at the true end of the message's text flow. It reserves
        exactly the horizontal room the real timestamp needs. If the last
        line has space, the spacer (and the gap it creates) sits at the end
        of that line. If the last line is already full (long word / full-
        width line), the spacer — being ordinary inline content — wraps to
        its own line, which is what keeps the real timestamp from ever
        overlapping text.
     2. .message-time-inline is the real, visible timestamp. It is
        absolutely positioned to the bubble's bottom-right (the bubble is
        `position: relative`), so it always renders into the gap the spacer
        reserved rather than needing float/clear layout. */
.message-time-inline {
    font-size: 0.65rem;
    /* 0.7 on BOTH bubble types — was 0.5 here with a 0.7 override for user bubbles
       only, which read as white on the blue fill but collapsed to #93989F on the
       grey one: dimmer than its own body text. One value, both sides. */
    color: rgba(255, 255, 255, 0.7);
    line-height: 1;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
    position: absolute;
    /* Offsets resolve against the bubble's PADDING box, so `0` would sit flush on
       the border — eating the bubble's padding and spilling past the corner arc
       (measured: 5.2px past a 16px radius). Inset by the bubble's own padding so
       the timestamp lands on the CONTENT box edge, exactly where
       .message-time-spacer reserved room for it. */
    right: var(--space-4);
    bottom: var(--space-3);
    pointer-events: none;
}

.message-time-spacer {
    visibility: hidden;
    font-size: 0.65rem;
    margin-left: 12px;
}

.message-time {
    font-size: 13px;
    color: rgba(200,215,230,0.88);
    margin-top: var(--space-1);
    margin-bottom: var(--space-1);
}

.message.user .message-time {
    text-align: right;
}

/* T-typing-below-input fix: was a normal-flow sibling AFTER .chat-messages
   inside .chat-container (a flex column). .chat-messages has flex:1, so this
   auto-height item got squeezed in right after it, landing in the same
   140px reserved clear-zone that .chat-messages's padding-bottom keeps free
   for the fixed .chat-input-area (position:fixed; bottom:0) — i.e. visually
   "below/behind the input box". Fix: take it out of flow with
   position:absolute, anchored to the BOTTOM of .chat-container (which is
   position:relative — see "#view-chat .chat-container" — and fills the
   viewport height below the orb).
   T-typing-position fix (round 2): the fixed 106px offset was measured
   against the WRONG (too-small) 140px clear-zone above, so once that
   clear-zone was too small the last message rendered lower than expected
   and the fixed-106px indicator ended up ABOVE the last message instead of
   after it. --typing-offset is now computed by the same
   updateChatLayoutClearance() JS routine that sizes --chat-input-clearance,
   so the two stay geometrically consistent: typing bottom clears the input
   top by 12px (comfortably over the 8px pass bar), and the clear-zone
   reserves enough extra room above that for the indicator's own height, so
   the indicator's top always lands below the last message's bottom. 106px
   is the pre-JS fallback only. */
.typing-indicator {
    display: none;
    position: absolute;
    left: var(--space-5);
    bottom: var(--typing-offset, 106px);
    z-index: 5;
    padding: var(--space-3) var(--space-4);
    background: rgba(255,255,255,0.10);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: var(--radius-lg);
    border-bottom-left-radius: var(--radius-sm);
    gap: 4px;
}

.typing-indicator.active {
    display: flex;
}

.typing-dot {
    width: 8px;
    height: 8px;
    background: var(--color-text-muted);
    border-radius: var(--radius-full);
    animation: typingBounce 1.2s infinite ease-in-out;
}

.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }

@keyframes typingBounce {
    0%, 80%, 100% { transform: translateY(0); }
    40% { transform: translateY(-6px); }
}

/* Chat Input */

.chat-input-area {
    padding: var(--space-3) var(--space-5);
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 151; /* above spotlight(150) and panel-full(50) */
    /* SOLID and OPAQUE, deliberately -- two earlier passes both left
       something of a message still visible behind the composer and got
       pushed back on:
         1. Flat `transparent` let a scrolled-through message show
            PERFECTLY CRISP, unobscured.
         2. A blurred, translucent tint (backdrop-filter, with or without
            an added gradient fill) still let a bright bubble show through
            as a soft blur -- explicitly rejected: "there is clearly
            something behind the composer... nothing should be behind
            it", not "it should be blurred".
       Only a fully opaque fill guarantees nothing behind ever shows,
       blurred or otherwise -- so this trades the glass/blur look for
       actually satisfying that requirement. Same navy as the composer
       pill's own tint, just alpha 1 instead of 0.55, so it still reads as
       the same surface, just solid.
       A THIRD complaint followed this: a flat opaque rectangle butting
       straight up against the animated page-background gradient reads as
       an "ugly bar" -- a hard-edged slab, not a graceful surface. Fixed
       WITHOUT reintroducing any transparency (the fill itself stays
       100% opaque, still hides everything behind it) by feathering the
       EDGE instead: a soft upward box-shadow in the same color, blurred
       well past the element's own box. A shadow is purely a paint effect
       outside the element's box -- it cannot let anything behind the
       element show through the element itself, so this softens the seam
       without compromising "nothing visible behind it" at all. */
    /* NO BACKGROUND ON THIS WRAPPER. It is a positioning box, not a surface.
       Everything visible down here belongs to .chat-input-wrapper -- the
       frosted pill -- which floats directly on the transcript with nothing
       but its own translucent fill and blur between it and the messages.

       This is the whole difference between a floating composer and a footer
       bar, and every previous attempt failed by treating the wrapper as
       something that needed a fill: transparent let messages scroll through
       perfectly crisp, a backdrop-blur left them as a ghost, an opaque slab
       read as a hard bar, and repainting the page's own backdrop here still
       left a rectangle whose gradient could not stay aligned with body's.
       The answer is that there is nothing to paint. Messages scrolling past
       are softened by the PILL's blur where they pass under it, and are
       simply visible either side of it, which is what "floating" means.

       If a fill ever reappears on this selector, the bar is back. */
    background: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    pointer-events: none;
}

.chat-input-wrapper {
    display: flex;
    align-items: flex-end;
    gap: var(--space-2);
    /* Frosted-glass fill (the product owner 2026-07-24). Same tint + light sheen as the
       phone bar so laptop reads as glass at rest too — the sheen gradient makes
       it look glassy even when there's no content behind to blur; the 0.55
       translucent navy + blur(12px) below do the rest. ONLY the fill is shared
       with mobile — desktop keeps its own rounded box / border / width. */
    background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02) 45%, rgba(255,255,255,0) 100%), rgba(20, 30, 44, 0.55);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.14);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.28);
    border-radius: 24px;
    padding: var(--space-2) var(--space-3);
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
    width: 100%;
    /* 92vw keeps a margin at every window size; the 1200px ceiling stops the
       line length running away on an ultrawide. .chat-input-area's own side
       padding sits outside this. */
    /* No cap: .chat-input-area and .chat-messages both pad their sides with
       --space-5, so a full-width composer lines up with where the message
       bubbles start. Matching a number would drift the moment that token
       changes; sharing the token cannot. */
    max-width: none;
    pointer-events: auto;
}

.chat-input-wrapper:focus-within {
    border-color: rgba(34, 167, 240, 0.6);
    box-shadow: 0 0 0 4px rgba(34, 167, 240, 0.1);
}

.chat-input-wrapper:focus-within {
    /* the product owner 2026-07-24: the old bright-white 38px glow was harsh. A soft
       shadow + a thin Mycroft-blue ring reads as "focused" without glaring. */
    border-color: rgba(34, 167, 240, 0.55);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.28), 0 0 0 2px rgba(34, 167, 240, 0.30);
}

.chat-input {
    flex: 1;
    border: none;
    background: none;
    resize: none;
    font: inherit;
    font-size: var(--font-size-base);
    /* Part E+F: light text on dark glass panel; 2-row height */
    color: #e8f0f8;
    line-height: 1.5;
    max-height: 120px;
    /* Single comfortable line by default (matches the 44px action buttons), so
       the placeholder/text sits at the SAME vertical center as the icons
       (the product owner 2026-07-24: text was higher than the icons). The old 48px/14px-top
       reserved 2 lines and top-aligned the single line, making it float high.
       Symmetric 10px top/bottom centers the single line; the JS auto-grow +
       align-items:flex-end keep the buttons at the bottom as it grows. */
    min-height: 44px;
    outline: none;
    /* No blue focus ring AND no grey tap-flash on mobile: the textarea's default
       -webkit-tap-highlight-color painted a highlight on touch (the product owner
       2026-07-24 "I don't like the highlight, remove it"). transparent kills it
       everywhere; the wrapper already carries the only intentional focus cue. */
    -webkit-tap-highlight-color: transparent;
    padding: 10px 0 10px var(--space-2);
}

.chat-input::placeholder {
    color: rgba(200,215,230,0.50);
}

.chat-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: var(--radius-full);
    border: none;
    cursor: pointer;
    transition: all var(--transition-fast);
    flex-shrink: 0;
    /* Part F: 44px touch target */
    color: rgba(200,215,230,0.65);
    background: none;
}

.chat-btn:hover {
    background: rgba(255,255,255,0.10);
    color: var(--color-primary);
}

.chat-btn.send-btn {
    background: var(--color-primary);
    color: white;
}

.chat-btn.send-btn:hover {
    background: var(--color-primary-dark);
}

.chat-btn.send-btn.stop-btn,
.chat-btn.send-btn.stop-btn:hover {
    background: var(--color-danger);
}

.chat-btn.send-btn.stop-btn:hover:not(:disabled) {
    filter: brightness(0.9);
}

.chat-btn.send-btn.stop-btn svg {
    width: 20px;
    height: 20px;
}

.chat-btn.send-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.chat-btn svg {
    width: 18px;
    height: 18px;
}

.voice-btn.recording {
    background: var(--color-danger);
    color: white;
    animation: pulse 1.5s infinite;
}

@keyframes pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
    50% { box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); }
}

/* === GPT-voice-mode-style "Listening" bar ===
   Replaces .chat-input-wrapper's contents while isListening (App.jsx). The
   waveform is a looping keyframe animation rather than a mic-level meter, but
   App.jsx only mounts it after LiveKit confirms microphone publication. The
   pre-capture Connecting state uses the static dot below. */

.listening-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    gap: var(--space-3);
    /* matches .chat-input's vertical rhythm so the pill doesn't jump height
       when it swaps in for the textarea */
    min-height: 44px;
    animation: listening-bar-enter 160ms ease-out;
}

@keyframes listening-bar-enter {
    from { opacity: 0.72; transform: translateY(2px); }
    to   { opacity: 1; transform: translateY(0); }
}

.listening-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: var(--radius-full);
    border: none;
    cursor: pointer;
    flex-shrink: 0;
    background: rgba(255, 255, 255, 0.92);
    color: #14202c;
    transition: all var(--transition-fast);
}

.listening-btn:hover {
    background: #ffffff;
}

.listening-btn svg {
    width: 18px;
    height: 18px;
}

.listening-btn-spacer {
    width: 44px;
    height: 44px;
    flex-shrink: 0;
}

.listening-center {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    flex: 1;
    min-width: 0;
}

.listening-label {
    color: rgba(232, 240, 248, 0.85);
    font-size: var(--font-size-base);
    white-space: nowrap;
}

/* Deliberately compact: this is an autoplay recovery affordance, not another
   voice control, and only exists while LiveKit says playback is blocked. */
.listening-enable-sound {
    border: 0;
    border-radius: var(--radius-full);
    padding: 3px 7px;
    background: rgba(34, 167, 240, 0.18);
    color: #8dd8ff;
    font: inherit;
    font-size: 11px;
    cursor: pointer;
}

.listening-connecting-dot {
    box-sizing: border-box;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: #22A7F0;
    box-shadow: 0 0 8px rgba(34, 167, 240, 0.45);
    opacity: 0.7;
}

/* Capture has stopped here, so the waveform must stay gone. A small progress
   ring communicates delivery/transcription work without resembling live mic
   activity or changing the bar's dimensions. */
.listening-bar--finishing .listening-connecting-dot {
    background: transparent;
    border: 2px solid rgba(34, 167, 240, 0.28);
    border-top-color: #22A7F0;
    box-shadow: none;
    opacity: 1;
    animation: listening-finish-spin 800ms linear infinite;
}

.listening-bar--finishing .listening-label {
    color: rgba(232, 240, 248, 0.95);
}

@keyframes listening-finish-spin {
    to { transform: rotate(360deg); }
}

.listening-waveform {
    display: flex;
    align-items: center;
    gap: 6px;
    height: 24px;
}

/* the product owner 2026-07-24: the old 7 white vertical bars scaling in height were a
   dead ringer for ChatGPT's voice equalizer. Redesigned as brand-blue glowing
   DOTS that bob up and down in a wave — reads as the companion's own signature,
   not a GPT clone (different shape: dots not bars; different motion: vertical
   bob + glow, not height-scale; different color: Mycroft Blue, not white). */
.listening-waveform span {
    display: block;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: #22A7F0;
    box-shadow: 0 0 8px rgba(34, 167, 240, 0.65);
    animation: listening-wave 1.3s ease-in-out infinite;
}

.listening-waveform span:nth-child(1) { animation-delay: 0s; }
.listening-waveform span:nth-child(2) { animation-delay: 0.1s; }
.listening-waveform span:nth-child(3) { animation-delay: 0.2s; }
.listening-waveform span:nth-child(4) { animation-delay: 0.3s; }
.listening-waveform span:nth-child(5) { animation-delay: 0.2s; }
.listening-waveform span:nth-child(6) { animation-delay: 0.1s; }
.listening-waveform span:nth-child(7) { animation-delay: 0s; }

@keyframes listening-wave {
    0%, 100% { transform: translateY(4px) scale(0.7); opacity: 0.45; }
    50%      { transform: translateY(-6px) scale(1.15); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .listening-bar,
    .listening-bar--finishing .listening-connecting-dot {
        animation: none;
    }
    .listening-waveform span {
        animation: none;
        transform: none;
        opacity: 0.8;
    }
}

/* Voice "reply incoming" dots — the same blue listening dots, shrunk to sit
   inside the typing-bubble shell (the product owner 2026-07-24: "same blue bubbles as
   listening, much smaller, fit these in the typing bubble"). Gentler bob so it
   stays inside the small bubble. Colour + round shape inherited from
   .listening-waveform span above. */
.listening-waveform--mini {
    gap: 4px;
    height: 12px;
}
.listening-waveform--mini span {
    width: 5px;
    height: 5px;
    box-shadow: 0 0 6px rgba(34, 167, 240, 0.6);
    animation: listening-wave-mini 1.1s ease-in-out infinite;
}
@keyframes listening-wave-mini {
    0%, 100% { transform: translateY(2px) scale(0.7); opacity: 0.5; }
    50%      { transform: translateY(-3px) scale(1.1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
    .listening-waveform--mini span { animation: none; transform: none; opacity: 0.85; }
}
/* The voice variant of the bubble hugs the small dots a touch tighter. */
.typing-indicator--voice { padding: 8px 12px; }

/* Context Meter */

.context-meter {
    /* Hidden — the product owner 2026-07-24 flagged this thin bar under the composer as an
       ugly "bottom patch". It's an agent context-usage meter, not user-facing.
       Reversible (delete this display:none to restore). */
    display: none;
    height: 3px;
    background: rgba(255,255,255,0.12);
    border-radius: 2px;
    margin-top: var(--space-2);
    overflow: hidden;
}

.context-meter-fill {
    height: 100%;
    border-radius: 2px;
    transition: width 0.3s ease, background-color 0.3s ease;
    width: 0%;
    background: var(--color-success);
}

.context-meter-fill.warn { background: var(--color-warning); }
.context-meter-fill.high { background: #F97316; }  /* Orange — 75-90% usage */
.context-meter-fill.danger { background: var(--color-danger); }

.context-label {
    font-size: var(--font-size-xs);
    color: rgba(200,215,230,0.55);
    text-align: right;
    margin-top: 2px;
}

/* === Docs View (Merged Wiki + Documents) === */

.docs-container {
    flex: 1;
    display: flex;
    overflow: hidden;
}

/* Collapsible category groups using native <details>/<summary> */
.docs-category-group {
    margin-bottom: var(--space-3);
}

.docs-category-group > summary {
    font-size: var(--font-size-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-muted);
    margin-bottom: var(--space-2);
    padding: var(--space-2);
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    user-select: none;
    border-radius: var(--radius-sm);
    transition: background var(--transition-fast);
}

.docs-category-group > summary:hover {
    background: var(--color-surface-hover);
}

/* Custom disclosure triangle */
.docs-category-group > summary::before {
    content: '';
    display: inline-block;
    width: 0;
    height: 0;
    border-left: 5px solid var(--color-text-muted);
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transition: transform var(--transition-fast);
    flex-shrink: 0;
}

.docs-category-group[open] > summary::before {
    transform: rotate(90deg);
}

/* Remove default marker in WebKit/Blink */
.docs-category-group > summary::-webkit-details-marker {
    display: none;
}

/* Doc links inside categories */
.doc-link {
    display: block;
    padding: var(--space-2);
    font-size: var(--font-size-sm);
    color: var(--color-text-secondary);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: all var(--transition-fast);
    border: none;
    background: none;
    width: 100%;
    text-align: left;
    font: inherit;
    font-size: var(--font-size-sm);
}

.doc-link:hover {
    background: var(--color-surface-hover);
    color: var(--color-text);
}

.doc-link.active {
    background: var(--color-primary-light);
    color: var(--color-primary);
    font-weight: 600;
}

/* Doc content area */
.docs-content {
    flex: 1;
    overflow-y: auto;
    padding: var(--space-6) var(--space-8);
    max-width: 900px;
}

/* Back button for mobile doc detail navigation */
.docs-back-btn {
    display: none;
    padding: var(--space-2) var(--space-3);
    margin-bottom: var(--space-4);
    font-size: var(--font-size-sm);
    color: var(--color-primary);
    background: var(--color-primary-light);
    border: 1px solid var(--color-primary);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font: inherit;
    font-size: var(--font-size-sm);
    font-weight: 500;
    transition: all var(--transition-fast);
}

.docs-back-btn:hover {
    background: var(--color-primary);
    color: white;
}

/* Rendered doc content typography */
.docs-content h1 { font-size: var(--font-size-2xl); margin-bottom: var(--space-4); }
.docs-content h2 { font-size: var(--font-size-xl); margin: var(--space-6) 0 var(--space-3); border-bottom: 1px solid var(--color-border-light); padding-bottom: var(--space-2); }
.docs-content h3 { font-size: var(--font-size-lg); margin: var(--space-4) 0 var(--space-2); }
.docs-content p { margin-bottom: var(--space-3); }
.docs-content ul, .docs-content ol { margin-bottom: var(--space-3); padding-left: var(--space-6); }
.docs-content li { margin-bottom: var(--space-1); }
.docs-content table { border-collapse: collapse; width: 100%; margin: var(--space-4) 0; }
.docs-content th, .docs-content td { border: 1px solid var(--color-border); padding: var(--space-2) var(--space-3); text-align: left; font-size: var(--font-size-sm); }
.docs-content th { background: var(--color-surface); font-weight: 600; }
.docs-content strong { color: var(--color-text); }
.docs-content blockquote { border-left: 3px solid var(--color-primary); padding-left: var(--space-4); margin: var(--space-4) 0; color: var(--color-text-secondary); }

.docs-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1;
    color: var(--color-text-muted);
    font-size: var(--font-size-lg);
    padding: var(--space-8);
    text-align: center;
}

/* === Images View === */

.images-container {
    flex: 1;
    overflow-y: auto;
    padding: var(--space-6);
}

.images-header {
    margin-bottom: var(--space-6);
}

.images-header h2 {
    font-size: var(--font-size-xl);
    font-weight: 600;
}

.image-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--space-4);
}

.image-card {
    border-radius: var(--radius-md);
    overflow: hidden;
    border: 1px solid var(--color-border-light);
    transition: transform var(--transition-fast), box-shadow var(--transition-fast);
    background: var(--color-surface);
}

.image-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-md);
}

.image-card img {
    width: 100%;
    height: 160px;
    object-fit: cover;
    display: block;
}

.image-card-info {
    padding: var(--space-3);
}

.image-card-name {
    font-size: var(--font-size-sm);
    font-weight: 500;
    color: var(--color-text);
}

.image-card-desc {
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    margin-top: var(--space-1);
}

.images-empty {
    text-align: center;
    color: var(--color-text-muted);
    padding: var(--space-10);
    font-size: var(--font-size-lg);
}

/* === Terminal View === */

.terminal-container {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: var(--space-4);
}

.terminal-output {
    flex: 1;
    background: #1a1a2e;
    color: #0faf0f;
    font-family: var(--font-code);
    font-size: var(--font-size-sm);
    padding: var(--space-4);
    border-radius: var(--radius-md);
    overflow-y: auto;
    line-height: 1.6;
    white-space: pre-wrap;
    word-break: break-all;
    margin: 0;
}

.terminal-status {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) 0;
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

.terminal-status-dot {
    width: 8px;
    height: 8px;
    border-radius: var(--radius-full);
    background: var(--color-success);
}

.terminal-status-dot.disconnected {
    background: var(--color-danger);
}

/* === Responsive (Mobile) === */

@media (max-width: 768px) {
    .sidebar {
        display: none;
    }

    .mobile-header {
        display: flex;
    }

    .main-content {
        height: 100vh;
    }

    /* Docs: hide desktop sidebar, show mobile list instead */
    .docs-sidebar {
        display: none;
    }

    .docs-mobile-list {
        display: flex;
        flex-direction: column;
    }

    /* By default on mobile: doc content is hidden, mobile list is shown.
       When a doc is selected, the JS adds .mobile-visible to content
       and .hidden to mobile-list to swap them. */
    .docs-content {
        display: none;
        padding: var(--space-4);
    }

    .docs-content.mobile-visible {
        display: flex;
        flex-direction: column;
        flex: 1;
        overflow-y: auto;
    }

    .docs-mobile-list.hidden {
        display: none;
    }

    /* Show back button on mobile */
    .docs-back-btn {
        display: inline-block;
    }

    .chat-messages {
        padding: var(--space-3);
    }

    .message {
        max-width: 95%;
    }

    .chat-input-area {
        /* the product owner 2026-07-23: NO gaps — full width, flush to the bottom and both
           sides. No side padding, no bottom safe-area gap; push it all the way
           down. */
        padding: 0;
        left: 0;
    }
    .chat-input-wrapper {
        /* full-bleed bar: no rounding, spans edge to edge, flush at the bottom.
           Internal padding (base rule) keeps text/icons comfortable; safe-area
           bottom padding extends the bar through the home-indicator strip.
           The product owner 2026-07-24: at low opacity the bar dissolved into the chat.
           Opacity alone can't separate it (bar color ≈ page bg, so it blends
           against empty gaps between bubbles). So DEFINE the bar: a hairline
           top divider + a soft upward shadow lift it off the messages, and a
           more solid background. Still full-bleed / flush — no side gaps, no
           box outline. */
        /* the product owner 2026-07-24: rounded all corners now. Glass-at-rest fix: a
           backdrop-blur only shows when there's CONTENT behind it to blur — at
           rest the bar sits over flat page bg, so the blur is invisible and it
           looked un-glassy (only felt glassy when a message color scrolled
           behind). So paint a subtle top-down light SHEEN onto the bar itself,
           layered over the 0.55 translucent fill — that reads as frosted glass
           regardless of what's behind it, and the backdrop-blur still adds the
           real refraction when a bubble passes under. */
        border-radius: 16px;
        max-width: none;
        border: none;
        border-top: 1px solid rgba(255, 255, 255, 0.14);
        background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02) 45%, rgba(255,255,255,0) 100%), rgba(20, 30, 44, 0.55);
        box-shadow: 0 -8px 22px rgba(0, 0, 0, 0.34);
        padding-bottom: calc(var(--space-2, 8px) + env(safe-area-inset-bottom, 0px));
    }
    .chat-input-wrapper:focus-within {
        /* Mobile: NO highlight at all on focus — no ring, no glow, no border
           tint. Explicitly reset border-color too so nothing (incl. a stale
           cached desktop rule) can paint a cyan edge. (the product owner 2026-07-24) */
        box-shadow: none;
        border-color: transparent;
    }
    .chat-input-wrapper, .chat-input, .chat-btn {
        -webkit-tap-highlight-color: transparent;
    }
}
/* === Utility === */

.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;
}

/* Pygments highlight compat */
.highlight { background: transparent; }

/* ========== Session Picker ========== */

.session-picker-wrapper {
    position: relative;
    display: none;
}

[data-debug] .session-picker-wrapper {
    display: inline-block;
}

.session-indicator {
    font-size: var(--font-size-sm);
    padding: var(--space-1) var(--space-3);
    border: 1px solid rgba(34,167,240,0.40);
    border-radius: var(--radius-sm);
    background: rgba(255,255,255,0.08);
    color: #c8d8ea;
    cursor: pointer;
    white-space: nowrap;
}

.session-indicator:hover {
    background: rgba(255,255,255,0.14);
}

.session-dropdown {
    display: none;
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 4px;
    min-width: 220px;
    max-height: 300px;
    overflow-y: auto;
    background: rgba(20,30,44,0.92);
    border: 1px solid rgba(34,167,240,0.50);
    border-radius: var(--radius-md);
    box-shadow: 0 8px 24px rgba(0,0,0,0.40);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    z-index: 100;
    padding: var(--space-1) 0;
}

.session-dropdown.visible {
    display: block;
}

.session-option {
    display: block;
    width: 100%;
    padding: var(--space-2) var(--space-3);
    border: none;
    background: transparent;
    color: #c8d8ea;
    font-size: var(--font-size-sm);
    text-align: left;
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.session-option:hover {
    background: rgba(255,255,255,0.10);
}

.session-option.active {
    font-weight: 600;
    color: var(--color-primary);
}


/* === Tasks View === */

.tasks-container {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
}

.tasks-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--space-5) var(--space-6);
    border-bottom: 1px solid var(--color-border);
    flex-shrink: 0;
}

.tasks-header h2 {
    font-size: var(--font-size-2xl);
    font-weight: 700;
    color: var(--color-text);
    margin: 0;
}

.tasks-content {
    flex: 1;
    overflow-y: auto;
    padding: var(--space-6);
}

.tasks-loading,
.tasks-error,
.tasks-empty {
    text-align: center;
    padding: var(--space-10) var(--space-4);
    color: var(--color-text-secondary);
    font-size: var(--font-size-lg);
}

.tasks-error {
    color: var(--color-danger);
}

.tasks-empty {
    font-size: var(--font-size-xl);
}

/* Task sections */
.task-section {
    margin-bottom: var(--space-8);
}

.task-section-title {
    font-size: var(--font-size-lg);
    font-weight: 600;
    color: var(--color-text);
    margin-bottom: var(--space-4);
    padding-bottom: var(--space-2);
    border-bottom: 2px solid var(--color-border-light);
}

.task-section-overdue .task-section-title {
    color: var(--color-danger);
}

.task-section-today .task-section-title {
    color: var(--color-warning);
}

.task-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

/* Task cards */
.task-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-4);
    transition: all var(--transition-fast);
}

.task-card:hover {
    box-shadow: var(--shadow-md);
    border-color: var(--color-primary);
}

.task-card-overdue {
    border-left: 4px solid var(--color-danger);
}

.task-card-today {
    border-left: 4px solid var(--color-warning);
}

.task-card-upcoming {
    border-left: 4px solid var(--color-primary);
}

.task-card-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--space-3);
    margin-bottom: var(--space-2);
}

.task-card-title {
    font-size: var(--font-size-base);
    font-weight: 600;
    color: var(--color-text);
    flex: 1;
    line-height: 1.4;
}

.task-card-actions {
    display: flex;
    gap: var(--space-2);
    flex-shrink: 0;
}

.task-action-btn {
    width: 32px;
    height: 32px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--color-border-light);
    background: var(--color-bg);
    color: var(--color-text-secondary);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all var(--transition-fast);
    padding: 0;
}

.task-action-btn:hover {
    background: var(--color-surface-hover);
    border-color: var(--color-border);
}

.task-action-btn svg {
    width: 16px;
    height: 16px;
}

.task-action-complete:hover {
    background: var(--color-success);
    border-color: var(--color-success);
    color: white;
}

.task-action-delete:hover {
    background: var(--color-danger);
    border-color: var(--color-danger);
    color: white;
}

.task-action-snooze-wrapper {
    position: relative;
}

.snooze-menu {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: var(--space-1);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    padding: var(--space-2);
    display: none;
    flex-direction: column;
    gap: var(--space-1);
    z-index: 100;
    min-width: 140px;
}

.snooze-menu.visible {
    display: flex;
}

.snooze-menu button {
    padding: var(--space-2) var(--space-3);
    border: none;
    background: none;
    color: var(--color-text);
    text-align: left;
    cursor: pointer;
    border-radius: var(--radius-sm);
    font-size: var(--font-size-sm);
    white-space: nowrap;
    transition: background var(--transition-fast);
}

.snooze-menu button:hover {
    background: var(--color-surface-hover);
}

.task-card-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-bottom: var(--space-2);
    font-size: var(--font-size-sm);
}

.task-meta-item {
    color: var(--color-text-secondary);
    display: flex;
    align-items: center;
    gap: var(--space-1);
}

.task-meta-due {
    font-weight: 500;
}

.task-meta-project {
    color: var(--color-primary);
}

.task-card-description {
    font-size: var(--font-size-sm);
    color: var(--color-text-secondary);
    line-height: 1.5;
    margin-top: var(--space-2);
    padding-top: var(--space-2);
    border-top: 1px solid var(--color-border-light);
}

/* New Task Modal */
.modal {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 1000;
    padding: var(--space-4);
}

.modal.visible {
    display: flex;
}

.modal-content {
    background: var(--color-bg);
    border-radius: var(--radius-lg);
    max-width: 500px;
    width: 100%;
    max-height: 90vh;
    overflow-y: auto;
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
}

.modal-header {
    padding: var(--space-5) var(--space-6);
    border-bottom: 1px solid var(--color-border);
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.modal-header h3 {
    font-size: var(--font-size-xl);
    font-weight: 700;
    color: var(--color-text);
    margin: 0;
}

.modal-close {
    width: 32px;
    height: 32px;
    border-radius: var(--radius-sm);
    border: none;
    background: none;
    color: var(--color-text-secondary);
    font-size: 24px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all var(--transition-fast);
}

.modal-close:hover {
    background: var(--color-surface-hover);
    color: var(--color-text);
}

.modal-content form {
    padding: var(--space-6);
}

.form-group {
    margin-bottom: var(--space-5);
}

.form-group label {
    display: block;
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--color-text);
    margin-bottom: var(--space-2);
}

.form-hint {
    margin: 0 0 var(--space-5);
    font-size: var(--font-size-sm);
    color: var(--color-text-secondary);
}

.form-input {
    width: 100%;
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    /* Use the input SURFACE, not --color-bg. --color-bg is the backdrop FIELD
       base (#c3cdd8 blue-grey in light — its own comment says "never used
       directly"); as an input fill it read as the ugly muddy grey the product owner
       flagged (2026-07-24). --color-surface is a clean near-white in light
       (#F1F3F4) / dark surface in night (#1a2430). */
    background: var(--color-surface);
    color: var(--color-text);
    font-family: inherit;
    font-size: var(--font-size-base);
    transition: border-color var(--transition-fast);
}

.form-input:focus {
    outline: none;
    border-color: var(--color-primary);
}

.form-input::placeholder {
    color: var(--color-text-muted);
}

textarea.form-input {
    resize: vertical;
    font-family: inherit;
}

/* T30: dark-mode native select popup — color-scheme tells the OS to render
   the dropdown list in dark mode, matching the portal theme. */
[data-theme="dark"] select.form-input {
    color-scheme: dark;
}
[data-theme="light"] select.form-input { color-scheme: light; }

.modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-3);
    padding-top: var(--space-5);
    border-top: 1px solid var(--color-border-light);
}

.btn {
    padding: var(--space-3) var(--space-5);
    border-radius: var(--radius-sm);
    font-family: inherit;
    font-size: var(--font-size-base);
    font-weight: 600;
    cursor: pointer;
    transition: all var(--transition-fast);
    border: none;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}

.btn-primary {
    background: var(--color-primary);
    color: white;
}

.btn-primary:hover {
    background: var(--color-primary-dark);
}

.btn-secondary {
    background: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-border);
}

.btn-secondary:hover {
    background: var(--color-surface-hover);
}

/* Mobile responsiveness for tasks */
@media (max-width: 768px) {
    .tasks-header {
        padding: var(--space-4);
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-3);
    }

    .tasks-header h2 {
        font-size: var(--font-size-xl);
    }

    .tasks-content {
        padding: var(--space-4);
    }

    .task-card-header {
        flex-direction: column;
        align-items: stretch;
    }

    .task-card-actions {
        justify-content: flex-end;
    }

    .modal-content {
        margin: var(--space-4);
    }
}


/* ========== Account Management ========== */

.account-container {
    padding: var(--space-6);
    max-width: 1100px;
    margin: 0 auto;
    /* T31: two-column grid on desktop; account-header spans both columns */
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-6);
    align-items: start;
}

.account-header {
    /* T31: header spans the full grid width */
    grid-column: 1 / -1;
    margin-bottom: var(--space-6);
}

.account-header h2 {
    font-size: var(--font-size-2xl);
    font-weight: 700;
    color: var(--color-text);
}

.account-section-title {
    font-size: var(--font-size-lg);
    font-weight: 600;
    color: var(--color-text);
    margin-bottom: var(--space-4);
}

.account-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-5);
}

.list-items {
    margin-bottom: var(--space-4);
}

.list-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    margin-bottom: var(--space-3);
    background: var(--color-bg);
}

.list-item-content {
    flex: 1;
}

.list-item-title {
    font-weight: 500;
    color: var(--color-text);
    margin-bottom: var(--space-1);
}

.list-item-subtitle {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    line-height: 1.5;
}

.list-item-actions {
    display: flex;
    gap: var(--space-2);
}

.list-empty {
    padding: var(--space-5);
    text-align: center;
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
}

.badge {
    display: inline-block;
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-full);
    font-size: var(--font-size-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.badge-primary {
    background: rgba(34, 167, 240, 0.15);
    color: var(--color-primary);
}

.btn-icon {
    background: none;
    border: none;
    color: var(--color-text-muted);
    cursor: pointer;
    font-size: var(--font-size-lg);
    padding: var(--space-2);
    transition: color var(--transition-fast);
    line-height: 1;
}

.btn-icon:hover {
    color: var(--color-primary);
}

.btn-danger {
    color: #EF4444;
}

.btn-danger:hover {
    color: #DC2626;
}

.add-item-form {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-border-light);
}

.checkbox-label {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--font-size-sm);
    color: var(--color-text);
    cursor: pointer;
}

.checkbox-label input[type="checkbox"] {
    cursor: pointer;
}

.form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-4);
}

.account-error {
    padding: var(--space-6);
    text-align: center;
    color: #EF4444;
    font-size: var(--font-size-lg);
}

/* Toast notifications */
.toast {
    position: fixed;
    bottom: var(--space-6);
    right: var(--space-6);
    background: var(--color-surface);
    color: var(--color-text);
    padding: var(--space-4) var(--space-5);
    border-radius: var(--radius-md);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    border: 1px solid var(--color-border);
    opacity: 0;
    transform: translateY(20px);
    transition: all var(--transition-normal);
    z-index: 10000;
}

.toast.visible {
    opacity: 1;
    transform: translateY(0);
}

/* Mobile responsiveness for account */
@media (max-width: 768px) {
    .account-container {
        padding: var(--space-4);
        /* T31: revert to single column on mobile */
        grid-template-columns: 1fr;
    }

    .account-header h2 {
        font-size: var(--font-size-xl);
    }

    .form-row {
        grid-template-columns: 1fr;
    }

    .list-item {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-3);
    }

    .list-item-actions {
        align-self: flex-end;
    }

    .toast {
        bottom: var(--space-4);
        right: var(--space-4);
        left: var(--space-4);
    }
}

/* === Settings modal — Text size scale buttons (font scale) === */

.account-scale-btns {
    display: flex;
    gap: var(--space-2);
    align-items: center;
}

/* Account view scale buttons share appearance with icon-rail scale buttons */
.account-scale-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    padding: var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    cursor: pointer;
    font: inherit;
    transition: background var(--transition-fast), color var(--transition-fast);
}

.account-scale-btn:hover {
    background: var(--color-surface-raised);
    color: var(--color-primary);
}

.account-scale-btn:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

.account-scale-btn:disabled:hover {
    background: var(--color-surface);
    color: var(--color-text);
}

.account-scale-reset {
    font-weight: 600;
}

/* === Debug Settings view === */

.debug-toggle-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 64px;
    min-height: 36px;
    padding: var(--space-1) var(--space-3);
    border: 2px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: rgba(44,62,80,0.55); /* OFF: muted navy-grey track */
    color: var(--color-text);       /* H: was color-text-muted (~3.5:1 dark, fails AA); color-text gives ≥9:1 dark, ≥5:1 light */
    font: inherit;
    font-size: 14px; /* ≥14px so On/Off label is readable */
    font-weight: 600;
    cursor: pointer;
    letter-spacing: 0.03em;
    transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}

.debug-toggle-btn:hover {
    background: var(--color-surface-raised);
    border-color: var(--color-primary);
    color: var(--color-primary);
}

/* ON state: filled Mycroft Blue track — unmistakably active */
.debug-toggle-btn.debug-toggle-on {
    background: #22A7F0; /* Mycroft Blue filled */
    border-color: #22A7F0;
    color: #fff;
    font-weight: 700;
}

.debug-info-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--color-border);
}

.debug-info-row:last-of-type {
    border-bottom: none;
}

.debug-info-label {
    font-size: var(--font-size-sm);
    color: var(--color-text-secondary);
    font-weight: 500;
    white-space: nowrap;
}

.debug-info-value {
    font-size: var(--font-size-sm);
    /* E: brighter value text for readability */
    color: var(--color-text);
    font-family: 'Fira Code', monospace;
    word-break: break-all;
    text-align: right;
    font-weight: 500;
    opacity: 1;
}

/* === Bottom-nav theme toggle button === */

/* Inherits .bottom-nav-item sizing/tap-targets; no extra sizing needed */
.bottom-nav-theme-btn {
    /* Explicit color to match nav icons; no special overrides — inherits all .bottom-nav-item styles */
    color: var(--color-text-muted);
}

.bottom-nav-theme-btn:hover,
.bottom-nav-theme-btn:active {
    color: var(--color-primary);
}



/* === Unified Chat: Source Badges === */

.source-badge {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    /* C: quieter — smaller font, lower opacity, reduced visual weight */
    font-size: 11px;
    font-weight: 400;
    padding: 0px 4px;
    border-radius: var(--radius-full);
    vertical-align: middle;
    margin-left: 5px;
    line-height: 1.4;
    letter-spacing: 0.01em;
    opacity: 0.55;
}

.source-badge.voice {
    background: rgba(34, 197, 94, 0.15);
    color: #16A34A;
}
[data-theme="dark"] .source-badge.voice {
    background: rgba(34, 197, 94, 0.2);
    color: #4ADE80;
}

.source-badge.portal {
    background: rgba(100, 100, 100, 0.12);
    color: #666;
}
[data-theme="dark"] .source-badge.portal {
    background: rgba(180, 180, 180, 0.20);
    color: #ccc;
    opacity: 0.9; /* raise from base 0.55 → effective text ~7:1 contrast on dark panel */
}

.source-badge.terminal {
    background: rgba(100, 100, 100, 0.12);
    color: #666;
}
[data-theme="dark"] .source-badge.terminal {
    background: rgba(160, 160, 160, 0.15);
    color: #999;
}

.source-badge.agent {
    background: rgba(147, 51, 234, 0.15);
    color: #7C3AED;
}
[data-theme="dark"] .source-badge.agent {
    background: rgba(147, 51, 234, 0.2);
    color: #A78BFA;
}

.source-badge.unknown {
    background: rgba(100, 100, 100, 0.1);
    color: #999;
}

/* Card reopen affordance — generalizes the old T29 video-only replay chip to
   every ui.card.* verb (image/video/web/pdf/markdown/audio/contact/file/form).
   Customer directive 2026-07-16 ("why is the text aligned to 'View' and not
   to 'Time'... it should align well with that"): "View" shares the message's
   LAST TEXT LINE with the timestamp — text, View and the clock all on one
   row, e.g. "Here's a form to fill out.  [View]  09:16 PM" — with NO added
   text line for it.

   MECHANISM: inline placement, inserted into the DOM immediately BEFORE
   .message-time-spacer (see app.js), i.e. as ordinary text-flow content at
   the true end of the message. This mirrors .message-time-spacer /
   .message-time-inline's own paired technique exactly: the button is a
   normal inline box participating in the SAME line-box computation as the
   text and the spacer, so the browser's own wrap decides whether it fits on
   the last line (and the reserved timestamp gap after it) or wraps —
   there is no reserved-space or float hack for the button itself, and no
   second box whose position can go stale relative to the text.

   THIS REPLACES an earlier `float: right` + first-child placement (pinned
   top-right of the bubble). That version knocked .message-time-inline OFF
   the last text line — floating the button out of the text flow left the
   timestamp with no text to tuck onto, so it dropped to its own row
   (measured: text row y-centre 106 vs. timestamp row y-centre 152, a 46px
   drop). Inline-before-the-spacer placement is what restores the tuck: the
   button now participates in exactly the flow the spacer/timestamp pair
   already reserves room against. */
.card-replay-affordance {
    display: inline-block;
    vertical-align: baseline;
    margin: 0 0 0 var(--space-2);
    padding: 1px var(--space-2);
    border: 1px solid var(--color-primary);
    border-radius: var(--radius-full);
    background: transparent;
    color: var(--color-primary);
    font-family: var(--font-body);
    font-size: 0.7rem;
    font-weight: 600;
    line-height: 1.4;
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--transition-fast), color var(--transition-fast);
}

.card-replay-affordance:hover {
    background: var(--color-primary);
    color: #fff;
}

[data-theme="dark"] .card-replay-affordance {
    background: var(--color-primary-light);
    color: var(--color-primary);
    border-color: var(--color-primary);
}

[data-theme="dark"] .card-replay-affordance:hover {
    background: var(--color-primary);
    color: #fff;
}

/* × on an inline card collapses it to this same pill in place (Target 2,
   customer feedback 2026-07-19: "what happened to the View" / "when I hit
   close (x) it goes away completely, maybe when closed bring back the view
   button?") -- see _collapseToViewPill in face-integration.js. It replaces
   the .af-card node directly inside .message-inline-card, so it's the sole
   occupant of that slot rather than sitting inline after bubble text; only
   the left margin (meant for that text-flow placement) is zeroed. */
.card-replay-affordance--inline {
    margin-left: 0;
}

/* === Unified Chat: Search Bar === */

.chat-header-right {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    justify-self: end;
}

.chat-search-wrapper {
    display: flex;
    align-items: center;
    position: relative;
}

.chat-search-toggle {
    background: none;
    border: none;
    color: rgba(200,215,230,0.60);
    cursor: pointer;
    padding: 4px;
    border-radius: var(--radius-sm);
    display: flex;
    align-items: center;
    transition: color var(--transition-fast);
}
.chat-search-toggle:hover {
    color: var(--color-primary);
}

.chat-search-input {
    width: 180px;
    padding: 4px 8px;
    font-size: var(--font-size-sm);
    border: 1px solid rgba(34,167,240,0.40);
    border-radius: var(--radius-sm);
    background: rgba(255,255,255,0.08);
    color: #e8f0f8;
    outline: none;
    transition: border-color var(--transition-fast);
    font-family: var(--font-body);
}
.chat-search-input:focus {
    border-color: var(--color-primary);
}
.chat-search-input::placeholder {
    color: rgba(200,215,230,0.45);
}

.chat-search-clear {
    background: none;
    border: none;
    color: var(--color-text-muted);
    cursor: pointer;
    font-size: 1.1rem;
    padding: 2px 6px;
    line-height: 1;
    transition: color var(--transition-fast);
}
.chat-search-clear:hover {
    color: var(--color-danger);
}

/* === Unified Chat: Loading Spinner === */

.chat-load-more {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-3);
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
}

.chat-spinner {
    width: 18px;
    height: 18px;
    border: 2px solid var(--color-border);
    border-top-color: var(--color-primary);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* === Unified Chat: Voice message play hint === */

.voice-hint {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    margin-top: 4px;
}

/* === Unified Chat: Inline attachment === */

.message-attachment {
    margin-top: 6px;
    max-width: 100%;
}
.message-attachment img {
    max-width: 280px;
    max-height: 200px;
    border-radius: var(--radius-sm);
    cursor: pointer;
}
.message-attachment a {
    color: var(--color-primary);
    font-size: var(--font-size-sm);
    text-decoration: underline;
}

/* === Unified Chat: Search result highlight === */

.search-highlight {
    background: rgba(255, 195, 0, 0.3);
    border-radius: 2px;
    padding: 0 1px;
}
[data-theme="dark"] .search-highlight {
    background: rgba(255, 195, 0, 0.2);
}

/* === Mobile adjustments for search === */

@media (max-width: 768px) {
    .chat-search-input {
        width: 120px;
    }
    .chat-header-right {
        gap: var(--space-1);
    }
}

/* ===================================================================
   Agent Face integration — T4
   Styles for the orb bar, card mount, and face card shell.
   Uses face CSS tokens (--af-*) where available, falls back to portal
   tokens for surfaces that render before face CSS loads.
   =================================================================== */

/* --- Chat face bar: orb + card mount row above the chat container --- */

.chat-face-bar {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    padding: 12px 20px 0;
    background: var(--color-bg, #fff);
    border-bottom: 1px solid var(--color-border-light, #e5e7eb);
    flex-wrap: wrap;
    min-height: 0;
    /* Collapse to zero height when empty (no cards mounted yet) */
}

/* Only show the bar if the orb canvas or a card is present */
#chat-face-bar:empty {
    display: none;
    padding: 0;
    border-bottom: none;
}

/* --- Orb wrapper: transparent, no well, no mask (orb halo self-fades) --- */

.chat-orb-wrap {
    width: 96px;
    height: 96px;
    border-radius: 50%;
    /* Transparent: the orb canvas clears to transparent each frame and composites
       onto whatever is behind the wrapper. No navy well — no dark disc on white. */
    background: transparent;
    flex-shrink: 0;
    /* No mask: the atmospheric halo ends at rgba(...,0) so it self-fades cleanly.
       A hard radial mask would clip the soft glow edge — removed. */
}

.chat-orb-wrap canvas {
    width: 100%;
    height: 100%;
    display: block;
    border-radius: 50%;
}

/* --- Card mount: beat-driven cards appear here --- */

.chat-card-mount {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-bottom: 12px;
}

/* --- Agent Face card shell (af-card) --------------------------------
   The face library builds cards with these classes. The portal styles
   them here to match the portal theme while consuming face tokens.
   -------------------------------------------------------------------- */

.af-card {
    background: var(--af-surface, var(--color-surface));
    border: 1px solid var(--af-border, var(--color-border));
    border-radius: var(--af-radius, var(--radius-md));
    box-shadow: var(--af-card-shadow, var(--shadow-md));
    overflow: hidden;
    max-width: 380px;
    width: 100%;
    font-family: var(--af-font, var(--font-body));
    font-size: var(--af-font-size-body, var(--font-size-base));
}

/* --- Inline-with-reply cards (T-inline-cards, customer directive 2026-07-18/19)
   image/web/markdown/pdf/video card beats render as a permanent sibling of
   .message-bubble, inside the assistant message's bare wrapper div (see
   appendMessage/createMessageElement in app.js) -- NOT routed to
   #portalAppStage/a modal, and not gated behind a "View" replay button.
   Left-alignment is free (.message.assistant{align-self:flex-start} above);
   this block only adds spacing and caps the stage-sized af-card variants
   (media/video/web/pdf/markdown normally sized for a centered modal) back
   down to the same compact ~380-400px width the already-inline audio/
   contact/file/form cards use, so every inline card reads as one family. */
.message-inline-card {
    margin-top: 6px;
    /* Customer feedback 2026-07-19 ("cards are not aligned with the other
       normal messages"): the earlier margin-left (var(--space-3)) pushed
       every inline card ~24px right of the assistant .message-bubble's left
       edge -- measured off a screenshot: bubble left x ~24-26px, card left x
       ~48-50px. Cards are a sibling of the bubble in the same bare message
       wrapper (see comment above), so they must share its left edge exactly;
       removed so the card's left border lines up flush with the message
       text above/below it, same as any normal assistant message. */
    /* Sibling-containment half of the 2026-07-24 Windows stacking fix (see
       the .af-card isolation comment, ~3323): each inline card already gets
       its own isolated stacking context, but isolating only the card and
       not its transcript slot still lets an engine that mis-orders layers
       reach past the slot boundary to a sibling. Isolating the SLOT too
       means a neighboring inline card's header can never be reached by
       this card's internal paint order, full stop -- same technique as
       .stage's isolation below (~4155), applied one level further in for
       the plain-transcript (non-stage) card path. */
    isolation: isolate;
}

.message-inline-card .af-card {
    margin: 0;
    /* Match the reply bubble's background (var(--color-bubble-ai)) so an inline
       card reads as part of the reply, not a separate opaque surface (the product owner
       2026-07-24). Header made transparent below so the whole card is one tone. */
    background: var(--color-bubble-ai);
    border-color: transparent;
}
.message-inline-card .af-card__header {
    background: transparent;
}

.message-inline-card .af-card--media,
.message-inline-card .af-card--video,
.message-inline-card .af-card--web,
.message-inline-card .af-card--pdf,
.message-inline-card .af-card--markdown {
    max-width: 380px;
    width: 100%;
}

/* Web/pdf cards default to a tall iframe; compact them for an inline
   transcript card. Web cards intentionally have no fullscreen control. */
.message-inline-card .af-card--web iframe {
    height: 240px;
}

.message-inline-card .af-card--pdf:not(.af-card--fullscreen) iframe {
    height: 280px;
}

/* Compact/modern chrome (customer feedback 2026-07-19: "the bar and button
   on the cards is very big and ugly, keep it modern"). Was a loud full-
   saturation accent-fill band (~25-30% of card height on a typical inline
   card); now a slim subtle surface strip -- Mycroft Blue only reappears as a
   restrained accent on the title chip below, not a full-bleed fill. */
.af-card__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 6px 10px;
    border-bottom: 1px solid var(--af-border, var(--color-border-light));
    background: var(--af-surface, var(--color-surface));
    color: var(--af-text, var(--color-text));
}

/* Type label ("Web"/"PDF"/etc.) -- was a bold heading-sized line reversed out
   of the full-blue header; now a small caption chip, accent used sparingly. */
.af-card__title {
    font-weight: 600;
    font-size: 0.6875rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-primary, #22A7F0);
    padding: 2px 8px;
    border-radius: var(--radius-full, 9999px);
    background: rgba(34, 167, 240, 0.12);
}

.af-card__dismiss {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--af-accent-contrast, #fff);
    /* Phase 1 fix C: rem so dismiss button scales with A−/A/A+ root font-size */
    font-size: 1.125rem; /* ≈18px at default 16px root */
    line-height: 1;
    padding: 0 2px;
    opacity: 0.75;
}

.af-card__dismiss:hover { opacity: 1; }

.af-card__body {
    padding: 12px;
    color: var(--af-text, var(--color-text));
    /* Stacking guard, UNCONDITIONAL not fullscreen-only (customer report
       2026-07-24, Windows: a photo card's image painted over its own ×/⛶
       controls AND over a neighboring markdown card's header -- see the
       .af-card isolation comment below for the full root-cause reasoning).
       Previously this guard existed only under .af-card--fullscreen
       (~3509), which left the INLINE state relying on implicit stacking
       (position:absolute controls vs. static body content) -- correct per
       spec, but with no explicit z-index of its own the body's stacking
       order was one GPU-compositing quirk away from flipping on some
       Chromium/Windows driver combos. Giving every body its own low
       z-index, always, means the controls' z-index:10 (.af-card__controls)
       never has to win by implication -- it's an explicit ordered pair in
       every state. */
    position: relative;
    z-index: 1;
}

.af-card__desc {
    margin-bottom: 10px;
    font-size: var(--font-size-sm);
    color: var(--af-text-muted, var(--color-text-secondary));
}

/* EmoteController card (Portal v2) — native trigger-button grid, no iframe */
.af-emote__row {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
}
.af-emote__btn {
    padding: 12px 10px;
    border-radius: var(--radius-md, 10px);
    border: 1px solid var(--color-border-light, rgba(0,0,0,0.1));
    background: var(--color-surface, #fff);
    color: var(--color-text);
    font-weight: 600;
    font-size: var(--font-size-sm, 0.875rem);
    letter-spacing: 0.02em;
    cursor: pointer;
    transition: transform 150ms ease, background 150ms ease, box-shadow 150ms ease;
}
.af-emote__btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(0,0,0,0.12);
}
.af-emote__btn:active {
    transform: translateY(0);
}
.af-emote__btn--joy { border-color: #f4c542; }
.af-emote__btn--surprise { border-color: #22a7f0; }
.af-emote__btn--skeptical { border-color: #9b59b6; }
.af-emote__btn--thinking { border-color: #2ecc71; }

/* Form card fields */
.af-card__form {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.af-card__field {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.af-card__field-label {
    font-size: var(--font-size-sm);
    font-weight: 500;
    color: var(--af-text, var(--color-text));
}

.af-card__input {
    padding: 8px 10px;
    border: 1px solid var(--af-border, var(--color-border));
    border-radius: 6px;
    background: var(--af-bg, var(--color-bg));
    color: var(--af-text, var(--color-text));
    font: inherit;
    font-size: var(--font-size-sm);
    outline: none;
    transition: border-color 150ms ease;
}

.af-card__input:focus {
    border-color: var(--af-accent, var(--color-primary));
}

.af-card__submit {
    padding: 9px 14px;
    background: var(--af-accent, var(--color-primary));
    color: var(--af-accent-contrast, #fff);
    border: none;
    border-radius: 6px;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    transition: opacity 150ms ease;
    align-self: flex-start;
}

.af-card__submit:hover { opacity: 0.88; }

/* File card download link -- compact/modern chrome (customer directive
   2026-07-19: "File -> Has a bad big looking ugly button"). Was a big solid
   Mycroft-Blue filled block; now a small outline/ghost control consistent
   with the rest of the compact card chrome (.af-card__ctrl, the web card's
   link-out CTA below) -- a secondary action, not a loud full-bleed button. */
.af-card__download {
    display: inline-block;
    padding: 5px 12px;
    background: transparent;
    color: var(--color-primary, #22A7F0);
    border: 1px solid rgba(34, 167, 240, 0.45);
    border-radius: var(--radius-md, 6px);
    font-weight: 600;
    font-size: 0.8125rem;
    text-decoration: none;
    transition: opacity 150ms ease, background 150ms ease;
}

.af-card__download:hover {
    background: rgba(34, 167, 240, 0.12);
    text-decoration: none;
}

/* Image card */
.af-card__image {
    display: block;
    max-width: 100%;
    height: auto;
    border-radius: 4px;
}

.af-card__caption {
    margin-top: 8px;
    font-size: var(--font-size-sm);
    color: var(--af-text-muted, var(--color-text-muted));
}

/* --- Card control cluster (T19a) ----------------------------------------
   Absolutely-positioned top-right cluster of icon buttons: fullscreen,
   reduce (pip), and dismiss. Appears at rest on video cards; appears on
   hover for lib cards. Semi-transparent so it doesn't dominate the card
   chrome but stays legible in both light and dark themes.
   ---------------------------------------------------------------------- */

.af-card {
    position: relative; /* anchor for the absolutely-positioned cluster */
    /* Root-cause hardening (customer report 2026-07-24, Windows: expanded
       photo card's image painted over its own controls AND over a
       neighboring markdown card's header; does not reproduce on this Linux
       sandbox). .af-card--fullscreen carries BOTH `transform` and
       `backdrop-filter` (~3437/3454), each of which already creates a
       stacking context by the ordinary cascade -- the media img inside
       .af-card__body has neither, so on paper the controls' z-index:10
       (.af-card__controls) already wins. But that correctness was
       IMPLICIT, riding entirely on which specific properties happen to be
       set on which element -- exactly the shape of thing that trips
       GPU-compositing layer-order bugs on some Chromium/Windows driver
       combos (backdrop-filter stacking is a known trouble spot). `isolation:
       isolate` makes every card its OWN stacking context explicitly and
       unconditionally, independent of transform/backdrop-filter/z-index
       happening to be present -- media and controls are then ALWAYS
       ordered relative to each other only, by their own z-index (body:1 /
       controls:10, see .af-card__body and .af-card__controls), and can
       never be reshuffled by a paint-order disagreement with content
       outside the card. This also contains the card from painting over a
       SIBLING card's header: isolation stops this card's stacking context
       from ever interleaving with a neighbor's (see .message-inline-card
       below for the sibling-containment half of this fix). */
    isolation: isolate;
}

.af-card__controls {
    position: absolute;
    top: 6px;
    right: 6px;
    display: flex;
    gap: 2px;
    z-index: 10;
    /* Compact/modern chrome (customer feedback 2026-07-19): was an oversized
       dark rounded pill capsule around the two glyphs; the header behind it
       is opaque on its own now (see .af-card__header), so the cluster no
       longer needs its own background/blur/pill to stay legible -- each
       button gets its own small ghost hover state instead (.af-card__ctrl). */
    background: transparent;
    border-radius: 0;
    padding: 0;
    /* Controls are always interactive — pointer-events:none was the hit-target bug:
       touch devices (iOS Safari) have no :hover so the controls were permanently
       non-tappable, and on desktop the browser's hit-test state doesn't refresh
       when pointer-events changes while the cursor is stationary, so the cached
       hit target (the header) won the click even though hover had fired.
       Fix: always auto; opacity handles discoverability (0.65 at rest, 1 on hover). */
    opacity: 0.65;
    transition: opacity 150ms ease;
}

/* Full opacity on hover */
.af-card:hover .af-card__controls {
    opacity: 1;
}

/* Always visible on video card — video has no header and needs persistent controls */
.af-card--video .af-card__controls {
    opacity: 1;
    pointer-events: auto;
}

/* In fullscreen/reduced modes the cluster is always visible regardless of hover */
.af-card--fullscreen .af-card__controls,
.af-card--reduced .af-card__controls {
    opacity: 1;
    pointer-events: auto;
}

/* --- One control per state (customer report 2026-07-24: on Windows the
   expanded photo card's × / ⛶ were both present, and the ⛶ duplicated the ×'s
   job of collapsing back to inline) --------------------------------------
   Inline (not fullscreen) = ⛶ only; expanded (fullscreen) = × only. Applies
   uniformly to every card type through the shared .af-card__ctrl-- classes
   (cardShell in face/src/cards/base.js + buildInlineCardChrome in
   inlineCardBuilders.js both emit the same class names).
   The ⛶ rule is unconditional: a fullscreen card never shows it.
   The × rule is scoped with :has() to cards that HAVE a ⛶ control at all —
   form/file are the NO-EXPAND variant (rewireCardShellForInlineNoExpand
   deletes ⛶ outright, vanilla:2449) and keep their single × as their only
   control; without the :has() scope this rule would hide it too and leave
   those two card types with no control at all. */
.af-card--fullscreen .af-card__ctrl--fullscreen {
    display: none;
}
.af-card:not(.af-card--fullscreen):has(.af-card__ctrl--fullscreen) .af-card__ctrl--dismiss {
    display: none;
}

/* Small ghost icon buttons (compact/modern chrome, customer feedback
   2026-07-19) -- was a bigger glyph reversed white-on-blue inside the dark
   pill; now a compact icon-sized hit target tinted with the card's own text
   color at rest and the Mycroft Blue accent on hover, so it reads correctly
   against the now-subtle (non-solid-color) header on both themes.
   ⚠️  2026-07-24 correction: the theme-matched ghost treatment above was
   right for a card whose controls always sit on an OPAQUE header -- it
   silently assumed that backing. It breaks for image/video cards, where the
   header goes transparent in fullscreen (`.af-card--fullscreen .af-card__header`)
   and the controls float directly over arbitrary photo/video content: a
   `background:none` + muted-gray glyph can vanish against a bright region of
   the photo (this was misread the first time as a z-index/stacking bug --
   z-index/click-target were already correct; the actual defect is contrast,
   no backing at all behind the glyph). Fix: a persistent (not hover-only)
   dark translucent scrim behind every control, same in inline and expanded
   states and for every card type (shared class, not per-kind) -- reads over
   bright OR dark media alike, independent of page theme. */
.af-card__ctrl {
    background: rgba(20, 30, 44, 0.55);
    border: none;
    cursor: pointer;
    color: #fff;
    font-size: 0.8125rem; /* ≈13px — compact glyph, was ≈15px */
    line-height: 1;
    width: 22px;
    height: 22px;
    padding: 0;
    border-radius: var(--radius-md, 6px);
    opacity: 0.75;
    transition: opacity 120ms ease, background 120ms ease, color 120ms ease;
    display: flex;
    align-items: center;
    justify-content: center;
}

.af-card__ctrl:hover {
    opacity: 1;
    background: rgba(34, 167, 240, 0.85);
    color: #fff;
}

.af-copy-icon {
    position: relative;
    display: block;
    width: 12px;
    height: 12px;
}
.af-copy-icon::before,
.af-copy-icon::after {
    content: '';
    position: absolute;
    width: 7px;
    height: 7px;
    border: 1.5px solid currentColor;
    border-radius: 2px;
}
.af-copy-icon::before { top: 0; left: 0; }
.af-copy-icon::after { right: 0; bottom: 0; background: inherit; }
.af-copy-icon--status {
    width: auto;
    height: auto;
    font-size: 0.875rem;
    font-weight: 800;
    line-height: 1;
}
.af-copy-icon--status::before,
.af-copy-icon--status::after { content: none; }
.af-markdown__copy-card.is-copied,
.af-markdown__code-copy.is-copied { color: #16835d; }
.af-markdown__copy-card.is-failed,
.af-markdown__code-copy.is-failed { color: var(--color-danger, #dc2626); }
.af-card__ctrl.af-markdown__copy-card.copy-button {
    width: 22px;
    height: 22px;
    padding: 4px;
    border: 0;
    background: rgba(20, 30, 44, 0.55);
    color: #fff;
    opacity: .75;
    box-shadow: none;
}
.af-card__ctrl.af-markdown__copy-card.copy-button:hover,
.af-card__ctrl.af-markdown__copy-card.copy-button:focus-visible {
    color: #fff;
    background: rgba(34, 167, 240, 0.85);
    opacity: 1;
}
.af-card__ctrl.af-markdown__copy-card.copy-button.is-copied { color: #fff; background: #16835d; opacity: 1; }
.af-card__ctrl.af-markdown__copy-card.copy-button.is-failed { color: #fff; background: var(--color-danger, #dc2626); opacity: 1; }

@media (hover: none), (pointer: coarse) {
    .af-card__controls {
        top: 0;
        right: 0;
        gap: 0;
    }
    .af-card__ctrl,
    .af-card__ctrl.af-markdown__copy-card.copy-button {
        position: relative;
        isolation: isolate;
        width: 44px;
        height: 44px;
        padding: 0;
        border: 0;
        background: transparent;
        opacity: 1;
    }
    .af-card__ctrl:hover,
    .af-card__ctrl.af-markdown__copy-card.copy-button:hover,
    .af-card__ctrl.af-markdown__copy-card.copy-button:focus-visible {
        background: transparent;
    }
    .af-card__ctrl::before {
        content: '';
        position: absolute;
        width: 22px;
        height: 22px;
        border-radius: var(--radius-md, 6px);
        background: rgba(20, 30, 44, 0.55);
        pointer-events: none;
        z-index: -1;
    }
    .af-card__ctrl:hover::before,
    .af-card__ctrl:focus-visible::before {
        background: rgba(34, 167, 240, 0.85);
    }
    .af-card__ctrl:focus-visible {
        outline: none;
    }
    .af-card__ctrl:focus-visible::before {
        box-shadow: 0 0 0 2px rgba(255, 255, 255, .9), 0 0 0 4px var(--color-primary, #22A7F0);
    }
    .af-card__ctrl.af-markdown__copy-card.copy-button.is-copied,
    .af-card__ctrl.af-markdown__copy-card.copy-button.is-failed {
        background: transparent;
    }
    .af-card__ctrl.af-markdown__copy-card.copy-button.is-copied::before { background: #16835d; }
    .af-card__ctrl.af-markdown__copy-card.copy-button.is-failed::before { background: var(--color-danger, #dc2626); }
    .af-card__ctrl > * {
        position: relative;
        z-index: 1;
    }
}

/* --- Card size states (T19a) ------------------------------------------ */

/* Expanded: escapes any parent overflow/clip; opens at the SAME centered
   modal size as the media library / settings modal (customer directive
   2026-07-20 -- expanded cards were covering the whole viewport, ask was to
   match the orb/settings modal footprint instead). Sizing/surface treatment
   intentionally mirrors .proto-library (style.css ~6494-6511). */
.af-card--fullscreen {
    position: fixed !important;
    top: 50% !important;
    left: 50% !important;
    right: auto !important;
    bottom: auto !important;
    transform: translate(-50%, -50%) !important;
    z-index: 9999 !important;
    width: min(880px, 92vw) !important;
    height: min(620px, 82vh) !important;
    max-width: none !important;
    border-radius: var(--radius-lg, 14px) !important;
    margin: 0 !important;
    /* Expanded card == the composer's glass (the product owner 2026-07-24: "when expanded
       I want it to match like how the composer is"). Same sheen + 0.55 navy fill
       as .chat-input-wrapper; light mode overrides to the light composer fill. */
    background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02) 45%, rgba(255,255,255,0) 100%), rgba(20, 30, 44, 0.55) !important;
    backdrop-filter: blur(16px) !important;
    -webkit-backdrop-filter: blur(16px) !important;
    border: 1px solid rgba(255,255,255,0.14) !important;
    box-shadow: 0 20px 48px rgba(0,0,0,0.35) !important;
}

/* Expanded card in day mode = the light composer fill (tinted glass + sheen). */
[data-theme="light"] .af-card--fullscreen {
    background: linear-gradient(180deg, rgba(255,255,255,0.55), rgba(255,255,255,0.12) 45%, rgba(255,255,255,0) 100%), rgba(232,240,248,0.72) !important;
    border-color: rgba(44,62,80,0.16) !important;
}

/* Header strip transparent in fullscreen so the card is one uniform glass
   surface like the composer, not a card with an opaque title bar. */
.af-card--fullscreen .af-card__header {
    background: transparent !important;
    border-bottom-color: rgba(255,255,255,0.12) !important;
}
[data-theme="light"] .af-card--fullscreen .af-card__header {
    border-bottom-color: rgba(44,62,80,0.14) !important;
}

/* Dimming backdrop behind the modal-sized card -- at 880x620 the rest of the
   page must not show through undimmed, same tint/blur as .proto-library-active
   (rgba(12,20,30,0.55) + blur(4px)).
   NOTE: this was previously a CSS-only `.af-card--fullscreen::before` with
   `position:fixed; inset:0`, on the theory that a negative z-index child
   would sit behind the card but still cover the full viewport. That's wrong:
   .af-card--fullscreen itself has `backdrop-filter` AND `transform`, and
   either one makes the element the *containing block* for its own
   `position:fixed` descendants (CSS Transforms/Filter Effects spec). So the
   ::before's `inset:0` was resolving against the 880x620 CARD box, not the
   viewport -- the dim/blur only ever covered the card itself, never the rest
   of the page. A fixed child of a transformed/filtered ancestor can never
   escape it, so this needed a real DOM element instead: a `.af-fullscreen-backdrop`
   div appended directly to <body> (a SIBLING of the card, not a descendant),
   created/removed by face-integration.js (_ensureFullscreenBackdrop /
   _removeFullscreenBackdrop) whenever a card enters/exits `.af-card--fullscreen`.
   Since <body> has no transform/filter, `inset:0` here is the real viewport. */
.af-fullscreen-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(12, 20, 30, 0.55);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    z-index: 9998; /* below the card's 9999 so the card stays on top */
}

/* In fullscreen the body fills the remaining space and the video fills it */
.af-card--fullscreen .af-card__body,
.af-card--fullscreen .af-card__body--video {
    height: 100%;
    display: flex;
    align-items: stretch;
    padding: 0;
}

/* Stacking guard (customer screenshot, Windows, 2026-07-24): an expanded
   photo card painted OVER the control cluster, hiding the × / ⛶ under the
   image, and (2026-07-24 follow-up report) over a neighboring markdown
   card's header too. .af-card__controls is position:absolute + z-index:10
   and SHOULD already win against the (non-positioned) media content by the
   ordinary cascade -- but .af-card--fullscreen also carries `transform` +
   `backdrop-filter`, and a full-bleed <img>/<video>/<iframe> body edge-to-
   edge with the controls is exactly the shape that trips GPU-compositing
   layer-order bugs on some Chromium/Windows driver combos (backdrop-filter
   stacking is a known trouble spot).
   This block was the FIRST fix attempt and stays as an explicit, redundant
   reinforcement for the fullscreen state specifically (still correct, no
   reason to remove it) -- but it did not fully close the report, because it
   only applied `position:relative; z-index:1` to the body while fullscreen,
   leaving the inline state to rely on implicit stacking, and did nothing to
   contain a card from a sibling's paint order. The actual fix is now the
   UNCONDITIONAL baseline on .af-card__body (~3172: position:relative;
   z-index:1, every state, every card) plus `isolation:isolate` on .af-card
   itself (~3323) and on .message-inline-card (~3085) for sibling
   containment -- see those comments for the full reasoning. */
.af-card--fullscreen .af-card__body {
    position: relative;
    z-index: 1;
}
.af-card--fullscreen .af-card__controls {
    z-index: 20;
}

.af-card--fullscreen .af-card__body--video video {
    width: 100%;
    height: 100%;
    max-width: none; /* override the normal 640px cap so fullscreen truly fills */
    aspect-ratio: auto; /* let the box fill 100%x100%; object-fit handles the ratio */
    object-fit: contain; /* letterbox — correct aspect ratio preserved */
}

/* Reduced / picture-in-picture: fixed corner pip */
.af-card--reduced {
    position: fixed !important;
    bottom: 16px !important;
    right: 16px !important;
    width: 240px !important;
    max-width: 240px !important;
    z-index: 9998 !important;
    border-radius: 12px !important;
    margin: 0 !important;
}

/* Video card — fills the stage above the orb, rounded corners clip the video */
.af-card--video {
    max-width: 640px;
    margin: 0 auto;
}

/* Zero padding so video fills edge-to-edge; af-card overflow:hidden clips corners */
.af-card__body--video {
    padding: 0;
}

/* Fix 1 (general): 16:9 aspect ratio so the card never collapses to a bar */
.af-card__body--video video {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
    display: block;
    object-fit: contain;
}

/* ── af-card--media: image / audio / video attachments opened in FOCUS ── */
/* F: explicit card-shell treatment — border, shadow, rounded corners */
.af-card--media {
    max-width: 640px;
    margin: 0 auto;
    /* Reinforce card framing so image doesn't look like a bare floating element */
    border: 1px solid rgba(34,167,240,0.40);
    border-radius: var(--radius-md);
    box-shadow:
        0 8px 24px rgba(0,0,0,0.38),
        0 2px 8px rgba(0,0,0,0.22),
        0 0 0 1px rgba(34,167,240,0.12);
    overflow: hidden; /* clips image to card border-radius */
}

/* Minimal padding for media body; let image/video fill naturally */
.af-card__body--media {
    padding: 8px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 120px;
}

/* Image: contain within card bounds; rounded corners inside the card body */
.af-card__body--media .af-card__media-img {
    max-width: 100%;
    max-height: 70vh;
    object-fit: contain;
    border-radius: 6px;
    display: block;
}

/* Audio: stretch to fill the card width */
.af-card__body--media .af-card__media-audio {
    width: 100%;
    max-width: 480px;
    display: block;
}

/* Video inside media card: same full-width contain as video card */
.af-card--media .af-card__body--media video {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
    display: block;
    object-fit: contain;
}

/* Onboarding success card */
.af-onboarding-success .af-card--form {
    border-color: var(--af-ok, #40DBB0);
}

/* ── T19b: Drag-to-dismiss — drag handle + dragging state + orb drop cue ── */

/* Drag handle: grip glyph, left of the control cluster. Color follows the
   same ghost-icon treatment as .af-card__ctrl (see compact-chrome comment
   above) -- the old white-on-dark-pill color would be invisible now that the
   cluster has no pill background of its own. */
.af-card__drag-handle {
    cursor: grab;
    color: var(--af-text-muted, var(--color-text-secondary));
    font-size: 15px;
    line-height: 1;
    padding: 3px 5px;
    border-radius: var(--radius-md, 6px);
    display: flex;
    align-items: center;
    justify-content: center;
    user-select: none;
    -webkit-user-select: none;
    transition: color 120ms ease, background 120ms ease;
}

.af-card__drag-handle:hover {
    color: var(--color-primary, #22A7F0);
    background: rgba(34, 167, 240, 0.12);
}

/* While a card is being dragged (re-parented to body, position:fixed) */
.af-card--dragging {
    cursor: grabbing !important;
    z-index: 10000 !important;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55), 0 0 0 2px rgba(34, 167, 240, 0.4) !important;
    opacity: 0.92;
    transition: box-shadow 100ms ease, opacity 100ms ease;
    pointer-events: none; /* let pointer events pass to underlying hit-test */
}

/* Allow pointer events on the card itself while dragging (needed for pointerup) */
.af-card--dragging {
    pointer-events: auto;
}

/* Orb drop-target cue: when a card is dragged over the orb */
.hero-orb-wrap.orb--drop-target {
    filter: drop-shadow(0 0 18px rgba(34, 167, 240, 0.85));
    transform: scale(1.06);
    transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1),
                filter 180ms ease;
}

/* Card highlights when over the orb — translucent blue outline + slight shrink */
.af-card--over-orb {
    box-shadow: 0 0 0 3px rgba(34, 167, 240, 0.75), 0 20px 60px rgba(0, 0, 0, 0.55) !important;
    opacity: 0.75;
    transition: box-shadow 120ms ease, opacity 120ms ease;
}

/* Mobile: hide orb on very small screens to save space */
@media (max-width: 480px) {
    .chat-orb-wrap {
        width: 64px;
        height: 64px;
    }
    .chat-face-bar {
        padding: 8px 12px 0;
        gap: 10px;
    }
}

/* ===================================================================
   Orb-First Portal Layout — v5 (2026-06-13)
   Icon rail + hero orb + stage materialization area
   =================================================================== */

/* ---- Icon rail: narrow left strip, icon-only, 3 items + theme toggle ---- */

.icon-rail {
    width: 56px;
    min-width: 56px;
    /* Semi-transparent so the depth field shows behind it */
    background: rgba(241,243,244,0.65);
    border-right: 1px solid rgba(214,218,220,0.55);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: var(--space-3) 0;
    gap: var(--space-2);
    height: 100vh;
    overflow: hidden;
    flex-shrink: 0;
    z-index: 10;
}

[data-theme="dark"] .icon-rail {
    background: rgba(28,39,51,0.65);
    border-right-color: rgba(45,61,78,0.55);
}

.rail-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    /* Part F: min touch target 44px for outer hit area handled by padding */
    min-width: 44px;
    min-height: 44px;
    border-radius: var(--radius-md);
    border: none;
    background: none;
    color: var(--color-text-muted);
    cursor: pointer;
    transition: all var(--transition-fast);
    flex-shrink: 0;
}

.rail-btn:hover {
    background: rgba(214,218,220,0.60);
    color: var(--color-text);
}

[data-theme="dark"] .rail-btn:hover {
    background: rgba(255,255,255,0.10);
}

.rail-btn.active {
    background: rgba(34,167,240,0.18);
    color: var(--color-primary);
}

.rail-btn svg {
    width: 20px;
    height: 20px;
}

.rail-theme-btn {
    color: var(--color-text-muted);
}

.rail-theme-btn:hover {
    color: var(--color-primary);
}

/* Spacer pushes theme toggle to the bottom of the rail */
.rail-spacer {
    flex: 1;
}

/* Font-scale buttons in the rail footer */
.rail-scale-btn {
    font-weight: 700;
    font-family: var(--font-body);
    line-height: 1;
    flex-direction: column;
    gap: 0;
}

/* Three label sizes so A− / A / A+ visually convey the scaling */
.scale-label-sm {
    font-size: 0.65rem;
    color: inherit;
    letter-spacing: -0.02em;
}
.scale-label-md {
    font-size: 0.80rem;
    color: inherit;
    letter-spacing: -0.02em;
}
.scale-label-lg {
    font-size: 0.95rem;
    color: inherit;
    letter-spacing: -0.02em;
}

/* Dim the reset button slightly so it reads as secondary */
.rail-scale-reset {
    opacity: 0.70;
}
.rail-scale-reset:hover {
    opacity: 1;
}

/* Disabled state: min/max boundary reached */
.rail-scale-btn:disabled {
    opacity: 0.25;
    cursor: not-allowed;
}
.rail-scale-btn:disabled:hover {
    background: none;
    color: var(--color-text-muted);
}

/* ---- Chat view: flex column — orb hero top, working-area below ----
   Scoped to .active so the ID specificity does not override .view{display:none}
   when another view is selected (else chat stays stacked behind library/account). */

#view-chat.active {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    height: 100%;
    background: transparent;
}

/* ---- Working area: flex:1 region below the orb ----
 *
 * .working-area wraps #portalAppStage (card band) + .chat-container (chat dock)
 * as a flex column. This guarantees the card band and chat panel SHARE the
 * remaining viewport space below the orb. Without this wrapper, the orb (flex-
 * shrink:0, up to 60vh) + stage (flex-shrink:0, up to 40vh) can sum to >100vh
 * and crush the chat panel to zero height.
 *
 * Structure inside #view-chat:
 *   .orb-hero        (flex-shrink:0 — orb band, fixed height)
 *   .working-area    (flex:1, min-height:0 — fills remaining space below orb)
 *     #portalAppStage (flex-shrink:0, max-height:40% of working area)
 *     .chat-container (flex:1, min-height:0 — fills rest of working area)
 */
.working-area {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    position: relative;
}

/* ---- Portal app stage: viewport-bounded normal-flow surface for beat-driven media cards (inc1b) ----
 *
 * #portalAppStage is a flex child of .working-area (a flex column below the orb).
 * Cards (image/video/web) append here as bordered blocks in the defined card band.
 * max-height:40% of the working area caps the band; .chat-container fills the rest.
 *
 * z-index:4 ensures cards paint above .chat-container (z-index:3 glass panel) so
 * they are never obscured by the glass when .chat-container has a stacking context.
 *
 * scrollbar-width:thin + themed track/thumb eliminates the stray white browser-
 * default scrollbar artifact that appeared on the right edge of the card frame.
 *
 * Fix C: mirror the chat container's horizontal margins so cards center over the panel.
 * The chat container uses margin: 0 clamp(0px, 12vw, 160px); the stage now matches so
 * both share the same horizontal center.
 *
 * align-items:flex-start ensures cards grow DOWNWARD from the stage top edge; they can
 * never protrude above the viewport. When empty the :empty rule collapses it to 0.
 */
.portal-app-stage {
    position: relative;
    z-index: 4;                             /* paint above .chat-container (z-index:3) */
    width: auto;                            /* Fix C: let margins constrain width, not 100% */
    /* Removed clamping constraint */
    display: flex;
    justify-content: center;
    align-items: flex-start; /* tall cards grow DOWN, never above the stage top */
    max-height: 40%;         /* relative to .working-area, not 40vh — prevents over-crush */
    overflow-y: auto;
    flex-shrink: 0;
    /* Collapsed by default: no pointer-events until a card is present */
    pointer-events: none;
    min-height: 0;
    /* Themed scrollbar — eliminates the stray white browser-default scrollbar artifact */
    scrollbar-width: thin;
    scrollbar-color: rgba(120,140,165,0.55) transparent;
}

.portal-app-stage::-webkit-scrollbar {
    width: 4px;
}
.portal-app-stage::-webkit-scrollbar-track {
    background: transparent;
}
.portal-app-stage::-webkit-scrollbar-thumb {
    background: rgba(120,140,165,0.55);
    border-radius: 2px;
}
.portal-app-stage::-webkit-scrollbar-thumb:hover {
    background: rgba(120,140,165,0.80);
}
[data-theme="light"] .portal-app-stage {
    scrollbar-color: rgba(44,62,80,0.30) transparent;
}
[data-theme="light"] .portal-app-stage::-webkit-scrollbar-thumb {
    background: rgba(44,62,80,0.30);
}
[data-theme="light"] .portal-app-stage::-webkit-scrollbar-thumb:hover {
    background: rgba(44,62,80,0.50);
}

/* :empty collapses the stage to zero height when no card is mounted */
.portal-app-stage:empty {
    display: none;
}

/* Active: a card is present — allow interaction, add breathing room below */
.portal-app-stage.app-stage--active {
    pointer-events: auto;
    padding-bottom: 8px;
}

/* The video element inside the stage — nested inside af-card shell */
.portal-app-stage video {
    width: 100%;
    display: block;
    /* border-radius + shadow provided by the af-card wrapper via overflow:hidden */
}

/* Ensure the video card has properly rounded corners and clips the video (T22) */
.portal-app-stage .af-card--video {
    border-radius: 16px !important; /* clearly rounded */
    overflow: hidden;               /* clips the video to the card's rounded corners */
    max-width: 640px;
    width: 100%;
    margin: 0;
}

/* The video body has zero padding so video fills edge-to-edge; the card's overflow:hidden clips corners */
.portal-app-stage .af-card__body--video {
    padding: 0;
    border-radius: 0; /* card overflow:hidden handles clipping */
}

/* Force 16:9 aspect ratio so the video card never collapses to a bar
   when autoplay is blocked and the video has no intrinsic size yet. */
.portal-app-stage .af-card__body--video video {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
    display: block;
    object-fit: contain; /* letterbox — preserves frame without cropping */
    border-radius: 0; /* card overflow:hidden clips to 16px */
}

/* inc2: web cards need more vertical room than media cards (iframe needs reading height).
 * Allow the stage to expand to 65% of .working-area for web cards.
 * Primary: :has() selector — no JS class needed in modern browsers.
 * Fallback: .app-stage--web class toggled by JS in openWebFocusCard / clearStage. */
.portal-app-stage:has(.af-card--web),
.portal-app-stage.app-stage--web {
    max-height: 65%;  /* % of .working-area — leaves at least 35% for the chat dock */
}

/* Web card sizing: fill the available stage width up to a comfortable max */
.portal-app-stage .af-card--web {
    max-width: 900px;
    width: 100%;
    margin: 0;
}

/* The iframe inside the web card: enforce min-height so it never collapses.
 * The buildWebCard builder sets height:60vh as an inline style; the stage
 * max-height:65vh (above) gives it room. We only add min-height here. */
.portal-app-stage .af-card--web iframe {
    min-height: 300px;
}

/* ── Part 2+4+5 (redesigned): Spotlight state — compact, in-flow card ───────
   PRIOR BEHAVIOR (removed): .app-stage--spotlight broke #portalAppStage out of
   the normal flow into position:fixed spanning ~97vw × 90vh — an invisible
   full-viewport click-blocker that also forced the chat panel to collapse
   (lowerChatPanel(), called by each opener in face-integration.js — also
   removed). Per the product owner's option-1 redesign: keep a focused card, but compact
   and non-intrusive — it must never cover or collapse the chat.

   This class is now just an entry/exit animation marker. The card stays in
   #portalAppStage's normal document flow (same positioning as a non-spotlight
   card — see the base .portal-app-stage rule above), sized by its own
   per-type rules (.af-card--media, .af-card--web, .af-card--video, etc. —
   already compact and on-brand). No position:fixed, no viewport-spanning
   click target, no forced chat collapse. */
/* T89 (5): Entry animation for the spotlight — fades in with a subtle scale-up
   so the card arrives rather than pops. 175ms ease-out keeps it snappy. */
@keyframes spotlightIn {
    from { opacity: 0; transform: translateY(28px); }
    to   { opacity: 1; transform: translateY(0); }
}

.portal-app-stage.app-stage--spotlight {
    background: transparent; /* NO dark backdrop on media spotlight */
    animation: spotlightIn 165ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

/* P3: clear the lingering animation-fill transform once entry settles. */
.portal-app-stage.app-stage--spotlight.app-stage--spotlight-settled {
    animation: none;
    transform: none;
}

/* P3: flip-down exit */
@keyframes spotlightOut {
    from { opacity: 1; transform: translateY(0); }
    to   { opacity: 0; transform: translateY(28px); }
}
.portal-app-stage.app-stage--spotlight.app-stage--spotlight-exiting {
    animation: spotlightOut 150ms cubic-bezier(0.4, 0, 1, 1) forwards;
}

/* Part 4: accompanying reply text inside the spotlight modal */
.af-card__spotlight-text {
    padding: 12px 16px;
    font-size: var(--font-size-base, 1rem);
    color: var(--color-text, #e8f0f8);
    line-height: 1.55;
    max-height: 100px;
    overflow-y: auto;
    text-align: left;
    width: 100%;
    box-sizing: border-box;
    border-top: 1px solid rgba(34,167,240,0.18);
    background: transparent;
    /* Themed scrollbar */
    scrollbar-width: thin;
    scrollbar-color: rgba(120,140,165,0.55) transparent;
}

/* Spotlight text is hidden when empty so no blank bar appears */
.af-card__spotlight-text:empty {
    display: none;
}

/* ---- Orb hero: top-center, large, the companion's physical presence ---- */

/* Orb-hero: transparent — the body's depth field is the backdrop.
   The orb floats over the field; the glass chat panel sits below/over it.
   inc1b: FIX2 z-index:4 removed — it was needed so the old #videoStage (inside orb-hero) could
   paint above .chat-container (z:3). Now that media cards live in #portalAppStage (normal
   flow, above .chat-container in DOM order), the z-index stacking trick is no longer needed.
   Orb layering: the orb canvas is inside .orb-hero which is in normal flow ABOVE
   .chat-container; since .chat-container has z-index:3 and .orb-hero has no z-index,
   the DOM order means .chat-container's z-index:3 stacking context sits on top when the
   panel is raised (panel-full). This is correct — FIX1 (margin-top:10px) keeps the visual
   overlap intentional and the orb shadow still shows through. */
.orb-hero {
    display: flex;
    justify-content: center;
    align-items: center;
    /* D: reduced top padding to tighten orb→chat vertical gap */
    padding: 8px 0 0;
    background: transparent;
    flex-shrink: 0;
    position: relative;
}

/* P2 state-machine hide classes */
#orbHero.state-sm-hidden { display: none; }
.working-area.state-sm-hidden { display: none; }

/* T73: Oya-match per luhonoco-website-pm verbatim spec — canvas-fill #16212e + canvas feather-mask 42%/70% */
/* The large orb canvas wrapper — take-four: viewport-relative, big, dominant */
/* min(78vh, 780px) — orb fills most of the upper viewport.
   Sizing the WRAP is what scales the orb and the eyes together: the eyes
   canvas below is sized in percentages of this box (46% x 32%), so it tracks
   any change here rather than needing its own number. Raised from
   min(60vh, 600px) on request, then again to min(78vh, 780px).

   ⚠ This is now LARGER than the splash-fullscreen override further down
   (min(72vh, 660px)), which used to be the bigger of the two. That override is
   deliberately untouched -- raise it too if the splash state should stay the
   dominant one. .orb-hero is flex-shrink:0 and the chat container is flex:1,
   so every pixel added here is taken from the transcript's height. */
/* Splash owns the large viewport-relative canvas. Chat supplies its own
   header-relative size below, so a docked orb never inherits this footprint. */
.hero-orb-wrap {
    background: transparent;
    flex-shrink: 0;
    position: relative;
    isolation: isolate;
    cursor: pointer;
}

#view-chat:not(.orb-chat-docked) .hero-orb-wrap {
    width: min(78vh, 780px);
    height: min(78vh, 780px);
    transition: width 400ms ease, height 400ms ease, transform 250ms cubic-bezier(0.34, 1.56, 0.64, 1), filter 250ms ease; /* #58: size animates on the return path (chat→splash); docked rule adds transform for the entry path. transform/filter added for Portal v2 drag-hover gravity-well. */
}

.hero-orb-wrap::before {
    /* T73: flat #16212e backing — covers canvas feather zone (42%→70% radius) with
       solid fill matching the canvas's own opaque background, then dissolves to
       transparent over a wide band so its outer edge never creates a second hard seam.
       Sized 140% of the wrap: canvas at 100% wrap → canvas 70%-mask edge = 50% of
       ::before's half-width → solid plateau held to 60% of ::before radius covers it
       with margin; fade from 60%→85% lands well outside the canvas area. */
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 140%;
    height: 140%;
    /* Disc removed (the product owner 2026-07-24): the orb canvas is now transparent
       (orbMount.js), so there's no opaque feather zone to cover — this backing
       disc only created a pale halo on the light panel. transparent = the orb
       blends on any background. (Was: radial-gradient of var(--orb-backdrop).) */
    background: transparent;
    border-radius: 50%;
    /* z-index: -1 places ::before behind the canvas (a normal-flow sibling child).
       isolation:isolate on .hero-orb-wrap forms the stacking context that contains
       this z-index:-1 backing — so it paints above the page background but below the
       canvas, not behind the page. (position:relative alone does NOT form a stacking
       context; filter did previously but was removed; isolation:isolate is purpose-built
       for this and does not reorder .hero-orb-wrap among its siblings.) */
    z-index: -1;
    pointer-events: none;
}

.hero-orb-wrap::after {
    /* Part D: CSS floor shadow ellipse beneath the orb — scaled with bigger orb */
    content: '';
    position: absolute;
    bottom: -20px;
    left: 50%;
    transform: translateX(-50%);
    width: 360px;
    height: 40px;
    background: radial-gradient(ellipse at center, rgba(20,28,40,0.24) 0%, transparent 70%);
    border-radius: 50%;
    pointer-events: none;
}

.hero-orb-wrap canvas {
    width: 100%;
    height: 100%;
    display: block;
    /* T73: Oya-match per luhonoco-website-pm verbatim spec — canvas-fill #16212e + canvas feather-mask 42%/70% */
    mask: radial-gradient(circle at center, #000 42%, transparent 70%);
    -webkit-mask: radial-gradient(circle at center, #000 42%, transparent 70%);
}

/* ---- Stage: materialization area below orb ---- */
/* Part E: stage is part of the glass panel — transparent, sits over the field */
/* Phase 1 fix A: cap stage height so a tall card stack cannot push .chat-container
   (and its docked input) off-screen. The stage scrolls internally; the dock stays
   visible. 30vh gives ~2 card heights on typical displays without starving the
   chat transcript below. */
/* T-overlap fix: .stage is a flex-shrink:0 column item above .chat-messages.
   It MUST collapse to zero space when empty of real cards (no gap, no separator)
   and occupy its own vertical band when cards are present (no overlap with messages).
   Three problems fixed below:
   1. .stage:not(:empty) was always true because .chat-card-mount is always present —
      replaced with :has(> .chat-card-mount > *) so the rule only fires when a real
      card child is mounted.
   2. Generic .chat-card-mount { padding-bottom:12px } bled phantom height into the
      stage even when no cards were present — overridden to 0 in stage context.
   3. .af-card { backdrop-filter } creates a new compositing layer; isolation:isolate
      on .stage contains that stacking context so it cannot paint over .chat-messages. */
.stage {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 8px 20px 0;
    background: transparent;
    flex-shrink: 0;
    min-height: 0;
    max-height: 40vh;
    overflow-y: auto;
    /* Contain backdrop-filter stacking context so cards never paint over .chat-messages */
    isolation: isolate;
}

/* Collapse the stage to zero space when no real card is mounted.
   :has() is supported in all current browsers (Chrome 105+, Safari 15.4+, FF 121+).
   When .chat-card-mount has no children the stage is hidden — no gap, no separator. */
.stage:not(:has(> .chat-card-mount > *)) {
    display: none;
}

.af-card--image .af-card__body { max-height: 30vh; overflow-y: auto; }

.af-card--image .af-card__image { max-height: 28vh; object-fit: contain; width: auto; max-width: 100%; display: block; margin: 0 auto; }

/* Card mount: beat-driven cards (image, file, form) appear here */
/* Fix D: in-flow card mount — no absolute/popover positioning, no detached shadow.
   Cards sit as a clear in-flow row between the header and the message transcript.
   A bottom separator visually separates cards from the first message bubble. */
.stage .chat-card-mount {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    gap: 10px;
    justify-content: center;
    width: 100%;
    max-width: 720px;
    /* Override the generic .chat-card-mount { padding-bottom:12px } — within the stage
       that padding contributed phantom height even when the mount was empty. */
    padding-bottom: 0;
}

/* Add separator + bottom padding only when a real card child is present.
   :has() mirrors the collapse rule above — fires only when mount has children. */
.stage:has(> .chat-card-mount > *) {
    border-bottom: 1px solid rgba(34,167,240,0.18);
    padding-bottom: 10px;
    margin-bottom: 0;
}

/* Fix D: af-card inside the chat stage — match the glass panel tone, don't use the
   opaque card surface that makes it read as a floating popover */
.stage .af-card {
    /* Tinted glass consistent with the panel — not a bright white/opaque popover */
    background: rgba(28, 40, 58, 0.72);
    border-color: rgba(34,167,240,0.35);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    /* Softer shadow than the default --af-card-shadow to keep it in-flow */
    box-shadow: 0 4px 16px rgba(0,0,0,0.28), 0 1px 4px rgba(0,0,0,0.16);
}

[data-theme="light"] .stage .af-card {
    background: rgba(236,240,245,0.88);
    border-color: rgba(34,167,240,0.30);
    box-shadow: 0 4px 14px rgba(100,120,140,0.18), 0 1px 4px rgba(100,120,140,0.10);
}

/* Video materialization slot — now lives inside the glass chat panel */
.stage-video-slot {
    width: 100%;
    max-width: 640px;
    display: flex;
    justify-content: center;
    margin: 0 auto;
    padding: 0 var(--space-4) var(--space-2);
    flex-shrink: 0;
}

/* Hide the slot when empty */
.stage-video-slot:empty {
    display: none;
    padding: 0;
}

/* The materialized video element */
.stage-video {
    width: 100%;
    max-width: 640px;
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    display: block;
    /* Animation: scales out from the orb center above */
    transform-origin: 50% -80px; /* approximately the orb center relative to video top */
    animation: stageVideoIn 420ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

@keyframes stageVideoIn {
    from {
        opacity: 0;
        transform: scale(0.08);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

/* ---- Override chat-container to fill remaining space ---- */
/* Part E: glass chat panel — dark translucent card floating over the field */
#view-chat .chat-container {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    min-height: 0;
    /* Full width column layout */
    margin: 0;
    /* Panel TRANSPARENT (the product owner 2026-07-24 "do the real fix"): the header sits
       INSIDE this panel, so any panel fill compounded with the header's own 0.55
       and made it look more opaque than the composer. Transparent = header AND
       composer both show their identical 0.55 glass over the app field, so they
       finally match. Bubbles are opaque and stay readable. */
    background: transparent;
    border: none;
    border-radius: 0;
    /* NO backdrop-filter. It used to blur(7px) here, and that did two things
       neither of which was wanted once the header and composer stopped being
       frosted:

       1. It tinted the whole panel. The header has no fill of its own, so what
          showed through it was a BLURRED copy of the page backdrop, not the
          backdrop -- a faint darker cast against the un-blurred page above the
          panel. That is the "looks transparent but obviously isn't once you
          scroll" band.
       2. Per spec a non-none backdrop-filter creates a containing block for
          position:fixed DESCENDANTS, which trapped .chat-input-area: its
          bottom:0 docked to this container's bottom instead of the viewport,
          and its background-attachment:fixed backdrop was then sampled from
          the container's origin rather than the viewport's -- so the composer
          painted a MISALIGNED slice of the gradient, which reads as a dark
          rectangle that shifts as the layout moves.

       This file already knew about (2): the .panel-lowered rule below sets
       backdrop-filter:none for exactly this reason and calls it "the root
       cause of T89". Removing it from the base rule makes the normal state
       behave the way the lowered state already had to be forced to. */
    box-shadow: none;
    position: relative;
    z-index: 3;
    /* FIX1: was -80px (95fad29) which caused ~80px orb-over-title overlap.
       Revert toward positive to give ~16px clearance below orb visual bottom.
       -80px overlap + 96px shift = +16px gap. Using +10px to be conservative
       (clearance without re-opening a large dead gap). */
    margin-top: 10px;
    /* Panel raise/lower — smooth CSS transition */
    transition: transform 360ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* T17: light-mode — lighter glass surface with partial transparency */
[data-theme="light"] #view-chat .chat-container {
    /* DAY MODE KEEPS ITS OFF-WHITE PANEL (the product owner 2026-07-24). The transparent-
       panel "real fix" looks good in NIGHT mode (dark field) but ruined day mode:
       on the pale silver-blue field, translucent AI bubbles either vanished
       (faint tint) or looked ugly (solid white). So the transparent panel is
       DARK-ONLY; light restores the original composed off-white glass panel +
       elevation, and the AI bubble goes back to its subtle 0.10 blue tint, which
       reads correctly against THIS panel. Trade-off: in day mode the header sits
       on this panel (slightly more solid than the composer) — acceptable; a
       clean, readable day mode beats a matched-but-ugly one. */
    background: rgba(236,240,245,0.74);
    border: 1px solid rgba(0,0,0,0.06);
    box-shadow:
        0 18px 42px rgba(100,120,140,0.22),
        0 6px 16px rgba(100,120,140,0.14),
        inset 0 1px 0 rgba(255,255,255,0.90);
    --color-bubble-ai: rgba(34,167,240,0.10);
    --color-bubble-ai-text: #1c2733;
}

/* ===================================================================
   Defect D fix — light-mode chat text: dark-on-light for readability
   All hard-coded near-white / light-blue-grey values in the chat are
   overridden here so every text element is clearly legible on the pale
   rgba(236,240,245,0.74) glass panel and the silver-blue 3D backdrop.
   Dark mode is untouched.
   =================================================================== */

/* Chat header title ("Companion") and header row */
/* The fade itself, on a pseudo-element so the MASK cannot touch the icons --
   a mask applies to an element's content as well as its background, so putting
   it on .chat-header would fade the icon row out along with the backdrop.

   It paints the PAGE'S OWN BACKDROP, not an approximation of it. body's stack
   is background-attachment:fixed, i.e. positioned against the viewport, so
   repeating it here samples the identical pixels -- glows included. A flat
   colour cannot do this: the page has a soft blue radial glow in the top-left
   corner, so any solid start colour is visibly darker than the page exactly
   where the header sits. That mismatch is the dark band.

   NOTE this only works because #view-chat .chat-container no longer sets a
   backdrop-filter. A filtered ancestor makes background-attachment:fixed
   resolve against that ancestor instead of the viewport, and the alignment
   silently breaks again. */
.chat-header::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background:
        radial-gradient(1300px 900px at 16% 6%,  rgba(34,167,240,0.22),  transparent 60%),
        radial-gradient(1100px 820px at 88% 94%, rgba(64,219,176,0.18),  transparent 60%),
        radial-gradient(1000px 760px at 60% 48%, rgba(44,62,80,0.10),    transparent 70%),
        linear-gradient(160deg, #c3cdd8 0%, #b6c3d1 45%, #aeb9c6 100%);
    background-attachment: fixed;
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 100%);
}

[data-theme="dark"] .chat-header::before {
    background:
        radial-gradient(1300px 900px at 16% 6%,  rgba(34,167,240,0.22),  transparent 60%),
        radial-gradient(1100px 820px at 88% 94%, rgba(64,219,176,0.18),  transparent 60%),
        radial-gradient(1000px 760px at 60% 48%, rgba(44,62,80,0.10),    transparent 70%),
        linear-gradient(160deg, #16212e 0%, #10212e 100%);
    background-attachment: fixed;
}

[data-theme="light"] .chat-header {
    /* No background here -- the ::before above already paints the light
       backdrop as the base case. */
    color: #1c2733;
}
[data-theme="light"] .chat-header-title {
    color: #1c2733;
}

/* Agent status display ("thinking…" / idle) */
[data-theme="light"] .agent-status-display {
    color: rgba(44,62,80,0.70);
}

/* Message timestamps — H: lifted contrast for WCAG AA legibility on light glass panel */
[data-theme="light"] .message-time {
    color: rgba(44,62,80,0.88); /* lifted from 0.80 → 0.88; still softer than body text */
}

/* Companion header title — swept in late: .robot-eyes-header #companionName carries
   a belt-and-suspenders ID-level specificity (1,1,0) rule (see :447) that beats ANY
   plain-class light override, including the .companion-badge-name ones already here
   (:449, :517) — measured live: computed color stayed #FFFFFF in light mode despite
   both existing overrides. Match its specificity with an ID selector of our own so this
   one actually wins, no !important needed. Dark ink, not Mycroft Blue: #22A7F0 measured
   ~2.1:1 on this header's glass background — well under AA — while #1c2733 clears 11:1. */
[data-theme="light"] .robot-eyes-header #companionName {
    color: #1c2733;
}

/* Agent-bubble inline timestamp — .message-time-inline has no light override at all
   anywhere in the file (hardcoded rgba(255,255,255,0.7)), same defect as .message-time
   above, just missed in that pass. Scoped to .message.assistant only: the user/blue-bubble
   timestamp is intentionally left alone (white-on-blue reads fine there; not the reported bug). */
[data-theme="light"] .message.assistant .message-time-inline {
    color: rgba(44,62,80,0.88);
}

/* Date divider — same hardcoded-white pattern (rgba(255,255,255,0.6) text on a
   rgba(255,255,255,0.05) chip), latent rather than reported yet, but it's the identical
   defect on a light glass panel. Text alpha matches .message-time's 0.88 above; chip
   background nudged from 0.05 → 0.08 so the pill still reads as a pill once its ink is dark. */
[data-theme="light"] .date-divider {
    color: rgba(44,62,80,0.88);
    background: rgba(44,62,80,0.08);
}

/* Message footer (model info, context, session badge) */
[data-theme="light"] .message-footer {
    color: rgba(44,62,80,0.60);
    border-top-color: rgba(44,62,80,0.12);
}

/* Chat input textarea */
[data-theme="light"] .chat-input {
    color: #1c2733;
}
[data-theme="light"] .chat-input::placeholder {
    color: rgba(44,62,80,0.45);
}

/* Chat input wrapper: light border on pale panel */
[data-theme="light"] .chat-input-wrapper {
    /* Tinted glass to match night's sheen (the product owner 2026-07-24: "night has a tint
       day doesn't"). Night is white-sheen over navy; day is white-sheen over a
       PALE COOL-BLUE tint (rgba(232,240,248,0.72)) so the sheen actually shows
       (a white sheen over pure white is invisible) and the bar reads as tinted
       glass, not flat white. NEUTRAL inactive border; blue on focus (below). */
    background: linear-gradient(180deg, rgba(255,255,255,0.55), rgba(255,255,255,0.12) 45%, rgba(255,255,255,0) 100%), rgba(232,240,248,0.72);
    border-color: rgba(44,62,80,0.16);
}

/* Light mode: blue accent on FOCUS (active), neutral at rest — swaps the old
   inactive-blue / active-white (the product owner 2026-07-24). The [data-theme] attribute
   raises specificity above the shared white :focus-within (~line 6858), so this
   wins in light theme without changing dark mode's neutral focus. */
[data-theme="light"] .chat-input-wrapper:focus-within {
    border-color: rgba(34,167,240,0.60);
    box-shadow: 0 0 0 2px rgba(34,167,240,0.22);
}

/* Listening bar in LIGHT mode stays LIGHT (the product owner 2026-07-24: "in light mode
   the listening background turns black, I don't want this"). Matches the light
   composer's tinted glass so entering listening doesn't flip to a dark pill.
   Label + buttons re-tinted for the light surface below. */
[data-theme="light"] .chat-input-wrapper.chat-input-wrapper--listening {
    background: linear-gradient(180deg, rgba(255,255,255,0.55), rgba(255,255,255,0.12) 45%, rgba(255,255,255,0) 100%), rgba(232,240,248,0.72);
    border-color: rgba(44,62,80,0.16);
}
/* Dark label text + defined buttons on the light listening pill (the defaults
   are near-white, built for the old dark pill). */
[data-theme="light"] .listening-label {
    color: rgba(44,62,80,0.85);
}
[data-theme="light"] .listening-bar--finishing .listening-label {
    color: rgba(44,62,80,0.95);
}
[data-theme="light"] .listening-btn {
    background: rgba(255,255,255,0.9);
    border: 1px solid rgba(44,62,80,0.14);
    color: #14202c;
}

/* Icon buttons in the input bar (paperclip, mic) */
[data-theme="light"] .chat-btn {
    color: rgba(44,62,80,0.60);
}
[data-theme="light"] .chat-btn:hover {
    background: rgba(44,62,80,0.08);
    color: var(--color-primary);
}

/* Search toggle icon in the header */
[data-theme="light"] .chat-search-toggle {
    color: rgba(44,62,80,0.60);
}

/* Search input text + placeholder */
[data-theme="light"] .chat-search-input {
    color: #1c2733;
    background: rgba(255,255,255,0.60);
    border-color: rgba(34,167,240,0.35);
}
[data-theme="light"] .chat-search-input::placeholder {
    color: rgba(44,62,80,0.45);
}

/* Context % label */
[data-theme="light"] .context-label {
    color: rgba(44,62,80,0.55);
}

/* Session indicator badge (debug mode only) */
[data-theme="light"] .session-indicator {
    color: #1c2733;
    background: rgba(255,255,255,0.55);
    border-color: rgba(34,167,240,0.35);
}
[data-theme="light"] .session-dropdown {
    background: rgba(255,255,255,0.96);
    border-color: rgba(34,167,240,0.40);
    box-shadow: 0 8px 24px rgba(100,120,140,0.20);
}
[data-theme="light"] .session-option { color: #1c2733; }
[data-theme="light"] .session-option:hover { background: rgba(44,62,80,0.07); }

/* Typing indicator dots */
[data-theme="light"] .typing-indicator {
    background: rgba(34,167,240,0.10);
    border-color: rgba(34,167,240,0.20);
}

/* ===================================================================
   Item E — chat scrollbar styling (dark + light mode)
   Applies to .chat-messages scroll area (and any other scroll area
   that shares the same scoping).
   =================================================================== */

/* Dark mode: Fix 4 — visible slate-blue thumb (was rgba(255,255,255,0.18) — a barely-visible smear) */
[data-theme="dark"] .chat-messages {
    scrollbar-width: thin;
    scrollbar-color: rgba(120,140,165,0.55) transparent;
}
[data-theme="dark"] .chat-messages::-webkit-scrollbar {
    width: 6px;
}
[data-theme="dark"] .chat-messages::-webkit-scrollbar-track {
    background: transparent;
}
[data-theme="dark"] .chat-messages::-webkit-scrollbar-thumb {
    background: rgba(120,140,165,0.55);
    border-radius: 3px;
}
[data-theme="dark"] .chat-messages::-webkit-scrollbar-thumb:hover {
    background: rgba(120,140,165,0.80);
}

/* Light mode: medium-grey thumb on transparent track — visible on pale panel */
[data-theme="light"] .chat-messages {
    scrollbar-width: thin;
    scrollbar-color: rgba(44,62,80,0.30) transparent;
}
[data-theme="light"] .chat-messages::-webkit-scrollbar {
    width: 6px;
}
[data-theme="light"] .chat-messages::-webkit-scrollbar-track {
    background: transparent;
}
[data-theme="light"] .chat-messages::-webkit-scrollbar-thumb {
    background: rgba(44,62,80,0.30);
    border-radius: 3px;
}
[data-theme="light"] .chat-messages::-webkit-scrollbar-thumb:hover {
    background: rgba(44,62,80,0.50);
}

/* Level 1 (mid): distinct mid-height — panel occupies roughly the lower half
   of the viewport, leaving the orb clearly visible above it.
   max-height caps the panel so it never fills more than 50vh in mid state,
   giving a clear three-step visual ladder: low → mid → full. */
/* Removed max-height so the chat panel extends fully to the bottom of the viewport,
   allowing messages to scroll behind the floating input without being harshly clipped. */
#view-chat .chat-container:not(.panel-lowered):not(.panel-full) {
    /* No max-height limit */
}

/* Level 0 (low): compact panel showing input bar + a ~120px message peek.
   No transform — avoids creating a containing block that breaks
   position:fixed on .chat-input-area (the root cause of T89).
   backdrop-filter:none — a non-none backdrop-filter also creates a containing
   block for position:fixed descendants (CSS spec); without this override the
   base #view-chat .chat-container { backdrop-filter:blur(7px) } rule traps
   .chat-input-area so bottom:0 docks to the container's bottom (~547px above
   the viewport), not the viewport.  Neutralising it here (lowered state only)
   lets the fixed input pin correctly to the viewport bottom.
   max-height math: 56px panel-header strip + 120px message peek + 34px breathing room = 210px.
   overflow:hidden clips older messages above the panel top; JS scroll-to-bottom
   keeps the newest messages in view at the bottom of the clipped area.
   The fixed .chat-input-area (position:fixed; bottom:0; unaffected by overflow:hidden
   on a non-transformed ancestor) stays fully visible and interactive below. */
#view-chat .chat-container.panel-lowered {
    flex: 0 0 auto;
    min-height: 0;
    max-height: 210px; /* 56px header + 120px message peek + 34px buffer */
    overflow: hidden;  /* clip old messages at top; newest at scroll-bottom remain visible */
    background: transparent;
    border: none;
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    mask-image: linear-gradient(to bottom, transparent 0%, black 20%);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 20%);
    /* Defect (e) — BOTTOM-DOCK the collapsed strip.
       .working-area is a column flex parent (justify-content:flex-start) and this
       .chat-container is its LAST flex child, so by default the strip packed at the
       TOP, leaving ~560px dead space below it down to the fixed input bar.
       margin-top:auto absorbs all free space ABOVE the item, pushing it to the
       BOTTOM of .working-area (= viewport bottom). Its visible bottom edge then
       sits just above the position:fixed input bar (which overlays bottom:0), and
       the empty depth-field space now appears ABOVE the strip.
       NOTE: margin (not transform/filter) — does NOT create a containing block, so
       the fixed .chat-input-area stays pinned to the viewport (guardrail #4). */
    margin-top: auto;
}

/* In lowered state: keep .chat-messages visible for the ~120px peek;
   hide #stage (card band) and .typing-indicator which add no value in the compact strip. */
#view-chat .chat-container.panel-lowered #stage,
#view-chat .chat-container.panel-lowered .typing-indicator {
    display: none;
}

/* Level 2 (full): panel TRULY fills the entire viewport (T21).
   position:fixed escapes the flex layout entirely, overriding margin/transform/border-radius.
   Fix 3: left edge starts at 56px (icon-rail width) so the rail stays visible + clickable.
   z-index:50 sits above the orb (2) and the stage (4) but below modals (1000+). */
#view-chat .chat-container.panel-full {
    position: fixed !important;
    top: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    left: 56px !important; /* icon-rail is 56px wide — keep it visible */
    width: auto !important;
    height: auto !important;
    max-width: none !important;
    margin: 0 !important;
    border-radius: 0 !important;
    transform: none !important;
    z-index: 50;
}

/* On mobile (rail hidden), full-screen chat can go edge-to-edge */
@media (max-width: 768px) {
    #view-chat .chat-container.panel-full {
        left: 0 !important;
    }
}

/* T33: full-screen panel is translucent so the orb shows through (dark) */
#view-chat .chat-container.panel-full {
    background: rgba(20,30,44,0.55) !important;
    backdrop-filter: blur(12px) !important;
    -webkit-backdrop-filter: blur(12px) !important;
}

/* T33: full-screen panel translucent (light mode) */
[data-theme="light"] #view-chat .chat-container.panel-full {
    background: rgba(236,240,245,0.55) !important;
    backdrop-filter: blur(12px) !important;
    -webkit-backdrop-filter: blur(12px) !important;
}

/* T33: keep the orb visible behind the translucent full-screen panel;
   pointer-events:none so clicks pass through to the panel */
#view-chat.panel-full-active #orbHero {
    opacity: 1;
    pointer-events: none;
    transition: opacity 360ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Portrait / narrow screens: full-width glass panel, no horizontal margins */
@media (orientation: portrait), (max-width: 600px) {
    #view-chat .chat-container {
        margin: 0;
        /* FIX1 (portrait): was -48px causing orb overlap on narrow screens.
           0px = panel top at exact orb bottom; no clearance but no overlap. */
        margin-top: 0;
        border-radius: 12px 12px 0 0;
    }
    /* Fix C: stage also goes full-width on portrait/narrow to match the panel */
    .portal-app-stage {
        margin-left: 0;
        margin-right: 0;
    }
}

/* ---- Mobile: hide icon rail, show bottom nav ---- */

@media (max-width: 768px) {
    .icon-rail {
        display: none;
    }

    .bottom-nav {
        display: none !important;
    }

    /* 3-icon bottom nav: icon-only, no text; min touch target 44px */
    .bottom-nav-item {
        font-size: 0;
        min-width: 44px;
        min-height: 44px;
    }

    /* Shrink orb hero on mobile — viewport-relative, still dominant */
    #view-chat:not(.orb-chat-docked) .hero-orb-wrap {
        width: min(60vw, 280px);
        height: min(60vw, 280px);
    }

    .stage {
        padding: 6px 12px 0;
    }
}

@media (max-width: 480px) {
    #view-chat:not(.orb-chat-docked) .hero-orb-wrap {
        width: min(55vw, 200px);
        height: min(55vw, 200px);
    }
}

/* Landscape needs the same cap before the desktop breakpoint: 78vh at 769–1023px
   made both the splash and the transitioning dock needlessly tall. */
@media (orientation: landscape) and (min-width: 481px) {
    #view-chat:not(.orb-chat-docked) .hero-orb-wrap {
        width: min(45vh, 460px);
        height: min(45vh, 460px);
    }
}

/* Part F: safe-area insets for notched / rounded-corner phones */
@supports (padding: env(safe-area-inset-left)) {
    .chat-input-area {
        padding-left: calc(var(--space-5) + env(safe-area-inset-left, 0px));
        padding-right: calc(var(--space-5) + env(safe-area-inset-right, 0px));
    }
    .bottom-nav {
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }
}

/* Mobile: Part F above re-adds the --space-5 desktop side padding to the
   composer, which is what kept it off the screen edges. On phones the bar must
   be FULL WIDTH (the product owner 2026-07-23), so drop that back to only the horizontal
   safe-area inset (0 on a portrait phone). Placed AFTER Part F so it wins by
   source order at equal specificity. */
@media (max-width: 768px) {
    .chat-input-area {
        padding-left: env(safe-area-inset-left, 0px);
        padding-right: env(safe-area-inset-right, 0px);
    }
    /* The chat-container's backdrop-filter creates a containing block that TRAPS
       the position:fixed composer, docking bottom:0 to the container's bottom
       (~20px above the viewport) instead of the true viewport bottom — that's
       the gap under the bar. Drop the blur on phones (same technique the
       lowered/level-0 states use, style.css:4300) so the composer pins to the
       real bottom and sits flush. Must come AFTER the base rule (~4086). */
    #view-chat .chat-container {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
}

/* =========================================================
   T39 — Files view (Provost-style inbox/outbox)
   Uses portal token vars — NO hardcoded blue-violet.
   ========================================================= */

.af-files-view {
    display: flex;
    flex-direction: column;
    height: 100%;
    /* No padding of its own — it now always renders inside .proto-library__body,
       which already supplies the library's standard padding. */
    box-sizing: border-box;
    font-family: var(--font-body);
    color: var(--color-text);
}

.af-files-top {
    display: flex;
    gap: var(--space-4);
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

/* --- Pane (inbox / outbox) --- */
.af-files-pane {
    flex: 1 1 50%;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-4);
    min-width: 0;
    overflow: hidden;
}

[data-theme="light"] .af-files-pane {
    background: #FFFFFF;
    border-color: var(--color-border);
}

/* Dark theme: the pane's default background is --color-surface, the same
   token the library panel itself uses — without this it blends into the
   panel with only the 1px border to separate it. Lighten it one step, same
   "elevated surface on dark" convention used elsewhere in this file. */
[data-theme="dark"] .af-files-pane {
    background: rgba(255, 255, 255, 0.04);
}

/* --- Pane heading --- */
.af-files-h {
    font-size: var(--font-size-base);
    font-weight: 600;
    color: var(--color-text);
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
}

.af-files-sub {
    font-weight: 400;
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

.af-files-refresh {
    margin-left: auto;
    width: 32px;
    height: 32px;
    display: grid;
    place-items: center;
    background: transparent;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    padding: 7px;
    color: var(--color-primary);
    cursor: pointer;
    transition: background var(--transition-fast), color var(--transition-fast);
}
.af-files-refresh:hover {
    background: var(--color-primary-light);
}
.af-files-refresh:focus-visible,
.af-files-action:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}
.af-files-refresh svg,
.af-files-action svg {
    width: 17px;
    height: 17px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* --- Drop zone --- */
.af-files-drop {
    border: 2px dashed var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-4) var(--space-3);
    display: flex;
    align-items: center;
    gap: var(--space-3);
    cursor: pointer;
    transition: border-color var(--transition-fast), background var(--transition-fast);
    flex-shrink: 0;
    outline: none;
}
.af-files-drop:hover,
.af-files-drop:focus {
    border-color: var(--color-primary);
    background: var(--color-primary-light);
}
.af-files-drop.--over {
    border-color: var(--color-primary);
    background: var(--color-primary-light);
}

.af-files-drop-ic {
    font-size: 1.5rem;
    color: var(--color-primary);
    flex-shrink: 0;
}

.af-files-drop-tx {
    font-size: var(--font-size-sm);
    color: var(--color-text-secondary);
    line-height: 1.4;
}

.af-files-browse {
    background: none;
    border: none;
    color: var(--color-primary);
    cursor: pointer;
    font-size: inherit;
    text-decoration: underline;
    padding: 0;
    font-family: inherit;
}
.af-files-browse:hover {
    color: var(--color-primary-dark);
}

.af-files-pick {
    display: none;
}

/* --- File list --- */
.af-files-list {
    list-style: none;
    margin: 0;
    padding: 0;
    flex: 1;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.af-files-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--color-surface-hover);
    transition: background var(--transition-fast);
}
.af-files-main {
    position: relative;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
}
.af-files-row:hover {
    background: var(--color-primary-light);
}
[data-theme="light"] .af-files-row {
    background: #F5F7FA;
}
[data-theme="light"] .af-files-row:hover {
    background: var(--color-primary-light);
}

.af-files-name {
    min-width: 0;
    font-size: var(--font-size-sm);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--color-text);
}

.af-files-file {
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.af-files-type {
    flex-shrink: 0;
    min-width: 2.7rem;
    max-width: 4.5rem;
    overflow: hidden;
    text-overflow: ellipsis;
    padding: 2px 6px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-primary-light);
    color: var(--color-primary);
    font-size: 0.625rem;
    font-weight: 700;
    line-height: 1.3;
    letter-spacing: 0.04em;
    text-align: center;
}

.af-files-meta {
    position: relative;
    margin-left: calc(2.7rem + var(--space-2));
    width: fit-content;
    font-size: 0.6875rem;
    color: var(--color-text-muted);
    white-space: nowrap;
    line-height: 1.2;
}
.af-files-meta summary {
    display: flex;
    align-items: center;
    gap: 5px;
    width: fit-content;
    border-radius: 3px;
    cursor: pointer;
    list-style: none;
}
.af-files-meta summary::-webkit-details-marker { display: none; }
.af-files-meta summary::after {
    content: '›';
    margin-left: 1px;
    font-size: 0.75rem;
    transform: rotate(90deg);
    transition: transform var(--transition-fast);
}
.af-files-meta[open] summary::after { transform: rotate(-90deg); }
.af-files-meta summary:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}
.af-files-meta-popover {
    display: flex;
    gap: 5px;
    margin-top: 5px;
    padding: 6px 8px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text-secondary);
    box-shadow: 0 6px 18px rgba(0,0,0,.16);
}
.af-files-meta:not([open]) .af-files-meta-popover { display: none; }

.af-files-action {
    width: 32px;
    height: 32px;
    padding: 7px;
    display: grid;
    place-items: center;
    box-sizing: border-box;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--color-text-secondary);
    text-decoration: none;
    cursor: pointer;
    transition: color var(--transition-fast), background var(--transition-fast);
}
.af-files-dl:hover {
    color: var(--color-primary);
    background: var(--color-primary-light);
}

.af-files-del {
    font: inherit;
}
.af-files-del:hover {
    color: #e74c3c;
    background: rgba(231, 76, 60, 0.1);
}

.af-files-empty {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    text-align: center;
    padding: var(--space-6) var(--space-4);
}

/* --- Status bar --- */
.af-files-status {
    margin-top: var(--space-3);
    font-size: var(--font-size-sm);
    color: var(--color-text-secondary);
    min-height: 1.5em;
    flex-shrink: 0;
}
.af-files-status.--err {
    color: var(--color-danger);
}

/* --- Mobile: stack panes and let the library body own scrolling --- */
@media (max-width: 768px) {
    .af-files-view {
        height: auto;
        min-height: 100%;
    }
    .af-files-top {
        flex-direction: column;
        flex: none;
        overflow: visible;
    }
    .af-files-pane {
        flex: none;
        padding: var(--space-3);
        overflow: visible;
    }
    .af-files-list {
        flex: none;
        overflow: visible;
    }
    .af-files-row {
        grid-template-columns: minmax(0, 1fr) auto auto;
        column-gap: var(--space-2);
        padding: var(--space-2);
    }
    .af-files-action,
    .af-files-refresh {
        min-width: 44px;
        min-height: 44px;
    }
    .af-files-meta-popover {
        position: fixed;
        z-index: 20;
        top: auto;
        right: var(--space-4);
        bottom: calc(env(safe-area-inset-bottom, 0px) + var(--space-4));
        left: var(--space-4);
        justify-content: center;
        padding: var(--space-3);
        font-size: var(--font-size-sm);
    }
    .af-files-drop {
        padding: var(--space-3);
    }
}

/* ===================================================================
   Voice Debug Card — ui.card.voice beat surface
   Farm palette: Mycroft Blue #22A7F0, Navy #2C3E50; Noto Sans.
   Renders as a small popover anchored above the mic button — see
   .af-card--voice-tooltip below and _positionVoiceTooltip() in
   face-integration.js. NOT mounted into #portalAppStage (customer feedback
   2026-07-19: "I don't want it on the header!!!").
   =================================================================== */

/* Voice debug card variant — glass-style body matching farm brand */
.af-card--voice-debug {
    background: rgba(44, 62, 80, 0.97); /* Navy glass */
    border: 1px solid rgba(34, 167, 240, 0.35);
    color: #f0f4f8;
    font-family: var(--font-body, 'Noto Sans', 'Segoe UI', Arial, sans-serif);
}

/* ---- Tooltip/popover geometry ----
   Small popover anchored just above the mic button (#voiceBtn). Positioned
   via inline left/bottom + the --vd-arrow-left custom property, both set by
   _positionVoiceTooltip() in face-integration.js on open and on resize. */
.af-card--voice-tooltip {
    position: fixed;
    width: 260px;
    max-width: calc(100vw - 16px);
    z-index: 200; /* above .chat-input-area (151), below modals (1000) */
    border-radius: 14px;
    box-shadow: 0 10px 32px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(34, 167, 240, 0.18);
    animation: vd-tooltip-in 140ms ease-out;
}

@keyframes vd-tooltip-in {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* Downward-pointing tail toward the mic button. Horizontal position tracks
   --vd-arrow-left so the tail still points at the mic even when the card
   itself is clamped away from center near a viewport edge. */
.af-card--voice-tooltip::after {
    content: "";
    position: absolute;
    left: var(--vd-arrow-left, 50%);
    bottom: -7px;
    width: 14px;
    height: 14px;
    background: inherit;
    border-right: 1px solid rgba(34, 167, 240, 0.35);
    border-bottom: 1px solid rgba(34, 167, 240, 0.35);
    transform: translateX(-50%) rotate(45deg);
    border-radius: 0 0 3px 0;
}

/* Tighter body padding/gaps — a tooltip, not a panel (customer feedback
   2026-07-19: same "trim the chrome" direction as the body padding below,
   applied on top of it for the smaller popover footprint). */
.af-card--voice-tooltip .af-card__body--voice {
    padding: 10px 12px 12px;
    gap: 10px;
}

.af-card--voice-tooltip .vd-controls-row {
    gap: 8px;
}

.af-card--voice-tooltip .vd-status-wrap {
    font-size: 13px;
}

/* Header — was a full-bleed solid-blue fill with no visible text (the
   "Voice" title chip renders blue-on-rgba-blue, so on a solid blue bar it's
   effectively invisible) and read as a fat unlabeled title strip. Customer
   feedback 2026-07-19 ("fat headers"): slim it to the same subtle surface
   strip the other cards already use (see .af-card__header above), with
   Mycroft Blue kept only as a thin accent underline. */
.af-card__header--voice {
    background: transparent;
    border-bottom: 1px solid rgba(34, 167, 240, 0.30);
    /* Idle status now lives on this row too (customer feedback 2026-07-19:
       collapse its separate line onto "Voice"'s row) — left-align the two
       instead of the base class's space-between, with a right gutter so the
       status never slides under the absolutely-positioned dismiss ×
       (.af-card__controls, top:6px/right:6px). */
    justify-content: flex-start;
    gap: 8px;
    padding-right: 32px;
}

/* Body: column layout, trimmed padding (customer feedback 2026-07-19:
   panel read as ~70-75% chrome for minimal content) */
.af-card__body--voice {
    display: flex;
    flex-direction: column;
    gap: 14px;
    padding: 14px 16px 16px;
    min-height: 0;
    flex: 1;
    overflow: hidden;
}

/* Expanded (modal size, not viewport) body fills remaining height */
.af-card--fullscreen .af-card__body--voice {
    height: calc(100% - 48px); /* 48px = controls + header */
    padding: 28px 32px 24px;
}

/* ---- Status indicator ---- */

/* Lives inline in .af-card__header--voice now (customer feedback
   2026-07-19), not the controls row — margin:0 clears the block-level
   default so it sits flush against the header's own gap spacing. */
.vd-status-wrap {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0;
    font-size: 15px;
    font-weight: 500;
    color: rgba(240, 244, 248, 0.85);
    flex-shrink: 0;
}

.vd-status-dot {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    display: inline-block;
    flex-shrink: 0;
    transition: background 250ms ease, box-shadow 250ms ease;
}

/* State colors: idle=muted, listening=green pulse, thinking=yellow, speaking=blue */
.vd-status-dot--idle {
    background: rgba(240, 244, 248, 0.30);
    box-shadow: none;
}

.vd-status-dot--listening {
    background: #40DBB0; /* state green */
    box-shadow: 0 0 0 3px rgba(64, 219, 176, 0.35);
    animation: vd-pulse 1.1s ease-in-out infinite;
}

.vd-status-dot--thinking {
    background: #FEE255; /* state yellow */
    box-shadow: 0 0 0 3px rgba(254, 226, 85, 0.30);
    animation: vd-pulse 0.9s ease-in-out infinite;
}

.vd-status-dot--speaking {
    background: #22A7F0; /* Mycroft Blue */
    box-shadow: 0 0 0 3px rgba(34, 167, 240, 0.35);
    animation: vd-pulse 0.7s ease-in-out infinite;
}

@keyframes vd-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.45; }
}

/* ---- Barge-in / conversation toggle button ---- */

/* Was display:block/width:100% at 14px/20px padding + 16px font — rendered
   ~3x the height of the Barge In pill next to it and read as a fat heading.
   Customer feedback 2026-07-19: shrink to a normal button, comparable height
   to Barge In, sitting inline in the controls row like the other controls. */
.vd-barge-btn {
    padding: 10px 18px;
    background: #22A7F0;
    color: #fff;
    border: none;
    border-radius: 8px;
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    text-align: center;
    transition: background 180ms ease, box-shadow 180ms ease, transform 80ms ease;
    flex-shrink: 0;
    letter-spacing: 0.01em;
}

.vd-barge-btn:hover {
    background: #1a94d8;
    box-shadow: 0 4px 18px rgba(34, 167, 240, 0.45);
}

.vd-barge-btn:active {
    transform: scale(0.98);
}

/* Active / conversation-running state — pulses to indicate live loop */
.vd-barge-btn--active {
    background: #D81159; /* state red — stop color */
    box-shadow: 0 0 0 3px rgba(216, 17, 89, 0.30);
    animation: vd-btn-pulse 1.4s ease-in-out infinite;
}

.vd-barge-btn--active:hover {
    background: #b80e4b;
    box-shadow: 0 4px 18px rgba(216, 17, 89, 0.50);
    animation: none;
}

@keyframes vd-btn-pulse {
    0%, 100% { box-shadow: 0 0 0 3px rgba(216, 17, 89, 0.30); }
    50% { box-shadow: 0 0 0 6px rgba(216, 17, 89, 0.15); }
}

/* ---- Transcript area ---- */

.vd-transcript {
    flex: 1;
    overflow-y: auto;
    background: rgba(0, 0, 0, 0.25);
    border: 1px solid rgba(34, 167, 240, 0.18);
    border-radius: 8px;
    padding: 12px 14px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    font-size: 14px;
    line-height: 1.55;
    min-height: 80px;
    scroll-behavior: smooth;
}

.vd-transcript-line {
    display: flex;
    gap: 6px;
    align-items: flex-start;
}

.vd-transcript-role {
    flex-shrink: 0;
    font-weight: 600;
    font-size: 13px;
    padding-top: 1px;
}

.vd-transcript-line--user .vd-transcript-role {
    color: #40DBB0; /* state green */
}

.vd-transcript-line--agent .vd-transcript-role {
    color: #22A7F0; /* Mycroft Blue */
}

.vd-transcript-line--system {
    color: rgba(240, 244, 248, 0.45);
    font-size: 12px;
    font-style: italic;
}

.vd-transcript-line--system .vd-transcript-role {
    display: none; /* system lines have no role label */
}

.vd-transcript-text {
    color: rgba(240, 244, 248, 0.90);
    word-break: break-word;
}

/* ---- Mic button: conversation-active indicator ---- */

/* voiceBtn gets .voice-conv-active while the loop is running */
.voice-btn.voice-conv-active {
    color: #22A7F0;
    background: rgba(34, 167, 240, 0.12);
    border-radius: 50%;
    animation: vd-btn-conv-pulse 1.4s ease-in-out infinite;
}

@keyframes vd-btn-conv-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(34, 167, 240, 0.0); }
    50% { box-shadow: 0 0 0 5px rgba(34, 167, 240, 0.25); }
}

/* ===================================================================
   Voice Debug Card — display beat: controls row + 3 diagnostic panels
   app.gwf parity: STT/BRAIN/EVENTS panels + echo mode + barge-in btn.
   Palette: Mycroft Blue #22A7F0, Navy #2C3E50; amber #F59E0B.
   =================================================================== */

/* ---- Controls row: [Start Voice] [Barge In] [Echo label] ---- */
/* Status moved to the header row (.af-card__header--voice) — see above. */

.vd-controls-row {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    flex-shrink: 0;
}

/* Barge In button — secondary action, subdued vs Start Voice */
.vd-barge-in-btn {
    padding: 10px 16px;
    background: rgba(255, 255, 255, 0.08);
    color: #f0f4f8;
    border: 1px solid rgba(240, 244, 248, 0.25);
    border-radius: 8px;
    font-family: inherit;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: background 150ms ease, border-color 150ms ease;
    flex-shrink: 0;
}

.vd-barge-in-btn:hover {
    background: rgba(255, 255, 255, 0.14);
    border-color: rgba(240, 244, 248, 0.45);
}

.vd-barge-in-btn:active {
    transform: scale(0.97);
}

/* Echo mode checkbox label */
.vd-echo-label {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    color: rgba(240, 244, 248, 0.72);
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
    flex-shrink: 0;
}

.vd-echo-label input[type="checkbox"] {
    accent-color: #F59E0B;
    width: 14px;
    height: 14px;
    cursor: pointer;
}

/* ECHO badge in header — amber, shown only when echo mode is on */
.vd-echo-badge {
    display: inline-block;
    padding: 2px 7px;
    background: #F59E0B;
    color: #1c2733;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    border-radius: 4px;
    margin-left: 8px;
    vertical-align: middle;
    text-transform: uppercase;
}

/* ---- DEBUG section: amber toggle + collapsible panels ---- */

.vd-debug-section {
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex: 1;
    min-height: 0;
}

/* Amber toggle button */
.vd-debug-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 7px 14px;
    background: rgba(245, 158, 11, 0.15);
    color: #F59E0B;
    border: 1px solid rgba(245, 158, 11, 0.35);
    border-radius: 6px;
    font-family: inherit;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 150ms ease, border-color 150ms ease, color 150ms ease;
    align-self: flex-start;
    flex-shrink: 0;
}

.vd-debug-toggle:hover {
    background: rgba(245, 158, 11, 0.25);
    border-color: rgba(245, 158, 11, 0.55);
}

.vd-debug-toggle.active {
    background: rgba(230, 126, 34, 0.25);
    border-color: #E67E22;
    color: #E67E22;
}

/* Panels container — vertical stack, fills available space */
.vd-panels {
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

/* Individual diagnostic panel — glass box, auto-scroll */
.vd-panel {
    flex: 1;
    display: flex;
    flex-direction: column;
    background: rgba(0, 0, 0, 0.30);
    border: 1px solid rgba(255, 255, 255, 0.10);
    border-radius: 7px;
    padding: 8px 12px 10px;
    overflow-y: auto;
    scroll-behavior: smooth;
    min-height: 60px;
    max-height: 200px;
}

/* Panel color label — uppercase, small */
.vd-panel__label {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.10em;
    text-transform: uppercase;
    margin-bottom: 6px;
    flex-shrink: 0;
    opacity: 0.90;
}

/* Panel text lines */
.vd-panel-line {
    font-size: 13px;
    line-height: 1.5;
    color: rgba(240, 244, 248, 0.82);
    word-break: break-word;
    padding: 1px 0;
}

.vd-panel-line + .vd-panel-line {
    border-top: 1px solid rgba(255, 255, 255, 0.05);
}

/* Observability gate — voice card debug rows hidden unless html[data-observability] is set.
   Matches the same gate used for .message-footer (#40): normal users never see
   the echo-mode toggle, debug section (DEBUG toggle + STT/BRAIN/EVENTS panels),
   which together expose LiveKit session internals. Functional controls
   (Start Voice, Barge In, status indicator) remain visible to everyone. */
html:not([data-observability]) .vd-echo-label {
    display: none;
}
html:not([data-observability]) .vd-debug-section {
    display: none;
}

/* ===================================================================
   Contact Card — ui.card.contact beat surface
   Farm palette: Mycroft Blue #22A7F0, Navy #2C3E50; Noto Sans.
   State colors: green #40DBB0, yellow #FEE255, red #D81159.
   NEVER color alone: every state pairs color + text label + icon.
   Light and dark mode: variables follow --af-* / data-af-theme /
   data-theme conventions matching existing cards above.
   =================================================================== */

/* Card variant marker */
.af-card--contact {
    max-width: 400px;
}

/* ---- Identity block: avatar + name/relationship/aliases row ---- */

.af-contact__identity {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--af-border, var(--color-border-light));
    margin-bottom: 12px;
}

/* Shared avatar container */
.af-contact__avatar {
    flex-shrink: 0;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    overflow: hidden;
    border: 2px solid var(--af-accent, #22A7F0);
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--af-surface, var(--color-surface));
}

/* Initials avatar: Mycroft Blue background */
.af-contact__avatar--initials {
    background: var(--af-accent, #22A7F0);
}

.af-contact__avatar-initials {
    font-family: var(--af-font, 'Noto Sans', 'Segoe UI', Arial, sans-serif);
    font-size: 20px;
    font-weight: 700;
    color: #1c2733; /* WCAG AA 5.66:1 on #22A7F0 bg — #fff was 2.67:1 (fails 3:1 large-text threshold) */
    line-height: 1;
    user-select: none;
}

/* Photo avatar: image fills the circle */
.af-contact__avatar-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* Text column beside the avatar */
.af-contact__identity-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.af-contact__name {
    font-family: var(--af-font, 'Noto Sans', 'Segoe UI', Arial, sans-serif);
    font-size: var(--af-font-size-heading, var(--font-size-base));
    font-weight: 700;
    color: var(--af-text, var(--color-text));
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.af-contact__relationship {
    font-size: var(--font-size-sm);
    color: var(--af-text-muted, var(--color-text-secondary));
    font-style: italic;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.af-contact__aliases {
    font-size: var(--font-size-sm);
    color: var(--af-text-muted, var(--color-text-secondary));
    line-height: 1.4;
}

.af-contact__aliases-label {
    font-weight: 500;
}

/* ---- Recognition status badge ---- */

.af-contact__badge {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 10px;
    border-radius: 12px;
    font-size: var(--font-size-sm);
    font-weight: 600;
    margin-bottom: 10px;
    border: 1px solid transparent;
    /* Always text + icon — NEVER color alone (brand standard) */
}

/* Recognized — green */
.af-contact__badge--recognized {
    background: rgba(64, 219, 176, 0.15);
    color: #0d6b4e; /* WCAG AA: 5.38:1 on composited bg rgb(214,239,234) — darkened for a11y */
    border-color: rgba(64, 219, 176, 0.45);
}

/* Uncertain — yellow */
.af-contact__badge--uncertain {
    background: rgba(254, 226, 85, 0.18);
    color: #6b5600; /* WCAG AA: 6.18:1 on composited bg rgb(243,240,215) — darkened for a11y */
    border-color: rgba(254, 226, 85, 0.55);
}

/* Unknown — red */
.af-contact__badge--unknown {
    background: rgba(216, 17, 89, 0.12);
    color: #8c0038; /* WCAG AA: 7.16:1 on composited bg rgb(238,216,225) — darkened for a11y */
    border-color: rgba(216, 17, 89, 0.40);
}

.af-contact__badge-icon {
    font-size: 12px;
    line-height: 1;
}

/* Dark mode: lighten text colors for legibility on dark surfaces.
   All values meet WCAG AA (≥4.5:1) against their live-measured composited
   badge backgrounds (measured 2026-06-16):
     recognized bg rgb(54,95,106)  → #50E8C0  4.55:1
     uncertain  bg rgb(88,101,92)  → #FEE255  4.73:1  (unchanged)
     unknown    bg rgb(72,66,93)   → #fba0c8  4.95:1
*/
[data-af-theme="dark"] .af-contact__badge--recognized,
[data-theme="dark"] .af-contact__badge--recognized {
    color: #50E8C0; /* WCAG AA 4.55:1 on composited bg rgb(54,95,106) — was #40DBB0 (3.99:1) */
}

[data-af-theme="dark"] .af-contact__badge--uncertain,
[data-theme="dark"] .af-contact__badge--uncertain {
    color: #FEE255; /* WCAG AA 4.73:1 on composited bg rgb(88,101,92) — unchanged */
}

[data-af-theme="dark"] .af-contact__badge--unknown,
[data-theme="dark"] .af-contact__badge--unknown {
    color: #fba0c8; /* WCAG AA 4.95:1 on composited bg rgb(72,66,93) — was #f04c8a (2.76:1) */
}

/* ---- Fields list (key-value rows) ---- */

.af-contact__fields {
    margin: 0 0 12px;
    padding: 0;
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 4px 12px;
    align-items: baseline;
    font-size: var(--font-size-sm);
}

.af-contact__field-label {
    font-weight: 600;
    color: var(--af-text-muted, var(--color-text-secondary));
    white-space: nowrap;
    margin: 0;
    padding: 0;
}

.af-contact__field-value {
    color: var(--af-text, var(--color-text));
    margin: 0;
    padding: 0;
    word-break: break-word;
}

/* ---- Recent events list ---- */

.af-contact__events {
    margin-bottom: 12px;
}

.af-contact__events-title {
    font-weight: 600;
    font-size: var(--font-size-sm);
    color: var(--af-text-muted, var(--color-text-secondary));
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 6px;
}

.af-contact__events-list {
    margin: 0;
    padding: 0 0 0 16px;
    list-style: disc;
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.af-contact__event-item {
    font-size: var(--font-size-sm);
    color: var(--af-text, var(--color-text));
    line-height: 1.5;
}

.af-contact__events-more {
    margin-top: 5px;
    font-size: var(--font-size-sm);
    color: #0b6799; /* WCAG AA 5.53:1 on light card bg #F1F3F4 — #22A7F0 was only 2.40:1 */
    font-weight: 500;
}

/* Dark mode: #22A7F0 on dark card bg #1a2430 = 5.87:1 — use brand primary */
[data-af-theme="dark"] .af-contact__events-more,
[data-theme="dark"] .af-contact__events-more {
    color: #22A7F0;
}

/* ---- Private / locked pane ---- */

.af-contact__private-pane {
    margin-top: 12px;
    padding: 12px 14px;
    border-radius: 8px;
    border: 1.5px dashed;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/* Locked state: Navy-tinted dashed border */
.af-contact__private-pane--locked {
    background: rgba(44, 62, 80, 0.07);
    border-color: rgba(44, 62, 80, 0.35);
}

[data-af-theme="dark"] .af-contact__private-pane--locked,
[data-theme="dark"] .af-contact__private-pane--locked {
    background: rgba(44, 62, 80, 0.35);
    border-color: rgba(255, 255, 255, 0.18);
}

.af-contact__private-lock-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

.af-contact__private-lock-icon {
    font-size: 18px;
    line-height: 1;
    flex-shrink: 0;
}

.af-contact__private-lock-label {
    font-weight: 600;
    font-size: var(--font-size-sm);
    color: var(--af-text, var(--color-text));
}

/* Reveal button */
.af-contact__private-reveal-btn {
    align-self: flex-start;
    padding: 6px 14px;
    background: var(--af-accent, #22A7F0);
    color: #1c2733; /* WCAG AA 5.66:1 on #22A7F0 bg — #fff was only 2.67:1 */
    border: none;
    border-radius: 6px;
    font-family: var(--af-font, 'Noto Sans', 'Segoe UI', Arial, sans-serif);
    font-size: var(--font-size-sm);
    font-weight: 600;
    cursor: pointer;
    transition: opacity 150ms ease;
}

.af-contact__private-reveal-btn:hover {
    opacity: 0.88;
}

.af-contact__private-reveal-btn[disabled] {
    opacity: 0.50;
    cursor: default;
}

/* Inline note (shown when no hook is wired) */
.af-contact__private-note {
    font-size: var(--font-size-sm);
    color: var(--af-text-muted, var(--color-text-secondary));
    line-height: 1.5;
    font-style: italic;
}

/* ──────────────────────────────────────────────────────────────────────────
   Agent Face — Markdown card (af-card--markdown)
   Renders agent-authored text/markdown in the uniform card shell.
   Light/dark: uses CSS-variable tokens throughout — no hardcoded light-only
   or dark-only colors (T34). WebKit/iOS Safari compatible.
   ─────────────────────────────────────────────────────────────────────────*/

/* Card shell override: slightly wider than default (380px) to give prose room */
.af-card--markdown {
    max-width: 480px;
}

/* Scrollable body for long content (poems, multi-section notes).
   max-height ensures the card never overflows the viewport; overflow-y:auto
   adds a scrollbar only when content exceeds the limit.
   -webkit-overflow-scrolling: touch for smooth momentum scrolling on iOS Safari. */
.af-card--markdown .af-card__body {
    max-height: 60vh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding: 16px 18px;
}

/* Expanded (modal size, not viewport): let the body use the full available
   height of the 620px-tall card rather than the old 100vh assumption.
   Also resets `display` back to block: `.af-card--fullscreen .af-card__body`
   (generic rule above, ~3220) forces `display:flex; align-items:stretch`,
   which exists for the VIDEO card (paired with `.af-card__body--video` right
   below it, to stretch/center the <video> element) -- it was never meant for
   text content, and this markdown-specific block only overrode padding/
   overflow/max-height, so markdown bodies were silently inheriting a flex
   row layout for a single prose/code child. Left as flex, this is suspected
   of contributing to the fullscreen code-block left-clip bug (first ~2-3
   rendered lines of a fenced code block losing their left few characters):
   a flex item with an auto flex-basis sizes from its content's max-content
   width, and that sizing pass interacting with the nested `overflow-x:auto`
   <pre> (itself nested inside `overflow-y:auto` .af-card__body, all under
   this card's own `backdrop-filter:blur(16px)` compositing boundary) is a
   known-shaky combination for first-paint layout/compositing glitches.
   `display:block` gives the markdown body plain block flow, which is all it
   ever needed. `transform: translateZ(0)` forces the scroll container onto
   its own compositing layer -- the standard workaround for the broader class
   of "content under backdrop-filter/filter paints wrong until a reflow"
   bugs (this file already documents a related WebKit repaint bug on this
   exact code block, see `.af-markdown__content pre code` below).
   NOTE: this fix is applied from static analysis (no browser was available
   in this environment to visually confirm). To verify: open a fullscreen
   markdown card with a multi-line fenced code block and check the first
   `<pre>`'s `getBoundingClientRect()` + `getComputedStyle(pre).overflowX` +
   `pre.scrollLeft` (should be 0, and rect.left should match later `<pre>`
   elements / the card's padding-box left edge) -- if the clip is still
   present, try forcing a reflow (scroll 1px and back) to tell a paint/
   compositing bug (reflow fixes it) apart from a persistent layout bug
   (reflow does not fix it). */
/* Expanded markdown card = flex column (header + body), so the body FILLS the
   space after the header instead of being capped at a fixed 100%-120px, which
   over-reserved ~80px and left a dead, unusable block at the bottom (the product owner
   2026-07-24). flex:1 + min-height:0 + overflow-y:auto = body uses all remaining
   height and scrolls if the content is taller. */
.af-card--markdown.af-card--fullscreen {
    display: flex !important;
    flex-direction: column !important;
}
.af-card--markdown.af-card--fullscreen .af-card__body {
    display: block;
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
    overflow-y: auto;
    transform: translateZ(0);
}

.af-card--inline-preview {
    pointer-events: none;
}

/* Content wrapper: readable prose styles */
.af-markdown__content {
    font-family: var(--af-font, 'Noto Sans', 'Segoe UI', Inter, 'Helvetica Neue', Arial, sans-serif);
    font-size: var(--af-font-size-body, var(--font-size-base, 16px));
    line-height: 1.65;
    color: var(--af-text, var(--color-text));
    word-break: break-word;
    overflow-wrap: break-word;
}

/* Headings */
.af-markdown__content h1,
.af-markdown__content h2,
.af-markdown__content h3,
.af-markdown__content h4,
.af-markdown__content h5,
.af-markdown__content h6 {
    font-family: var(--af-font, 'Noto Sans', 'Segoe UI', Inter, 'Helvetica Neue', Arial, sans-serif);
    font-weight: 700;
    color: var(--af-text, var(--color-text));
    margin: 0.9em 0 0.4em;
    line-height: 1.3;
}

.af-markdown__content h1 { font-size: 1.35em; }
.af-markdown__content h2 { font-size: 1.18em; }
.af-markdown__content h3 { font-size: 1.05em; }
.af-markdown__content h4,
.af-markdown__content h5,
.af-markdown__content h6 { font-size: 1em; }

/* First heading at top of card — no top margin */
.af-markdown__content > h1:first-child,
.af-markdown__content > h2:first-child,
.af-markdown__content > h3:first-child {
    margin-top: 0;
}

/* Paragraphs */
.af-markdown__content p {
    margin: 0 0 0.85em;
    line-height: 1.65;
}

.af-markdown__content p:last-child {
    margin-bottom: 0;
}

/* Bold + italic */
.af-markdown__content strong {
    font-weight: 700;
    color: var(--af-text, var(--color-text));
}

.af-markdown__content em {
    font-style: italic;
    color: var(--af-text, var(--color-text));
}

/* Inline code */
.af-markdown__content code {
    font-family: 'Noto Sans Mono', 'Cascadia Code', 'Fira Mono', 'Courier New', monospace;
    font-size: 0.88em;
    background: var(--af-code-bg, rgba(44, 62, 80, 0.08));
    color: var(--af-code-text, var(--color-text));
    padding: 0.12em 0.38em;
    border-radius: 4px;
    /* word-break prevents code spans from overflowing narrow cards */
    word-break: break-all;
}

/* Dark-mode code background: slightly lighter tint */
[data-af-theme="dark"] .af-markdown__content code,
[data-theme="dark"] .af-markdown__content code {
    background: rgba(255, 255, 255, 0.10);
    color: var(--af-text, #e0e6ee);
}

/* Fenced code blocks: <pre><code> — monospace box, whitespace preserved
   natively by <pre>, horizontal scroll for long lines instead of breaking
   card layout. */
.af-markdown__content pre {
    margin: 0 0 0.85em;
    padding: 0.85em 1em;
    background: var(--af-code-block-bg, rgba(44, 62, 80, 0.06));
    border: 1px solid var(--af-code-block-border, rgba(44, 62, 80, 0.12));
    border-radius: 8px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

.af-markdown__code-shell {
    position: relative;
    margin: 0 0 0.85em;
}
.af-markdown__code-shell:last-child { margin-bottom: 0; }
.af-markdown__code-shell pre,
.af-markdown__code-shell pre:last-child {
    margin: 0;
    padding-top: 48px;
}
.af-markdown__code-copy {
    position: absolute;
    z-index: 1;
    top: 7px;
    right: 7px;
    background: rgba(44, 62, 80, .055);
    border-color: rgba(44, 62, 80, .1);
}

[data-af-theme="dark"] .af-markdown__code-copy:not(:hover):not(:focus-visible):not(.is-copied):not(.is-failed),
[data-theme="dark"] .af-markdown__code-copy:not(:hover):not(:focus-visible):not(.is-copied):not(.is-failed) {
    background: rgba(255, 255, 255, .045);
    border-color: rgba(255, 255, 255, .08);
}

.af-markdown__content pre:last-child {
    margin-bottom: 0;
}

/* Code inside a fenced block: reset the inline-code chrome (no background/
   padding of its own — the <pre> already provides the box) and switch to
   the code font stack (Fira Code, loaded in index.html, first).

   display:inline-block + min-width:100% (rather than plain display:block) is
   the horizontal-scroll fix (customer report: scrolling a wide code block
   right loses the box — page background shows through past a point instead
   of the code block's own background/padding continuing to cover it). <pre>
   is both the horizontal scroll container (overflow-x:auto below) and the
   painted box (background/border/padding, below); <code>'s own box must be
   able to grow AT LEAST as wide as the scrollable content (min-width:100%
   with inline-block sizing lets it grow past the visible viewport instead of
   being clipped to it the way a block box with width:auto would be), so the
   scrolled area is always backed by real content-box, not empty space past
   the box edge. This also sidesteps the known WebKit/iOS Safari glitch where
   a plain ancestor background can fail to repaint correctly during
   `-webkit-overflow-scrolling: touch` momentum scroll inside a rounded,
   `overflow:hidden` ancestor (.af-card, see above) — relevant since this
   card explicitly targets iOS Safari. */
.af-markdown__content pre code {
    display: inline-block;
    min-width: 100%;
    font-family: 'Fira Code', 'Noto Sans Mono', 'Cascadia Code', 'Fira Mono', 'Courier New', monospace;
    font-size: 0.85em;
    line-height: 1.5;
    background: none;
    color: var(--af-code-text, var(--color-text));
    padding: 0;
    border-radius: 0;
    white-space: pre;
    word-break: normal;
}

/* Dark-mode fenced code block background */
[data-af-theme="dark"] .af-markdown__content pre,
[data-theme="dark"] .af-markdown__content pre {
    background: rgba(255, 255, 255, 0.06);
    border-color: rgba(255, 255, 255, 0.12);
}

/* Links */
.af-markdown__content a {
    color: var(--af-accent, #22A7F0);
    text-decoration: underline;
    text-underline-offset: 2px;
    word-break: break-all;
}

.af-markdown__content a:hover {
    opacity: 0.82;
}

/* Dark-mode links: brand primary is readable on dark card surface */
[data-af-theme="dark"] .af-markdown__content a,
[data-theme="dark"] .af-markdown__content a {
    color: #22A7F0;
}

/* Lists */
.af-markdown__content ul,
.af-markdown__content ol {
    margin: 0 0 0.85em;
    padding-left: 1.5em;
}

.af-markdown__content li {
    margin-bottom: 0.25em;
    line-height: 1.55;
}

.af-markdown__content li:last-child {
    margin-bottom: 0;
}

/* Nested list tightening */
.af-markdown__content ul ul,
.af-markdown__content ol ol,
.af-markdown__content ul ol,
.af-markdown__content ol ul {
    margin-top: 0.15em;
    margin-bottom: 0.15em;
}

/* Strikethrough */
.af-markdown__content del {
    text-decoration: line-through;
    color: var(--af-text-muted, var(--color-text-secondary));
}

/* Horizontal rule */
.af-markdown__content hr {
    border: none;
    border-top: 1px solid var(--af-border, var(--color-border));
    margin: 1em 0;
}

[data-af-theme="dark"] .af-markdown__content hr,
[data-theme="dark"] .af-markdown__content hr {
    border-top-color: rgba(255, 255, 255, 0.14);
}

/* Tables (GitHub pipe tables). The wrapper owns horizontal scrolling so the
   table keeps native intrinsic column sizing. Making <table> itself a block
   lets overflow-wrap collapse columns to one-character widths. */
.af-markdown__table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    max-width: 100%;
    margin: 0 0 0.85em;
}

.af-markdown__content table {
    display: table;
    width: max-content;
    min-width: 100%;
    border-collapse: collapse;
    font-size: 0.92em;
}

.af-markdown__table-scroll:last-child {
    margin-bottom: 0;
}

.af-markdown__content th,
.af-markdown__content td {
    border: 1px solid var(--af-border, var(--color-border));
    padding: 0.45em 0.75em;
    line-height: 1.5;
    text-align: left;
}

.af-markdown__table-cell {
    display: block;
    max-width: 32ch;
    overflow-wrap: anywhere;
    word-break: normal;
}

.af-markdown__content thead th {
    background: var(--af-code-block-bg, rgba(44, 62, 80, 0.06));
    font-weight: 700;
    color: var(--af-text, var(--color-text));
}

.af-markdown__content tbody tr:nth-child(even) {
    background: var(--af-code-bg, rgba(44, 62, 80, 0.03));
}

/* Dark-mode table borders/backgrounds — consistent with the code-block
   dark-mode treatment above. */
[data-af-theme="dark"] .af-markdown__content th,
[data-af-theme="dark"] .af-markdown__content td,
[data-theme="dark"] .af-markdown__content th,
[data-theme="dark"] .af-markdown__content td {
    border-color: rgba(255, 255, 255, 0.12);
}

[data-af-theme="dark"] .af-markdown__content thead th,
[data-theme="dark"] .af-markdown__content thead th {
    background: rgba(255, 255, 255, 0.06);
    color: var(--af-text, #e0e6ee);
}

[data-af-theme="dark"] .af-markdown__content tbody tr:nth-child(even),
[data-theme="dark"] .af-markdown__content tbody tr:nth-child(even) {
    background: rgba(255, 255, 255, 0.03);
}

/* ──────────────────────────────────────────────────────────────────────────
   Agent Face — Audio card (af-card--audio)
   Renders an <audio controls> element for music/audio playback.
   Light/dark: uses CSS-variable tokens throughout.
   WebKit/iOS Safari compatible: <audio controls> is supported everywhere.
   ─────────────────────────────────────────────────────────────────────────*/

/* Card shell: compact width — native audio controls don't need much space */
.af-card--audio {
    max-width: 400px;
}

/* Body: tight padding around the audio element; no scroll needed */
.af-card--audio .af-card__body {
    padding: 16px 18px;
}

/* The <audio> element fills available width and inherits a neutral look.
   accent-color tints the native controls to the brand primary on supporting
   browsers (Chrome 93+, Firefox 92+, Safari 15.4+). */
.af-card--audio audio {
    width: 100%;
    display: block;
    accent-color: var(--af-accent, #22A7F0);
    /* Ensure the native control bar is visible in dark themes */
    color-scheme: light dark;
}

/* ──────────────────────────────────────────────────────────────────────────
   Agent Face — Web card (af-card--web)
   Embeds an external page in a sandboxed <iframe>.
   Light/dark: card shell adapts; iframe renders the framed page's own styles.
   WebKit/iOS Safari compatible: <iframe> + sandbox are well-supported.
   ─────────────────────────────────────────────────────────────────────────*/

/* Card shell: wider than default to give the framed page room to breathe */
.af-card--web {
    max-width: 720px;
    width: 90vw;
}

/* Body: no padding so the iframe fills edge-to-edge within the card */
.af-card--web .af-card__body {
    padding: 0;
    /* Round the inner corner to match the card shell border-radius */
    overflow: hidden;
    border-radius: 0 0 8px 8px;
}

/* Link-out variant (data-web-kind="link", cross-origin URL -- no iframe,
   see face/src/cards/web.js) is plain URL text, not edge-to-edge iframe
   chrome: the padding:0 above left the link jammed against the card's
   left border (customer feedback 2026-07-19: "I don't see any margin or
   gap between card and link"). Restore the standard .af-card__body inset
   so it reads like every other text-body card (contact/form/file) and
   lines up with the header's own horizontal padding above it. Higher
   specificity than the rule above (attribute selector) so this wins
   regardless of source order. */
.af-card--web[data-web-kind="link"] .af-card__body {
    padding: 12px;
}

/* iframe sizing: fills the card body width; 60vh gives a usable page view */
.af-card--web iframe {
    width: 100%;
    height: 60vh;
    border: none;
    display: block;
}

/* URL-as-hyperlink (customer directive 2026-07-19: "why is 'Open example.com
   in new tab' an entire button, this is too long and needs to go away. Just
   hyperlink the URL and remove the button"). The library's separate "Open
   <host> in new tab" CTA <a> (face/src/cards/web.js) is removed in JS
   (face-integration.js's _dehyperlinkifyWebCardCta, called from
   buildInlineCardNode) and the URL text itself becomes the link instead --
   this selector now targets THAT anchor, so it reads as plain inline link
   text, not a button. !important is required: the library's own inline
   element.style on the surrounding <p> (opacity/margin/word-break) would
   otherwise leave an unstyled default-blue-underline anchor look mismatched
   with the portal's link color token. */
.af-card--web[data-web-kind="link"] .af-card__body > div > p > a[target="_blank"] {
    color: var(--color-primary, #22A7F0) !important;
    text-decoration: underline !important;
    word-break: break-all !important;
}

.af-card--web[data-web-kind="link"] .af-card__body a[target="_blank"]:hover {
    opacity: 0.8;
}

/* Enriched external preview: preserve ordinary text wrapping rather than the
   raw-URL rule above. The dismiss control lives in the header. */
.af-card--web[data-web-preview="true"] .af-card__web-preview {
    display: block;
    min-width: 0;
    padding: 4px 0;
    color: inherit;
    text-decoration: none;
    overflow-wrap: anywhere;
}

.af-card__web-preview-image {
    display: block;
    width: 100%;
    max-height: 240px;
    object-fit: cover;
    border-radius: 6px;
    margin-bottom: 10px;
}

.af-card__web-preview-title {
    margin-top: 6px;
    font-size: 1rem;
    font-weight: 650;
    line-height: 1.35;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
}

.af-card__web-preview-description {
    margin-top: 6px;
    font-size: 0.9rem;
    line-height: 1.45;
    opacity: 0.78;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
}

.af-card--web .af-card__title {
    min-width: 0;
    max-width: calc(100% - 36px);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 480px) {
    .af-card--web {
        width: 100%;
    }
    .af-card--web[data-web-kind="link"] .af-card__body {
        padding: 10px;
    }
}

/* ──────────────────────────────────────────────────────────────────────────
   Agent Face — PDF card (af-card--pdf)
   Renders a PDF using the browser's native viewer in a sandboxed <iframe>.
   Light/dark: card shell adapts; the iframe renders the browser's own PDF UI.
   WebKit/iOS Safari compatible: native PDF rendering supported in Safari;
   other browsers fall back gracefully to a download link inside the iframe.
   ─────────────────────────────────────────────────────────────────────────*/

/* Card shell: wide for comfortable document reading */
.af-card--pdf {
    max-width: 720px;
    width: 90vw;
}

/* Body: no padding so the iframe fills edge-to-edge within the card */
.af-card--pdf .af-card__body {
    padding: 0;
    /* Round the inner corner to match the card shell border-radius */
    overflow: hidden;
    border-radius: 0 0 8px 8px;
}

/* iframe sizing: tall enough for document reading (70vh) */
.af-card--pdf iframe {
    width: 100%;
    height: 70vh;
    border: none;
    display: block;
}

/* Expanded (modal size, not viewport): expand iframe to fill the 620px-tall
   card (minus card header). */
.af-card--pdf.af-card--fullscreen iframe {
    height: calc(100% - 120px);
}

/* PWA standalone: clear iOS status-bar area under black-translucent */
@media (display-mode: standalone) {
    #app {
        padding-top: env(safe-area-inset-top);
    }
}

/* iOS Safari standalone: body background-attachment:fixed causes black bg */
@supports (-webkit-touch-callout: none) {
    body {
        background-attachment: scroll;
    }
}

/* T49 / #58: hide persisted splash orb + eyes + name while spotlight overlay (S2) is active;
   fades back on return. Targets the persisted orb+eyes (no separate header orb/canvas). */
#orbHero, #companionName, .hero-orb-wrap canvas.splash-eyes-canvas { transition: opacity 150ms ease; }
#view-chat.overlay-active #orbHero,
#view-chat.overlay-active #companionName,
#view-chat.overlay-active .hero-orb-wrap canvas.splash-eyes-canvas { opacity: 0; pointer-events: none; }
/* =====================================================================
   #58: orb-chat-docked — persist + transform splash orb to a compact
   top-center header band when chat is active. The splash orb (#orbHero /
   .hero-orb-wrap) is NEVER display:none'd in chat; instead it is taken
   out of flex flow (position:absolute) and scaled down in place.
   All geometry below is a FIRST PASS — reviewer measures with
   getBoundingClientRect() and tunes the REVIEWER KNOB values.
   ===================================================================== */

/* Positioning context for the absolute #orbHero child */
#view-chat.orb-chat-docked {
    position: relative;
}

/* Take orb out of flex flow so .working-area fills full viewport height */
#view-chat.orb-chat-docked #orbHero {
    position: absolute;
    top: 6px;
    left: 20px;
    right: auto;
    width: 44px;
    height: 44px;
    padding: 0;
    align-items: flex-start; /* keep wrap anchored at top */
    justify-content: center;
    overflow: visible;
    /* REVIEWER KNOB: z-index controls orb vs glass layering in chat mode.
       chat-container=3, portalAppStage=4.
       5 → orb paints above glass and stage (fully visible; "orb on top").
       2 → orb behind glass (glass glassmorphism "frames" it).
       First pass = 5 (visible header band). Reviewer confirms desired layering. */
    z-index: 5;
    pointer-events: none;   /* let taps fall through to working-area below */
}
/* Portal v2: the orb itself must stay clickable (toggles the ingested drawer)
   even though its #orbHero ancestor is pointer-events:none above. */
#view-chat.orb-chat-docked .hero-orb-wrap {
    pointer-events: auto;
    /* The orb and eyes share this one box. Seven header heights renders a 56px
       avatar at the 0.144 dock scale, matching the established laptop size
       without deriving it from viewport height. */
    width: calc(var(--header-height) + var(--header-height) + var(--header-height) + var(--header-height) + var(--header-height) + var(--header-height) + var(--header-height));
    height: calc(var(--header-height) + var(--header-height) + var(--header-height) + var(--header-height) + var(--header-height) + var(--header-height) + var(--header-height));
}

/* Portal v2: gentle idle breathing pulse for the docked top-center header orb.
   Subtle by design — the wrap already has its own radial-gradient backdrop
   (::before), so this only nudges scale a hair around the docked baseline
   (0.28, set below) rather than layering a second glow on top of it (an
   earlier version stacked a drop-shadow pulse here, which combined with the
   backdrop into an oversized blurry halo).
   NOTE: an `animation` targeting `transform` takes over that property from
   any static `transform` declaration at the same specificity for the
   duration of the animation — so the keyframes below use the docked scale
   (0.12) as their baseline, not scale(1), or the orb would jump full-size.
   MEASURED 2026-07-16: the docked baseline is scale(0.12) — the value these
   keyframes hold — NOT 0.28. The static `transform: scale(0.12)` below is kept
   in lockstep with the 0%/100% keyframe so the two agree; if you retune one you
   MUST retune the other, or the orb will jump when the pulse hands over. */
@keyframes orb-idle-pulse {
    0%, 100% { transform: scale(0.144); }
    50% { transform: scale(0.156); }
}

/* Boot travel — the orb's animated journey from greeting size to the docked slot.
   WHY AN ANIMATION AND NOT THE `transition: transform` BELOW (measured, not guessed):
   the transform transition CANNOT fire on this element, for two independent reasons —
     1. orb-idle-pulse (above) owns `transform` in the after-change style, and an
        animation's applied value beats a transition → transform snapped to 0.12
        on the first docked frame.
     2. orb-greet-beat (the greeting bounce) still owns `transform` in the
        BEFORE-change style: app.js drops .orb-greeting and adds .orb-chat-docked
        in the SAME tick, so `transform` hands off animation→static in a single
        style-change event, and Chromium then starts NO transform transition at all.
        Verified via getAnimations(): at the dock frame only CSSTransition:width and
        CSSTransition:height were running — transform had no CSSTransition. width and
        height transition fine because no animation ever owned them.
   Suppressing the pulse alone does NOT restore travel (cause 2 still bites, it just
   snaps to the static value instead) — so the travel is driven by keyframes, which
   apply unconditionally and are immune to both causes.
   THE TRANSLATE IS NOT DECORATION — it is what makes this a journey rather than a
   relocation. Docking flips #orbHero from static to position:absolute (top:6 left:20),
   so the orb's LAYOUT position jumps centre-screen → top-left in one frame. That jump
   is a layout change and is not animatable. Scaling alone therefore produced a big orb
   that snapped to the corner at full size (measured: rect x 396 → -282, i.e. 282px off
   the left edge) and only then shrank — a worse artefact than the bug being fixed.
   So the `from` keyframe translates the orb back to exactly where it sat on the splash,
   and the animation walks it home while shrinking. Measured geometry at 1440×900:
     • docked origin (transform-origin: top center) = x 42px, y 6px.
       42 = #orbHero left(20) + width(44)/2 — the wrap is flex-centred in a 44px box,
       so its centre is pinned at 42 no matter what the wrap's own width is.
     • splash origin = x 50vw (720), y (100vh − wrap height)/2 = 126.
     • delta = (678, 120) → the two calc()s below resolve to exactly that.
   COUPLING — these calc()s hard-code values owned by OTHER rules. If you retune
   #orbHero's `left`/`width`/`top` (near line 5452) or the splash wrap height
   min(72vh,660px) (near line 5863), this `from` MUST be retuned to match or the orb
   will visibly jump at the first travel frame. Verify by sampling rect x across the
   dock: it must run ~396 → ~17.7 with no discontinuity.
   Starts at scale(1) — exactly where orb-greet-beat's 100% leaves the orb — and ends
   at translate(0,0) scale(0.12), the real docked baseline, so there is no jump at
   either seam. translate(0,0) scale(0.12) renders the identical matrix to the idle
   pulse's bare scale(0.12), so the handoff to breathing is seamless too.
   400ms `ease` MATCHES the width/height transitions below on purpose: the on-screen
   orb size is width(t) × scale(t), so the two curves must be identical or the
   composite size ramp goes non-linear. Retune both together or neither. */
@keyframes orb-dock-travel {
    from {
        transform: translate(calc(50vw - 42px),
                             calc((100vh - min(72vh, 660px)) / 2 - 6px))
                   scale(1);
    }
    to   { transform: translate(0, 0) scale(0.144); }
}

/* Travel once (400ms), then the idle breathing takes over. animation-delay:400ms on
   the pulse is what sequences them: during the delay the pulse is in its "before
   phase" with fill-mode:none, so it does not apply and orb-dock-travel owns transform;
   at 400ms orb-dock-travel has finished (fill-mode:none → stops applying) and the
   pulse starts at its 0% keyframe, scale(0.12) — precisely where the travel landed.
   Seamless handoff with no third value in play. */
#view-chat.orb-chat-docked .hero-orb-wrap {
    /* Idle breathing REMOVED on request -- the docked orb no longer pulses at
       rest. orb-idle-pulse used to occupy a second slot here and, because
       transform-origin is top center, its scale wobble read as the orb moving
       up and down rather than swelling.

       Dropping it is safe at the seam: orb-dock-travel has fill-mode:none, so
       when it finishes the transform falls through to the static
       scale(0.144) on the rule below -- the same value the pulse's 0%/100%
       keyframe held. Nothing jumps.

       The keyframes themselves are left defined: orb-listening-pulse still
       runs while the companion is listening, and that is deliberate feedback,
       not idle decoration. */
    animation: orb-dock-travel 400ms ease 1;
}
#view-chat.orb-chat-docked .hero-orb-wrap[data-af-dragging="true"] {
    animation: none;
}

/* Scale the orb wrap down to a compact header band with animation */
#view-chat.orb-chat-docked .hero-orb-wrap {
    /* REVIEWER KNOB — transform geometry:
       transform-origin:top center → scale collapses the wrap DOWNWARD from the top edge,
       keeping the orb near the top of the viewport.

       scale(0.12) — CORRECTED 2026-07-16 from a measurement, was scale(0.28).
       0.28 was DEAD CODE: orb-idle-pulse owns `transform` whenever the orb is docked
       and holds 0.12, so 0.28 never rendered — the docked orb has always been 0.12.
       The comment block that used to live here described the 0.28 geometry that no
       user ever saw. Do not "restore" 0.28: it lands the orb at ~113px, half of it
       off the left edge (measured x=-14.7). Real geometry at laptop 1440×900:
         • wrap = min(45vh,460px) = 405px — set by the `(orientation: landscape) and
           (min-width: 1024px)` guard near line 4234, NOT the min(60vh,600px) base.
           That rule's `:not(.chat-docked)` never excludes anything (the docking class
           is `orb-chat-docked`, and it lives on #view-chat, not on the wrap), so it
           applies docked too — this is what actually sizes the docked orb.
         • 405 × 0.12 = 48.6px visual orb at x≈17.5, y=6 — the shipped docked slot.
       This value is duplicated across FOUR live sites, all 0.12, all of which must
       move together — this static transform, the orb-idle-pulse 0%/100% keyframe,
       the orb-dock-travel `to` keyframe, and orb-listening-pulse's 0%/100% keyframe
       (near line 6205 — easy to miss, it sits far below with the orb-state rules).
       Changing three of the four leaves the orb a different size while listening.

       To shift the band up or down, add translateY(+Npx) for down / (-Npx) for up.
       To change orb size, adjust the scale value in all FOUR places above. */
    /* DOCKED ORB SIZE. This is the knob that changes the orb you see in chat,
       not .hero-orb-wrap's width/height -- the wrapper is the SPLASH size and
       this scales it down to dock. Rendered size is wrapper x this factor.

       ⚠ FOUR SITES MUST HOLD THE SAME BASELINE or the orb jumps between
       states: this rule, orb-dock-travel's `to` keyframe, orb-idle-pulse's
       0%/100%, and orb-listening-pulse's 0%/100%. The two pulses also carry a
       peak a little above the baseline; those were raised in the same
       proportion. Raised from 0.12, then trimmed 20% to 0.144 on request. Mobile is a
       SEPARATE knob (the scale in the max-width media query below) and is
       deliberately untouched -- it was already the right size. */
    transform: scale(0.144);
    transform-origin: top center;
    transition: transform 400ms ease, width 400ms ease, height 400ms ease, filter 250ms ease; /* #58: width+height added so splash-fullscreen↔chat size change animates, not snaps (both directions) */
}

@media (max-width: 768px) and (orientation: portrait) {
    /* Portrait phones use an explicit 200px source box at the established .26
       scale, yielding a stable 52px docked avatar at every mobile width. */
    #view-chat.orb-chat-docked .hero-orb-wrap {
        width: 200px;
        height: 200px;
        transform: scale(0.26);
        animation: none;
    }
    /* #58/mobile-overlap-fix: proportional content offset so the "Companion" name
       clears the docked orb — container-level nudge only, does NOT touch the
       orb-idle-pulse/orb-dock-travel/orb-listening-pulse scale(0.12) sites or the
       mobile scale(0.42) above.
       Computed from the CSS rules above (not a real-device measurement):
       .hero-orb-wrap is pinned to a flat 280px in the 467-768px width
       band (60vw exceeds the 280px cap everywhere in that range) and to 200px in
       the 364-480px band (55vw exceeds the 200px cap there too) — so the docked
       visual orb is a flat 117.6px (280×0.42) down to 84px (200×0.42) across
       nearly the whole mobile range, narrower only on very small phones. #orbHero
       sits at top:6px (style.css:5930, unaffected by this padding — it's
       position:absolute against #view-chat, not .working-area), so worst-case
       visual bottom edge ≈ 6 + 117.6 ≈ 124px. 10px left the header/name sitting
       squarely inside that footprint; 130px clears it with a small margin. */
    #view-chat.orb-chat-docked .working-area {
        padding-top: 130px;
    }
}

/* In orb-chat-docked: remove horizontal margins so glass spans full input width
   (input is position:fixed; left:56px; right:0 on desktop — match that width).
   Also remove margin-top so the chat panel touches the top edge of the screen perfectly. */
#view-chat.orb-chat-docked .chat-container {
    margin-left: 0;
    margin-right: 0;
    margin-top: 0;
}

/* #58: orb band height — name/content clears the orb; reviewer tunes with the orb scale.
   .working-area is flex:1 in orb-chat-docked so it fills the full viewport (orbHero is absolute).
   Adaptive calc: orb_core_bottom ≈ 0.2576 × min(60vh,600px) = min(15.456vh,154.56px).
   calc(min(15.5vh, 154px) − 2px) sets panel ~8px below the orb core across all viewport heights
   (chat-container margin-top:10px supplies the remaining clearance → total ~8px gap).
   This collapses the previously-fixed 130px dead band: on a 600px-tall viewport the band
   shrinks from 140px to ~104px, closing the visible ~47px gap between orb and panel.
   box-sizing:border-box (global): padding absorbed into the border-box so
   .chat-container (flex:1 inside) fills the remaining content height without overflowing. */
#view-chat.orb-chat-docked .working-area {
    padding-top: 0; /* Remove padding so chat spans to the very top edge */
}

/* Portal v2: floating bottom-right utility bar (replaces the removed sidebar) */
.utility-bar {
    position: fixed;
    right: 20px;
    bottom: 88px;
    display: flex;
    gap: 8px;
    padding: 6px;
    background: var(--color-surface, rgba(255,255,255,0.85));
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    border: 1px solid var(--color-border-light, rgba(0,0,0,0.08));
    border-radius: var(--radius-full, 999px);
    box-shadow: 0 8px 24px rgba(0,0,0,0.14);
    z-index: 30;
}
.utility-bar-btn {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: none;
    background: transparent;
    font-size: 18px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 150ms ease, transform 150ms ease;
}
.utility-bar-btn:hover {
    background: var(--color-surface-hover, rgba(0,0,0,0.06));
    transform: translateY(-1px);
}

/* Portal v2: settings modal */
.settings-modal-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(12, 20, 30, 0.55);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    align-items: center;
    justify-content: center;
    z-index: 200; /* match .proto-library-active — above .chat-input-area (151) */
}
.settings-modal-overlay.settings-modal-overlay--open {
    display: flex;
}
/* Settings modal: sidebar + panes, wider-than-tall (~4:3). One category is
   active in the sidebar at a time (showSettingsCategory() in app.js); the
   header stays fixed while only the ACTIVE pane's body scrolls. The scroll fix
   is the flex column + min-height:0 on .settings-modal__pane-body below — keep
   both when a pane grows, or overflow silently stops working again. */
.settings-modal {
    /* Match the media-library (orb) modal's footprint exactly — customer
       directive 2026-07-19. Keep the mobile @media override below (full-screen
       on narrow viewports) intact; this only sets the desktop size. */
    width: min(880px, 92vw);
    height: min(620px, 82vh);
    background: var(--color-surface, rgba(255,255,255,0.9));
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    border-radius: var(--radius-lg, 14px);
    box-shadow: 0 20px 60px rgba(0,0,0,0.3);
    padding: var(--space-4, 20px);
    display: flex;
    flex-direction: column;
}
.settings-modal__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--space-3, 14px);
    flex-shrink: 0;
}
.settings-modal__header h2 {
    margin: 0;
    font-size: 1.1rem;
}
.settings-modal__close {
    background: none;
    border: none;
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    color: var(--color-text-secondary);
}

/* Sidebar + content row filling the space under the header */
.settings-modal__layout {
    flex: 1;
    min-height: 0;
    display: flex;
    gap: var(--space-4);
}

/* Category sidebar (~24% width) — a flat list of category buttons, one active
   pill at a time. A future category is one more button here + one more pane. */
.settings-modal__sidebar {
    flex: 0 0 min(26%, 200px);
    display: flex;
    flex-direction: column;
    gap: 2px;
    overflow-y: auto;
    padding-right: var(--space-2);
    border-right: 1px solid var(--color-border);
}
.settings-modal__nav-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border: none;
    border-left: 3px solid transparent; /* reserved so activating never shifts the layout */
    border-radius: var(--radius-md);
    background: none;
    color: var(--color-text);
    font: inherit;
    font-weight: 500;
    text-align: left;
    cursor: pointer;
    transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
}
.settings-modal__nav-item svg {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    opacity: 0.7;
}
.settings-modal__nav-item:hover {
    background: var(--color-surface-hover);
}
/* Active state carries THREE signals, not color alone: a left accent bar, a
   tinted pill background, and bold text/full-opacity icon. The pill tint alone
   read too faint in light theme (~14% blue over a near-white surface is low
   contrast) — bumped for light; dark keeps the original since the same tint
   already reads clearly over the dark surface. */
.settings-modal__nav-item.active {
    background: rgba(34, 167, 240, 0.14);
    border-left-color: var(--color-primary);
    color: var(--color-primary);
    font-weight: 600;
}
[data-theme="light"] .settings-modal__nav-item.active {
    background: rgba(34, 167, 240, 0.22);
    /* rgba(34,167,240,0.14/.22) text over this tint measures ~1.97:1 in light —
       fails AA. Darkened Mycroft blue clears 4.5:1 while staying on-brand. */
    color: #0d5a86;
}
.settings-modal__nav-item.active svg {
    opacity: 1;
}

.settings-modal__content {
    flex: 1;
    min-width: 0;
    min-height: 0;
}

/* Only the selected category's pane renders; JS toggles .active */
.settings-modal__pane {
    display: none;
    flex-direction: column;
    height: 100%;
}
.settings-modal__pane.active {
    display: flex;
}
.settings-modal__pane-header {
    flex-shrink: 0;
    padding-bottom: var(--space-3);
    margin-bottom: var(--space-3);
    border-bottom: 1px solid var(--color-border);
}
.settings-modal__pane-header h3 {
    margin: 0 0 2px;
    font-size: var(--font-size-lg);
    font-weight: 700;
    color: var(--color-text);
}
.settings-modal__pane-header p {
    margin: 0;
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

/* The pane body is the ONLY thing that scrolls, and only when its own
   category's cards overflow — not the whole modal. */
.settings-modal__pane-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding-right: var(--space-2);
}

/* Settings modal on mobile: full-screen sheet instead of a centered card, and
   the sidebar becomes a horizontal top tab-bar instead of a left rail (26%
   width + side-by-side content doesn't fit a narrow phone). The internal
   flex-column (fixed header, scrolling pane-body) is unchanged — only the
   modal's own size and the layout/sidebar direction change here. */
@media (max-width: 768px) {
    /* Pin the modal to the TOP on phones so its header + close are never
       clipped above the viewport (the product owner 2026-07-23: "cut off at the top"). */
    .settings-modal-overlay {
        align-items: flex-start;
    }
    .settings-modal {
        width: 100vw;
        box-sizing: border-box;
        height: 100vh; /* fallback for browsers without dvh support */
        /* dvh LAST so it wins where supported: the dynamic viewport EXCLUDES
           browser chrome, so a centered/pinned 100vh modal no longer overflows
           the visible area and clips its own top. (Order was reversed — 100vh
           was overriding 100dvh — which is what cut the header off.) */
        height: 100dvh;
        max-width: none;
        max-height: none;
        border-radius: 0;
        padding: var(--space-3, 14px);
        /* Clear the OS status bar / notch (viewport-fit=cover is set, so the
           inset is real) plus a real breathing gap so the header isn't jammed
           at the very top. */
        padding-top: calc(env(safe-area-inset-top, 0px) + var(--space-4, 20px));
    }
    .settings-modal__layout {
        flex-direction: column;
        gap: var(--space-2);
    }
    /* Sidebar → horizontal tab bar. Scrolls sideways if the tabs don't fit
       rather than wrapping, so the row stays a single clean strip. */
    .settings-modal__sidebar {
        flex: 0 0 auto;
        flex-direction: row;
        gap: 2px;
        overflow-x: auto;
        overflow-y: visible;
        padding-right: 0;
        padding-bottom: var(--space-2);
        border-right: none;
        border-bottom: 1px solid var(--color-border);
    }
    .settings-modal__nav-item {
        flex: 0 0 auto;
        /* Left accent bar reads wrong on a horizontal strip — swap to a
           bottom accent bar so the active tab is still obvious. Reserve the
           bottom border on all items (like the desktop left border) so
           activating a tab never shifts the layout. */
        border-left: none;
        border-bottom: 3px solid transparent;
        border-radius: var(--radius-md) var(--radius-md) 0 0;
    }
    .settings-modal__nav-item.active {
        border-left-color: transparent;
        border-bottom-color: var(--color-primary);
    }
    .settings-modal__content {
        min-width: 0;
        overflow-x: hidden;
    }
    /* Defensive: nothing inside a pane should force horizontal scroll on a
       narrow phone viewport. */
    .settings-modal__pane-body {
        max-width: 100%;
    }
    .settings-modal__pane-body * {
        max-width: 100%;
    }

    /* Close button: reverted to the site's original style per the product owner
       2026-07-23 ("missing it is not the problem — go back to how the website
       has it"). The real issue was the modal's top being cut off, fixed above
       via the top pin + dvh height + top safe-area padding. Keep only an
       invisible 44px touch target — it doesn't change the look. */
    .settings-modal__close {
        min-width: 44px;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
}

.message.user {
    flex-direction: row-reverse;
}

.message.system-trigger {
    flex-direction: row-reverse;
}

/* Setting-card row: title(+description) left, control right-aligned. Used as
   .account-card.toggle-row — a card whose entire body IS the row (no separate
   <h3> title needed above it; .toggle-label carries the bold title instead). */
.toggle-row {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
}

.toggle-info {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.toggle-label {
    color: var(--color-text);
    font-weight: 500;
}

.toggle-subtitle {
    font-size: 0.75rem;
    color: rgba(200, 215, 230, 0.6);
}

[data-theme="light"] .toggle-subtitle {
    /* rgba(60,80,100,0.6) measured ~2.88:1 in light — fails AA. Solid
       --color-text-secondary (#4a5a6b) clears 4.5:1. */
    color: var(--color-text-secondary);
}

/* Switch styling */
.switch {
    position: relative;
    display: inline-block;
    width: 44px;
    height: 24px;
    flex-shrink: 0;
}

.switch input { 
    opacity: 0;
    width: 0;
    height: 0;
}

.slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(255,255,255,0.15);
    transition: .4s;
}

[data-theme="light"] .slider {
    background-color: rgba(0,0,0,0.15);
}

.slider:before {
    position: absolute;
    content: "";
    height: 18px;
    width: 18px;
    left: 3px;
    bottom: 3px;
    background-color: white;
    transition: .4s;
    box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}

input:checked + .slider {
    background-color: var(--color-primary);
}

input:focus + .slider {
    box-shadow: 0 0 1px var(--color-primary);
}

input:checked + .slider:before {
    transform: translateX(20px);
}

.slider.round {
    border-radius: 24px;
}

.slider.round:before {
    border-radius: 50%;
}

/* Portal v2: drag-and-drop file ingestion onto the orb ("gravity well") */
.hero-orb-wrap {
    transition: transform 250ms cubic-bezier(0.34, 1.56, 0.64, 1), filter 250ms ease;
    cursor: pointer;
}
.hero-orb-wrap.orb-gravity-well,
#view-chat.orb-chat-docked .hero-orb-wrap.orb-gravity-well {
    /* 1.5x the docked scale(0.28) baseline so the "gravity well" grows relative
       to its current on-screen size whether docked (header) or full splash. */
    transform: scale(0.42);
    filter: drop-shadow(0 0 40px var(--orb-backdrop, rgba(34,167,240,0.6))) drop-shadow(0 0 80px rgba(34,167,240,0.35));
}
#view-chat.orb-chat-docked .hero-orb-wrap.orb-gravity-well {
    /* Keep the drag affordance in the header rather than restoring the splash
       scale and 80px glow. */
    transform: scale(0.156);
    filter: drop-shadow(0 0 6px var(--orb-backdrop, rgba(34,167,240,0.6)));
}
#view-chat.splash-fullscreen .hero-orb-wrap.orb-gravity-well {
    transform: scale(1.5);
}
.hero-orb-wrap.orb-ingest-pulse {
    animation: orb-ingest-pulse-kf 500ms ease-out;
}
@keyframes orb-ingest-pulse-kf {
    0% { transform: scale(1); }
    35% { transform: scale(1.18); }
    100% { transform: scale(1); }
}

.whoosh-file-icon {
    position: fixed;
    z-index: 9999;
    font-size: 32px;
    line-height: 1;
    pointer-events: none;
    transform: translate(0, 0) scale(1) rotate(0deg);
    opacity: 1;
    transition: transform 480ms cubic-bezier(0.55, 0, 0.85, 0.35), opacity 480ms ease-in;
    will-change: transform, opacity;
}

/* T49 P4: robot-eyes face centered over the splash hero orb (override the inherited feather-mask + 100% sizing) */
.hero-orb-wrap canvas.splash-eyes-canvas {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 46%;
    height: 32%;
    z-index: 1;
    mask: none;
    -webkit-mask: none;
    pointer-events: none;
}
/* T49 P4: S0 splash — center + enlarge the hero orb as the full-screen centerpiece */
#view-chat.splash-fullscreen .orb-hero {
    flex: 1 1 auto;
    align-items: center;
    justify-content: center;
    padding: 0;
}
#view-chat.splash-fullscreen .hero-orb-wrap {
    width: min(72vh, 660px);
    height: min(72vh, 660px);
}
@media (max-width: 768px) {
    #view-chat.splash-fullscreen .hero-orb-wrap { width: min(70vw, 320px); height: min(70vw, 320px); }
}

/* UI Refactor: Utilities, Drawers, Orbs, and Emotes */
.chat-input-actions {
    display: flex;
    align-items: center;
    gap: 4px;
}

.util-btn {
    background: none;
    border: none;
    color: rgba(255,255,255,0.6);
    cursor: pointer;
    font-size: 1.1rem;
    padding: 6px;
    border-radius: 4px;
    transition: background 0.2s, color 0.2s;
    opacity: 0.7;
}

.util-btn:hover {
    background: rgba(255,255,255,0.1);
    opacity: 1;
}

@keyframes pulse-glow {
    0% { filter: drop-shadow(0 0 10px rgba(34, 167, 240, 0.2)); }
    50% { filter: drop-shadow(0 0 30px rgba(34, 167, 240, 0.8)); }
    100% { filter: drop-shadow(0 0 10px rgba(34, 167, 240, 0.2)); }
}

.hero-orb-wrap {
    animation: pulse-glow 3s infinite ease-in-out;
    cursor: pointer;
}

/* =====================================================================
   Boot greeting — every load (new AND returning) greets with the big orb,
   breathes twice (glow→dim, ×2), then travels to the docked slot via the
   existing #58 orb-chat-docked transform. Driven by bootGreeting() in app.js.
   ===================================================================== */

/* The greeting's glow breath. Values are identical to pulse-glow, and the
   duplication is DELIBERATE — the distinct name is the fix, not an oversight.
   See the NOTE on the .orb-greeting rule below before merging these back. */
@keyframes orb-greet-glow {
    0%   { filter: drop-shadow(0 0 10px rgba(34, 167, 240, 0.2)); }
    50%  { filter: drop-shadow(0 0 30px rgba(34, 167, 240, 0.8)); }
    100% { filter: drop-shadow(0 0 10px rgba(34, 167, 240, 0.2)); }
}

/* One breath of the greeting's two, settling back to rest. Symmetric so the
   swell peaks WITH the glow peak (orb-greet-glow is symmetric too, both run at
   650ms, and both are created fresh at class-add so their cycles are in phase).
   Starts AND ends at scale(1) — the splash orb has no static transform
   of its own, so ending at rest means the handoff to the docked transform
   (scale .12) starts from exactly where the beat left the orb, with no jump
   when .orb-greeting is removed. */
@keyframes orb-greet-beat {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.055); }
    100% { transform: scale(1); }
}

/* NOTE — three constraints here that the code cannot show. All were measured in
   a real browser, not theorised; do not "simplify" any of them away.

   1. The greeting glow MUST use a name distinct from pulse-glow. `animation` is
      a shorthand, so this rule replaces the whole list and the glow has to be
      re-stated — but re-stating it as `pulse-glow` does NOT restart it. The base
      .hero-orb-wrap rule already has pulse-glow running from page load, and
      changing only the duration/iteration-count of a running animation reuses
      the same Animation object and keeps its original startTime. The cycle then
      stays anchored to orb-mount instead of greeting-start: the glow snaps to
      mid-cycle brightness instead of rising from dim, and it extinguishes
      220-358ms EARLY, leaving the orb visibly dark before it docks. A distinct
      name creates a FRESH Animation at class-add (currentTime=0), which is what
      makes the breath start from dim and stay anchored to the greeting.
   2. `both` fill-mode is REQUIRED on both animations. Without it an animation
      stops applying the instant it ends, so `filter` reverts to none — a dark
      orb — in any gap before the class drops.
   3. The two animate different properties (glow drives filter, beat drives
      transform), so they compose rather than fight.

   During the greeting the glow runs FAST and FINITE (650ms, two iterations)
   rather than the resting 3s infinite: the greeting must show two complete
   glow→dim breaths, and at 3s the user only ever saw a ~23% slice of one. It
   starts and ends at the same dim rest value, so two iterations land exactly
   where the resting 3s infinite pulse-glow resumes — no snap at the handoff.
   Total is 1300ms (650×2). THIS ANIMATION decides when the orb docks: app.js
   waits for orb-greet-beat's `animationend` rather than running a timer, because
   an animation starts PENDING and its real start was measured 165-457ms after
   class-add — any timer would fire mid-beat. Retiming these keyframes therefore
   retimes the dock automatically; there is no JS duration to keep in sync. */
#view-chat.splash-fullscreen.orb-greeting .hero-orb-wrap {
    animation: orb-greet-glow 650ms ease-in-out 2 both,
               orb-greet-beat 650ms ease-in-out 2 both;
}

/* Reveal-on-dock: history fades up AS the orb travels. 400ms matches the
   .hero-orb-wrap travel transition so the two land together. This is an
   animation rather than a transition because the element leaves display:none
   (.state-sm-hidden) at the same instant — a transition would not run. */
@keyframes history-reveal-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}
.working-area.history-reveal {
    animation: history-reveal-in 400ms ease-out 1;
}

/* Reduced motion — NON-NEGOTIABLE: no greeting beat, no travel, no reveal fade.
   The orb simply starts docked and the history is just there. app.js skips the
   greeting hold entirely on this path — it MUST, because dropping the beat below
   also means orb-greet-beat's `animationend` never fires, and that event is what
   docks the orb on the normal path. These rules remove the motion itself, so even
   though the class swap still happens the orb never animates between positions. */
@media (prefers-reduced-motion: reduce) {
    #view-chat.splash-fullscreen.orb-greeting .hero-orb-wrap {
        animation: pulse-glow 3s infinite ease-in-out;  /* beat dropped; resting glow kept */
    }
    #view-chat.orb-chat-docked .hero-orb-wrap {
        transition: none;   /* no travel — the orb is simply already docked */
        /* The travel is a keyframe animation (orb-dock-travel), so `transition: none`
           above is NOT enough to suppress it — it must be dropped by name. Longhand,
           not the `animation` shorthand, and deliberately so: the shorthand would also
           reset the mobile block's `animation: none` and hand mobile a pulse it does
           not have today. Slot 1 (travel) → none; slot 2 keeps the idle breathing,
           which is pre-existing behaviour under reduced motion and is left as-is —
           this fix does not widen its scope to that question.
           animation-delay 0s: with no travel there is no 400ms window to wait out, so
           the orb breathes from the first docked frame instead of pausing first. */
        /* Slot 2 was orb-idle-pulse; now none, matching the base rule above
           where the idle breathing was removed. Still animation-name rather
           than the shorthand, so this cannot reset the mobile block's
           `animation: none`. */
        animation-name: none, none;
        animation-delay: 0s, 0s;
    }
    .working-area.history-reveal {
        animation: none;    /* history is immediately visible, no fade */
    }
}

.af-emote__row {
    display: grid !important;
    grid-template-columns: repeat(4, 1fr) !important;
    gap: 8px !important;
    margin-top: 8px !important;
    background: transparent !important;
    border: none !important;
}

.af-emote__btn {
    border-radius: 12px !important;
    padding: 4px 8px !important;
    font-size: 0.8rem !important;
    border: 1px solid rgba(255,255,255,0.1) !important;
}

/* =========================================================
   Portal v2: TV & Large-Screen Responsive
   ========================================================= */

/* The composer's width comes from the base .chat-input-wrapper rule at every
   size above the mobile breakpoint: min(1200px, 92vw). This block used to pin
   it to 1000px above 1400px, which silently overrode that rule on exactly the
   displays it was meant to help. */
@media (min-width: 1400px) {
    .message.user .message-bubble,
    .message.assistant .message-bubble {
        max-width: 70%;
    }
}

/* =========================================================
   Portal v2: D-Pad / Remote / Keyboard Focus Styles
   High-contrast :focus-visible glow for mouseless navigation.
   ========================================================= */

.chat-btn:focus-visible {
    outline: 2px solid var(--color-primary, #22A7F0);
    outline-offset: 2px;
    box-shadow: 0 0 0 4px rgba(34, 167, 240, 0.3);
    border-radius: 50%;
}

.app-icon-btn:focus-visible {
    outline: 2px solid var(--color-primary, #22A7F0);
    outline-offset: 2px;
    box-shadow: 0 0 0 4px rgba(34, 167, 240, 0.3);
    border-radius: 8px;
}

.ur-btn:focus-visible {
    outline: 2px solid var(--color-primary, #22A7F0);
    outline-offset: 2px;
    box-shadow: 0 0 0 4px rgba(34, 167, 240, 0.3);
}

/* Focus ring on the input wrapper (desktop / keyboard nav). Neutral grey-white
   rather than blue — the product owner 2026-07-24 "blue looking kinda weird, make it
   grey/white". Dropped the 20px blue glow. Mobile kills this entirely via the
   @media(max-width:768px) override further below. */
.chat-input-wrapper:focus-within {
    border-color: rgba(255, 255, 255, 0.45);
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.22);
}

/* =========================================================
   Portal v2: Mobile Touch Targets (44px minimum)
   ========================================================= */

@media (max-width: 768px) {
    .chat-btn,
    .app-icon-btn,
    .ur-btn {
        min-width: 44px;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* Kill the composer focus GLOW on mobile but KEEP the bar's own definition.
       The "Enhanced TV focus glow" (.chat-input-wrapper:focus-within ~line 6858,
       for mouseless D-pad nav) lives outside any media query and AFTER the
       mobile block at ~1790, so it wins the cascade and repaints a blue ring on
       every touch focus — the highlight the product owner rejected. This override sits
       after 6858 (equal specificity) so it wins on mobile. It repeats the
       RESTING bar's top-divider + upward shadow rather than zeroing them, so
       tapping to type adds NO ring yet the bar keeps its separation from the
       chat. Desktop keyboard-nav glow (6858) untouched. */
    .chat-input-wrapper:focus-within {
        box-shadow: 0 -8px 22px rgba(0, 0, 0, 0.34);
        border-color: rgba(255, 255, 255, 0.14);
    }
}

/* =========================================================
   Portal v2: Voice-First Orb Animation States
   ========================================================= */

/* --- Thinking: soft spinning glow --- */
@keyframes orb-thinking-spin {
    0%   { box-shadow: 0 0 25px 8px rgba(34,167,240,0.25), 0 0 60px 15px rgba(34,167,240,0.08); filter: hue-rotate(0deg); }
    50%  { box-shadow: 0 0 35px 12px rgba(34,167,240,0.35), 0 0 80px 20px rgba(34,167,240,0.12); filter: hue-rotate(15deg); }
    100% { box-shadow: 0 0 25px 8px rgba(34,167,240,0.25), 0 0 60px 15px rgba(34,167,240,0.08); filter: hue-rotate(0deg); }
}

.hero-orb-wrap.orb-state-thinking {
    animation: orb-thinking-spin 3s ease-in-out infinite !important;
    border-radius: 50%;
}

/* Header state feedback is deliberately tighter than splash feedback.  These
   rules preserve the large splash effects while keeping the docked avatar's
   paint footprint inside its header slot. */
@keyframes orb-docked-thinking-spin {
    0%, 100% { box-shadow: 0 0 3px 1px rgba(34,167,240,0.25); filter: hue-rotate(0deg); }
    50% { box-shadow: 0 0 5px 2px rgba(34,167,240,0.35); filter: hue-rotate(15deg); }
}

#view-chat.orb-chat-docked .hero-orb-wrap.orb-state-thinking {
    animation: orb-docked-thinking-spin 3s ease-in-out infinite !important;
}

/* --- Listening: radial pulse waves breathing outward --- */
@keyframes orb-listening-pulse {
    0%, 100% { transform: scale(0.144); }
    50%      { transform: scale(0.162); }
}

@keyframes orb-listening-ring {
    0%   { transform: translate(-50%, -50%) scale(0.8); opacity: 0.6; }
    100% { transform: translate(-50%, -50%) scale(1.6); opacity: 0; }
}

.hero-orb-wrap.orb-state-listening {
    animation: orb-listening-pulse 1.5s ease-in-out infinite !important;
}

.hero-orb-wrap.orb-state-listening::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 100%;
    height: 100%;
    border-radius: 50%;
    border: 2px solid rgba(34,167,240,0.4);
    animation: orb-listening-ring 1.5s ease-out infinite;
    pointer-events: none;
}

@keyframes orb-docked-listening-pulse {
    0%, 100% { transform: scale(0.144); }
    50%      { transform: scale(0.156); }
}

#view-chat.orb-chat-docked .hero-orb-wrap.orb-state-listening {
    animation: orb-docked-listening-pulse 1.5s ease-in-out infinite !important;
}

#view-chat.orb-chat-docked .hero-orb-wrap.orb-state-listening::after {
    animation: none;
    opacity: 0;
}

@keyframes orb-mobile-docked-listening-pulse {
    0%, 100% { transform: scale(0.26); }
    50%      { transform: scale(0.28); }
}

@media (max-width: 768px) and (orientation: portrait) {
    #view-chat.orb-chat-docked .hero-orb-wrap.orb-gravity-well {
        transform: scale(0.28);
    }

    #view-chat.orb-chat-docked .hero-orb-wrap.orb-state-listening {
        animation: orb-mobile-docked-listening-pulse 1.5s ease-in-out infinite !important;
    }
}

/* --- Idle: default, uses existing orb-idle-pulse (no override needed) --- */
.hero-orb-wrap.orb-state-idle {
    /* Falls back to the existing orb-idle-pulse keyframes defined above */
}
/* =========================================================
   PROTOTYPE — orb-click media library (throwaway design-review scaffolding)
   ========================================================= */

/* Full centre-modal over a darkened backdrop, same fixed-positioning
   technique as .app-stage--spotlight but with its own class so it never
   touches the real spotlight state machine. */
.portal-app-stage.proto-library-active {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 200;
    max-height: none;
    overflow: visible;
    margin: 0;
    align-items: center;
    justify-content: center;
    background: rgba(12, 20, 30, 0.55);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
}

.proto-library {
    display: flex;
    flex-direction: column;
    /* Fixed width AND height — constant across all tabs regardless of content
       (grid of tiles vs. list rows). Content scrolls inside .proto-library__body;
       the frame itself never resizes on tab switch. */
    width: min(880px, 92vw);
    height: min(620px, 82vh);
    background: var(--color-surface, rgba(255,255,255,0.9));
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    border: 1px solid var(--color-border-light, rgba(0,0,0,0.08));
    border-radius: var(--radius-lg, 16px);
    box-shadow: 0 20px 48px rgba(0,0,0,0.35);
    color: var(--color-text);
    font-family: var(--font-body);
    overflow: hidden;
}

.proto-library__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-4, 16px) var(--space-5, 20px);
    border-bottom: 1px solid var(--color-border-light, rgba(0,0,0,0.08));
    flex-shrink: 0;
}

.proto-library__title {
    font-size: var(--font-size-lg, 1.125rem);
    font-weight: 700;
    color: var(--color-text);
}

.proto-library__close {
    appearance: none;
    border: none;
    background: transparent;
    color: var(--color-text-secondary, #6b7280);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    padding: 4px 8px;
    border-radius: var(--radius-sm, 6px);
}

.proto-library__close:hover {
    background: var(--color-surface-hover, rgba(0,0,0,0.06));
    color: var(--color-text);
}

.proto-library__tabs {
    display: flex;
    gap: 6px;
    padding: var(--space-3, 12px) var(--space-5, 20px);
    border-bottom: 1px solid var(--color-border-light, rgba(0,0,0,0.08));
    flex-shrink: 0;
    overflow-x: auto;
}

.proto-library__tab {
    appearance: none;
    border: 1px solid transparent;
    background: transparent;
    color: var(--color-text-secondary, #6b7280);
    font-family: var(--font-body);
    font-size: var(--font-size-sm, 0.875rem);
    font-weight: 600;
    padding: 6px 14px;
    border-radius: var(--radius-full, 9999px);
    cursor: pointer;
    white-space: nowrap;
}

.proto-library__tab:hover {
    background: var(--color-surface-hover, rgba(0,0,0,0.06));
}

.proto-library__tab--active {
    background: var(--color-primary, #22A7F0);
    color: #ffffff;
}

/* Hovering the active tab must KEEP its blue. `.proto-library__tab:hover` and
   `.proto-library__tab--active:hover` both compute (0,2,0) — a tie — so a bare
   `--active:hover` rule would win only by source order, silently regressing if
   the rules are ever reordered. Scoping under `.proto-library__tabs` makes this
   (0,3,0) so it wins by specificity, not by luck. Without it, the hover fill
   (--color-surface-hover: #E9ECEF in light) replaced the blue while the white
   text stayed — white on near-white, ~1.19:1, invisible in day mode.
   NB: the resulting white-on-#22A7F0 is ~2.67:1 — the app's pre-existing
   active-tab look everywhere, sub-AA but out of scope for this fix. */
.proto-library__tabs .proto-library__tab--active:hover {
    background: var(--color-primary, #22A7F0);
    color: #ffffff;
}

.proto-library__body {
    padding: var(--space-4, 16px) var(--space-5, 20px);
    overflow-y: auto;
    flex: 1;
}

.proto-library__empty {
    padding: var(--space-8, 32px) var(--space-4, 16px);
    text-align: center;
    color: var(--color-text-secondary, #6b7280);
    font-size: var(--font-size-sm, 0.875rem);
}

/* Images / videos — thumbnail grid */
.proto-library__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: var(--space-3, 12px);
}

.proto-library__tile {
    position: relative;
    aspect-ratio: 1 / 1;
    border-radius: var(--radius-md, 10px);
    overflow: hidden;
    background: var(--color-surface-hover, rgba(0,0,0,0.06));
    cursor: pointer;
    border: 1px solid var(--color-border-light, rgba(0,0,0,0.08));
    display: flex;
}

.proto-library__tile:hover {
    border-color: var(--color-primary, #22A7F0);
}

.proto-library__tile-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.proto-library__tile-video {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
    height: 100%;
    padding: var(--space-3, 12px);
    text-align: center;
    background: linear-gradient(160deg, var(--color-secondary, #005B91), var(--color-primary, #22A7F0));
    color: #ffffff;
}

.proto-library__tile-video-icon {
    font-size: 1.75rem;
}

.proto-library__tile-video-label {
    font-size: var(--font-size-xs, 0.75rem);
    word-break: break-all;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
}

.proto-library__tile-date-tag {
    position: absolute;
    right: 6px;
    bottom: 6px;
    background: rgba(0,0,0,0.55);
    color: #fff;
    font-size: 0.65rem;
    padding: 2px 6px;
    border-radius: var(--radius-sm, 6px);
    pointer-events: none;
}

/* Dead-link honest failure state — never a broken-image icon. */
.proto-library__tile-broken {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: var(--space-2, 8px);
    text-align: center;
    color: var(--color-text-secondary, #6b7280);
    background: var(--color-surface-hover, rgba(0,0,0,0.06));
}

.proto-library__tile-broken-icon {
    font-size: 1.25rem;
    color: var(--color-warning, #F59E0B);
}

.proto-library__tile-broken-msg {
    font-size: var(--font-size-xs, 0.75rem);
    font-weight: 600;
}

.proto-library__tile-date {
    font-size: 0.65rem;
    opacity: 0.8;
}

/* Web / files / yours — list rows */
.proto-library__list {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.proto-library__list-item {
    display: flex;
    align-items: center;
    gap: var(--space-3, 12px);
    padding: 10px 12px;
    border-radius: var(--radius-md, 10px);
    cursor: pointer;
    color: inherit;
    text-decoration: none;
    border: 1px solid transparent;
}

.proto-library__list-item:hover {
    background: var(--color-surface-hover, rgba(0,0,0,0.06));
    border-color: var(--color-border-light, rgba(0,0,0,0.08));
}

.proto-library__list-domain {
    flex-shrink: 0;
    font-weight: 700;
    font-size: var(--font-size-sm, 0.875rem);
    color: var(--color-primary, #22A7F0);
}

.proto-library__list-badge {
    flex-shrink: 0;
    font-size: 0.65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-secondary, #005B91);
    background: var(--color-primary-light, #e8f4fb);
    padding: 2px 8px;
    border-radius: var(--radius-full, 9999px);
}

.proto-library__list-url {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--font-size-sm, 0.875rem);
    color: var(--color-text-secondary, #6b7280);
}

.proto-library__list-date {
    flex-shrink: 0;
    font-size: var(--font-size-xs, 0.75rem);
    color: var(--color-text-secondary, #6b7280);
    opacity: 0.8;
}

.proto-library__list-item--error .proto-library__list-date {
    color: var(--color-danger, #dc2626);
    opacity: 1;
}

@media (max-width: 768px), (hover: none) and (pointer: coarse) and (max-width: 950px) {
    /* Match the settings modal EXACTLY on phones (the product owner 2026-07-23: "settings
       and media library modal are not sized the same, match them"). Both are
       now full-screen, pinned to the top, with status-bar clearance. */
    .portal-app-stage.proto-library-active {
        align-items: flex-start;
    }
    .proto-library {
        width: 100vw;
        box-sizing: border-box;
        height: 100vh;   /* fallback */
        height: 100dvh;  /* dvh last so it wins where supported */
        border-radius: 0;
        padding-top: calc(env(safe-area-inset-top, 0px) + var(--space-4, 20px));
    }
    .proto-library__grid {
        grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
    }

    .proto-library__body {
        padding: var(--space-3, 12px);
    }

    /* Keep Inbox / Outbox prominent and impossible to clip on phones. */
    .proto-library__tabs {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 6px;
        overflow: visible;
        padding: var(--space-2) 0;
        mask-image: none;
        -webkit-mask-image: none;
    }

    .proto-library__tab--iobox {
        grid-column: 1 / -1;
    }

    /* Touch targets — bring the tab pills and close button up to the
       44px minimum (mirrors .account-scale-btn at style.css:2470). */
    .proto-library__tab {
        min-height: 44px;
        min-width: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        padding-inline: var(--space-2);
        text-align: center;
    }

    .proto-library__close {
        min-width: 44px;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        /* Reverted to original style per the product owner 2026-07-23 ("go back to how the
           website has it") — invisible 44px touch target only, no chip. */
    }
}


/* Delivered-file link in the transcript.
 *
 * A file saved to Outbox reads as one clickable line under the reply, not as
 * a bordered card the width of the conversation. It sits in
 * .message-inline-card, the same slot a card would occupy, so it inherits the
 * bubble's left edge and needs no alignment of its own -- see that rule's own
 * comment for why cards are flush with the message text.
 *
 * The .af-card rules above are scoped to .af-card and do not reach this node.
 */
.message-inline-card .message-file-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--color-primary);
    font-size: 0.9375rem;
    font-weight: 600;
    text-decoration: none;
    /* Hit target: the line is short, so padding is what makes it comfortably
       tappable on a phone rather than a 15px-tall sliver. */
    padding: 4px 0;
    word-break: break-all;
}

/* The leading glyph is decorative and NOT an emoji: emoji render at wildly
   different sizes and baselines per platform, and this has to sit on the same
   line as the filename. A geometric character inherits the text metrics. */
.message-inline-card .message-file-link::before {
    content: '\2913';   /* ⤓ downwards arrow to bar */
    font-weight: 400;
    opacity: 0.85;
}

.message-inline-card .message-file-link:hover,
.message-inline-card .message-file-link:focus-visible {
    text-decoration: underline;
}

/* Keyboard focus must be visible on its own, not only via the underline --
   underline alone is the state a hover already produces, so it does not tell
   a keyboard user where focus is. */
.message-inline-card .message-file-link:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
    border-radius: 3px;
}

/* Delivered-file link INSIDE the reply's own sentence.
 *
 * "Saved the weekly summary to your Outbox as report.pdf." -- the filename is
 * the link. Costs no extra line and no card, which is the whole point of it
 * (customer, 2026-08-28: "Just have the report.pdf clickable, it reduces next
 * line thing").
 *
 * Distinct from .message-file-link above: that one is the standalone line,
 * still used when the reply never names the file in its text.
 */
.message-bubble .message-file-inline {
    color: var(--color-primary);
    font-weight: 600;
    text-decoration: underline;
    /* Thicker than the browser default and held off the glyphs, so it reads as
       a link at body size rather than as an artifact of the descenders in
       filenames like "report.pdf". */
    text-decoration-thickness: 1.5px;
    text-underline-offset: 2px;
    /* A filename must not be broken across lines mid-word -- half a name on
       each line stops reading as one clickable thing. */
    word-break: normal;
    overflow-wrap: anywhere;
}

.message-bubble .message-file-inline:hover {
    text-decoration-thickness: 2.5px;
}

.message-bubble .message-file-inline:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
    border-radius: 3px;
}

/* The user bubble is solid Mycroft Blue, so the same blue on it would be
   invisible. Inherit the bubble's own foreground there and lean on the
   underline to carry the affordance. */
.message.user .message-bubble .message-file-inline {
    color: inherit;
}
