/* ============================================================
   Hazy operator UI.
   Mobile-first; theming via CSS custom properties on :root.

   Theme modes:
     - <html> with no data-theme attribute → light by default,
       dark when prefers-color-scheme: dark
     - <html data-theme="light"> → forced light
     - <html data-theme="dark">  → forced dark

   Visual language is slate-blue ink + magenta accent gradient with
   a colored glow on emphasized actions. Inspired by portal.zonventure.com.
   ============================================================ */

/* Author rules like `.field { display: flex }` would otherwise beat the
   UA `[hidden] { display: none }`, leaving JS-toggled fields visible. */
[hidden] { display: none !important; }

:root {
    /* Brand — pulled from the favicon's bottom-left teal where the
       infinity loop reads most green. --accent is the readable
       dark-teal that anchors buttons + links; --accent-2 shifts
       toward seafoam so the grad-primary sweep (brand wordmark, card
       stripe) lands as "teal → green" rather than a flat hue. */
    --accent:       #168a8e;
    --accent-2:     #1f9e7c;
    --accent-deep:  #0c5c5f;

    /* Ink (text) — hazy-web's --ink / --ink-muted; soft + faint are
       interpolated points the SPA needs for layered captions. */
    --ink:          #1a1f2c;
    --ink-soft:     #3d4456;
    --muted:        #5a6478;
    --faint:        #8590a3;

    /* Surfaces — bg + surface match hazy-web; -2 / -3 are interior
       layers the SPA stacks for popovers and pressed states. */
    --bg:           #fbfbfd;
    --surface:      #ffffff;
    --surface-2:    #f4f5f9;
    --surface-3:    #eaedf3;

    /* Borders */
    --border:       #e2e6ee;
    --border-strong:#c2c9d4;

    /* Status palette — chosen to harmonize with the slate ink + magenta */
    --status-pending:   #6b7f96;
    --status-running:   #2f6fea;
    --status-completed: #1f8a3d;
    --status-failed:    #d23a3a;
    --status-on-hold:   #b07b00;

    /* Semantic aliases used by destructive/success affordances (delete
       swipe, retry buttons, etc). Resolved against --status-* so the
       theme overrides for those flow through automatically. */
    --danger:  var(--status-failed);
    --success: var(--status-completed);

    /* Streams */
    --stdout:       var(--ink);
    --stderr:       #c5471f;

    /* Shadows — barely-there. Layer separation is carried by
       borders + surface contrast; shadow only steps in when
       something genuinely floats above its peers (popovers, modal
       dialogs). Decorative button glows are gone — hover states
       lean on color shift instead.
       RGB tracks the light --ink so the cast stays in-family. */
    --shadow:       0 1px 2px rgba(26,31,44,.04);
    --shadow-lg:    0 2px 8px rgba(26,31,44,.06);
    --shadow-pop:   0 4px 14px rgba(26,31,44,.10);
    --glow-accent:  none;

    /* Gradients */
    --grad-primary: linear-gradient(135deg, var(--accent), var(--accent-2));
    --grad-active:  linear-gradient(90deg,
                       color-mix(in srgb, var(--accent) 12%, transparent),
                       color-mix(in srgb, var(--accent) 0%, transparent));

    /* Geometry — radius bumped to 12px to match hazy-web's card +
       button rounding. Small radius stays at 8px for chips and
       compact controls where 12px would feel mushy. */
    --radius:       12px;
    --radius-sm:    8px;
    --radius-pill:  999px;
    --header-h:     56px;
    --aside-w:      280px;

    color-scheme: light;
}

/* Dark via system preference (only when no explicit choice). */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --accent:       #4ec5cf;
        --accent-2:     #65d5a6;
        --accent-deep:  #2a8d92;

        --ink:          #e9ecf3;
        --ink-soft:     #c5cee0;
        --muted:        #95a0b5;
        --faint:        #6f7d96;

        --bg:           #0e1622;
        --surface:      #14202e;
        --surface-2:    #1a2740;
        --surface-3:    #213050;

        --border:       #233347;
        --border-strong:#354a6d;

        --status-running:   #5b9eff;
        --status-completed: #4ec96d;
        --status-failed:    #ff6464;
        --status-on-hold:   #e0a93a;

        --stderr:       #ff9a6a;

        --shadow:       0 1px 2px rgba(0,0,0,.25);
        --shadow-lg:    0 2px 8px rgba(0,0,0,.35);
        --shadow-pop:   0 4px 14px rgba(0,0,0,.5);
        --glow-accent:  none;

        color-scheme: dark;
    }
}
/* Forced dark (explicit user choice). */
:root[data-theme="dark"] {
    --accent:       #4ec5cf;
    --accent-2:     #65d5a6;
    --accent-deep:  #2a8d92;
    --ink:          #e9ecf3;
    --ink-soft:     #c5cee0;
    --muted:        #95a0b5;
    --faint:        #6f7d96;
    --bg:           #0e1622;
    --surface:      #14202e;
    --surface-2:    #1a2740;
    --surface-3:    #213050;
    --border:       #233347;
    --border-strong:#354a6d;
    --status-running:   #5b9eff;
    --status-completed: #4ec96d;
    --status-failed:    #ff6464;
    --status-on-hold:   #e0a93a;
    --stderr:       #ff9a6a;
    --shadow:       0 1px 2px rgba(0,0,0,.25);
    --shadow-lg:    0 2px 8px rgba(0,0,0,.35);
    --shadow-pop:   0 4px 14px rgba(0,0,0,.5);
    --glow-accent:  none;
    color-scheme: dark;
}

/* ============================================================ */

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; height: 100%; }

body {
    background: var(--bg);
    color: var(--ink);
    /* System-font stack matches the hazy-web marketing pages — no
       Open Sans web-font dependency, identical rendering on every
       OS. Antialias + optimizeLegibility lift small UI text without
       reaching for a custom font. */
    font: 14.5px/1.55 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    display: grid;
    grid-template-rows: var(--header-h) 1fr;
    grid-template-columns: 1fr;
    min-height: 100dvh;
}

/* Pre-auth (sign in, signup, forgot, reset, TOTP step, fresh-install
   setup) replaces the app-slate background with the card's surface
   tone so the page reads as a single neutral sheet, and drops the
   app header entirely — the operator-app wordmark + chrome have no
   business framing a sign-in form. The grid's first track collapses
   to 0 so the auth card centers naturally in the viewport instead
   of carrying a 56px ghost gap above it. */
body.signed-out {
    background: var(--surface);
    grid-template-rows: 0 1fr;
}
body.signed-out .app-header { display: none; }

a { color: var(--accent); text-decoration: none; font-weight: 600; }
a:hover { text-decoration: underline; }

::selection { background: color-mix(in srgb, var(--accent) 30%, transparent); }

/* Focus ring everywhere — accessible and on-brand. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 4px;
}

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

.app-header {
    grid-row: 1;
    /* Span every column at every breakpoint. Without this, on the
       multi-column desktop grid (≥960px) the header would land only in
       the first cell and leave the rest of row 1 empty. */
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: 0 .9rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
    z-index: 30;
}
/* Wordmark — matches the hazy-web topbar treatment: favicon
   thumbnail + bold "Hazy" in solid ink, no gradient fill. The
   grad-primary is now reserved for the card stripe (.card::before)
   only — a thin decorative edge, not a brand spread. */
.brand {
    margin: 0;
    display: inline-flex;
    align-items: center;
    gap: .55rem;
    color: var(--ink);
    font-size: 1.15rem;
    font-weight: 700;
    letter-spacing: -.005em;
}
.brand img {
    width: 28px;
    height: 28px;
}
.spacer { flex: 1; }

/* Top-right account menu. The kebab button opens a small popup list
   with the actions that used to clutter the drawer foot (Settings,
   Sign out). Anchored to the trigger via the .account-menu wrapper so
   the menu can use absolute positioning relative to the icon without
   leaking out of the header's stacking context. */
.account-menu { position: relative; }
.account-menu__trigger { color: var(--muted); }
.account-menu__trigger[aria-expanded="true"] {
    background: var(--surface-2);
    color: var(--ink);
}
.account-menu__list {
    position: absolute;
    top: calc(100% + .35rem);
    right: 0;
    min-width: 168px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-pop);
    padding: .25rem;
    display: flex;
    flex-direction: column;
    gap: .1rem;
    z-index: 40;
}
.account-menu__item {
    display: inline-flex;
    align-items: center;
    gap: .55rem;
    background: transparent;
    border: none;
    color: var(--ink);
    font: inherit;
    font-size: .88rem;
    padding: .55rem .7rem;
    border-radius: 6px;
    cursor: pointer;
    text-align: left;
}
.account-menu__item:hover,
.account-menu__item:focus-visible {
    background: var(--surface-2);
    outline: none;
}
.account-menu__item svg { color: var(--muted); flex: 0 0 auto; }
.account-menu__item:hover svg { color: var(--accent); }

/* ---------- Theme picker (segmented) ---------- */

.theme-picker {
    display: inline-flex;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 2px;
}
.theme-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border: none;
    background: transparent;
    color: var(--muted);
    border-radius: 6px;
    cursor: pointer;
    transition: background .12s, color .12s, transform .12s;
}
.theme-btn:hover { color: var(--ink); }
.theme-btn[aria-pressed="true"] {
    background: var(--surface);
    color: var(--accent);
    box-shadow: var(--shadow);
}

/* ---------- Buttons ---------- */

.icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 44px is the iOS HIG / Material minimum tap target. The SVG inside
       stays its declared size (typically 16-20px); the button just gets
       enough hit area around it. */
    width: 44px;
    height: 44px;
    border: 1px solid transparent;
    background: transparent;
    color: var(--muted);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background .12s, color .12s, border-color .12s, transform .12s;
}
.icon-btn:hover {
    background: var(--surface-2);
    color: var(--accent);
}
.icon-btn:active { transform: scale(.96); }
.icon-btn:disabled {
    opacity: .35;
    cursor: not-allowed;
    color: var(--faint);
}
.icon-btn:disabled:hover { background: transparent; color: var(--faint); }

.text-btn {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--ink);
    border-radius: var(--radius-sm);
    padding: .65rem 1rem;
    min-height: 44px;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    transition: border-color .12s, color .12s, background .12s;
}
.text-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--surface-2); }

/* Primary button — flat accent pill in the hazy-web vocabulary: no
   gradient, no glow, just a solid fill that lifts to a subtle hover
   ring. The previous accent-glow shadow lived to balance a vivid
   cerise; the new teal accent + opaque ink-shadow combo reads
   cleaner on its own. */
button.primary {
    background: var(--accent);
    color: #fff;
    border: none;
    border-radius: var(--radius-pill);
    padding: .85rem 1.5rem;
    min-height: 48px;
    font: inherit;
    font-weight: 600;
    letter-spacing: .005em;
    cursor: pointer;
    transition: transform .12s, box-shadow .12s, background .12s, opacity .12s;
}
button.primary:hover {
    background: var(--accent-deep);
}
button.primary:active { opacity: .85; }
/* Destructive primary — used for confirmAction({ danger: true }) so the
   "Delete" button reads as the consequence it carries. Keeps the same
   geometry/min-height as the regular .primary so dialogs don't shift.
   Hover is a tone shift on the same fill — no halo. */
button.primary--danger {
    background: var(--danger);
}
button.primary--danger:hover {
    background: color-mix(in srgb, var(--danger) 85%, var(--ink));
}
/* Submit-in-flight state set by wireDialog while onSubmit's promise is
   pending. Faded + cursor + faint pulse so the operator sees the click
   was received and isn't tempted to mash the button. */
button.primary.is-busy {
    opacity: .75;
    cursor: progress;
    animation: busy-pulse 1.2s ease-in-out infinite;
}
@keyframes busy-pulse {
    0%, 100% { opacity: .75; }
    50%      { opacity: 1; }
}

/* ---------- Login card ---------- */

.card {
    grid-row: 2;
    /* Span every body grid column so the card stays viewport-centred
       even after the desktop 3-column layout kicks in. Without this it
       would land in column 1 only (≈280px) and feel cramped. */
    grid-column: 1 / -1;
    align-self: start;
    justify-self: center;
    width: min(440px, calc(100% - 2rem));
    margin-top: clamp(2rem, 8vh, 5rem);
    padding: 1.75rem 1.75rem 1.5rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
    position: relative;
    overflow: hidden;
}
/* A thin gradient stripe at the top of the card pulls the brand color in
   without filling the whole surface. */
.card::before {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 3px;
    background: var(--grad-primary);
}
.card h2 {
    margin: 0 0 1.2rem;
    font-size: 1.2rem;
    font-weight: 700;
    color: var(--ink);
}
.setup-blurb {
    color: var(--muted);
    font-size: .85rem;
    margin: -.6rem 0 1.1rem;
    line-height: 1.5;
}
/* All auth-card forms (login + setup + signup + forgot + reset +
   TOTP step) share the same visual: stacked uppercase label /
   surface-2 input / accent-focused border. Selector list rather
   than a shared class because the markup already commits to ID-
   based form-name routing in the JS, and each form has slightly
   different submit semantics — same look, different wiring. */
#login-form,
#setup-form,
#signup-form,
#forgot-form,
#reset-form,
#totp-step-form { display: flex; flex-direction: column; gap: .9rem; }
#login-form label,
#setup-form label,
#signup-form label,
#forgot-form label,
#reset-form label,
#totp-step-form label {
    display: flex;
    flex-direction: column;
    gap: .3rem;
    color: var(--muted);
    font-size: .8rem;
    font-weight: 600;
    letter-spacing: .02em;
    text-transform: uppercase;
}
#login-form input,
#setup-form input,
#signup-form input,
#forgot-form input,
#reset-form input,
#totp-step-form input {
    background: var(--surface-2);
    color: var(--ink);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: .65rem .75rem;
    font: 14.5px inherit;
    font-weight: 400;
    letter-spacing: normal;
    text-transform: none;
    transition: border-color .12s, box-shadow .12s, background .12s;
}
#login-form input:focus,
#setup-form input:focus,
#signup-form input:focus,
#forgot-form input:focus,
#reset-form input:focus,
#totp-step-form input:focus {
    outline: none;
    background: var(--surface);
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
#login-form button.primary,
#setup-form button.primary,
#signup-form button.primary,
#forgot-form button.primary,
#reset-form button.primary,
#totp-step-form button.primary { margin-top: .25rem; }
.error { color: var(--status-failed); margin: 0; min-height: 1.2em; font-size: .85rem; }

/* ---------- App layout ---------- */

#main, .drawer { grid-row: 2; }

#main {
    grid-column: 1;
    display: block;
    min-height: 0;
    overflow: hidden;
    /* Reserve space for the collapsed log drawer at the bottom so the
       last task isn't obscured by the drawer header. Matches
       .section-head height. */
    padding-bottom: 44px;
}

.pane {
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
    background: var(--bg);
}
.tasks-pane {
    height: 100%;
}
.logs-pane { background: var(--surface-2); }

/* ---------- Logs drawer ---------- */
/* Fixed-bottom drawer. Collapsed = section-head only. Open = ~65vh,
   capped so the drawer never reaches all the way to the top. */
.logs-pane {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 5;
    height: 44px;
    max-height: 100vh;
    /* No upward halo — the border-top alone marks the divider. */
    border-top: 1px solid var(--border);
    transition: height .28s cubic-bezier(.2,.8,.2,1);
}
/* Mobile (<960px): the drawer is fixed to the bottom of the viewport;
   when opened it stretches all the way up to (but not under) the app
   header so the operator gets the full screen for log inspection.
   dvh handles iOS' shrinking URL bar — viewport-height units would
   leave a sliver of tasks visible when the URL bar collapses. */
.logs-pane.open { height: calc(100dvh - var(--header-h)); }

/* Logs-pane header. .logs-head replaces the bare-button .section-head
   because we now host a sibling Clear button next to the toggle, and
   nesting a <button> inside a <button> is invalid HTML. The container
   keeps the section-head's border/bg; the toggle button carries its
   own padding so the click target reaches the bar's left edge. */
.logs-head {
    padding: 0;
    gap: 0;
}
.logs-toggle {
    /* Reset the UA chrome so it looks like the other section-head bars. */
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .6rem;
    padding: .65rem .9rem;
    border: none;
    background: transparent;
    cursor: pointer;
    text-align: left;
    color: inherit;
    font: inherit;
    min-height: 44px;
}
.logs-toggle:hover { background: color-mix(in srgb, var(--accent) 6%, var(--surface)); }
.logs-toggle:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}
.logs-clear { flex: 0 0 auto; margin-right: .25rem; }
.logs-head-text {
    display: inline-flex;
    align-items: center;
    gap: .6rem;
    min-width: 0;
}
.logs-chevron {
    color: var(--muted);
    transition: transform .25s ease;
    flex: 0 0 auto;
}

/* Pending-approval count, shown on the logs-toggle header so the operator
   sees "claude is waiting" even when the drawer is collapsed. Pulses to
   draw the eye. */
.approvals-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 22px;
    padding: 0 6px;
    border-radius: 11px;
    background: var(--accent);
    color: #fff;
    font-size: .72rem;
    font-weight: 700;
    line-height: 1;
    margin-left: .15rem;
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 50%, transparent);
    animation: approvals-pulse 1.6s ease-out infinite;
}
@keyframes approvals-pulse {
    0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 50%, transparent); }
    70%  { box-shadow: 0 0 0 8px color-mix(in srgb, var(--accent) 0%,  transparent); }
    100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%,  transparent); }
}
.logs-pane.open .logs-chevron { transform: rotate(180deg); }

.logs-pane-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    display: flex;
    flex-direction: column;
}

/* Discussion thread — per-task comments. Lives in its own dialog
   (#comments-dialog) reached via the chat icon on each task row,
   not in the logs pane. Operators get a first-class surface
   instead of a buried section below the chain log. */
.comments-dialog-form .dialog-body {
    min-height: 220px;
}
.comments-head {
    margin: 0 0 .5rem;
    font-size: .8rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
    font-weight: 600;
}
/* Chat icon button on each task row. Sits with the other row
   actions; opens the per-task discussion dialog. Small count
   badge appears when there are known comments (loaded once the
   operator has opened the dialog for that task). */
.task-actions .chat-btn {
    position: relative;
    /* Quiet at rest — the chat icon is a per-row affordance, not
       a status signal; the row's name + status pill carry the
       primary information. Operators get a darker tone on hover
       (inherited from .task-action:hover) when they're actually
       reaching for the button. */
    color: var(--faint, var(--muted));
    opacity: .7;
}
.task-actions .chat-btn:hover { opacity: 1; }
/* Count badge: subtle pill that reads as informational rather than
   an action. Same tone the existing `.opt` and quiet labels use,
   so it sits in the visual hierarchy below the task name. */
.chat-badge {
    position: absolute;
    top: -.2rem;
    right: -.3rem;
    min-width: 1rem;
    height: 1rem;
    padding: 0 .25rem;
    box-sizing: border-box;
    border-radius: .5rem;
    background: var(--surface-2, #eee);
    color: var(--muted);
    border: 1px solid var(--border, #ddd);
    font-size: .62rem;
    font-weight: 600;
    line-height: calc(1rem - 2px);
    text-align: center;
}
.task-actions .chat-btn.has-comments { opacity: .85; }
/* Alert variant: takes the comment-count badge's slot when an MCP
   prompt is open for the task. Red + gentle pulse so the operator
   immediately spots which row is blocking the chain. Intentionally
   louder than the count badge — this is the only "you need to act"
   surface in the chain list. */
.chat-badge--alert {
    background: var(--status-failed, #dc2626);
    color: #fff;
    border-color: transparent;
    animation: chat-badge-pulse 1.6s ease-out infinite;
}
@keyframes chat-badge-pulse {
    0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-failed, #dc2626) 60%, transparent); }
    70%  { box-shadow: 0 0 0 .35rem color-mix(in srgb, var(--status-failed, #dc2626) 0%, transparent); }
    100% { box-shadow: 0 0 0 0 transparent; }
}
.task-actions .chat-btn.needs-input {
    color: var(--status-failed, #dc2626);
    opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
    .chat-badge--alert { animation: none; }
}
.comments-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: .55rem;
}
/* Chat bubbles. Own posts float right, everyone else (other
   operators + agent turns) float left — classic messenger
   layout. Bubbles tag-align with a sharper corner on the side
   that touches the row's edge so the speech-bubble origin reads
   correctly. */
.comment-row {
    align-self: flex-start;
    max-width: 80%;
    background: var(--surface, transparent);
    border: 1px solid var(--border, #ddd);
    border-radius: 1rem 1rem 1rem .25rem;
    padding: .55rem .8rem;
    font-size: .9rem;
}
.comment-row--mine {
    align-self: flex-end;
    background: color-mix(in srgb, var(--accent, currentColor) 14%, var(--surface, transparent));
    border-color: color-mix(in srgb, var(--accent, currentColor) 35%, var(--border, #ddd));
    border-radius: 1rem 1rem .25rem 1rem;
}
.comment-row--agent {
    background: color-mix(in srgb, var(--accent, currentColor) 6%, var(--surface, transparent));
    border-color: color-mix(in srgb, var(--accent, currentColor) 25%, var(--border, #ddd));
}
/* Bubble-in animation for freshly-arrived messages. Fade + a small
   upward slide + a touch of scale so the row reads as "popping
   into the conversation" rather than just appearing. The easing is
   the standard "ease-out expo"-ish curve — quick at the start,
   gentle landing. `both` keeps the from-state for any sliver of
   delay between class add and the first frame. */
@keyframes comment-bubble-in {
    from {
        opacity: 0;
        transform: translateY(10px) scale(.96);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}
.comment-row--new {
    animation: comment-bubble-in .28s cubic-bezier(.16, 1, .3, 1) both;
}
@media (prefers-reduced-motion: reduce) {
    .comment-row--new { animation: none; }
}
.comment-plugin {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.1rem;
    height: 1.1rem;
    margin-right: .15rem;
}
.comment-plugin svg {
    width: 1rem;
    height: 1rem;
}
.comment-head {
    display: flex;
    align-items: baseline;
    gap: .6rem;
    margin-bottom: .15rem;
}
.comment-author {
    font-weight: 600;
    font-size: .85rem;
}
.comment-time {
    color: var(--faint, var(--muted));
    font-size: .75rem;
}
.comment-body {
    white-space: pre-wrap;
    word-break: break-word;
    line-height: 1.45;
}
.comment-form {
    margin-top: .8rem;
    display: flex;
    flex-direction: column;
    gap: .35rem;
}
.comment-form textarea {
    width: 100%;
    box-sizing: border-box;
    font: inherit;
    padding: .5rem .65rem;
    border-radius: .35rem;
    border: 1px solid var(--border, #ddd);
    background: var(--surface-2, transparent);
    resize: vertical;
}
.comment-form-foot {
    display: flex;
    align-items: center;
    gap: .75rem;
}
.comment-form-err {
    color: var(--status-failed);
    font-size: .8rem;
}
/* Reply composer sits right under the human-discussion composer
   on continue-capable tasks (claude / future agents). Visually
   separated by a thin top border so the operator sees two
   distinct affordances: "talk to teammates" vs "talk to the
   agent". Same submit-on-Enter pattern, with Ctrl/Cmd+Enter
   here since the reply textarea is taller. */
.reply-form {
    margin-top: 1.1rem;
    padding-top: .9rem;
    border-top: 1px dashed var(--border, #ddd);
}
.reply-form .reply-head {
    margin: 0 0 .35rem;
}

@media (min-width: 768px) {
    .tasks-pane { border-bottom: none; }
}

/* ≥960px: classic email-client layout — projects nav on the left,
   tasks list filling the upper-right, logs/preview pinned to the
   lower-right. All three regions persistent. The threshold (~aside
   280 + 1fr min) covers Android Firefox/Chrome "Request Desktop
   Site" mode and small laptops. Below it the drawer is offcanvas and
   the logs pane is the bottom drawer. */
@media (min-width: 960px) {
    :root { --logs-h: clamp(220px, 40dvh, 420px); }
    body { grid-template-columns: var(--aside-w) 1fr; }

    /* Drawer becomes a static left column. */
    .drawer {
        position: static !important;
        transform: none !important;
        grid-column: 1;
        grid-row: 2;
        display: flex !important;
        height: auto !important;
        width: auto !important;
        background: var(--surface-2);
    }
    #menu-btn { display: none; }

    /* Right side: tasks fill the upper part, logs occupy a fixed slice
       at the bottom. Use a flex column so logs-pane (flex 0) carves
       its --logs-h and tasks-pane (flex 1) fills the rest. */
    #main {
        grid-column: 2;
        display: flex;
        flex-direction: column;
        padding-bottom: 0;
    }
    .tasks-pane {
        flex: 1 1 0;
        min-height: 0;
        height: auto;
    }

    /* Logs becomes a static lower pane below the tasks list — drop the
       fixed-bottom rule from the smaller-screen styles. The collapsed
       state keeps only the toggle bar visible so the operator can hide
       logs and reclaim that vertical space; the .open class restores
       the configured --logs-h slice. The header itself remains the
       click-target for the toggle, same as on mobile. */
    .logs-pane {
        position: static;
        flex: 0 0 auto;
        min-height: 0;
        max-height: none;
        height: 44px;
        left: auto;
        right: auto;
        bottom: auto;
        border-top: 1px solid var(--border);
        border-left: none;
        box-shadow: none;
        z-index: 1;
    }
    .logs-pane.open { height: var(--logs-h); }
    .logs-pane:not(.open) .logs-pane-body { display: none; }

    /* FAB sits above the logs pane in the lower-right of the tasks
       region, with comfortable clearance from the divider. When logs
       is collapsed the pane is just the header bar (~44px), so the
       FAB sits much closer to the bottom edge. */
    .fab {
        right: 1rem;
        bottom: calc(var(--logs-h) + 1rem + env(safe-area-inset-bottom));
    }
    body:has(.logs-pane:not(.open)) .fab {
        bottom: calc(44px + 1rem + env(safe-area-inset-bottom));
    }
}

/* ---------- Drawer (mobile + tablet overlay) ---------- */

.drawer {
    position: fixed;
    top: var(--header-h);
    left: 0;
    width: var(--aside-w);
    height: calc(100dvh - var(--header-h));
    background: var(--surface-2);
    border-right: 1px solid var(--border);
    transform: translateX(-100%);
    transition: transform .2s cubic-bezier(.2,.7,.2,1);
    z-index: 20;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    box-shadow: var(--shadow-lg);
}
.drawer.open { transform: translateX(0); }

/* Empty-state CTA in the projects drawer when state.projects is []. */
.drawer-empty {
    margin: .5rem 1rem 1rem;
    padding: 1rem;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    color: var(--muted);
    font-size: .9rem;
    line-height: 1.5;
    text-align: center;
}
.drawer-empty strong {
    color: var(--accent);
    font-weight: 700;
}

/* Drawer footer — pinned below the projects list. Holds identity + theme
   picker + sign-out so they're tucked away in the same place users open
   to switch projects, instead of taking up header real-estate. */
.drawer-foot {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: .35rem;
    padding: .55rem .55rem max(.55rem, env(safe-area-inset-bottom));
    border-top: 1px solid var(--border);
    background: var(--surface-2);
}

#backdrop {
    position: fixed;
    inset: var(--header-h) 0 0 0;
    /* Plain fade — no blur. The slightly heavier alpha (.5 → .55)
       compensates for the dropped backdrop-filter so the underlying
       content still recedes visually. */
    background: rgba(20,32,46,.55);
    z-index: 15;
}

@media (min-width: 960px) {
    #backdrop { display: none !important; }
    .drawer { box-shadow: none; }
}

/* ---------- Section heads ---------- */

.section-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: .65rem .9rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
    min-height: 44px;
}
.section-head h3 {
    margin: 0;
    font-size: .72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .08em;
    color: var(--muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    min-width: 0;
}

/* Sub-section head: the "Shared with me" divider in the drawer below
   the owned-projects list. Same uppercase label treatment, lighter
   weight border so the eye reads it as a continuation, not a new
   pane. */
.section-head--sub {
    border-top: 1px solid var(--border);
    background: transparent;
    margin-top: .35rem;
}
/* Tasks-pane header: the project name is dynamic and often long, so
   the section-head gets a two-row layout — title on its own line as
   a proper heading, actions on the row below right-aligned. The
   single-row uppercase-tiny treatment of section-head h3 still
   applies to every other pane (drawer, Templates, Workers, etc.). */
.tasks-pane .section-head {
    flex-direction: column;
    align-items: stretch;
    gap: .35rem;
    padding-top: .75rem;
    padding-bottom: .55rem;
}
.tasks-pane .section-head h3#tasks-title {
    text-transform: none;
    letter-spacing: -.005em;
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--ink);
    width: 100%;
    white-space: normal;
    overflow-wrap: anywhere;
}
.tasks-pane .section-head .head-actions {
    align-self: flex-end;
}
/* Project icon next to the tasks-pane title. Shown only when the
   drawer is offcanvas (<960px) — above that breakpoint the drawer
   row already surfaces the icon so duplicating it here would be
   noise. */
.tasks-title-icon {
    display: none;
}
.tasks-title-name {
    min-width: 0;
    overflow-wrap: anywhere;
}
/* Owner-only: the title becomes a button that opens the project
   settings dialog. Strip all native button chrome so it reads as a
   heading; a subtle hover ink-shift tells the operator it's
   interactive. */
.tasks-title-link {
    appearance: none;
    background: transparent;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    min-width: 0;
    text-align: left;
}
.tasks-title-link:hover .tasks-title-name { color: var(--accent); }
.tasks-title-link:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: 4px;
}
.tasks-title-static {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    min-width: 0;
}
@media (max-width: 959px) {
    .tasks-title-icon {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 1.1em;
        height: 1.1em;
        color: var(--muted);
        flex: 0 0 auto;
    }
    .tasks-title-icon svg, .tasks-title-icon img {
        width: 100%;
        height: 100%;
        object-fit: contain;
        display: block;
        border-radius: 2px;
    }
}
.rename-row {
    display: flex;
    gap: .5rem;
    align-items: center;
}
.rename-row input {
    flex: 1;
    min-width: 0;
}
#rename-status.ok {
    color: var(--accent);
}
#icon-status.ok {
    color: var(--accent);
}

/* Icon picker in the project-settings dialog. The label wrapping the
   file input gets .text-btn so it renders like the Save / Remove
   buttons; the input itself stays hidden. */
.icon-row {
    display: flex;
    gap: .6rem;
    align-items: center;
}
.icon-preview {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: var(--radius-md, 6px);
    background: var(--surface-3);
    color: var(--muted);
    overflow: hidden;
}
.icon-preview svg, .icon-preview img {
    width: 32px;
    height: 32px;
    object-fit: contain;
    display: block;
    border-radius: 2px;
}
.icon-pick {
    /* The <label> wrapping the file input behaves like a button. */
    cursor: pointer;
}
.icon-pick input[type="file"] {
    /* Belt-and-braces — the hidden attr handles it, but in case
       browser quirks make it occupy space, neuter the layout. */
    position: absolute;
    width: 0;
    height: 0;
    opacity: 0;
    pointer-events: none;
}
#icon-remove:disabled {
    opacity: .4;
    cursor: not-allowed;
}

/* ---------- Lists nav ---------- */

.nav-list { list-style: none; margin: 0; padding: .35rem 0; overflow-y: auto; flex: 1; }
.nav-list li {
    /* Vertical padding pushes the row to a comfortable ~48px tap target
       even with the row's icons folded in. */
    padding: .85rem .9rem;
    min-height: 48px;
    cursor: pointer;
    border-left: 3px solid transparent;
    color: var(--ink);
    font-size: .95rem;
    font-weight: 500;
    user-select: none;
    transition: background .12s, color .12s, border-color .12s;
}
.nav-list li:hover { background: var(--surface-3); }
.nav-list li.active {
    background: var(--grad-active);
    border-left-color: var(--accent);
    color: var(--ink);
}

/* List status badge — pipeline aggregate visible in the drawer. */
.project-badge {
    display: inline-flex;
    align-items: center;
    margin-left: .35rem;
    padding: 1px 7px;
    font-size: .68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    border-radius: var(--radius-pill);
    border: 1px solid color-mix(in srgb, var(--muted) 50%, transparent);
    background: color-mix(in srgb, var(--muted) 10%, transparent);
    color: var(--muted);
    flex: 0 0 auto;
    white-space: nowrap;
}
.project-badge--done     {
    color: var(--status-completed);
    border-color: color-mix(in srgb, var(--status-completed) 50%, transparent);
    background: color-mix(in srgb, var(--status-completed) 10%, transparent);
}
.project-badge--running  {
    color: var(--status-running);
    border-color: color-mix(in srgb, var(--status-running) 50%, transparent);
    background: color-mix(in srgb, var(--status-running) 10%, transparent);
    animation: pulse-list 1.5s ease-in-out infinite;
}
.project-badge--failed   {
    color: var(--status-failed);
    border-color: color-mix(in srgb, var(--status-failed) 50%, transparent);
    background: color-mix(in srgb, var(--status-failed) 10%, transparent);
}
@keyframes pulse-list {
    0%, 100% { opacity: 1; }
    50%      { opacity: .6; }
}

/* Lists with rename/delete buttons that reveal on hover (or always on touch). */
.nav-list li.project-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
}
.project-name {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    flex: 1;
    overflow: hidden;
    min-width: 0;
}
.project-name__text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
/* Project icon in the drawer row. Sized to match plugin badges and
   the role tag — see PLUGIN_GLYPHS / plugin-icon-badge styles. */
.project-icon {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    color: var(--muted);
}
.project-icon svg, .project-icon img {
    width: 18px;
    height: 18px;
    display: block;
    object-fit: contain;
    border-radius: 2px;
}

/* Inline actions on a shared-with-me row. Hover-revealed on pointer
   devices so the row stays clean at rest; always-visible on touch
   where there's no hover signal. */
.project-row-actions {
    display: inline-flex;
    gap: .15rem;
    margin-left: auto;
    opacity: 0;
    transition: opacity .12s;
}
.nav-list li.project-row:hover .project-row-actions,
.nav-list li.project-row.active .project-row-actions { opacity: 1; }
@media (pointer: coarse) {
    .project-row-actions { opacity: 1; }
}
.project-row-actions .task-action { width: 28px; height: 28px; }
.project-row-actions .task-action.leave-btn:hover {
    color: var(--danger);
    border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}

/* ---------- Task list (sortable) ---------- */

.task-list {
    list-style: none;
    margin: 0;
    padding: .35rem 0;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    flex: 1 1 0;
    min-height: 0;
    background: var(--bg);
}
.task-list li {
    position: relative;
    overflow: hidden;
    cursor: pointer;
    border-left: 3px solid transparent;
}
.task-list li.active {
    border-left-color: var(--accent);
}

/* Swipeable foreground sits above the action background and owns the row's
   visible background. Active/hover style fg directly (not li) so the swipe
   bg behind never peeks through during a re-render. No transition: status
   changes redraw the whole li, transitions don't fire across replacements
   anyway and only added a frame-of-flicker. */
.task-list .task-fg {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: .65rem;
    /* Slimmer row: ~48px effective tap area (still meeting Material's
       48dp minimum) but visually denser so a long todo list isn't
       dominated by whitespace. */
    padding: .55rem .8rem;
    min-height: 48px;
    background: var(--bg);
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    touch-action: pan-y;
}
.task-list li:hover  .task-fg { background: var(--surface-2); }
.task-list li.active .task-fg { background: var(--grad-active); }

/* Action layer revealed by swipe. Direction class flips justification +
   color so the icon trails the user's finger. `committing` adds a subtle
   scale to confirm the threshold is crossed. */
.task-bg {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 1.1rem;
    color: white;
    font-weight: 600;
    font-size: .85rem;
    letter-spacing: .03em;
    background: var(--surface-2);
    pointer-events: none;
    z-index: 0;
}
.task-bg.right {
    /* swipe-right reveals run-once on the left edge — green */
    background: var(--success, #1f8a3d);
    justify-content: flex-start;
}
.task-bg.left {
    /* swipe-left reveals delete on the right edge — red */
    background: var(--danger);
    justify-content: flex-end;
}
.task-bg-icon {
    display: none;
    align-items: center;
    gap: .4rem;
    transition: transform .12s;
}
.task-bg.right .task-bg-icon--run    { display: inline-flex; }
.task-bg.left  .task-bg-icon--delete { display: inline-flex; }
.task-bg.committing .task-bg-icon { transform: scale(1.08); }
.task-list .drag-handle {
    flex: 0 0 auto;
    width: 28px;
    height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--faint);
    cursor: grab;
    touch-action: none;
    transition: color .12s;
}
.task-list .drag-handle svg { width: 16px; height: 20px; }
.task-list li:hover .drag-handle { color: var(--muted); }
.task-list .drag-handle:active { cursor: grabbing; color: var(--accent); }

.task-body {
    flex: 1;
    min-width: 0;
    display: grid;
    grid-template-columns: auto auto 1fr auto auto;
    align-items: center;
    column-gap: .55rem;
    row-gap: .25rem;
}

/* ---- Plugin icon (who's the assignee) ---- */

.task-plugin {
    grid-column: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 5px;
    color: var(--muted);
    flex: 0 0 auto;
    transition: color .12s, background .12s;
}
.task-plugin svg { width: 16px; height: 16px; border-radius: 2px; }
.task-plugin > svg { display: block; }
.task-plugin img { width: 16px; height: 16px; display: block; border-radius: 2px; }

/* Human tasks pop visually so the operator scans-and-spots them: a soft
   accent-tinted chip with the brand color. */
.task-plugin--human {
    color: var(--accent);
    background: color-mix(in srgb, var(--accent) 12%, transparent);
}
/* Agent rows stay neutral; per-plugin tweaks for the built-ins. */
/* Per-plugin colour rules used to live here for shell / git / claude.
   Dropped along with the per-plugin SVGs in pluginIcon — every
   non-human plugin now shares the neutral muted colour from
   .task-plugin above. Operators who want visual differentiation per
   plugin should use template colours, which carry per-task. */

/* Template badge — replaces the plugin SVG when a task carries an
   icon stamped from a template. Same grid slot + size as .task-plugin
   so the row geometry doesn't shift; emoji renders centered, with a
   colour-tinted tile when the template set --badge-color. */
.task-badge {
    --badge-color: var(--muted);
    grid-column: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 5px;
    background: color-mix(in srgb, var(--badge-color) 18%, var(--surface-2));
    color: var(--badge-color);
    flex: 0 0 auto;
    /* Slightly smaller than the tile so emojis don't crowd the edge. */
    font-size: 14px;
    line-height: 1;
    user-select: none;
}

/* ---- Checkbox (todo-style) ---- */

/* Stable checkbox geometry across all states. Only the inner mark + fill
   color differ; the box itself doesn't shape-shift, so toggling looks
   instant rather than animated through intermediate appearances.

   Empty-state border uses --ink so the box reads as near-black on the
   light pane background and near-white on dark — visible in either
   theme without per-theme rules. */
.task-check {
    grid-column: 1;
    /* Visual checkbox; the row padding still gives a 48px-ish tap
       area that meets Material's mobile minimum. */
    width: 24px;
    height: 24px;
    border: 2px solid var(--ink);
    background: var(--surface);
    border-radius: 6px;
    cursor: pointer;
    color: #fff;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
}
.task-check svg { width: 15px; height: 15px; }
.task-check > * { pointer-events: none; }
button.task-check:hover { border-color: var(--accent); }

.task-check--readonly {
    cursor: default;
    pointer-events: auto;
}
.task-check--readonly:hover { border-color: var(--ink); }

.task-check--completed {
    background: var(--accent);
    border-color: var(--accent);
}

.task-check--failed {
    background: var(--danger);
    border-color: var(--danger);
}

/* Running keeps the default empty-box look; the spinner inside carries
   the state. No border or fill change → no flash on the redraw. */

.task-check__spinner {
    width: 10px;
    height: 10px;
    border: 2px solid color-mix(in srgb, var(--accent) 30%, transparent);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin .8s linear infinite;
}
@keyframes spin {
    to { transform: rotate(360deg); }
}

/* ---- Name + meta ---- */

.task-name {
    grid-column: 3;
    font-size: .92rem;
    font-weight: 500;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* The name is the click target for opening the edit dialog (for roles
   and statuses that allow editing). Pointer cursor + a faint hover tint
   so the affordance is visible without adding a separate button. */
.task-name--editable {
    cursor: pointer;
}
.task-name--editable:hover { color: var(--accent); }
/* Single, low-key "checked" treatment — line-through + muted text. No
   opacity or color transitions; the redraw is instant and we don't want
   the row to pulse on every toggle. */
.task-name--done {
    text-decoration: line-through;
    color: var(--muted);
}

.task-meta {
    grid-column: 4;
    font-size: .78rem;
    font-weight: 600;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: .04em;
    white-space: nowrap;
}

.task-actions {
    grid-column: 5;
    display: inline-flex;
    align-items: center;
    gap: .15rem;
}

/* Second grid row carrying the description text. Spans from the name
   column to the right edge so it lines up under the name, not under the
   checkbox. URLs inside have already been linkified by app.js. */
.task-desc {
    grid-column: 3 / -1;
    grid-row: 2;
    margin: 0;
    color: var(--muted);
    font-size: .82rem;
    line-height: 1.4;
    white-space: pre-wrap;
    word-break: break-word;
}

/* SortableJS classes */
.sortable-ghost { opacity: 0; }
.sortable-chosen { background: var(--surface-2); }
.sortable-drag {
    box-shadow: var(--shadow-pop);
    background: var(--surface) !important;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
}

/* ---------- Status pill / dot ---------- */

.status-pill, .status-dot {
    --status: var(--status-pending);
}
.status-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--status);
    flex: 0 0 auto;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--status) 18%, transparent);
}
.status-pill {
    font-size: .68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--status);
    padding: .22rem .6rem;
    border: 1px solid color-mix(in srgb, var(--status) 50%, transparent);
    background: color-mix(in srgb, var(--status) 10%, transparent);
    border-radius: var(--radius-pill);
}
.status-pending   { --status: var(--status-pending); }
.status-running   { --status: var(--status-running); }
.status-completed { --status: var(--status-completed); }
.status-failed    { --status: var(--status-failed); }
.status-on_hold   { --status: var(--status-on-hold); }

/* ---------- Task details (above logs when a task is selected) ---------- */

.task-details {
    padding: .65rem .9rem .8rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    flex: 0 0 auto;
}
.td-head {
    display: flex;
    align-items: center;
    gap: .55rem;
    margin-bottom: .25rem;
}
.td-name {
    font-size: .95rem;
    font-weight: 600;
    color: var(--ink);
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.td-plugin {
    font-size: .68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
    padding: 1px 7px;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
}
/* Edit affordance in the task-details head; opens the new-task dialog
   in edit mode pre-filled with this task's values. */
.td-edit {
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid transparent;
    background: transparent;
    color: var(--muted);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background .12s, color .12s, border-color .12s;
}
.td-edit:hover {
    color: var(--accent);
    border-color: color-mix(in srgb, var(--accent) 35%, transparent);
    background: color-mix(in srgb, var(--accent) 6%, transparent);
}
.td-desc {
    color: var(--muted);
    font-size: .82rem;
    margin: .25rem 0 .5rem;
}
.td-cmd-label {
    font-size: .68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--faint);
    margin: .35rem 0 .2rem;
}
.td-cmd {
    margin: 0;
    padding: .55rem .65rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
    font-size: 12.5px;
    color: var(--ink);
    overflow-x: auto;
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 12rem;
    overflow-y: auto;
}

/* ---------- Approvals ---------- */

.approvals {
    padding: .5rem .9rem;
    background: var(--surface-2);
    border-bottom: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    gap: .5rem;
    flex: 0 0 auto;
    max-height: 50%;
    overflow-y: auto;
}
.approval-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
    padding: .65rem .75rem;
    box-shadow: var(--glow-accent);
    animation: approval-in .25s cubic-bezier(.2, .7, .2, 1);
}
@keyframes approval-in {
    from { transform: translateY(-4px); opacity: 0; }
    to   { transform: translateY(0);    opacity: 1; }
}
.approval-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    margin-bottom: .35rem;
}
.approval-label {
    font-size: .68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--accent);
}
.approval-task {
    font-size: .8rem;
    color: var(--muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.approval-body {
    margin-bottom: .55rem;
}
.approval-tool {
    display: inline-block;
    font-family: ui-monospace, SFMono-Regular, monospace;
    font-size: .85rem;
    font-weight: 600;
    color: var(--accent);
    padding: 1px 8px;
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    border-radius: 4px;
}
.approval-input {
    margin: .35rem 0 0;
    padding: .5rem .65rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-family: ui-monospace, SFMono-Regular, monospace;
    font-size: 12px;
    color: var(--ink);
    overflow-x: auto;
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 8rem;
    overflow-y: auto;
}
.approval-actions {
    display: flex;
    gap: .5rem;
    justify-content: flex-end;
}
.approval-actions .approval-allow { padding: .45rem 1rem; }
.approval-actions .approval-deny  { padding: .35rem .85rem; }

/* ---------- Questions (ask_user) — mirror approvals ---------- */

.questions {
    padding: .5rem .9rem;
    background: var(--surface-2);
    border-bottom: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    gap: .5rem;
    flex: 0 0 auto;
    max-height: 50%;
    overflow-y: auto;
}
.question-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
    padding: .65rem .75rem;
    box-shadow: var(--glow-accent);
    animation: approval-in .25s cubic-bezier(.2, .7, .2, 1);
}
.question-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    margin-bottom: .35rem;
}
.question-label {
    font-size: .68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--accent);
}
.question-task {
    font-size: .8rem;
    color: var(--muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.question-body { margin-bottom: .55rem; }
.question-prompt {
    margin: 0 0 .4rem;
    font-size: .92rem;
    line-height: 1.4;
    color: var(--ink);
    white-space: pre-wrap;
    word-break: break-word;
}
.question-context {
    margin: 0 0 .4rem;
    padding: .5rem .65rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-family: ui-monospace, SFMono-Regular, monospace;
    font-size: 12px;
    color: var(--muted);
    overflow-x: auto;
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 8rem;
    overflow-y: auto;
}
.question-input {
    width: 100%;
    box-sizing: border-box;
    padding: .5rem .65rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-family: inherit;
    font-size: .9rem;
    color: var(--ink);
    resize: vertical;
    min-height: 3rem;
}
.question-input:focus {
    outline: none;
    border-color: var(--accent);
}
.question-actions {
    display: flex;
    gap: .5rem;
    justify-content: flex-end;
    margin-top: .4rem;
}
.question-actions .question-submit { padding: .45rem 1rem; }

/* ---------- Logs pane ---------- */

.logs {
    flex: 1;
    margin: 0;
    padding: .85rem 1.1rem;
    overflow: auto;
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
    font-size: 12.5px;
    line-height: 1.55;
    white-space: pre-wrap;
    word-break: break-word;
    background: var(--surface-2);
    color: var(--stdout);
}
.logs > div {
    padding: 1px 0;
    border-left: 2px solid transparent;
    padding-left: .35rem;
    margin-left: -.35rem;
    transition: background .12s, border-color .12s;
}
.logs > div.log-active-task {
    border-left-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 7%, transparent);
}
.logs .stdout { color: var(--stdout); }
.logs .stderr { color: var(--stderr); }

/* Run-boundary divider: rendered when /tasks/{id}/run inserts the
   sentinel log row. A horizontal rule with a centered label so the
   operator can scroll up to compare to the previous run's output. */
.logs > .log-rerun {
    display: flex;
    align-items: center;
    gap: .65rem;
    margin: .65rem 0 .35rem;
    color: var(--accent);
    font-size: .72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .08em;
    user-select: none;
}
.logs > .log-rerun::before,
.logs > .log-rerun::after {
    content: "";
    flex: 1 1 0;
    height: 1px;
    background: color-mix(in srgb, var(--accent) 35%, transparent);
}
.logs .ts {
    color: var(--faint);
    margin-right: .6em;
    user-select: none;
    font-weight: 500;
}
.logs .task-tag {
    display: inline-block;
    margin-right: .6em;
    padding: 0 .4em;
    border-radius: 3px;
    background: color-mix(in srgb, var(--ink) 12%, transparent);
    color: var(--muted);
    font-size: .85em;
    user-select: none;
    max-width: 12em;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: middle;
    white-space: nowrap;
}

/* Markdown bubble for claude-task prose. The surrounding .logs is a
   monospace `pre`-wrap container; inside the bubble we switch back to a
   proportional font and let HTML elements handle their own spacing. */
.logs > .md-bubble {
    background: color-mix(in srgb, var(--accent) 5%, transparent);
    border-left-color: color-mix(in srgb, var(--accent) 35%, transparent);
    border-radius: var(--radius-sm);
    padding: .35rem .55rem;
    margin: .35rem -.55rem;
}
.md-body {
    display: block;
    white-space: normal;
    font-family: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
    font-size: 13.5px;
    line-height: 1.5;
    color: var(--ink);
}
.md-body > *:first-child { margin-top: .1rem; }
.md-body > *:last-child { margin-bottom: .1rem; }
.md-body p { margin: .35rem 0; }
.md-body ul, .md-body ol { margin: .35rem 0; padding-left: 1.4rem; }
.md-body li { margin: .1rem 0; }
.md-body h1, .md-body h2, .md-body h3, .md-body h4 {
    margin: .55rem 0 .25rem;
    font-weight: 700;
    color: var(--ink);
}
.md-body h1 { font-size: 1.15rem; }
.md-body h2 { font-size: 1.05rem; }
.md-body h3 { font-size: 1rem; }
.md-body code {
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
    font-size: .85em;
    background: var(--surface-2);
    padding: 1px 5px;
    border-radius: 3px;
}
.md-body pre {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: .55rem .7rem;
    overflow-x: auto;
    margin: .45rem 0;
}
.md-body pre code { background: transparent; padding: 0; font-size: .85em; }
.md-body a { color: var(--accent); text-decoration: underline; }
.md-body blockquote {
    border-left: 3px solid var(--border);
    margin: .45rem 0;
    padding: .1rem .65rem;
    color: var(--muted);
}
.md-body table { border-collapse: collapse; margin: .45rem 0; }
.md-body th, .md-body td {
    border: 1px solid var(--border);
    padding: .25rem .5rem;
}

.empty {
    margin: 1.5rem auto;
    text-align: center;
    color: var(--faint);
    font-size: .9rem;
    font-style: italic;
}

/* Subtle scrollbar styling so it doesn't clash with the dark theme. */
.nav-list::-webkit-scrollbar,
.task-list::-webkit-scrollbar,
.logs::-webkit-scrollbar { width: 10px; height: 10px; }
.nav-list::-webkit-scrollbar-thumb,
.task-list::-webkit-scrollbar-thumb,
.logs::-webkit-scrollbar-thumb {
    background: color-mix(in srgb, var(--muted) 35%, transparent);
    border-radius: 999px;
    border: 2px solid transparent;
    background-clip: padding-box;
}
.nav-list::-webkit-scrollbar-track,
.task-list::-webkit-scrollbar-track,
.logs::-webkit-scrollbar-track { background: transparent; }

/* ---------- Role indicators + members panel ---------- */
.project-role-tag {
    margin-left: .4rem;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: .7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .05em;
    vertical-align: middle;
}
.project-role-tag--editor {
    background: color-mix(in srgb, var(--accent) 15%, transparent);
    color: var(--accent);
}
.project-role-tag--viewer {
    background: color-mix(in srgb, var(--muted) 18%, transparent);
    color: var(--muted);
}
/* When the operator is only a viewer on the active project, drop the
   drag handles entirely so the row is visually static. The swipe handler
   already short-circuits, but hiding the handle reinforces it. */
body.role-viewer .task-list .drag-handle { visibility: hidden; }

.members-list {
    list-style: none;
    margin: 0 0 .8rem;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: .35rem;
}
.members-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: .45rem .6rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
}
.members-row-text { display: flex; align-items: center; gap: .55rem; min-width: 0; }
.members-email {
    flex: 1 1 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.members-role-select {
    flex: 0 0 auto;
    background: var(--surface);
    color: var(--ink);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: .35rem .55rem;
    font-size: .82rem;
    font-weight: 600;
    cursor: pointer;
}
.members-role-select:focus { outline: 2px solid var(--accent); outline-offset: -2px; }
.members-empty {
    text-align: center;
    color: var(--muted);
    font-size: .85rem;
    padding: .5rem 0;
}
.members-remove { color: var(--muted); }
.members-remove:hover { color: var(--danger); }
.members-invite {
    display: flex;
    flex-direction: column;
    gap: .55rem;
    margin-top: .8rem;
    padding-top: .8rem;
    border-top: 1px solid var(--border);
}
.members-invite-actions {
    display: flex;
    justify-content: flex-end;
}

/* Public-link section in the share dialog. The "create" CTA shows
   when no token exists; the URL row + actions show when one does. */
.public-link-h { margin-top: 1.25rem; }
.public-link {
    display: flex;
    flex-direction: column;
    gap: .55rem;
    padding: .7rem .8rem;
    border: 1px solid color-mix(in srgb, var(--accent) 25%, var(--border));
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--accent) 5%, var(--surface-2));
    margin-top: .35rem;
}
.public-link-row {
    display: flex;
    gap: .45rem;
    align-items: center;
}
.public-link-url {
    flex: 1 1 0;
    min-width: 0;
    padding: .5rem .65rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: .82rem;
    color: var(--ink);
    user-select: all;
}
.public-link-actions {
    display: flex;
    gap: .45rem;
    flex-wrap: wrap;
}
.public-link-actions .text-btn { font-size: .82rem; padding: .45rem .75rem; min-height: 36px; }
#public-link-disable { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, transparent); }
#public-link-disable:hover { color: var(--danger); border-color: var(--danger); background: color-mix(in srgb, var(--danger) 8%, transparent); }
.public-link-enable {
    align-self: flex-start;
    margin-top: .35rem;
    display: inline-flex;
    align-items: center;
    gap: .45rem;
}

/* ---------- Settings page ---------- */
/* Each settings section is a .plugin-card so the page reads as a
   stack of clearly bordered groups. The page itself is a flex
   column with gap; cards inherit their padding from .plugin-card. */
.settings-page {
    padding: 1rem;
    max-width: 60ch;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: .9rem;
}
.settings-page .plugin-card {
    padding: 1rem 1.1rem;
}
.settings-section-title {
    margin: 0 0 .55rem;
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--ink);
    letter-spacing: -.005em;
}

/* Labeled toggle row used in the settings panel. The <input> is visually
   hidden; the .switch-track styles + the :checked sibling selector
   render the iOS-style pill. */
.setting-toggle {
    display: flex;
    align-items: center;
    gap: .7rem;
    cursor: pointer;
    user-select: none;
}
.setting-toggle-label {
    color: var(--ink);
    font-size: .9rem;
}
.switch {
    position: relative;
    display: inline-block;
    width: 36px;
    height: 20px;
    flex: 0 0 auto;
}
.switch input {
    position: absolute;
    inset: 0;
    opacity: 0;
    margin: 0;
    cursor: pointer;
}
.switch-track {
    position: absolute;
    inset: 0;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 999px;
    transition: background .12s, border-color .12s;
}
.switch-track::before {
    content: "";
    position: absolute;
    left: 2px;
    top: 50%;
    width: 14px;
    height: 14px;
    background: var(--ink);
    border-radius: 50%;
    transform: translateY(-50%);
    transition: transform .15s, background .12s;
}
.switch input:checked + .switch-track {
    background: var(--accent);
    border-color: var(--accent);
}
.switch input:checked + .switch-track::before {
    background: #fff;
    transform: translate(16px, -50%);
}
.switch input:focus-visible + .switch-track {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
.settings-identity {
    margin: 0 0 1rem;
    color: var(--muted);
}
.settings-identity strong { color: var(--ink); font-weight: 700; }
.settings-role {
    font-style: normal;
    margin-left: .35rem;
    padding: 1px 7px;
    border-radius: 3px;
    background: color-mix(in srgb, var(--accent) 15%, transparent);
    color: var(--accent);
    font-size: .72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .05em;
}
.settings-form {
    display: flex;
    flex-direction: column;
    gap: .85rem;
    margin-top: .35rem;
}
.settings-form-foot {
    display: flex;
    justify-content: flex-end;
    margin-top: .25rem;
}

/* Drawer-foot button alignment + bigger tap target now that we have
   two stacked actions (Settings + Sign out). */
.drawer-foot-btn {
    display: inline-flex;
    align-items: center;
    gap: .55rem;
    width: 100%;
    justify-content: flex-start;
}
.drawer-foot-btn svg { color: var(--muted); flex: 0 0 auto; }

/* ---------- Quick-add: multi-row task input ---------- */
.quick-add-rows {
    display: flex;
    flex-direction: column;
    gap: .35rem;
}
.quick-add-row {
    display: flex;
    align-items: center;
    gap: .35rem;
}
.quick-add-row input {
    flex: 1;
    min-width: 0;
}
.quick-add-row .qa-remove {
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid transparent;
    background: transparent;
    color: var(--muted);
    border-radius: var(--radius-sm);
    cursor: pointer;
}
.quick-add-row .qa-remove svg { width: 16px; height: 16px; }
.quick-add-row .qa-remove:hover {
    color: var(--danger);
    border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}

/* ---------- Floating action button (quick-add todo) ---------- */
/* Bottom-right corner, sits above the collapsed logs drawer header
   (44px) plus the iOS home-indicator safe area. */
.fab {
    position: fixed;
    right: 1rem;
    bottom: calc(44px + 1rem + env(safe-area-inset-bottom));
    width: 56px;
    height: 56px;
    border-radius: 50%;
    border: none;
    cursor: pointer;
    background: var(--accent);
    color: #fff;
    /* FAB lives over the task list so a tiny low-blur shadow is
       what marks it as floating — no accent halo. */
    box-shadow: var(--shadow-lg);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    z-index: 6;
    transition: background .12s, opacity .12s;
}
.fab:hover { background: var(--accent-deep); }
.fab:active { opacity: .85; }

/* ---------- Dialogs ---------- */

.dialog {
    padding: 0;
    /* Phone-default cap. Wider breakpoints bump this below — a 460px
       dialog on a 27" monitor feels like reading through a slit. */
    width: min(460px, calc(100vw - 1.5rem));
    max-width: 100%;
    /* Cap height so a long form scrolls inside the dialog instead of
       overflowing the viewport. The form's body becomes the scrolling
       region; head + footer stay pinned.

       Important: do NOT set `display` here — the browser's UA rule
       `dialog:not([open]) { display: none }` is the only thing keeping
       closed dialogs hidden. Setting display here would override it and
       leave the dialog visible on page load. The flex layout is moved
       to .dialog[open] below. */
    /* Cap height to the visual viewport (post-keyboard, post-browser-chrome).
       The custom prop is set from JS via window.visualViewport.height
       because CSS `dvh` doesn't reliably shrink for the soft keyboard
       on Safari iOS. Fallback to dvh when no JS value is available. */
    max-height: calc(var(--viewport-h, 100dvh) - 2rem);
    /* Anchor near the top of the viewport instead of centred. On mobile
       the soft keyboard rises from the bottom, so a centred dialog ends
       up half-hidden when an input is focused; a top-anchored one stays
       visible. UA default `margin: auto` centres both axes — we keep
       auto on the sides for horizontal centring and override the top. */
    margin-top: max(1rem, env(safe-area-inset-top));
    margin-bottom: auto;
    background: var(--surface);
    color: var(--ink);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-pop);
    overflow: hidden;
    flex-direction: column;
}
.dialog-form {
    flex-direction: column;
    min-height: 0;
    flex: 1;
}
/* Open: applies the layout AND the entrance animation. */
.dialog[open] {
    display: flex;
    animation: dialog-in .18s cubic-bezier(.2,.7,.2,1);
}
.dialog[open] .dialog-form {
    display: flex;
}
.dialog::backdrop {
    /* Plain dim — no blur. Heavier alpha than the drawer backdrop
       because dialogs cover the full viewport and should mute
       everything underneath them rather than just dim it. */
    background: rgba(20, 32, 46, .62);
}
/* Backdrop fades in to match the dialog's entrance. The dialog's own
   animation is set on the `.dialog[open]` rule above where display is
   switched on. */
.dialog[open]::backdrop { animation: backdrop-in .18s ease-out; }
@keyframes dialog-in {
    from { transform: translateY(8px) scale(.985); opacity: 0; }
    to   { transform: translateY(0) scale(1);     opacity: 1; }
}
@keyframes backdrop-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* Wider modals on bigger viewports. The new-task dialog's prompt
   textarea + the settings dialog's stacked sections both benefit
   from extra horizontal room; smaller dialogs (confirm prompts) are
   bounded by their own content's natural width via `.dialog-body`
   gap + form widths, so they don't visibly stretch to the new cap.
   Breakpoints match the existing 960/index.html responsive pattern. */
@media (min-width: 960px) {
    .dialog { width: min(560px, calc(100vw - 1.5rem)); }
}
@media (min-width: 1280px) {
    .dialog { width: min(720px, calc(100vw - 1.5rem)); }
}

/* The 3px gradient stripe pulls the brand colour into the modal. */
.dialog-form { position: relative; }
.dialog-body { overflow-y: auto; -webkit-overflow-scrolling: touch; flex: 1; min-height: 0; }
.dialog-head, .dialog-foot { flex-shrink: 0; }
.dialog-form::before {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 3px;
    background: var(--grad-primary);
}

.dialog-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1.1rem 1.25rem .75rem;
    gap: 1rem;
}
.dialog-head h2 {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: -.005em;
}

.dialog-body {
    padding: .25rem 1.25rem 1.1rem;
    display: flex;
    flex-direction: column;
    gap: .9rem;
}

.field {
    display: flex;
    flex-direction: column;
    gap: .35rem;
    color: var(--muted);
    font-size: .72rem;
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
}

/* Schema slots inside dialog forms. The slot div is a child of
   .dialog-body, which uses `gap: .9rem` between its direct children —
   but the schema's generated <label.field> elements are children of
   the slot, not the dialog-body, so without this they'd stack flush
   while the static fields above them get proper breathing room. Same
   gap as .dialog-body so visually they're indistinguishable. */
#new-task-schema:not([hidden]),
#template-schema:not([hidden]) {
    display: flex;
    flex-direction: column;
    gap: .9rem;
}
.field .opt {
    font-style: normal;
    text-transform: none;
    letter-spacing: 0;
    color: var(--faint);
    font-weight: 500;
    font-size: .72rem;
}
/* Task-ref affordances. Three pieces:
 *  .ref-insert-btn — "{ }" button rendered after each schema text /
 *    textarea label. Opens a picker that inserts a
 *    ${TaskName.output} reference at the input's cursor.
 *  .ref-chip       — small chip next to a field's label, one per
 *    reference present in the value. Read-only signal so the
 *    operator sees "→ Hello.body" without parsing the placeholder
 *    string. Non-interactive in v1.
 *  .ref-picker     — popover anchored under the insert button,
 *    listing predecessor tasks × their declared outputs.
 */
.field .ref-chip {
    margin-left: .5rem;
    padding: .05rem .4rem;
    border: 1px dashed var(--border, #ddd);
    border-radius: .65rem;
    color: var(--faint);
    font-size: .7rem;
    line-height: 1.2;
}
.ref-insert-btn {
    align-self: start;
    margin-top: .35rem;
    padding: .1rem .45rem;
    border: 1px solid var(--border, #ddd);
    border-radius: .4rem;
    background: transparent;
    color: var(--muted);
    font: inherit;
    font-size: .75rem;
    cursor: pointer;
    line-height: 1.2;
}
.ref-insert-btn:hover:not([disabled]) {
    color: var(--text, currentColor);
    border-color: var(--text, currentColor);
}
.ref-insert-btn[disabled] {
    opacity: .5;
    cursor: not-allowed;
}
.ref-picker {
    position: absolute;
    z-index: 100;
    min-width: 260px;
    max-height: 260px;
    overflow: auto;
    border: 1px solid var(--border, #ddd);
    border-radius: .4rem;
    background: var(--bg);
    box-shadow: var(--shadow-pop);
    padding: .25rem 0;
}
.ref-picker-row {
    display: flex;
    flex-direction: column;
    width: 100%;
    border: none;
    background: transparent;
    text-align: left;
    padding: .35rem .65rem;
    cursor: pointer;
    font: inherit;
    color: inherit;
}
.ref-picker-row:hover,
.ref-picker-row:focus-visible {
    background: var(--hover, rgba(0,0,0,.04));
}
.ref-picker-task {
    font-weight: 600;
    font-size: .85rem;
}
.ref-picker-output {
    color: var(--faint);
    font-size: .75rem;
}
/* Canonical text-input / textarea / select look. Applied to .field
   wrappers (the dialog form pattern) plus the inline rows that mix an
   input with a sibling button — those bypass the .field label
   structure but still want the same chrome so the app reads as
   visually coherent. New input-bearing rows should be added to this
   selector list (or use .field) rather than rolling their own styles. */
.field input,
.field textarea,
.field select,
.rename-row input,
.quick-add-row input {
    background: var(--surface-2);
    color: var(--ink);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    /* Hit ~48px tall and 16px text — 16px stops iOS from auto-zooming
       when an input is focused. */
    padding: .8rem .85rem;
    min-height: 48px;
    font: 16px "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
    font-weight: 400;
    letter-spacing: normal;
    text-transform: none;
    transition: border-color .12s, box-shadow .12s, background .12s;
}
/* Textareas don't want a min-height fight with their rows attribute. */
.field textarea { min-height: 0; }
.field textarea[name="cmd"],
.field textarea[name="worker_config"] {
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
    font-size: 13px;
    line-height: 1.5;
    resize: vertical;
    min-height: 84px;
}
.field input:focus,
.field textarea:focus,
.field select:focus,
.rename-row input:focus,
.quick-add-row input:focus {
    outline: none;
    background: var(--surface);
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* Action cluster in pane heads (multiple buttons side-by-side) */
.head-actions { display: inline-flex; gap: .25rem; }

/* Per-row inline buttons (Run / Cancel) shown beside the status meta */
.task-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border: 1px solid transparent;
    background: transparent;
    color: var(--muted);
    border-radius: var(--radius-sm);
    cursor: pointer;
    flex: 0 0 auto;
    transition: background .12s, color .12s, border-color .12s;
}
.task-action svg { width: 16px; height: 16px; }
.task-action:hover { color: var(--accent); background: var(--surface); border-color: var(--border); }

/* List-level run/pause button: green tint when running so the operator
   knows the chain is currently progressing. */
#run-project.is-running {
    color: var(--success, #1f8a3d);
    border-color: color-mix(in srgb, var(--success, #1f8a3d) 50%, transparent);
    background: color-mix(in srgb, var(--success, #1f8a3d) 10%, transparent);
}
/* "Pipeline complete" — disabled by JS (no work left to drive),
   styled with an accent-tinted ✓ so it reads as a status, not as an
   inert disabled control. */
#run-project.is-complete,
#run-project.is-complete:disabled {
    color: var(--accent);
    border-color: color-mix(in srgb, var(--accent) 50%, transparent);
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    opacity: 1;
    cursor: default;
}
#run-project.is-complete:disabled:hover {
    background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.task-action.delete-btn:hover { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, transparent); }

/* Inline checkbox + label, used for the auto-approve toggle. */
.field-inline {
    display: flex;
    align-items: flex-start;
    gap: .55rem;
    cursor: pointer;
    font-size: .85rem;
    color: var(--ink);
    padding: .35rem 0;
}
.field-inline input[type="checkbox"] {
    margin-top: 3px;
    accent-color: var(--accent);
    flex: 0 0 auto;
}
.field-inline em {
    color: var(--muted);
    font-style: normal;
    font-weight: 400;
    display: block;
}

.field-hint {
    margin: -.4rem 0 0;
    font-size: .82rem;
    color: var(--muted);
    font-weight: 400;
}
.field-hint em { font-style: normal; color: var(--ink); font-weight: 600; }

.dialog-foot {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: .6rem;
    padding: .9rem 1.25rem 1.1rem;
    background: var(--surface-2);
    border-top: 1px solid var(--border);
}
.dialog-foot .primary { padding: .75rem 1.4rem; }

@media (max-width: 480px) {
    .dialog {
        /* Bottom-sheet style on phones — feels native and avoids the
           keyboard pushing the dialog around. */
        width: 100vw;
        max-width: none;
        margin: 0;
        margin-top: auto;
        margin-bottom: 0;
        border-radius: var(--radius) var(--radius) 0 0;
    }
    .dialog[open] {
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
        top: auto;
        animation: sheet-in .22s cubic-bezier(.2,.7,.2,1);
    }
    @keyframes sheet-in {
        from { transform: translateY(20px); opacity: 0; }
        to   { transform: translateY(0); opacity: 1; }
    }
}

/* Inline button styled as a text link. Used inside .field-hint copy where
   a real <a> would push the page. The native <button> keeps focus and
   keyboard semantics; we just strip the chrome. */
.link-btn {
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    color: var(--accent);
    font: inherit;
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.link-btn:hover { color: var(--accent-deep); }

/* ---------- Expandable "Advanced" disclosure inside dialog forms ---------- */
/* Wraps less-common fields (system prompt, model, cwd) in <details> so the
   form stays short for the common case but the operator can pop them open
   on demand. */
.field-details {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--surface-2) 60%, transparent);
}
.field-details > summary {
    cursor: pointer;
    list-style: none;
    padding: .65rem .85rem;
    font-size: .72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .08em;
    color: var(--muted);
    user-select: none;
    display: flex;
    align-items: center;
    gap: .4rem;
}
.field-details > summary::-webkit-details-marker { display: none; }
.field-details > summary::before {
    content: "";
    width: 0; height: 0;
    border-left: 4px solid currentColor;
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transition: transform .15s ease;
}
.field-details[open] > summary::before { transform: rotate(90deg); }
.field-details > .field,
.field-details > .field-inline {
    margin: 0 .85rem .75rem;
}
.field-details > .field:first-of-type { margin-top: .4rem; }

/* ---------- Library page (saved task templates) ---------- */

/* Shares the body grid with #main. On mobile (single-column body),
   library + workers slot into column 1 — same place #main lives.
   On desktop (≥960px, two-column body) #main moves to column 2 so
   the drawer can occupy column 1; the library/workers panes need
   to follow #main into column 2 or they end up behind the drawer
   and the right side renders empty. */
.library-pane {
    grid-row: 2;
    grid-column: 1;
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: auto;
    background: var(--bg);
}
@media (min-width: 960px) {
    .library-pane {
        grid-column: 2;
    }
}

/* ---------- Home pane (no project selected) ---------- */
/* Sibling of #main; rendered when state.view === "home". Same grid
   placement rules as .library-pane so the desktop two-column layout
   doesn't push it behind the drawer. */
.home-pane {
    grid-row: 2;
    grid-column: 1;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding: clamp(1.25rem, 5vh, 3rem) 1rem 3rem;
    display: flex;
    justify-content: center;
    background: var(--bg);
}
@media (min-width: 960px) {
    .home-pane { grid-column: 2; }
}
.home-card {
    width: min(560px, 100%);
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 1rem;
}
/* Brand mark on the welcome card. PNG is transparent so no rounded
   crop or drop shadow is needed — those would only paint a phantom
   rectangle behind the logo silhouette. */
.home-mark { display: block; }
/* Welcome-card heading. Hazy-web's hero h1 is plain ink with tight
   tracking; matching that here keeps the home pane reading as a
   sober landing rather than a brand splash. */
.home-title {
    margin: .25rem 0 0;
    font-size: clamp(1.6rem, 4.5vw, 2rem);
    font-weight: 800;
    letter-spacing: -.015em;
    line-height: 1.1;
    color: var(--ink);
}
.home-lead {
    margin: 0;
    color: var(--ink-soft);
    font-size: .98rem;
    line-height: 1.55;
    max-width: 46ch;
}
.home-actions {
    margin-top: .5rem;
    display: flex;
    flex-wrap: wrap;
    gap: .55rem;
    justify-content: center;
}
.home-actions .primary,
.home-actions .text-btn {
    padding: .6rem 1.05rem;
    font-size: .92rem;
    font-weight: 600;
}
.home-tips {
    list-style: none;
    margin: 1.6rem 0 0;
    padding: 0;
    width: 100%;
    display: grid;
    gap: .55rem;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.home-tip {
    width: 100%;
    display: flex;
    align-items: center;
    gap: .65rem;
    padding: .7rem .8rem;
    border-radius: var(--radius);
    background: var(--surface);
    border: 1px solid var(--border);
    color: inherit;
    text-align: left;
    cursor: pointer;
    font: inherit;
    transition: border-color .12s, transform .12s, box-shadow .12s;
}
.home-tip:hover,
.home-tip:focus-visible {
    border-color: var(--accent);
    box-shadow: var(--shadow);
    transform: translateY(-1px);
    outline: none;
}
.home-tip-icon {
    flex: 0 0 auto;
    color: var(--accent);
    display: inline-flex;
}
.home-tip-text {
    display: flex;
    flex-direction: column;
    gap: .08rem;
    min-width: 0;
}
.home-tip-text strong {
    font-size: .92rem;
    font-weight: 700;
    color: var(--ink);
}
.home-tip-text em {
    font-style: normal;
    font-size: .8rem;
    color: var(--muted);
    line-height: 1.35;
}
.home-stats {
    margin: 1.4rem 0 0;
    color: var(--muted);
    font-size: .82rem;
}
.home-stats strong {
    color: var(--ink);
    font-weight: 700;
}
/* Library / Workers / Settings page headers. Match the height a
   .section-head naturally takes on when its .head-actions hold a 44px
   .icon-btn (Library + Workers have a "+" button; Settings doesn't,
   which would otherwise collapse its bar to the bare 44px min-height
   and visibly differ from the other pages). */
.library-head { min-height: calc(44px + 1.3rem); }
.library-blurb {
    margin: .9rem 1.1rem 0;
    color: var(--muted);
    font-size: .9rem;
    max-width: 60ch;
}
.library-empty { margin-top: 3rem; }

/* Plugin reference cards. Top row pairs the plugin icon with the
   display name + slug; description follows as a one-line summary;
   technical reference (outputs + requires) lives behind a
   collapsed <details>. */
.plugins-list {
    list-style: none;
    margin: 1rem;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: .9rem;
    max-width: 70ch;
}
.plugin-card {
    border: 1px solid var(--border, #ddd);
    border-radius: var(--radius);
    padding: .9rem 1.1rem;
    background: var(--bg);
}
.plugin-head {
    display: flex;
    align-items: center;
    gap: .7rem;
}
.plugin-icon {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    color: var(--accent);
    font-size: 1.1rem;
    line-height: 1;
}
.plugin-icon svg, .plugin-icon img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}
/* Tile variant for templates: emoji/initial sat in a tinted square.
   --tpl-color is set inline from the operator's chosen template
   colour (falls back to the brand accent). */
.plugin-icon--tile {
    --tpl-color: var(--accent);
    width: 36px;
    height: 36px;
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--tpl-color) 18%, var(--surface-2));
    color: var(--tpl-color);
    font-size: 1.25rem;
    /* Emojis keep their full-colour rendering inside the tint. */
    text-shadow: 0 0 0 transparent;
}
/* Status tint for workers: the icon's foreground colour tracks the
   worker's connection status. Online = green, everything else
   inherits the muted default. */
.plugin-icon--status { color: var(--muted); }
.plugin-icon--status.online { color: var(--success, #1f8a3d); }
.plugin-text {
    display: flex;
    align-items: baseline;
    gap: .5rem;
    flex-wrap: wrap;
    min-width: 0;
    flex: 1 1 auto;
}
.plugin-name {
    margin: 0;
    font-size: 1rem;
    font-weight: 700;
    color: var(--ink);
}
/* Machine-readable slug next to the display name — used as the
   foreign-key identifier in tasks/templates, kept visible so an
   operator authoring a YAML knows what `worker_type` value to use. */
.plugin-slug {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: .78rem;
    color: var(--muted);
    background: var(--surface-2);
    padding: .05rem .4rem;
    border-radius: 4px;
}
/* Right-aligned slot in the head row for inline actions (worker
   delete, future per-card affordances). flex-end via margin keeps
   the icon+text on the left flush. */
.plugin-head-actions {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    flex: 0 0 auto;
}
.plugin-head-actions .icon-btn { color: var(--muted); }
.plugin-head-actions .icon-btn:hover { color: var(--accent); }
.plugin-head-actions .icon-btn.danger:hover {
    color: var(--status-failed);
    background: color-mix(in srgb, var(--status-failed) 12%, transparent);
}
.plugin-desc {
    margin: .55rem 0 0;
    color: var(--ink-soft);
    font-size: .92rem;
}
/* Cluster of small chips below the description — worker_types on a
   worker card, the prefilled plugin slug on a template card. */
.plugin-chips {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem;
    margin-top: .55rem;
}
.plugin-chip {
    padding: .15rem .55rem;
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    color: var(--muted);
    font-size: .72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .06em;
}
.plugin-chip--missing {
    background: color-mix(in srgb, #d97706 22%, var(--surface-2));
    color: #b45309;
}
/* Clickable variant: the whole card is a button (templates). Cursor
   + a subtle border-tint on hover mark it as interactive. */
.plugin-card--clickable {
    cursor: pointer;
    transition: border-color .12s, transform .12s;
}
.plugin-card--clickable:hover,
.plugin-card--clickable:focus-visible {
    border-color: var(--accent);
    outline: none;
}
.plugin-details {
    margin-top: .6rem;
}
.plugin-details summary {
    cursor: pointer;
    color: var(--muted);
    font-size: .8rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
    padding: .2rem 0;
}
.plugin-details summary:hover { color: var(--accent); }
.plugin-details[open] summary { color: var(--ink-soft); }
.plugin-block { margin-top: .55rem; }
.plugin-block h5 {
    margin: 0 0 .2rem;
    font-size: .75rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--faint, var(--muted));
}
.plugin-block ul {
    margin: 0;
    padding-left: 1.2rem;
    font-size: .85rem;
}
.plugin-block li { line-height: 1.5; }
.plugin-none {
    margin: 0;
    color: var(--faint, var(--muted));
    font-size: .85rem;
    font-style: italic;
}

/* Inline outputs list shown under the plugin description in the
   new-task dialog hint. Smaller + slightly muted; the operator
   already has the dialog's primary affordances above it. */
.plugin-hint-outputs {
    display: inline-block;
    margin-top: .15rem;
    font-size: .8rem;
    color: var(--muted);
}
.plugin-hint-outputs code {
    font-family: inherit;
    font-weight: 600;
}

.library-grid {
    list-style: none;
    margin: 1rem;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
    gap: .75rem;
}

/* Template + worker cards reuse the .plugin-card vocabulary now —
   see the "Plugin reference cards" section above for the shared
   structure. Page-level grid layout (.library-grid) stays
   responsive so multiple entries pack into columns. */

/* Project-level coverage banner: shown above the task list when the
   project has pending tasks of a worker_type that no online worker
   handles. Soft-warning colour (the same family as the cross-check
   warnings on worker cards) so it reads as "FYI, this is why nothing
   moves" rather than "something exploded". */
.tasks-banner {
    display: flex;
    align-items: center;
    gap: .55rem;
    margin: .5rem .9rem 0;
    padding: .55rem .75rem;
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--status-failed) 9%, transparent);
    border-left: 3px solid var(--status-failed);
    color: var(--ink);
    font-size: .85rem;
    line-height: 1.35;
}
.tasks-banner-icon {
    color: var(--status-failed);
    font-size: 1rem;
    flex: 0 0 auto;
}
.tasks-banner-text {
    flex: 1 1 auto;
}
.tasks-banner .link-btn {
    flex: 0 0 auto;
}

/* Workers + templates render through the shared .plugin-card
   vocabulary now (.plugin-icon, .plugin-text, .plugin-chips,
   .plugin-head-actions). The status colouring lives on
   .plugin-icon--status above. */

/* Copy row: read-only input + Copy button side-by-side. The input
   takes the remaining width, the button stays at its natural size. */
.copy-row {
    display: flex;
    align-items: stretch;
    gap: .4rem;
}
.copy-row input,
.copy-row textarea {
    flex: 1 1 auto;
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
    font-size: 13px;
}
.copy-row textarea {
    resize: vertical;
}
.copy-row .text-btn {
    flex: 0 0 auto;
    align-self: flex-start;
}

/* Warning-flavored field-hint, used for the "this is the only time
   you'll see this key" notice in the worker-key reveal dialog. */
.warning-hint {
    background: color-mix(in srgb, var(--status-failed) 10%, transparent);
    border-left: 3px solid var(--status-failed);
    padding: .55rem .75rem;
    border-radius: var(--radius-sm);
}

/* Missing-binary install hint inside the new-task dialog. Same
   amber-y "you need to fix something" affordance as warning-hint but
   nests a list of per-distro install commands. */
.requires-warning {
    background: color-mix(in srgb, #d97706 10%, transparent);
    border-left: 3px solid #d97706;
    padding: .65rem .75rem;
    border-radius: var(--radius-sm);
    margin: .35rem 0 .25rem;
    font-size: 13px;
    line-height: 1.4;
}
.requires-warning__sub {
    margin: .3rem 0 .5rem;
    color: var(--muted);
}
.requires-warning__item + .requires-warning__item {
    margin-top: .55rem;
}
.requires-warning__head { font-weight: 500; }
.requires-warning__installs {
    list-style: none;
    margin: .3rem 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: .2rem;
}
.requires-warning__os {
    color: var(--muted);
    display: inline-block;
    min-width: 56px;
    text-transform: capitalize;
}
.requires-warning code {
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
    font-size: 12.5px;
    background: color-mix(in srgb, var(--ink) 8%, transparent);
    padding: 1px 5px;
    border-radius: 3px;
}

/* Template dialog: icon / colour / name on one row at wider widths,
   stacked on narrow phones. */
.template-id-row {
    display: grid;
    grid-template-columns: 96px 92px 1fr;
    gap: .65rem;
}
.template-icon-field input { font-size: 22px; text-align: center; padding: .55rem; }
.template-color-field input[type="color"] {
    padding: 4px;
    height: 48px;
    cursor: pointer;
}
@media (max-width: 480px) {
    .template-id-row {
        grid-template-columns: 96px 1fr;
    }
    .template-color-field { grid-column: 1 / span 2; }
}

/* ---------- Webhooks (project-settings dialog) ---------- */

.webhooks-list {
    list-style: none;
    margin: .25rem 0 .75rem;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: .5rem;
}
.webhooks-empty {
    margin: .25rem 0;
    text-align: left;
    font-style: italic;
    color: var(--faint);
}

.webhook-row {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    padding: .55rem .65rem;
}
.webhook-head {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: wrap;
}
.webhook-events {
    display: flex;
    flex-wrap: wrap;
    gap: .25rem;
    flex: 0 0 auto;
}
.webhook-event {
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
    font-size: .8rem;
    background: color-mix(in srgb, var(--accent) 12%, transparent);
    color: var(--accent);
    padding: .1rem .4rem;
    border-radius: var(--radius-pill);
    flex: 0 0 auto;
}
.webhook-events-pick {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: .35rem .55rem .45rem;
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    gap: .25rem .9rem;
}
.webhook-events-pick legend {
    padding: 0 .25rem;
    font-size: .85rem;
    color: var(--muted);
}
.webhook-events-pick label {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
    font-size: .8rem;
    cursor: pointer;
}
.webhook-events-pick input[type="checkbox"] {
    margin: 0;
    accent-color: var(--accent);
}
.webhook-url {
    flex: 1 1 auto;
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
    font-size: .8rem;
    color: var(--ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.webhook-toggle {
    flex: 0 0 auto;
}
.webhook-toggle input[type="checkbox"] {
    margin: 0;
    accent-color: var(--accent);
}
.webhook-delete {
    width: 28px;
    height: 28px;
    flex: 0 0 auto;
}
.webhook-delete:hover {
    color: var(--status-failed);
    background: color-mix(in srgb, var(--status-failed) 12%, transparent);
}

.webhook-deliveries {
    list-style: none;
    margin: .35rem 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: .15rem;
    max-height: 200px;
    overflow-y: auto;
}
.webhook-deliveries-empty {
    color: var(--faint);
    font-style: italic;
    font-size: .75rem;
}

.delivery-row {
    display: grid;
    grid-template-columns: 44px auto auto auto 1fr auto;
    align-items: center;
    gap: .35rem;
    padding: .15rem .25rem;
    font-size: .75rem;
    border-radius: var(--radius-sm);
    line-height: 1.3;
}
.delivery-row.delivery-ok .delivery-status {
    color: #16a34a;
    font-weight: 600;
}
.delivery-row.delivery-bad .delivery-status {
    color: var(--status-failed);
    font-weight: 600;
}
.delivery-status {
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
    font-size: .75rem;
}
.delivery-when,
.delivery-dur,
.delivery-attempt {
    color: var(--muted);
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
}
.delivery-err {
    color: var(--status-failed);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.delivery-retry {
    flex: 0 0 auto;
    padding: .15rem .55rem;
    font-size: .72rem;
    line-height: 1.2;
    min-height: 0;
}

.webhooks-add {
    display: flex;
    flex-direction: column;
    gap: .35rem;
    background: var(--surface-2);
    padding: .55rem .65rem;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    margin-bottom: .75rem;
}
.webhooks-add .field {
    margin: 0;
}

/* ---------- Schedule (project-settings dialog) ---------- */

.schedule-row {
    display: flex;
    align-items: flex-end;
    gap: .35rem;
    margin-bottom: .25rem;
}
.schedule-cron-field {
    flex: 1 1 auto;
}
.schedule-cron-field input {
    font-family: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, monospace;
}
#schedule-status {
    font-size: .8rem;
    color: var(--muted);
}
.schedule-next {
    font-size: .8rem;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
    margin-top: .15rem;
}
.schedule-tz {
    font-size: .78rem;
    color: var(--muted);
    margin-top: .35rem;
}

/* Danger zone in the project-settings dialog. The heading colour-codes the
   section so the operator's eye lands on the delete affordance instead of
   missing it among the other settings. */
.danger-zone-h { color: var(--danger); margin-top: 2rem; }
.danger-btn {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    color: var(--danger);
    border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}
.danger-btn:hover {
    color: #fff;
    background: var(--danger);
    border-color: var(--danger);
}
.danger-btn svg { width: 14px; height: 14px; }

/* AGPL-3.0 §13 compliance: a small source-code link surfaced on every
   network-served view (drawer footer, login card, public viewer foot).
   Tiny + muted so it doesn't compete with content. */
.drawer-source,
.card-source,
.pub-source {
    margin: .4rem 0 0;
    font-size: .72rem;
    color: var(--faint);
    text-align: center;
}
.drawer-source a,
.card-source a,
.pub-source a {
    color: var(--faint);
    text-decoration: none;
}
.drawer-source a:hover,
.card-source a:hover,
.pub-source a:hover {
    color: var(--muted);
    text-decoration: underline;
}

/* Undo toast for archive/delete. A single transient bar pinned to the
   bottom of the viewport. Slides in from below; reusable across actions
   so chained operations replace rather than stack. Sits above the logs
   drawer (z-index 5) and the modal backdrop where applicable. */
.undo-toast {
    position: fixed;
    left: 50%;
    bottom: max(env(safe-area-inset-bottom, 0), 1rem);
    transform: translate(-50%, calc(100% + 1rem));
    z-index: 50;
    display: inline-flex;
    align-items: center;
    gap: .9rem;
    max-width: min(420px, calc(100vw - 1.5rem));
    padding: .7rem .85rem .7rem 1rem;
    background: var(--ink);
    color: var(--bg);
    border-radius: 999px;
    box-shadow: var(--shadow-pop);
    font-size: .9rem;
    line-height: 1.2;
    pointer-events: none;
    opacity: 0;
    transition: transform .22s cubic-bezier(.2,.8,.2,1), opacity .22s ease;
}
.undo-toast--visible {
    transform: translate(-50%, 0);
    opacity: 1;
    pointer-events: auto;
}
.undo-toast-msg {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.undo-toast-btn {
    flex: 0 0 auto;
    background: transparent;
    border: 1px solid color-mix(in srgb, var(--bg) 40%, transparent);
    color: var(--accent);
    border-radius: 999px;
    padding: .35rem .8rem;
    font: inherit;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    font-size: .78rem;
    cursor: pointer;
    transition: background .12s, color .12s, border-color .12s;
}
.undo-toast-btn:hover {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
}

/* Generic toast for M2 auth flows (verification confirmation, reset
   handoff). Slides in from the bottom like .undo-toast but without
   an action button. Auto-dismisses; the JS removes the node after
   the animation. */
.toast {
    position: fixed;
    left: 50%;
    bottom: max(env(safe-area-inset-bottom, 0), 1rem);
    transform: translate(-50%, 0);
    z-index: 60;
    max-width: min(460px, calc(100vw - 1.5rem));
    padding: .7rem .95rem;
    background: var(--ink);
    color: var(--bg);
    border-radius: 999px;
    box-shadow: var(--shadow-pop);
    font-size: .9rem;
    line-height: 1.25;
    animation: toast-in .22s cubic-bezier(.2,.8,.2,1);
}
.toast-error {
    background: var(--danger, #c0392b);
    color: #fff;
}
.toast-out {
    animation: toast-out .35s cubic-bezier(.2,.6,.2,1) forwards;
}
@keyframes toast-in {
    from { transform: translate(-50%, calc(100% + 1rem)); opacity: 0; }
    to   { transform: translate(-50%, 0);                  opacity: 1; }
}
@keyframes toast-out {
    from { transform: translate(-50%, 0);                  opacity: 1; }
    to   { transform: translate(-50%, calc(100% + 1rem)); opacity: 0; }
}

/* Card actions footer (login screen "Forgot password?" / "Sign up").
   Sits between the form and the source-link footer so an
   unauthenticated visitor has a single place to find every
   pre-auth surface. */
.card-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    margin: .9rem 0 0;
    font-size: .92rem;
}
.card-actions a {
    color: var(--accent);
    text-decoration: none;
}
.card-actions a:hover { text-decoration: underline; }

/* Terms-checkbox row on the signup form. The default label styling
   stacks input above text; for the consent checkbox we want them
   side-by-side and aligned. */
.checkbox-row {
    display: flex;
    gap: .55rem;
    align-items: flex-start;
    font-size: .9rem;
    line-height: 1.35;
    color: var(--ink-muted, currentColor);
    margin: .3rem 0;
}
.checkbox-row input[type=checkbox] {
    flex: 0 0 auto;
    margin: .15rem 0 0;
}

/* "If that email matches…" info banner on the forgot-password form.
   Same shape as .error but in the accent (not danger) tone. */
.info {
    color: var(--accent);
    font-size: .9rem;
    margin: .6rem 0 0;
}

/* Two-factor (TOTP) Settings body. State-machine of nested <div>s:
   loading → disabled / enrol / codes / enabled / disable
   confirmation. The wrapping .plugin-card supplies border + padding
   now; this just resets margins so the state views stack neatly. */
.totp-card { margin: 0; }
.totp-qr-row {
    display: flex;
    gap: 1.2rem;
    align-items: flex-start;
    margin: .6rem 0;
}
.totp-manual { flex: 1; min-width: 0; }
.totp-secret {
    display: inline-block;
    padding: .35rem .55rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    font-family: ui-monospace, SFMono-Regular, monospace;
    font-size: .9rem;
    word-break: break-all;
    user-select: all;
}
.totp-codes {
    margin: .6rem 0;
    padding: .85rem 1rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 8px;
    font-family: ui-monospace, SFMono-Regular, monospace;
    font-size: .95rem;
    line-height: 1.6;
    white-space: pre;
    user-select: all;
}
.totp-actions {
    display: flex;
    flex-wrap: wrap;
    gap: .6rem;
    margin: .6rem 0 0;
}
.totp-recovery-fallback {
    margin: .8rem 0;
    font-size: .9rem;
}
.totp-recovery-fallback summary {
    cursor: pointer;
    color: var(--accent);
}
.totp-recovery-fallback summary:hover { text-decoration: underline; }

/* Reduced motion: kill our transitions. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        transition-duration: .01ms !important;
    }
}
