/* ==========================================================================
   TalkWithMe — Dark theme chat UI
   ========================================================================== */

/* --------------------------------------------------------------------------
   Reset & base
   -------------------------------------------------------------------------- */
*, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

:root {
    --bg-primary: #1a1a2e;
    --bg-secondary: #16213e;
    --bg-tertiary: #0f3460;
    --bg-input: #1a1a3e;
    --text-primary: #e0e0e0;
    --text-secondary: #a0a0b0;
    --text-muted: #606070;
    --accent: #e94560;
    --accent-hover: #ff6b81;
    --bubble-user: #0f3460;
    --bubble-ai: #16213e;
    --border-color: #2a2a4e;
    --sidebar-width: 310px;
    /* The iPad and iPhone status bar sits OVER the page, by design: the
       viewport is `viewport-fit=cover` and the status bar style is
       `black-translucent`, so the app reaches under the rounded corners
       instead of being letterboxed. Nothing was giving it back its space at
       the top, so on an installed iPad PWA the clock and battery sat on top
       of the toolbar buttons.

       Folded into the variable rather than patched onto #topbar, because
       four other rules are positioned from it — the main area's height, and
       two drawer offsets. Correct the variable and they all follow; correct
       the bar alone and the drawers open 20 pixels too high. */
    --safe-top: env(safe-area-inset-top, 0px);
    --topbar-height: calc(52px + var(--safe-top));
    --radius: 12px;
}

body[data-theme="light"] {
    --bg-primary: #f5f7fb;
    --bg-secondary: #ffffff;
    --bg-tertiary: #e9eef7;
    --bg-input: #ffffff;
    --text-primary: #1d2433;
    --text-secondary: #445067;
    --text-muted: #6f7d97;
    --accent: #2f6fed;
    --accent-hover: #4d85f2;
    --bubble-user: #d9e6ff;
    --bubble-ai: #ffffff;
    --border-color: #ccd6e6;
}

body[data-theme="matrix"] {
    --bg-primary: #020b03;
    --bg-secondary: #041706;
    --bg-tertiary: #0a260d;
    --bg-input: #041706;
    --text-primary: #9dff9d;
    --text-secondary: #71cf71;
    --text-muted: #4b9b4b;
    --accent: #35ff6c;
    --accent-hover: #6eff95;
    --bubble-user: #0a2a10;
    --bubble-ai: #08220d;
    --border-color: #1f5f2f;
}

body[data-theme="blues"] {
    --bg-primary: #0a1f3f;
    --bg-secondary: #102c57;
    --bg-tertiary: #1a3d73;
    --bg-input: #15325f;
    --text-primary: #edf6ff;
    --text-secondary: #bfd8f7;
    --text-muted: #8eb0db;
    --accent: #66b3ff;
    --accent-hover: #8cc8ff;
    --bubble-user: #174073;
    --bubble-ai: #12335e;
    --border-color: #2a4f82;
}

html, body {
    height: 100%;
    /* Same dynamic-viewport correction as #main-layout: on iOS Safari a
       percentage height resolves against the toolbar-less viewport. */
    height: 100dvh;
    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
    background: var(--bg-primary);
    color: var(--text-primary);
    overflow: hidden;
    /* Stops the whole page rubber-banding when a scroll gesture reaches the
       end of the transcript, which on iOS drags the fixed chrome around. */
    overscroll-behavior: none;
}

/*
 * Generic show/hide utility. JS toggles the "hidden" class on modal
 * overlays, error banners, and the list/form views inside the persona and
 * chat room editors. The !important is load-bearing, not decoration:
 * #pe-list-view and #pe-form-view set their layout via ID selectors, and
 * an ID rule would out-specify a plain .hidden rule and defeat it.
 */
.hidden {
    display: none !important;
}

/* --------------------------------------------------------------------------
   Top bar
   -------------------------------------------------------------------------- */
#topbar {
    height: var(--topbar-height);
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* The inset as top padding, so the 52px of bar is BELOW the status bar
       rather than sharing space with it. box-sizing is border-box, so the
       height above already accounts for this padding.
       Left and right matter too: in landscape the notch eats a corner, and
       the first and last buttons are the ones it eats. */
    padding: var(--safe-top) max(20px, env(safe-area-inset-right, 0px))
             0 max(20px, env(safe-area-inset-left, 0px));
    background: var(--bg-secondary);
    border-bottom: 1px solid var(--border-color);
    position: relative;
    z-index: 10;
}

#topbar h1 {
    font-size: 1.2rem;
    font-weight: 600;
    color: var(--accent);
    letter-spacing: 0.5px;
}

.topbar-actions {
    display: flex;
    gap: 10px;
    align-items: center;
}

.topbar-actions button {
    background: var(--bg-tertiary);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    padding: 6px 14px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.85rem;
    transition: background 0.15s, border-color 0.15s;
}

.topbar-actions button:hover {
    background: var(--accent);
    border-color: var(--accent);
}

.theme-label {
    font-size: 0.8rem;
    color: var(--text-secondary);
}

#theme-select {
    background: var(--bg-tertiary);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 6px 8px;
    font-size: 0.82rem;
}

#theme-select:focus {
    outline: 1px solid var(--accent);
    outline-offset: 1px;
}

.icon-btn {
    font-size: 1.1rem !important;
    padding: 6px 10px !important;
}

/*
 * No cross glyph. The icon itself already swaps between a loudspeaker and a
 * muted loudspeaker, so appending a ❌ put two symbols in one button and made
 * the muted state read as an error rather than a state.
 */
#tts-icon.muted {
    opacity: 0.55;
}

/* Skip button: dimmed and unclickable when there is no audio to skip. */
#btn-tts-skip:disabled {
    opacity: 0.35;
    cursor: default;
}

/* --------------------------------------------------------------------------
   Layout
   -------------------------------------------------------------------------- */
#main-layout {
    display: flex;
    /*
     * 100vh is a lie in mobile Safari: it measures the viewport as if the
     * address bar and toolbar were hidden, so with them on screen the layout
     * runs taller than what you can actually see and the composer is pushed
     * under the bottom bar. Hiding the toolbar, or installing to the home
     * screen, removes the chrome and makes 100vh accidentally correct - which
     * is exactly the reported symptom.
     *
     * dvh tracks the *dynamic* viewport, shrinking and growing as the chrome
     * slides in and out. The vh rule stays first as the fallback for browsers
     * that do not understand dvh.
     */
    height: calc(100vh - var(--topbar-height));
    height: calc(100dvh - var(--topbar-height));
}

/* --------------------------------------------------------------------------
    Sidebar
    -------------------------------------------------------------------------- */
#sidebar {
    width: var(--sidebar-width);
    min-width: var(--sidebar-width);
    background: var(--bg-secondary);
    border-right: 1px solid var(--border-color);
    padding: 16px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

#sidebar h3 {
    font-size: 0.8rem;
    text-transform: uppercase;
    color: var(--text-muted);
    letter-spacing: 1px;
    margin-top: 8px;
}

/* Chat room selector dropdown */
#chat-room-selector {
    margin-bottom: 4px;
}

#chat-room-dropdown {
    background: var(--bg-tertiary);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 6px 8px;
    font-size: 0.85rem;
    width: 100%;
    cursor: pointer;
    outline: none;
    transition: border-color 0.15s;
}

#chat-room-dropdown:focus {
    border-color: var(--accent);
}

/* "Add persona" button in sidebar */
.btn-add-persona {
    background: var(--bg-tertiary);
    color: var(--text-secondary);
    border: 1px dashed var(--border-color);
    border-radius: 6px;
    padding: 6px 10px;
    font-size: 0.8rem;
    cursor: pointer;
    width: 100%;
    text-align: center;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.btn-add-persona:hover {
    background: var(--accent);
    border-color: var(--accent);
    border-style: solid;
    color: #fff;
}

/* Remove persona "x" button on persona cards */
.persona-remove-btn {
    background: transparent;
    border: none;
    color: #e74c3c;
    font-size: 1rem;
    font-weight: 700;
    cursor: pointer;
    padding: 2px 6px;
    border-radius: 4px;
    line-height: 1;
    flex-shrink: 0;
    transition: background 0.15s, color 0.15s;
    opacity: 0.5;
}

.persona-remove-btn:hover {
    background: rgba(231, 76, 60, 0.2);
    opacity: 1;
}

/* Who-chooser */
#who-chooser {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.chooser-option {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border-radius: 6px;
    cursor: pointer;
    transition: background 0.15s;
    font-size: 0.9rem;
}

.chooser-option:hover {
    background: var(--bg-tertiary);
}

.chooser-option input[type="radio"] {
    accent-color: var(--accent);
    width: 16px;
    height: 16px;
}

/* Echo chamber checkbox — styled to match the who-chooser radio buttons */
.checkbox-option {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border-radius: 6px;
    cursor: pointer;
    transition: background 0.15s;
    font-size: 0.9rem;
    user-select: none;
}

.checkbox-option:hover {
    background: var(--bg-tertiary);
}

.checkbox-option input[type="checkbox"] {
    accent-color: var(--accent);
    width: 16px;
    height: 16px;
    cursor: pointer;
}

.checkbox-option input[type="checkbox"]:disabled {
    cursor: not-allowed;
    opacity: 0.5;
}

/* Persona list */
#persona-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-top: 4px;
}

.persona-card {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border-radius: 8px;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
    border: 2px solid transparent;
}

.persona-card:hover {
    background: var(--bg-tertiary);
}

.persona-card.selected {
    background: var(--bg-tertiary);
    border-color: var(--accent);
    color: var(--text-primary);
}

.persona-avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.95rem;
    color: #fff;
    flex-shrink: 0;
    position: relative;
    overflow: hidden;
}

.persona-avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 50%;
}

/* Mute indicator for non-TTS personas */
.persona-avatar .mute-indicator {
    position: absolute;
    bottom: -1px;
    right: -1px;
    font-size: 0.65rem;
    background: var(--bg-primary);
    border-radius: 50%;
    width: 16px;
    height: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}

.persona-info {
    flex: 1;
    min-width: 0;
}

.persona-name {
    font-weight: 600;
    font-size: 0.9rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.persona-desc {
    font-size: 0.75rem;
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --------------------------------------------------------------------------
   Chat panel
   -------------------------------------------------------------------------- */
#chat-panel {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;
}

#messages {
    flex: 1;
    overflow-y: auto;
    padding: 20px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* Scrollbar styling */
#messages::-webkit-scrollbar,
#sidebar::-webkit-scrollbar,
.pe-list::-webkit-scrollbar {
    width: 6px;
}

#messages::-webkit-scrollbar-thumb,
#sidebar::-webkit-scrollbar-thumb,
.pe-list::-webkit-scrollbar-thumb {
    background: var(--border-color);
    border-radius: 3px;
}

/* --------------------------------------------------------------------------
   Chat bubbles
   -------------------------------------------------------------------------- */
.message-row {
    display: flex;
    align-items: flex-end;
    gap: 8px;
    max-width: 80%;
}

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

.message-row.assistant {
    align-self: flex-start;
    align-items: flex-start; /* Top-align avatar with bubble, not bottom */
    gap: 12px;               /* Wider gap to match the larger avatar */
}

.bubble-avatar {
    width: 64px;
    height: 64px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 1.5rem;
    color: #fff;
    flex-shrink: 0;
    overflow: hidden;
}

.bubble-avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 50%;
}

.bubble-content {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.bubble-name {
    font-size: 0.7rem;
    color: var(--text-muted);
    padding: 0 4px;
    font-weight: 600;
}

.bubble {
    padding: 10px 14px;
    border-radius: var(--radius);
    font-size: 0.92rem;
    line-height: 1.5;
    word-wrap: break-word;
    white-space: pre-wrap;
}

.message-row.user .bubble {
    background: var(--bubble-user);
    border-bottom-right-radius: 4px;
}

.message-row.assistant .bubble {
    background: var(--bubble-ai);
    border: 1px solid var(--border-color);
    border-bottom-left-radius: 4px;
}

/* A tool that is running. Dimmed and pulsing rather than a spinner: it says
   "this is what the wait is for" without competing with the reply. */
.tool-chip.pending {
    opacity: 0.62;
    cursor: default;
    animation: tool-chip-pulse 1.4s ease-in-out infinite;
}

@keyframes tool-chip-pulse {
    0%, 100% { opacity: 0.62; }
    50%      { opacity: 0.95; }
}

/* Respect a reader who has asked for less motion. */
@media (prefers-reduced-motion: reduce) {
    .tool-chip.pending { animation: none; }
}

/* The send button while it is a stop button. Red, because stopping is a
   different action from sending and should not be reached by muscle memory
   alone — but in the same place, because that is the point. */
#btn-send.is-stopping {
    background: var(--error-color, #c0392b);
    border-color: var(--error-color, #c0392b);
}

/* Pictures sent with a message. Several wrap rather than shrinking, so two
   images are still each readable enough to recognise. */
.message-images {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    padding: 4px;
}

.message-image {
    max-width: min(240px, 60vw);
    max-height: 200px;
    width: auto;
    height: auto;
    border-radius: 8px;
    border: 1px solid var(--border-color);
    cursor: zoom-in;
    display: block;
}

/* User bubbles are right-aligned, and their pictures follow them. */
.user-message-content .message-images {
    justify-content: flex-end;
}

/* The viewer. Sized to the window rather than the picture, so a photograph
   from a phone camera does not open larger than the screen. */
.modal-box-image {
    max-width: 96vw;
    max-height: 92vh;
    display: flex;
    flex-direction: column;
}

.image-viewer-body {
    overflow: auto;
    padding: 8px;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 0;
}

.image-viewer-body img {
    max-width: 100%;
    max-height: 80vh;
    object-fit: contain;
    display: block;
}

/* When a message was said. Quiet on purpose: it is reference, not content,
   and it should be findable without ever competing with the words. */
.bubble-time {
    display: block;
    font-size: 0.68rem;
    line-height: 1.6;
    padding: 0 4px;
    color: var(--text-secondary);
    opacity: 0.6;
}

/* User bubbles sit on the right, so their time follows them there. */
.user-message-content .bubble-time {
    text-align: right;
}

/* Reasoning — the model's own thinking, collapsed until asked for.
   Same placement rule as the chips below: it sits in .bubble-content, never
   inside .bubble, because streaming tokens rewrite bubble.textContent.
   Deliberately quieter than the reply: this is working-out, not speech. */
.reasoning-box {
    margin: 0 4px 6px;
    font-size: 0.78rem;
    color: var(--text-secondary);
    border-left: 2px solid var(--border-color);
    padding-left: 8px;
}

.reasoning-box > summary {
    cursor: pointer;
    user-select: none;
    list-style: none;
    opacity: 0.75;
}

.reasoning-box > summary::before {
    content: "\25B8\00A0";  /* ▸ then a non-breaking space */
    display: inline-block;
    transition: transform 0.12s ease;
}

.reasoning-box[open] > summary::before {
    content: "\25BE\00A0";  /* ▾ */
}

/* Safari and older WebKit draw their own marker unless this is cleared. */
.reasoning-box > summary::-webkit-details-marker {
    display: none;
}

.reasoning-text {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    margin-top: 4px;
    opacity: 0.85;
}

/* Tool-call chips — display-only indicators of MCP tool invocations.
   They live in .bubble-content (not inside .bubble) because streaming
   tokens rewrite bubble.textContent, which would destroy child elements. */
.tool-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 0 4px;
}

.tool-chip {
    font-size: 0.7rem;
    line-height: 1.4;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--bg-input);
    border: 1px solid var(--border-color);
    color: var(--text-secondary);
    cursor: default;
    user-select: none;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tool-chip.error {
    color: #ff6b6b;
    border-color: #ff6b6b;
}

/*
 * The chip is a button: it opens the detail modal. It keeps the pill look
 * of the old static chip, and only gains the affordances that say it can
 * be pressed - a pointer, a hover tint, and a focus ring for the keyboard.
 */
.tool-chip {
    cursor: pointer;
    font-family: inherit;
}

.tool-chip:hover,
.tool-chip:focus-visible {
    border-color: var(--accent-color);
    color: var(--text-primary);
}

.tool-detail-body {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.tool-detail-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.tool-detail-tag {
    font-size: 0.7rem;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--bg-input);
    border: 1px solid var(--border-color);
    color: var(--text-secondary);
}

.tool-detail-tag.error {
    color: #ff6b6b;
    border-color: #ff6b6b;
}

.tool-detail-label {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-secondary);
    margin: 6px 0 0;
}

/*
 * Tool output is machine text: JSON, tables, log lines. It keeps its own
 * line breaks and scrolls in its own box rather than stretching the modal,
 * and the height cap keeps a long directory listing from pushing the close
 * button off a phone screen.
 */
.tool-detail-pre {
    margin: 0;
    padding: 8px;
    max-height: 30vh;
    overflow: auto;
    white-space: pre-wrap;
    word-break: break-word;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.75rem;
    line-height: 1.45;
    background: var(--bg-input);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    color: var(--text-primary);
}

/* Loading indicator */
.loading-bubble {
    display: flex;
    gap: 4px;
    padding: 12px 16px;
    align-items: center;
}

/* :not(.loading-status) is load-bearing, not defensive tidiness.
   `.loading-bubble span` was written when the only spans in here were the
   three dots. The status caption is also a span, so it inherited an 8px box,
   a round background and the pulse animation - and 8px of width renders a
   sentence ONE CHARACTER PER LINE, which is the vertical column of letters
   running down the transcript. flex: none keeps the dots round when a long
   caption sits beside them; without it they are squeezed into ovals. */
.loading-bubble span:not(.loading-status) {
    flex: none;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--text-muted);
    animation: pulse 1.2s infinite ease-in-out;
}

.loading-bubble span:not(.loading-status):nth-child(2) { animation-delay: 0.2s; }
.loading-bubble span:not(.loading-status):nth-child(3) { animation-delay: 0.4s; }

/* The reader's own message, read back. Right-aligned with the rest of the
   user bubble's furniture, and quieter than a persona's audio controls:
   it is a convenience on something they wrote themselves, not part of the
   conversation's record. */
.user-message-content .message-audio {
    justify-content: flex-end;
}

.audio-play-btn.user-speak {
    opacity: 0.65;
}

.audio-play-btn.user-speak:hover,
.audio-play-btn.user-speak:focus-visible {
    opacity: 1;
}

/* A reply the reader stopped. Muted rather than red: nothing failed, they
   asked for it to stop — but the bubble still has to say why it ends
   mid-sentence, or a deliberate stop is indistinguishable from a break. */
.bubble-interrupted {
    margin-top: 8px;
    font-size: 0.85em;
    color: var(--text-muted);
}

/* The sentence beside the dots: what the server is doing right now. Sized
   below body text and muted, because it is a caption on the wait rather
   than content - it is replaced by the reply itself the moment one starts.

   min-width: 0 lets it wrap inside the bubble instead of forcing the bubble
   wider than the column; flex-basis auto lets it take the width it needs
   first. A caption is allowed to be two lines, never one letter wide. */
.loading-status {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 0.85em;
    color: var(--text-muted);
    margin-left: 4px;
}

@keyframes pulse {
    0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
    40% { transform: scale(1); opacity: 1; }
}

/* --------------------------------------------------------------------------
   Input bar
   -------------------------------------------------------------------------- */
#input-bar {
    display: flex;
    align-items: flex-end;
    gap: 10px;
    padding: 14px 20px;
    background: var(--bg-secondary);
    border-top: 1px solid var(--border-color);
    /* Keeps the composer clear of the home indicator once the page is
       allowed under the rounded corners by viewport-fit=cover. Falls back
       to the plain padding wherever the env() is unknown or zero. */
    padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
    flex-shrink: 0;
}

#message-input {
    flex: 1;
    background: var(--bg-input);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-radius: 8px;
    padding: 10px 14px;
    font-size: 0.92rem;
    font-family: inherit;
    resize: none;
    max-height: 120px;
    outline: none;
    transition: border-color 0.15s;
    line-height: 1.4;
}

#message-input:focus {
    border-color: var(--accent);
}

#message-input::placeholder {
    color: var(--text-muted);
}

@keyframes mic-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    #btn-mic.transcribing .mic-icon,
    #btn-mic.recording {
        animation: none;
    }
}

@keyframes mic-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.6; }
}

/* --------------------------------------------------------------------------
   Empty state
   -------------------------------------------------------------------------- */
.empty-state {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
    font-size: 1rem;
    text-align: center;
    padding: 40px;
}

.empty-state p {
    margin-bottom: 8px;
}

.empty-state .hint {
    font-size: 0.8rem;
    color: var(--text-muted);
    opacity: 0.6;
}

/* --------------------------------------------------------------------------
   Compaction marker

   The line that says a stretch of conversation was summarised. Deliberately
   NOT a .message-row: that class caps width at 80% and pushes the row to one
   side, and this is furniture rather than speech, so it spans the panel.

   Every colour comes from a variable. Four themes redefine the same names,
   so a literal here would look right in one of them and wrong in three.
   -------------------------------------------------------------------------- */
.compaction-marker {
    margin: 4px 0;
    font-size: 0.68rem;
    color: var(--text-secondary);
}

/* Dimming on the closed label only. When it sat on the whole element the
   opacities multiplied — 0.7 on the marker times 0.9 on the text gave 0.63,
   at 0.68rem, in a secondary colour. The expanded chapter is the one thing
   here a reader is meant to actually read and check, and it was the faintest
   text in the transcript. */
.compaction-marker > summary {
    cursor: pointer;
    user-select: none;
    list-style: none;
    display: flex;
    align-items: center;
    gap: 10px;
    opacity: 0.7;
}

/* The label is an anonymous flex item, so it needs min-width: 0 to be
   allowed to shrink. Without it, nowrap made its min-content width the full
   string width, the two rule pseudo-elements collapsed to nothing, and a
   56-character label overflowed a 360px phone — giving the whole transcript
   a horizontal scrollbar. */
.compaction-marker > summary > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The rules on either side of the label. Two pseudo-elements sharing the
   remaining width is what makes it read as a seam in the transcript rather
   than as another thing someone said. */
.compaction-marker > summary::before,
.compaction-marker > summary::after {
    content: "";
    flex: 1;
    border-top: 1px solid var(--border-color);
}

.compaction-marker > summary::-webkit-details-marker {
    display: none;
}

.compaction-marker[open] > summary {
    margin-bottom: 6px;
}

/* Opened, it is the record itself — indented like the reasoning panel, for
   the same reason: it is what the model reads, not what anyone said. */
/* Full opacity, and a size a person can read: this is what the personas
   will actually be working from, and the whole reason the marker opens. */
.compaction-text {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    border-left: 2px solid var(--border-color);
    padding-left: 8px;
    margin: 0 4px;
    font-size: 0.78rem;
    color: var(--text-primary);
}

.compaction-marker .bubble-time {
    padding-left: 12px;
}

/* --------------------------------------------------------------------------
   Persona Editor Modal
   -------------------------------------------------------------------------- */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.65);
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}

.modal-box {
    background: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: var(--radius);
    width: 100%;
    max-width: 680px;
    max-height: 90vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.modal-box-sm {
    max-width: 400px;
}

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

.modal-header h2 {
    font-size: 1rem;
    font-weight: 600;
    color: var(--text-primary);
}

.modal-header-actions {
    display: flex;
    gap: 8px;
    align-items: center;
}

.btn-icon-close {
    background: transparent;
    border: none;
    color: var(--text-secondary);
    font-size: 1rem;
    cursor: pointer;
    padding: 4px 8px;
    border-radius: 4px;
    line-height: 1;
    transition: color 0.15s, background 0.15s;
}

.btn-icon-close:hover {
    color: var(--text-primary);
    background: var(--bg-tertiary);
}

.btn-accent {
    background: var(--accent);
    color: #fff;
    border: none;
    border-radius: 6px;
    padding: 6px 14px;
    font-size: 0.85rem;
    cursor: pointer;
    transition: background 0.15s;
}

.btn-accent:hover {
    background: var(--accent-hover);
}

.btn-secondary {
    background: var(--bg-tertiary);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 6px 14px;
    font-size: 0.85rem;
    cursor: pointer;
    transition: background 0.15s;
}

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

.btn-danger {
    background: #c0392b;
    color: #fff;
    border: none;
    border-radius: 6px;
    padding: 6px 14px;
    font-size: 0.85rem;
    cursor: pointer;
    transition: background 0.15s;
}

.btn-danger:hover {
    background: #e74c3c;
}

/* Persona editor list view — constrained so the list scrolls within the modal */
#pe-list-view {
    display: flex;
    flex-direction: column;
    max-height: 90vh;
    overflow: hidden;
}

/* Persona editor list — capped so the form view (shown in-place during edit) has room */
.pe-list {
    overflow-y: auto;
    padding: 12px 20px;
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-height: 60px;
    max-height: 45vh;
}

.pe-list-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border-radius: 8px;
    border: 1px solid var(--border-color);
    background: var(--bg-primary);
}

.pe-list-item-avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.95rem;
    color: #fff;
    flex-shrink: 0;
    overflow: hidden;
}

.pe-list-item-info {
    flex: 1;
    min-width: 0;
}

.pe-list-item-name {
    font-weight: 600;
    font-size: 0.9rem;
}

.pe-list-item-desc {
    font-size: 0.75rem;
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.pe-list-item-actions {
    display: flex;
    gap: 6px;
    flex-shrink: 0;
}

.pe-list-item-actions button {
    background: var(--bg-tertiary);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-radius: 5px;
    padding: 4px 10px;
    font-size: 0.78rem;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}

.pe-list-item-actions button:hover {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

.pe-list-item-actions button.btn-delete:hover {
    background: #c0392b;
    border-color: #c0392b;
}

.pe-empty {
    color: var(--text-muted);
    font-size: 0.9rem;
    text-align: center;
    padding: 24px 0;
}

/* Persona editor form — constrained so long forms scroll within the modal */
#pe-form-view {
    display: flex;
    flex-direction: column;
    max-height: 90vh;
    overflow: hidden;
}

#pe-form {
    padding: 16px 20px;
    display: flex;
    flex-direction: column;
    gap: 14px;
    overflow-y: auto;
    flex: 1;
    min-height: 0;
}

.form-row {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.form-row label {
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--text-secondary);
}

.required {
    color: var(--accent);
}

.field-hint {
    font-size: 0.72rem;
    color: var(--text-muted);
}

/* Persona editor — file fields (avatar image, reference audio) */
.pf-file-row {
    display: flex;
    align-items: center;
    gap: 12px;
}

.pf-file-preview {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-input);
    border: 1px solid var(--border-color);
    overflow: hidden;
    flex-shrink: 0;
    color: var(--text-muted);
    font-weight: 700;
}

.pf-file-preview img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.pf-file-controls {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    min-width: 0;
}

.pf-file-status {
    font-size: 0.75rem;
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 240px;
}

.btn-small {
    padding: 3px 10px;
    font-size: 0.75rem;
}

.form-row input[type="file"] {
    color: var(--text-secondary);
    font-size: 0.78rem;
    max-width: 100%;
}

.form-row input[type="text"],
.form-row textarea {
    background: var(--bg-input);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 8px 10px;
    font-size: 0.88rem;
    font-family: inherit;
    outline: none;
    transition: border-color 0.15s;
    resize: vertical;
}

.form-row input[type="text"]:focus,
.form-row textarea:focus {
    border-color: var(--accent);
}

/* Read-only fields — slightly muted, no focus ring */
.form-row input.form-input-readonly {
    background: var(--bg-tertiary);
    color: var(--text-muted);
    cursor: default;
}

.form-row input.form-input-readonly:focus {
    border-color: var(--border-color);
}

.form-row input[type="color"] {
    width: 48px;
    height: 34px;
    border: 1px solid var(--border-color);
    border-radius: 6px;
    background: var(--bg-input);
    cursor: pointer;
    padding: 2px;
}

.form-row-inline {
    flex-direction: row;
    gap: 20px;
    align-items: flex-start;
}

.form-col {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.form-actions {
    display: flex;
    gap: 10px;
    justify-content: flex-end;
    padding: 12px 20px;
    border-top: 1px solid var(--border-color);
    flex-shrink: 0;
}

.pe-form-error {
    background: rgba(192, 57, 43, 0.18);
    color: #e74c3c;
    border: 1px solid #c0392b;
    border-radius: 6px;
    padding: 8px 14px;
    font-size: 0.85rem;
    margin: 0 20px 0 20px;
}

.pe-confirm-msg {
    padding: 16px 20px;
    font-size: 0.9rem;
    color: var(--text-secondary);
}

/* ==========================================================================
   Settings Modal
   ========================================================================== */

#settings-form {
    display: flex;
    flex-direction: column;
}

.settings-body {
    padding: 16px 20px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 16px;
    max-height: 65vh;
}

/* Fieldset sections */
.settings-section {
    border: 1px solid var(--border-color);
    border-radius: 8px;
    padding: 12px 14px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.settings-section legend {
    padding: 0 6px;
    font-size: 0.82rem;
    font-weight: 700;
    color: var(--text-primary);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    cursor: default;
}

/* Settings fields container (shown/hidden based on toggle) */
.settings-fields {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.settings-fields.disabled {
    opacity: 0.35;
    pointer-events: none;
}

/* Toggle switch (for TTS/STT enable/disable) */
.settings-toggle-label {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    user-select: none;
}

.settings-toggle {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.settings-toggle-slider {
    position: relative;
    display: inline-block;
    width: 36px;
    height: 20px;
    background: var(--border-color);
    border-radius: 10px;
    transition: background 0.2s;
    flex-shrink: 0;
}

.settings-toggle-slider::after {
    content: "";
    position: absolute;
    top: 2px;
    left: 2px;
    width: 16px;
    height: 16px;
    background: var(--text-secondary);
    border-radius: 50%;
    transition: transform 0.2s, background 0.2s;
}

.settings-toggle:checked + .settings-toggle-slider {
    background: var(--accent);
}

.settings-toggle:checked + .settings-toggle-slider::after {
    transform: translateX(16px);
    background: #fff;
}

.settings-toggle-text {
    font-size: 0.82rem;
    font-weight: 700;
    color: var(--text-primary);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* Styled checkbox (for streaming toggle) */
.settings-checkbox-label {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    font-size: 0.85rem;
    color: var(--text-secondary);
    user-select: none;
}

.settings-checkbox-label input[type="checkbox"] {
    accent-color: var(--accent);
    width: 16px;
    height: 16px;
    cursor: pointer;
}

/* Number input styling for settings */
.settings-body .form-row input[type="number"] {
    background: var(--bg-input);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 8px 10px;
    font-size: 0.88rem;
    font-family: inherit;
    outline: none;
    transition: border-color 0.15s;
    width: 100%;
}

.settings-body .form-row input[type="number"]:focus {
    border-color: var(--accent);
}

/* Remove spinner arrows from number inputs */
.settings-body input[type="number"]::-webkit-inner-spin-button,
.settings-body input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.settings-body input[type="number"] {
    -moz-appearance: textfield;
}

/* ==========================================================================
   Chat Rooms Editor Modal
   ========================================================================== */

.cr-list {
    overflow-y: auto;
    padding: 12px 20px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-height: 60px;
    max-height: 50vh;
}

.cr-list-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-radius: 8px;
    border: 1px solid var(--border-color);
    background: var(--bg-primary);
}

.cr-list-item-name {
    flex: 1;
    font-weight: 600;
    font-size: 0.9rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cr-list-item-count {
    font-size: 0.75rem;
    color: var(--text-muted);
    flex-shrink: 0;
}

.cr-list-item-delete {
    background: transparent;
    border: none;
    color: var(--text-muted);
    font-size: 0.85rem;
    cursor: pointer;
    padding: 4px 8px;
    border-radius: 4px;
    transition: color 0.15s, background 0.15s;
    flex-shrink: 0;
}

.cr-list-item-delete:hover {
    color: #e74c3c;
    background: rgba(231, 76, 60, 0.15);
}

.cr-empty {
    color: var(--text-muted);
    font-size: 0.9rem;
    text-align: center;
    padding: 24px 0;
}

/* ==========================================================================
   Persona Picker Modal (for adding to chat rooms)
   ========================================================================== */

.pp-list {
    overflow-y: auto;
    padding: 12px 20px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-height: 60px;
    max-height: 50vh;
}

.pp-list-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border-radius: 8px;
    cursor: pointer;
    transition: background 0.15s;
    border: 1px solid transparent;
}

.pp-list-item:hover {
    background: var(--bg-tertiary);
}

.pp-list-item.selected {
    background: var(--bg-tertiary);
    border-color: var(--accent);
}

.pp-list-item-avatar {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.8rem;
    color: #fff;
    flex-shrink: 0;
}

.pp-list-item-info {
    flex: 1;
    min-width: 0;
}

.pp-list-item-name {
    font-weight: 600;
    font-size: 0.85rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.pp-list-item-desc {
    font-size: 0.72rem;
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Custom checkbox for persona picker */
.pp-checkbox {
    accent-color: var(--accent);
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    cursor: pointer;
}

.pp-empty {
    color: var(--text-muted);
    font-size: 0.9rem;
    text-align: center;
    padding: 24px 0;
}

/* ==========================================================================
   Message audio playback (persisted chat)
   ========================================================================== */

/* Wrapper for user messages — keeps bubble + audio stacked vertically
   despite the row using flex-direction: row-reverse. */
.user-message-content {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
}

.message-audio {
    display: flex;
    gap: 6px;
    margin-top: 6px;
    flex-wrap: wrap;
}

.audio-play-btn {
    background: var(--bg-tertiary);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    color: var(--text-secondary);
    font-size: 0.85rem;
    /* Roomier than the original 3px/8px: these now carry an icon plus a
       segment number, and the old box clipped the pair together. */
    padding: 4px 9px;
    cursor: pointer;
    transition: background 0.15s, color 0.15s, opacity 0.15s;
    line-height: 1.2;
    /* Tabular digits stop the row from jiggling as numbers change width. */
    font-variant-numeric: tabular-nums;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

/* Audible right now.
 *
 * This selector was missing entirely. `is-playing` is toggled on every chunk
 * button (setAudioButtonState), but the only rule for it was
 * `.icon-btn.is-playing` — a different class, used by the header controls. So
 * the button that was actually making sound looked exactly like the nine that
 * were not, and the state was carried solely by a small emoji.
 *
 * Same accent and pulse as the header control, so "this one is playing" reads
 * the same wherever it appears. */
.audio-play-btn.is-playing {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
    animation: audio-pulse 1.2s ease-in-out infinite;
}

/* Being synthesized: busy, but not audible — a slow spin rather than a pulse,
   so waiting and playing are never confused for one another. */
.audio-play-btn.is-processing .audio-btn-icon {
    display: inline-block;
    animation: audio-spin 1.4s linear infinite;
}

@keyframes audio-spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    /* Colour still distinguishes them; motion is the part people ask to
       switch off, and a pulsing row of buttons is exactly the kind of thing
       that setting exists for. */
    .audio-play-btn.is-playing,
    .audio-play-btn.is-processing .audio-btn-icon {
        animation: none;
    }
}

.audio-play-btn:hover {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
}

.audio-play-btn:active {
    transform: scale(0.95);
}

/* A failed reply. Kept inside the bubble so it stays attached to the persona
   that failed, but visually separated so it is not mistaken for their words. */
.bubble-error {
    margin-top: 6px;
    padding: 6px 10px;
    border-left: 3px solid #e5484d;
    border-radius: 4px;
    background: rgba(229, 72, 77, 0.12);
    color: #ff9ea1;
    font-size: 0.85rem;
    line-height: 1.35;
    white-space: normal;
}

/* No top margin when the error is the only thing in the bubble - otherwise a
   reply that produced no text at all renders with a blank gap above it. */
.bubble.has-error:empty .bubble-error,
.bubble.has-error > .bubble-error:first-child {
    margin-top: 0;
}

/* The header skip control while a clip is audible: it stops that clip, so
   it should read as active rather than as a passive "next" button. */
.icon-btn.is-playing {
    color: var(--accent);
    animation: audio-pulse 1.2s ease-in-out infinite;
}

/* The glyph spins on its own so the chunk number beside it stays upright. */
.audio-btn-icon {
    display: inline-block;
    line-height: 1;
}

.audio-play-btn.is-processing .audio-btn-icon {
    animation: mic-spin 1.1s linear infinite;
}

/* Synthesis in progress is work, not unavailability: keep it legible rather
   than dimming it to the same 0.5 as a merely-queued chunk. */
.audio-play-btn.is-processing,
.audio-play-btn.is-processing:disabled {
    opacity: 1;
    border-color: var(--accent);
    cursor: progress;
}

@media (prefers-reduced-motion: reduce) {
    .audio-play-btn.is-processing .audio-btn-icon {
        animation: none;
    }
}

/* A pending chunk is not clickable, and should not look it. */
.audio-play-btn:disabled {
    opacity: 0.5;
    cursor: default;
}

.audio-play-btn:disabled:hover {
    background: var(--bg-tertiary);
    color: var(--text-secondary);
    border-color: var(--border-color);
}

/* The chunk you are hearing right now, so it is obvious which button the
   skip applies to when several are lined up. */
.audio-play-btn.is-playing {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
    animation: audio-pulse 1.2s ease-in-out infinite;
}

@keyframes audio-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.65; }
}

/* --------------------------------------------------------------------------
   Mobile
   --------------------------------------------------------------------------
   The layout is a two-column desktop app: a fixed 310px sidebar beside the
   chat, and a top bar of eight labelled buttons. Neither survives a phone —
   the sidebar leaves almost nothing for the transcript, and the buttons wrap
   into several rows that eat the viewport.

   Below the breakpoint the sidebar becomes a slide-over drawer and the top
   bar scrolls sideways, so the chat keeps the whole screen.
   -------------------------------------------------------------------------- */

/* The drawer toggle is meaningless while the sidebar is a permanent column. */
#btn-sidebar {
    display: none;
}

@media (max-width: 820px) {
    #btn-sidebar {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        background: var(--bg-tertiary);
        color: var(--text-primary);
        border: 1px solid var(--border-color);
        border-radius: 6px;
        cursor: pointer;
        flex-shrink: 0;
    }

    #topbar {
        gap: 10px;
        padding: 0 10px;
        /* Clear of the notch in landscape, where the inset is on the sides. */
        padding-left: max(10px, env(safe-area-inset-left));
        padding-right: max(10px, env(safe-area-inset-right));
    }

    /* The title is the first thing worth sacrificing: the drawer button and
       the audio controls both have to stay reachable. */
    #topbar h1 {
        font-size: 1rem;
        flex-shrink: 0;
    }

    /*
     * Scroll the actions sideways rather than wrapping them. Wrapping grew
     * the bar to three rows and pushed the composer off the bottom of the
     * screen - the overflow that made the app unusable in Safari.
     */
    .topbar-actions {
        flex: 1;
        min-width: 0;
        overflow-x: auto;
        overflow-y: hidden;
        /*
         * flex-start, not flex-end. With the items right-aligned, anything
         * that overflows spills off the LEFT edge, and an overflow container
         * cannot be scrolled backwards past its start - so the first buttons
         * became permanently unreachable instead of merely off-screen.
         */
        justify-content: flex-start;
        gap: 6px;
        padding: 6px 0;
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
    }

    .topbar-actions::-webkit-scrollbar {
        display: none;
    }

    /*
     * The word "Theme" costs more room than it earns, but hiding it outright
     * left a bare dropdown as the only control in the bar with no glyph.
     * Swap the text for an icon instead: the <label for> association survives,
     * so tapping it still focuses the select.
     */
    .theme-label {
        font-size: 0;
        flex-shrink: 0;
        cursor: pointer;
        line-height: 1;
    }

    .theme-label::before {
        content: "\1F3A8";  /* 🎨 */
        font-size: 1.05rem;
    }

    /* Just wide enough for the colour name, matching the icon buttons. */
    #theme-select {
        flex-shrink: 0;
        max-width: 92px;
        padding: 7px 6px;
        font-size: 0.78rem;
    }

    /*
     * Icons only. Eight labelled buttons need roughly 700px of bar; the
     * glyphs alone fit a phone without scrolling, and every button keeps its
     * title and aria-label so the meaning is not lost.
     */
    .btn-label {
        display: none;
    }

    .topbar-actions button {
        flex-shrink: 0;
        font-size: 1.05rem;
        line-height: 1;
        padding: 8px 9px;
        min-width: 38px;
    }

    /*
     * The drawer. Fixed rather than absolute so it is measured against the
     * viewport, not against #main-layout - which is itself shorter than the
     * screen on iOS while the toolbar is showing.
     */
    #sidebar {
        position: fixed;
        top: var(--topbar-height);
        left: 0;
        bottom: 0;
        z-index: 30;
        width: min(300px, 85vw);
        min-width: 0;
        transform: translateX(-100%);
        transition: transform 0.22s ease;
        box-shadow: 2px 0 16px rgba(0, 0, 0, 0.45);
        padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    }

    #sidebar.open {
        transform: translateX(0);
    }

    #sidebar-backdrop {
        position: fixed;
        top: var(--topbar-height);
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 20;
        background: rgba(0, 0, 0, 0.5);
    }

    /* A bubble capped at 80% of a phone screen wastes the margin it leaves. */
    .message-row {
        max-width: 94%;
    }

    #messages {
        padding: 14px 12px;
        gap: 10px;
    }

    #input-bar {
        padding: 10px 12px;
        padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
        gap: 8px;
    }

    /*
     * 16px exactly. Safari zooms the whole page in when a focused input has
     * text smaller than that, and it does not zoom back out on blur - so a
     * single tap on the composer left the app permanently magnified and
     * scrolled sideways.
     */
    #message-input {
        font-size: 16px;
    }

    /* Comfortable touch targets: 40px is below the ~44px both platforms
       recommend, and these sit next to each other. */
    #btn-mic,
    #btn-send {
        width: 44px;
        height: 44px;
        flex-shrink: 0;
    }

    /* Modals were sized for a desktop window; on a phone they should simply
       take the screen. */
    .modal-overlay {
        padding: 0;
    }

    .modal-box,
    .modal-box-sm {
        max-width: 100%;
        width: 100%;
        height: 100%;
        max-height: 100%;
        border-radius: 0;
        border: none;
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }
}

@media (max-width: 480px) {
    #topbar h1 {
        /* Below this width the title is competing with the controls for
           space it cannot win. */
        display: none;
    }
}

/* --------------------------------------------------------------------------
   Message actions (resend, retry)
   -------------------------------------------------------------------------- */
.message-actions {
    display: flex;
    justify-content: flex-end;
    gap: 6px;
    margin-top: 4px;
}

/*
 * Deliberately faint until the message is hovered: these sit on every user
 * bubble, and at full strength a long transcript reads as a column of
 * buttons with some text beside them.
 */
.msg-action-btn {
    background: transparent;
    color: var(--text-muted);
    border: 1px solid transparent;
    border-radius: 6px;
    padding: 2px 7px;
    font-size: 0.8rem;
    line-height: 1.3;
    cursor: pointer;
    opacity: 0.45;
    transition: opacity 0.15s, color 0.15s, border-color 0.15s;
}

.message-row:hover .msg-action-btn,
.msg-action-btn:focus-visible {
    opacity: 1;
    color: var(--text-secondary);
    border-color: var(--border-color);
}

.msg-action-btn:hover {
    opacity: 1;
    color: var(--accent);
    border-color: var(--accent);
}

/* On a touch screen there is no hover to reveal them with. */
@media (hover: none) {
    .msg-action-btn {
        opacity: 0.75;
        border-color: var(--border-color);
    }
}

/* The retry inside an error bubble is the point of that bubble, so it is
   never faint. */
.bubble-retry {
    opacity: 1;
    margin-top: 6px;
    color: #ff6b6b;
    border-color: rgba(255, 107, 107, 0.5);
}

/* --------------------------------------------------------------------------
   Composer buttons
   --------------------------------------------------------------------------
   One rule for all three, because they were three different sizes: mic and
   send were 40px squares set by their own ID rules, while the attach button
   inherited .icon-btn's padding and grew to whatever its glyph needed. The
   icons are SVG in currentColor, so they take the theme's text colour instead
   of an emoji's fixed palette.
   -------------------------------------------------------------------------- */
.composer-btn {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Transparent, not a filled tile: these sit beside the input, and three
       solid blocks read as heavier than the field they belong to. */
    background: transparent;
    color: var(--text-secondary);
    border: 1px solid var(--border-color);
    border-radius: 8px;
    padding: 0;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.composer-btn:hover:not(:disabled) {
    color: var(--accent);
    border-color: var(--accent);
    background: transparent;
}

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

/* Send is the primary action, so it keeps its filled treatment - but at
   exactly the same size and alignment as its neighbours. */
.composer-btn.is-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

.composer-btn.is-primary:hover:not(:disabled) {
    background: var(--accent-hover);
    border-color: var(--accent-hover);
    color: #fff;
}

.composer-btn svg {
    display: block;
}

/* Recording and transcribing both act on the icon, never the button, so the
   border and background stay put while the glyph animates. */
#btn-mic.recording {
    color: var(--accent);
    border-color: var(--accent);
}

#btn-mic.recording .mic-icon {
    animation: mic-pulse 1s infinite ease-in-out;
}

#btn-mic.transcribing {
    color: var(--accent);
    border-color: var(--accent);
    opacity: 1;
    cursor: progress;
}

/* The glyph spins, not the button: rotating the button drags its border
   round with it. This was lost when the mic moved from emoji to SVG. */
#btn-mic.transcribing .mic-icon {
    animation: mic-spin 1.1s linear infinite;
}

#btn-image.transcribing {
    color: var(--accent);
    border-color: var(--accent);
    opacity: 1;
    cursor: progress;
}

#btn-image.transcribing svg {
    animation: mic-spin 1.1s linear infinite;
}

.mic-icon {
    display: inline-flex;
}
