/* Fonts (Barlow Condensed / Inter / Space Mono) are loaded via a
   <link rel="preconnect"> + <link rel="stylesheet"> pair in
   templates/base.html's <head>, NOT an @import here -- an @import inside
   this already-fetched stylesheet is a second-order request the browser
   only discovers late, which left the Bubbles diagram laid out with the
   wide fallback font on a cold first load (see the connector-reanchor
   logic in static/js/pipeline_bubble_connectors.js). */

/* One dark visual world, matched to the sign-in / marketing / create-
   account pages (templates/staff_login.html et al) -- the exact same
   token values, fonts (Barlow Condensed / Inter / Space Mono) and the
   subtle dot field on <body>. Deliberately single-theme now: there's no
   light mode and no data-theme toggle (that control and static/js/
   theme.js were removed). Status/badge colors are tuned as translucent
   fills so they read on the dark surface without being their own bright
   objects. */
:root {
    /* Call Scheduler timeline layout -- shared by .timeline-row-label/
       .timeline-row's gap AND .timeline-today-line's position calc below,
       so the "today" line's coordinate space always matches the track
       columns it needs to align with instead of two hardcoded numbers
       silently drifting apart. */
    --timeline-label-width: 220px;
    --timeline-track-gap: 16px;

    /* One dark world -- makes native controls (inputs, selects, checkboxes,
       date pickers, scrollbars) render dark-aware instead of white. */
    color-scheme: dark;

    --bg: #0B0C0E;
    --surface: #16181C;
    --surface-2: #101114;
    --border: #26292F;
    --text: #ECEDEE;
    --text-muted: #8A8F98;
    --text-faint: #565A62;
    --brand: #4FD1C5;
    --brand-dim: #2C7A72;
    --brand-text: #4FD1C5;
    --brand-bg: rgba(79, 209, 197, 0.12);
    --hot: #E5615A;
    --hot-bg: rgba(229, 97, 90, 0.12);
    --attention: #E2A63B;
    --attention-bg: rgba(242, 169, 59, 0.12);
    --cold: #8A8F98;
    --cold-bg: #1B1D22;
    --green: #4ADE80;
    --green-text: #86EFAC;
    --green-bg: rgba(74, 222, 128, 0.12);
    --closed-bg: #15803D;
    --closed-border: #166534;
    --gold: #EAB308;
    --gold-bg: #EAB308;
    --gold-text: #0B0C0E;
    --lost-bg: #1B1D22;
    --lost-text: #ECEDEE;
    --violet: #A78BFA;

    /* Status outline convention -- every status-coloured box, tile, card
       and pill in the app uses a neutral fill with a colour-tinted
       border + matching text, never a filled colour block. These are the
       standard border tints; small pills derive the same tint from their
       own text colour via color-mix (see .status / .badge below). */
    --hot-border: rgba(229, 97, 90, 0.5);
    --attention-border: rgba(242, 169, 59, 0.5);
    --green-border: rgba(74, 222, 128, 0.45);
    --brand-border: rgba(79, 209, 197, 0.45);
    --cold-border: rgba(138, 143, 152, 0.4);
    --violet-border: rgba(167, 139, 250, 0.5);
    --gold-border: rgba(234, 179, 8, 0.5);

    /* Solid red for the one genuinely destructive control -- Delete Lead.
       Deliberately a filled block, not the outline treatment: it has to
       read as "this is different / careful" at a glance. */
    --danger-solid: #E5484D;
    --danger-solid-hover: #EC5D5E;

    /* Pipeline Bubbles view -- one colour per branch so a lead's origin
       stays traceable after all three tracks merge into Pending Closed.
       teal = Appointment Set, amber = Reached/Attempts, purple = Voicemail. */
    --track-appt: #4FD1C5;
    --track-appt-bg: rgba(79, 209, 197, 0.10);
    --track-reached: #E2A63B;
    --track-reached-bg: rgba(242, 169, 59, 0.09);
    --track-vm: #A78BFA;
    --track-vm-bg: rgba(167, 139, 250, 0.10);
}

* { box-sizing: border-box; }

body {
    margin: 0;
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    color: var(--text);
    /* Solid dark everywhere; the only decorative background is the
       optional sunrise-corner gradient below (a per-user setting). */
    background: var(--bg);
}

/* --- Sunrise-corner background gradient --------------------------------
   A per-user Appearance setting (Settings > Appearance; stored on
   staff_users, injected onto <body> by base.html via the context
   processor in app.py). base.html renders .sunrise-glow + .sunrise-rays
   and sets data-sunrise-corner / data-sunrise-color only when the user
   has it enabled -- so when it's off there are no layers and no cost.

   Two fixed layers behind everything: a radial bloom from the chosen
   bottom corner(s) (bright core -> soft falloff) plus faint conic rays,
   both radially masked so they dissolve into the page's dark background
   with no seam. Colour is driven by one --sunrise-rgb triple. The per-
   corner pieces are custom props so "both" just stacks left + right. */
.sunrise-glow,
.sunrise-rays {
    position: fixed;
    inset: 0;
    pointer-events: none;
}
.sunrise-glow { z-index: -2; }
.sunrise-rays { z-index: -1; }

body {
    --sunrise-rgb: 255, 255, 255;

    --sunrise-glow-bl:
        radial-gradient(circle at 0% 100%, rgba(var(--sunrise-rgb), 0.32) 0%, rgba(var(--sunrise-rgb), 0) 13%),
        radial-gradient(135% 135% at 0% 100%, rgba(var(--sunrise-rgb), 0.13) 0%, rgba(var(--sunrise-rgb), 0.04) 30%, rgba(var(--sunrise-rgb), 0) 60%);
    --sunrise-glow-br:
        radial-gradient(circle at 100% 100%, rgba(var(--sunrise-rgb), 0.32) 0%, rgba(var(--sunrise-rgb), 0) 13%),
        radial-gradient(135% 135% at 100% 100%, rgba(var(--sunrise-rgb), 0.13) 0%, rgba(var(--sunrise-rgb), 0.04) 30%, rgba(var(--sunrise-rgb), 0) 60%);
    --sunrise-rays-bl: repeating-conic-gradient(from -90deg at 0% 100%,
        rgba(var(--sunrise-rgb), 0.055) 0deg 3deg, rgba(var(--sunrise-rgb), 0) 3deg 13deg);
    --sunrise-rays-br: repeating-conic-gradient(from 180deg at 100% 100%,
        rgba(var(--sunrise-rgb), 0.055) 0deg 3deg, rgba(var(--sunrise-rgb), 0) 3deg 13deg);
    --sunrise-mask-bl: radial-gradient(95% 95% at 0% 100%, #000 0%, rgba(0, 0, 0, 0.45) 24%, transparent 62%);
    --sunrise-mask-br: radial-gradient(95% 95% at 100% 100%, #000 0%, rgba(0, 0, 0, 0.45) 24%, transparent 62%);
    /* tighter reveal used only for "both", so the two ray fans keep to
       their own half and the middle stays clear */
    --sunrise-mask-bl-half: radial-gradient(72% 96% at 0% 100%, #000 0%, rgba(0, 0, 0, 0.4) 20%, transparent 54%);
    --sunrise-mask-br-half: radial-gradient(72% 96% at 100% 100%, #000 0%, rgba(0, 0, 0, 0.4) 20%, transparent 54%);
}
body[data-sunrise-color="brand"] { --sunrise-rgb: 120, 231, 219; }

body[data-sunrise-corner="bottom-left"] .sunrise-glow { background: var(--sunrise-glow-bl); }
body[data-sunrise-corner="bottom-left"] .sunrise-rays {
    background: var(--sunrise-rays-bl);
    -webkit-mask-image: var(--sunrise-mask-bl);
            mask-image: var(--sunrise-mask-bl);
}

body[data-sunrise-corner="bottom-right"] .sunrise-glow { background: var(--sunrise-glow-br); }
body[data-sunrise-corner="bottom-right"] .sunrise-rays {
    background: var(--sunrise-rays-br);
    -webkit-mask-image: var(--sunrise-mask-br);
            mask-image: var(--sunrise-mask-br);
}

/* Both corners at once -- one sun in each, same colour on each. */
body[data-sunrise-corner="both"] .sunrise-glow {
    background: var(--sunrise-glow-bl), var(--sunrise-glow-br);
}
body[data-sunrise-corner="both"] .sunrise-rays {
    background: var(--sunrise-rays-bl), var(--sunrise-rays-br);
    -webkit-mask-image: var(--sunrise-mask-bl-half), var(--sunrise-mask-br-half);
            mask-image: var(--sunrise-mask-bl-half), var(--sunrise-mask-br-half);
    -webkit-mask-composite: source-over;
            mask-composite: add;
}

@media (prefers-reduced-motion: reduce) {
    /* static gradient anyway -- nothing animates -- but keep the rays
       calmer for anyone who's asked for less visual noise */
    .sunrise-rays { opacity: 0.6; }
}

h1, h2, h3, .brand, .page-header h1 {
    font-family: 'Barlow Condensed', 'Inter', sans-serif;
    font-weight: 700;
    letter-spacing: -0.01em;
}

th, .tile-label, .eyebrow {
    font-family: 'Space Mono', 'Inter', monospace;
}

.topbar, .panel, .tile, .pipeline-column, .pipeline-card, .source-card,
.pipeline-stats, input, select, textarea, .btn {
    transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}

a {
    color: var(--brand-text);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

.topbar {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    /* Own stacking context above page content so the inbox dropdown
       (.inbox-panel) paints over pipeline cards etc. */
    position: relative;
    z-index: 20;
}

.topbar-inner {
    padding: 14px 24px;
    display: flex;
    align-items: center;
    gap: 32px;
}

.brand {
    font-weight: 700;
    font-size: 1.35rem;
    /* It's a link to the Pipeline (home) now -- keep the plain logo look,
       not the default anchor colour/underline. */
    color: var(--text);
}

.brand:hover {
    text-decoration: none;
}

nav a {
    margin-right: 20px;
    text-decoration: none;
    color: var(--text-muted);
    font-size: 0.95rem;
}

nav a.active {
    color: var(--brand-text);
    font-weight: 600;
}

/* Notifications bell in the header -- shares the message-inbox icon
   styling (.inbox-toggle / .inbox-icon / .inbox-badge); this just adds
   the current-page highlight, matching .inbox-toggle[aria-expanded]. */
.topbar-bell { text-decoration: none; }
.topbar-bell-active {
    color: var(--brand-text);
    background: var(--surface-2);
}

/* --- Notifications tab (templates/notifications/index.html) --- */
.notif-card { margin-bottom: 14px; }
.notif-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    flex-wrap: wrap;
}
.notif-outcome .notif-head span { font-size: 1rem; }
.notif-actions {
    display: flex;
    gap: 10px;
    margin-top: 12px;
}
.notif-appt-table { width: 100%; border-collapse: collapse; margin-top: 12px; }
.notif-appt-table th, .notif-appt-table td {
    text-align: left;
    padding: 6px 10px;
    border-bottom: 1px solid var(--border);
}
.notif-resolved { margin-top: 8px; }
.notif-resolved summary { cursor: pointer; color: var(--text-muted); font-size: 0.9rem; }
.notif-resolved-list { margin: 8px 0 0; padding-left: 18px; color: var(--text-muted); }

/* Account name + inbox icon grouped at the far right of the bar. */
.topbar-account {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 14px;
}

.tenant-indicator {
    color: var(--text-muted);
    font-size: 0.8rem;
}

/* --- Global header inbox (base.html + static/js/inbox.js) -------------
   Message icon + numeric badge on every page but Settings; badge counts
   text threads where the customer sent the last message and no one has
   replied. Click opens a WhatsApp/iMessage-style conversation list --
   unanswered threads bold + highlighted like unread email. Sits by the
   account name at the right end of the bar. */
.topbar-inbox {
    position: relative;
    display: flex;
    align-items: center;
}

.inbox-toggle {
    position: relative;
    display: inline-flex;
    padding: 6px;
    border: 0;
    border-radius: 8px;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
    line-height: 0;
    transition: color 0.15s ease, background-color 0.15s ease;
}

.inbox-toggle:hover {
    color: var(--text);
    background: var(--surface-2);
}

.inbox-toggle[aria-expanded="true"] {
    color: var(--brand-text);
    background: var(--surface-2);
}

.inbox-icon {
    width: 22px;
    height: 22px;
    fill: currentColor;
    display: block;
}

.inbox-badge {
    position: absolute;
    top: -2px;
    right: -3px;
    box-sizing: border-box;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    border-radius: 999px;
    border: 2px solid var(--surface);
    background: var(--danger-solid);
    color: #fff;
    font-family: 'Inter', sans-serif;
    font-size: 0.66rem;
    font-weight: 700;
    line-height: 13px;
    text-align: center;
}

.inbox-panel {
    position: absolute;
    top: calc(100% + 10px);
    /* Right-anchored: the widget sits near the right edge of the bar. */
    right: 0;
    left: auto;
    width: 344px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: 0 14px 36px rgba(0, 0, 0, 0.5);
    overflow: hidden;
    z-index: 50;
}

.inbox-panel-header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: 12px 14px;
    border-bottom: 1px solid var(--border);
    font-family: 'Barlow Condensed', 'Inter', sans-serif;
    font-weight: 700;
    font-size: 1.05rem;
    letter-spacing: -0.01em;
}

.inbox-panel-sub {
    font-family: 'Inter', sans-serif;
    font-weight: 500;
    font-size: 0.75rem;
    color: var(--text-muted);
    letter-spacing: 0;
}

.inbox-list {
    list-style: none;
    margin: 0;
    padding: 0;
    /* ~4 rows visible, then scroll. */
    max-height: 296px;
    overflow-y: auto;
}

.inbox-row {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 10px 14px;
    border-bottom: 1px solid var(--border);
    cursor: pointer;
    transition: background-color 0.12s ease;
}

.inbox-row:last-child {
    border-bottom: 0;
}

.inbox-row:hover {
    background: var(--surface-2);
}

.inbox-row:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -2px;
}

.inbox-row-name {
    font-size: 0.85rem;
    font-weight: 500;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.inbox-row-preview {
    font-size: 0.82rem;
    color: var(--text-faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Needs a reply -- unread-email treatment: highlighted, bold, accent edge. */
.inbox-row-unread {
    background: var(--brand-bg);
    box-shadow: inset 3px 0 0 var(--brand);
}

.inbox-row-unread:hover {
    background: color-mix(in srgb, var(--brand) 18%, var(--surface));
}

.inbox-row-unread .inbox-row-name {
    color: var(--text);
    font-weight: 700;
}

.inbox-row-unread .inbox-row-preview {
    color: var(--text);
    font-weight: 600;
}

.inbox-empty {
    margin: 0;
    padding: 22px 14px;
    text-align: center;
    color: var(--text-muted);
    font-size: 0.85rem;
}

/* --- Messaging page (templates/messaging/index.html) -----------------
   Dealer-wide conversation list -- reuses the .inbox-row read/unread
   convention from the header dropdown -- plus a compose card for texting
   or calling a number that isn't a lead yet. No new tokens; same panels,
   inputs and buttons as the rest of the app. */
.msg-compose {
    margin-bottom: 20px;
}

.msg-compose-fields {
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
    margin-bottom: 12px;
}

.msg-compose label {
    display: flex;
    flex-direction: column;
    gap: 5px;
    font-size: 0.8rem;
    color: var(--text-muted);
    flex: 1 1 220px;
}

.msg-compose textarea {
    resize: vertical;
    min-height: 60px;
    font-family: inherit;
}

.msg-optional {
    color: var(--text-faint);
}

.msg-compose-actions {
    display: flex;
    gap: 10px;
    margin-top: 14px;
}

.msg-compose-hint {
    margin: 10px 0 0;
    font-size: 0.78rem;
    color: var(--text-faint);
}

/* Thread list -- same rows as the header .inbox-list, but on a full page
   it expands to its full height and scrolls with the page rather than in
   its own box (drop .inbox-list's max-height + overflow). Adds a
   timestamp on each row. */
.msg-thread-list {
    max-height: none;
    overflow-y: visible;
}

/* Row now carries the tappable main area + a trash / restore control at
   its far right, so it lays out as a row, not the header dropdown's
   stacked column. */
.msg-thread-list .inbox-row {
    padding: 0;
    flex-direction: row;
    align-items: stretch;
    cursor: default;
}

.msg-thread-main {
    display: flex;
    flex: 1 1 auto;
    min-width: 0;
    flex-direction: column;
    gap: 3px;
    padding: 12px 14px;
    color: inherit;
    text-decoration: none;
}

.msg-thread-main:hover {
    text-decoration: none;
}

/* Read/unread toggle -- a small dot at the right of a Chat Box thread row,
   same idea as most email inboxes' own toggle (filled = unread, empty
   outline = read; clicking flips it). Sits left of the trash icon. */
.msg-read-toggle {
    display: flex;
    flex: none;
    align-items: center;
}

.msg-read-toggle-btn {
    width: 12px;
    height: 12px;
    padding: 0;
    border: 1.5px solid var(--text-faint);
    border-radius: 50%;
    background: transparent;
    cursor: pointer;
    transition: background-color 0.12s ease, border-color 0.12s ease;
}

.msg-read-toggle-btn:hover {
    border-color: var(--brand);
}

.msg-read-toggle-btn-unread {
    background: var(--brand);
    border-color: var(--brand);
}

.msg-read-toggle-btn:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

/* Trash icon at the far right of a Texts thread / Calls log row. */
.msg-log-delete {
    display: flex;
    flex: none;
}

.msg-log-delete-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 14px;
    border: none;
    background: transparent;
    color: var(--text-faint);
    cursor: pointer;
    transition: color 0.12s ease, background 0.12s ease;
}

.msg-log-delete-btn:hover {
    color: var(--hot);
    background: var(--hot-bg);
}

.msg-log-delete-btn:focus-visible {
    outline: 2px solid var(--hot);
    outline-offset: -2px;
}

.trash-icon {
    width: 16px;
    height: 16px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Restore control in the Deleted Logs tab. */
.msg-log-restore {
    display: flex;
    flex: none;
    align-items: center;
    padding: 0 14px;
}

/* Calls table: trash cell hugs the right edge. */
.call-log-actions {
    width: 1%;
    white-space: nowrap;
    text-align: right;
}

.call-log-actions .msg-log-delete-btn {
    padding: 4px 6px;
    border-radius: 6px;
}

/* Third folder tab (Deleted Logs) pushed to the right end of the strip. */
.folder-tab--end {
    margin-left: auto;
}

/* "STOP CUSTOMERS" tab -- larger, bolder, uppercase (in addition to the
   --danger red treatment) so it reads as its own emphasized destination. */
.folder-tab--emphasis {
    font-size: 1.18rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* --- Settings > Admin Control (templates/settings/admin_control.html) --
   Owner-only employee management. Reuses the app's panels, table and
   buttons; only the invite row, the per-employee action cluster and the
   one-time secret box need their own rules. */
.admin-notice {
    border-color: var(--brand);
    margin-bottom: 20px;
}

.admin-notice p {
    margin: 0;
}

.admin-secret {
    display: block;
    margin-top: 10px;
    padding: 10px 12px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 8px;
    font-family: 'Space Mono', 'Inter', monospace;
    font-size: 0.82rem;
    color: var(--text);
    word-break: break-all;
}

.admin-invite-row {
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
    align-items: flex-end;
}

.admin-invite-row label {
    display: flex;
    flex-direction: column;
    gap: 5px;
    font-size: 0.8rem;
    color: var(--text-muted);
}

.admin-invite-row input {
    min-width: 240px;
}

.admin-row-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
}

.admin-staff-actions {
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: flex-start;
}

.admin-temp-form {
    display: flex;
    gap: 6px;
    align-items: center;
}

.admin-temp-form input {
    width: 220px;
}

.msg-thread-top {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
}

.msg-thread-time {
    flex: none;
    font-size: 0.72rem;
    color: var(--text-faint);
    white-space: nowrap;
}

.msg-thread-list .inbox-row-preview {
    /* room for the whole first line of the message on a wide page */
    max-width: 100%;
}

/* Calls tab -- "Call through Nova" box (same panel/compose treatment as
   the Texts tab's box) with the dial pad sitting to its right. */
.call-compose-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 20px;
    margin-bottom: 20px;
}

.call-compose {
    flex: 1 1 340px;
    margin-bottom: 0;
}

.dialpad {
    flex: 0 0 auto;
}

.dialpad-grid {
    display: grid;
    grid-template-columns: repeat(3, 58px);
    gap: 8px;
}

/* Outline keys, same convention as the folder tabs / secondary buttons. */
.dialpad-key {
    appearance: none;
    -webkit-appearance: none;
    font-family: 'Barlow Condensed', 'Inter', sans-serif;
    font-size: 1.2rem;
    font-weight: 700;
    height: 46px;
    color: var(--text);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 10px;
    cursor: pointer;
    transition: color 0.12s ease, background 0.12s ease, border-color 0.12s ease;
}

.dialpad-key:hover {
    color: var(--brand);
    border-color: var(--brand);
    background: var(--surface-2);
}

.dialpad-key:active {
    background: var(--brand-bg);
}

.dialpad-key:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -2px;
}

/* Calls tab -- a plain log table (reuses the app's <table> + .status
   pill styling); only the direction cell and the secondary number line
   need their own rules. */
.call-log-scroll {
    overflow-x: auto;
}

.call-log {
    width: 100%;
}

.call-log-number {
    display: block;
    margin-top: 2px;
    font-size: 0.75rem;
    color: var(--text-faint);
}

.call-dir {
    font-size: 0.8rem;
    color: var(--text-muted);
    white-space: nowrap;
}

.call-dir-inbound {
    color: var(--brand-text);
}

/* (The Appearance dark-mode toggle and static/js/theme.js were removed --
   the whole app is one dark theme now. The functional inventory-source
   on/off switch further down, .source-toggle, is a different control and
   stays.) */

.page {
    padding: 24px;
}

h1 {
    font-size: 1.5rem;
    margin: 0 0 20px;
}

h2 {
    font-size: 1.05rem;
    margin: 0 0 12px;
}

.tiles {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 16px;
    margin-bottom: 28px;
}

.tile {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 18px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
    padding: 18px 20px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.tile-label {
    font-size: 0.85rem;
    color: var(--text-muted);
}

.tile-value {
    font-size: 2rem;
    font-weight: 700;
}

/* Status tiles: colour-tinted border + value, neutral (surface) fill --
   the app-wide colored-outline convention, never a filled colour block. */

.tile-blue {
    border-color: var(--brand-border);
}

.tile-blue .tile-value {
    color: var(--brand);
}

.tile-yellow {
    border-color: var(--gold-border);
}

.tile-yellow .tile-value {
    color: var(--gold);
}

.tile-danger {
    border-color: var(--hot-border);
}

.tile-danger .tile-value {
    color: var(--hot);
}

/* A tile that's a link (Pipeline footer: Wins -> Leads/Sold, Close Rate
   -> Sales Trend). Keeps the plain tile look, just an accent edge +
   surface lift on hover, no anchor colour/underline. */
a.tile-link {
    text-decoration: none;
    color: inherit;
    transition: border-color 0.12s ease, background 0.12s ease;
}

a.tile-link:hover {
    text-decoration: none;
    border-color: var(--brand);
    background: var(--surface-2);
}

a.tile-link:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

/* --- Sales Trend page (templates/pipeline_sales_trend.html) ----------
   Hand-drawn SVG bar chart, same tokens/fonts as the rest of the app;
   bars use the colour-tinted-outline convention (translucent fill +
   brand border), not a solid block. */
.sales-trend-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 16px;
    flex-wrap: wrap;
    margin-bottom: 20px;
}

.sales-trend-head h1 {
    margin: 0;
}

.sales-trend-bar {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 16px;
    margin-bottom: 6px;
}

.sales-trend-range {
    min-width: 150px;
    text-align: center;
    font-family: 'Space Mono', 'Inter', monospace;
    font-size: 0.8rem;
    color: var(--text-muted);
}

.sales-trend-bar .btn[disabled] {
    opacity: 0.4;
    pointer-events: none;
}

.sales-trend-chart {
    width: 100%;
    overflow-x: auto;
}

.sales-trend-svg {
    display: block;
    width: 100%;
    max-width: 620px;
    height: auto;
    margin: 0 auto;
}

.trend-grid {
    stroke: var(--border);
    stroke-width: 1;
}

.trend-axis {
    stroke: var(--text-faint);
    stroke-width: 1;
}

.trend-ylabel,
.trend-xlabel {
    fill: var(--text-muted);
    font-family: 'Space Mono', 'Inter', monospace;
    font-size: 11px;
}

.trend-value {
    fill: var(--text);
    font-family: 'Inter', sans-serif;
    font-size: 13px;
    font-weight: 700;
}

.trend-bar {
    fill: var(--brand-bg);
    stroke: var(--brand);
    stroke-width: 1.5;
}

.sales-trend-note {
    margin: 12px 0 0;
    text-align: center;
    font-size: 0.78rem;
    color: var(--text-faint);
}

.panels {
    display: flex;
    flex-direction: column;
    gap: 20px;
}

.panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 18px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
    padding: 18px 20px;
}

table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9rem;
}

th, td {
    text-align: left;
    padding: 8px 10px;
    border-bottom: 1px solid var(--border);
}

th {
    color: var(--text-muted);
    font-weight: 600;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

tr:last-child td {
    border-bottom: none;
}

.empty {
    color: var(--text-muted);
    font-size: 0.9rem;
    margin: 4px 0;
}

/* Row-level highlight for a confirmed Sold/Lost outcome (Leads and
   Customers grids). Colored-outline convention, adapted to a table row:
   no fill, status-coloured text, and a tinted bar down the first cell. */
.row-sold, .row-lost {
    background: transparent;
}

.row-sold { color: var(--gold); }
.row-lost { color: var(--text-muted); }

.row-sold td:first-child { box-shadow: inset 3px 0 var(--gold-border); }
.row-lost td:first-child { box-shadow: inset 3px 0 var(--cold-border); }

.row-sold a, .row-lost a {
    color: inherit;
}

/* Status / badge pills -- colored-outline convention: transparent fill,
   a border tinted from the pill's own text colour, coloured text. Each
   variant only needs to set `color`; the border follows via color-mix. */
.status {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.78rem;
    text-transform: capitalize;
    background: transparent;
    border: 1px solid;
    border-color: color-mix(in srgb, currentColor 45%, transparent);
}

.status-scheduled { color: var(--brand-text); }
.status-completed { color: var(--green-text); }
.status-missed { color: var(--hot); }
.status-failed { color: var(--violet); }
.status-cancelled { color: var(--text-muted); }
.status-sold { color: var(--green-text); }
.status-lost { color: var(--text-muted); }
.status-cold { color: var(--brand-text); }
.status-voicemail { color: var(--attention); }
/* Call Scheduler table's "Next Call" column (see
   templates/outbound_calls/list.html) -- reuses the backend's own
   action_due flag (queries.get_outbound_queue_for_dealer) rather than
   recomputing "is this in the past" in the template, so this badge and
   the Step column's "due" wording can never disagree about the same row.
   --hot, not --attention/--missed -- distinct from both the Voicemail
   outcome badge and a resolved "No Answer" outcome badge in the Outcome
   column, since this is flagging a still-pending row whose own schedule
   has slipped, not a completed call's result. */
.status-overdue { color: var(--hot); margin-left: 6px; }

.attempt-dots {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

.attempt-dot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    font-size: 0.7rem;
    line-height: 1;
    background: var(--cold-bg);
    border: 1px solid var(--border);
    color: transparent;
}

.attempt-dot.done {
    background: var(--green);
    border-color: var(--green);
    color: #0B0C0E;
}


.badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.78rem;
    font-weight: 600;
    margin-right: 4px;
    text-decoration: none;
    background: transparent;
    border: 1px solid;
    border-color: color-mix(in srgb, currentColor 45%, transparent);
}

.badge-attention { color: var(--attention); }
.badge-unread { color: var(--brand-text); }
.badge-repeat { color: var(--brand-text); }
.badge-repeat:hover { text-decoration: underline; }
/* Simple View only -- a merged Voicemail-Attempts card sits in an FU
   sub-column by its real attempt number even though it's red/never-
   reached, so this spells the number out instead of leaving it to be
   inferred from column position alone (see templates/pipeline.html's
   pcard macro). */
.badge-unreached-attempt { color: var(--hot); }

/* "What's next" indicator (queries._next_action) -- a layer on top of
   pipeline_stage, not a replacement, so it's a full-width pill of its own
   rather than squeezed inline with those. Colors reuse the same tokens
   status badges already use, remapped to urgency, so the palette stays
   consistent across the app instead of introducing a second color
   language. */
.badge-action {
    display: block;
    width: fit-content;
    margin: 4px 0;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 600;
    background: transparent;
    border: 1px solid;
    border-color: color-mix(in srgb, currentColor 45%, transparent);
}
.badge-action-overdue { color: var(--hot); }
.badge-action-appt_upcoming { color: var(--green-text); }
.badge-action-manual_review { color: var(--attention); }
.badge-action-call_now { color: var(--hot); }
.badge-action-text_due { color: var(--brand-text); }
.badge-action-awaiting_reply { color: var(--cold); }
.badge-action-try_tomorrow { color: var(--text-muted); }
.badge-action-follow_up { color: var(--attention); }

/* --- Folder-tab view switchers (app-wide convention) ------------------
   Browser-tab / manila-folder metaphor, used for every view switcher
   (Pipeline Bubbles/Columns, Call Scheduler Timeline/Table, Leads status
   + what's-next rows). The strip's bottom border is the top edge of the
   content below; the ACTIVE tab is accent-coloured text + accent
   outline, bottom-open, filled with --tab-fill (the colour of whatever
   sits directly below it) and pulled 1px down to cover the baseline, so
   it reads as one continuous surface with that content. INACTIVE tabs
   sit flat behind the baseline like unselected folder tabs and only lift
   (fill + border) on hover.

   --tab-fill defaults to the panel surface (most content is a .panel);
   a switcher whose tabs open straight onto the page background adds
   .folder-tabs--on-page. */
.folder-tabs {
    --tab-fill: var(--surface);
    display: flex;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: 4px;
    padding: 0 4px;
    margin-bottom: 0;
    border-bottom: 1px solid var(--border);
}

/* Tabs that open straight onto the page (Pipeline -- no content panel).
   The gap below is the same colour as the active tab's fill, so it still
   reads as one continuous surface. */
.folder-tabs--on-page {
    --tab-fill: var(--bg);
    margin-bottom: 22px;
}

/* Tabs backed by a .panel (Call Scheduler, Leads): drop the panel's own
   top edge so the strip's baseline IS the panel's top and the active tab
   breaks through it into the panel. The second selector covers a switcher
   with two stacked view panels (Scheduler's timeline + table). */
.folder-tabs + .panel,
.folder-tabs + .panel + .panel {
    border-top-color: transparent;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

.folder-tab {
    appearance: none;
    -webkit-appearance: none;
    font: inherit;
    font-family: 'Barlow Condensed', 'Inter', sans-serif;
    font-weight: 700;
    font-size: 1rem;
    letter-spacing: 0.02em;
    line-height: 1.2;
    cursor: pointer;
    text-decoration: none;
    padding: 8px 24px;
    color: var(--text-muted);
    background: transparent;
    border: 1px solid transparent;
    border-bottom: none;
    border-radius: 12px 12px 0 0;
    transition: color 0.12s ease, background 0.12s ease, border-color 0.12s ease;
}

.folder-tab:hover:not(.active) {
    color: var(--text);
    background: var(--surface-2);
    border-color: var(--border);
}

.folder-tab.active {
    color: var(--brand);
    background: var(--tab-fill);
    border-color: var(--brand);
    /* covers the strip's baseline -> tab merges into the content below */
    margin-bottom: -1px;
    padding-bottom: 9px;
}

.folder-tab:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -2px;
}

/* A page action (e.g. "+ Add Lead") riding the right end of a folder-tab
   row instead of stacked above it -- pushed right by an auto margin and
   floated a few px clear of the strip's baseline so it doesn't read as a
   tab sitting on the line. */
.folder-tabs-action {
    flex: none;
    margin-left: auto;
    margin-bottom: 5px;
    align-self: flex-end;
}

/* "+ Add Appointment" + "+ Add Lead" (Pipeline board only) -- grouped into
   one flex item rather than two separate .folder-tabs-action elements, so
   they always move and wrap together as a single unit on a narrow
   viewport. Two independent auto-margin items can end up stranded on
   different flex-wrap lines (one riding the tab row, the other alone on
   its own line below) -- exactly the "floating separately" bug this
   replaces. */
.folder-tabs-action-group {
    display: flex;
    gap: 8px;
    flex: none;
    flex-wrap: wrap;
    margin-left: auto;
    margin-bottom: 5px;
    align-self: flex-end;
}

/* Two stacked rows (Leads: status = primary, what's next = nested sub).
   The primary row butts straight onto the sub-row's faintly recessed
   shelf (its active tab fills with that shelf colour and merges into it);
   the sub row's own active tab then merges into the .panel below. */
.folder-tabs--primary {
    margin-bottom: 0;
    --tab-fill: var(--surface-2);
}

.folder-tabs--sub {
    background: var(--surface-2);
    border-radius: 10px 10px 0 0;
    padding: 5px 6px 0 14px;
}

.folder-tabs--sub .folder-tab {
    font-size: 0.82rem;
    font-weight: 600;
    padding: 6px 16px;
}

.folder-tabs--sub .folder-tab.active {
    padding-bottom: 7px;
}

/* The Deleted tab -- same folder-tab behaviour, but red instead of the
   teal accent, both in its label text and (when active) its outline, so
   it reads as the "danger" destination it is. */
.folder-tab--danger {
    color: var(--hot);
}
.folder-tab--danger:hover:not(.active) {
    color: var(--hot);
}
.folder-tab--danger.active {
    color: var(--hot);
    border-color: var(--hot);
}
.folder-tab--danger:focus-visible {
    outline-color: var(--hot);
}

.timeline-legend {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 16px;
    margin-bottom: 16px;
    font-size: 0.82rem;
    color: var(--text-muted);
}
.timeline-legend-swatch, .timeline-icon {
    fill: none;
    stroke: currentColor;
    stroke-width: 2px;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.timeline-legend-swatch {
    display: inline-block;
    width: 16px;
    height: 16px;
    margin-right: 5px;
    vertical-align: middle;
}
/* Legend versions of the two composite icons (see .timeline-icon-badge
   in the timeline track's own CSS, further down) -- a small wrapper so
   the badge glyph has something to position itself against without
   disturbing the swatch's own inline layout in the legend row. Simpler
   than the track's version (top/right instead of a transform) since this
   context is static, not a %-based track position. */
.timeline-legend-composite {
    position: relative;
    display: inline-block;
    width: 16px;
    height: 16px;
    margin-right: 5px;
    vertical-align: middle;
}
.timeline-legend-composite .timeline-legend-swatch {
    margin-right: 0;
}
.timeline-legend-badge {
    position: absolute;
    top: -3px;
    right: -3px;
    width: 9px;
    height: 9px;
    fill: none;
    stroke: var(--green);
    stroke-width: 4px;
    stroke-linecap: round;
    stroke-linejoin: round;
    background: var(--surface);
    border-radius: 50%;
}

.timeline-body {
    position: relative;
    overflow-x: auto;
}

/* Relative-date gridline row (see routes/outbound_calls.py's
   _header_labels) -- reuses .timeline-row-label's exact width as a blank
   spacer so the label track below lines up perfectly with every row's
   own track, rather than duplicating the flex math. */
.timeline-header-row {
    display: flex;
    gap: var(--timeline-track-gap);
    margin-bottom: 4px;
}
.timeline-header-track {
    position: relative;
    flex: 1 1 auto;
    height: 16px;
    min-width: 400px;
}
.timeline-header-label {
    position: absolute;
    top: 0;
    font-size: 0.72rem;
    color: var(--text-muted);
    white-space: nowrap;
}
.timeline-header-label-start { transform: translateX(0); }
.timeline-header-label-center { transform: translateX(-50%); }
.timeline-header-label-end { transform: translateX(-100%); }

/* One shared vertical line across every row, positioned by the same
   global calendar-time mapping every row's dots use (see
   routes/outbound_calls.py's _build_timeline) -- spans the full height
   of every row via top/bottom: 0 on a position:relative ancestor rather
   than per-row absolute lines that would need to agree on a height.
   Explicitly blue (var(--brand)) per request -- this is also the text
   icon's color, so a text icon sitting exactly on "today" can read as
   ambiguous (on the line, or just next to a same-colored one?) in a way
   it wouldn't with a neutral line; accepted as a known tradeoff rather
   than worked around. */
/* left is NOT plain X% of .timeline-body's own width -- .timeline-body
   spans the label column *and* the track column together, but every
   icon/header-label's X% is relative to just its own .timeline-track/
   .timeline-header-track, which starts after the label column + gap.
   Naive X% here landed ~46px off from where the same pct places an icon
   or the "Today" header label -- this calc reconstructs the track's own
   coordinate space (skip the label column + gap, then X% of what's left)
   so this line's position matches theirs exactly instead of coincidentally. */
.timeline-today-line {
    position: absolute;
    top: 0;
    bottom: 0;
    left: calc(var(--timeline-label-width) + var(--timeline-track-gap) + (100% - var(--timeline-label-width) - var(--timeline-track-gap)) * (var(--timeline-today-pct) / 100));
    width: 2px;
    background: var(--brand);
    opacity: 0.5;
    z-index: 1;
}

.timeline-row {
    display: flex;
    align-items: center;
    gap: var(--timeline-track-gap);
    padding: 10px 0;
    /* Subtle -- one flat 1px hairline vs. each row's own 3px, darker
       .timeline-thread, so rows read as visually separated without
       competing with a lead's actual cadence line for attention. */
    border-bottom: 1px solid var(--border);
}
.timeline-row:last-child { border-bottom: none; }

.timeline-row-label {
    flex: 0 0 var(--timeline-label-width);
    min-width: 0;
}
.timeline-row-vehicle {
    display: block;
    font-size: 0.78rem;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* 60px, not the 24px this used to be -- tall enough that a stacked pair
   (see .timeline-icon-stack-top/-bottom below) has room for two full-size
   26px .timeline-icon-ring circles with clear daylight between them
   instead of a single trunk line's worth of height. A row with no stacked
   pair still just centers its one icon in the middle of this taller box
   (top:50% on .timeline-icon doesn't care how tall the box is), so the
   extra height reads as a little more breathing room on every row, not a
   layout change for the common case. */
.timeline-track {
    position: relative;
    flex: 1 1 auto;
    height: 60px;
    min-width: 400px;
}

/* A bit heavier than a hairline -- roughly the same visual weight as the
   lead name text next to it (normal-weight, ~1rem) rather than a
   barely-there rule, but still clearly secondary to the icons sitting on
   it, and clearly bolder than .timeline-row's own 1px divider. A single
   straight polyline through every icon (see routes/outbound_calls.py's
   _build_thread_path) -- never broken with a gap at each icon -- fills the
   whole track so its viewBox's 0-100 x-range lines up exactly with every
   icon's own left:X% positioning. Almost every icon on every row sits at
   the same one y (see THREAD_TRUNK_Y), so almost every segment is flat;
   the one exception is the short near-vertical hop between a stacked
   pair (see .timeline-icon-stack-top/-bottom below and _assign_stacks) --
   still dead straight, just not flat, and that's deliberate: it's what
   visually says "these two belong together" instead of two unrelated
   dots that happen to land close together. The ring around each icon
   (see .timeline-icon-ring below) is opaque and sits above the line in
   stacking order, so the line just visually disappears under it rather
   than needing to stop exactly at the ring's edge itself.
   preserveAspectRatio="none" independently stretches x (0-100, matching
   pct) and y (0-60, matching this element's own pixel height) to fit,
   which is also why the path needs vector-effect="non-scaling-stroke" in
   the template -- without it, that same non-uniform stretch would smear
   the stroke's own thickness unevenly instead of just the path geometry. */
.timeline-thread {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}
.timeline-thread path {
    fill: none;
    stroke: var(--text-muted);
    stroke-width: 3px;
    stroke-linecap: round;
}

/* Icons replace the old plain colored dots -- shape now carries the
   event type (phone/message-bubble/check, see the <symbol> sprite in
   templates/outbound_calls/list.html) so color is reinforcement, not the
   only signal. top:50% + translate(-50%,-50%) centers on this element's
   OWN box regardless of its width/height, which is exactly why the
   smaller 12x12 arrow icon (see svg.timeline-icon.timeline-icon-more
   below) still lands on the identical vertical center as a full 16x16
   one -- centering math that's independent of size, not a coincidence
   that needed separate tuning. */
.timeline-icon {
    position: absolute;
    top: 50%;
    width: 16px;
    height: 16px;
    transform: translate(-50%, -50%);
    cursor: default;
    z-index: 2;
}
/* Wraps a clickable icon (see dot.link_url in
   routes/outbound_calls.py) without disturbing the icon's own absolute
   positioning/lane placement -- display:contents removes the anchor's own
   box entirely, so the nested svg positions itself exactly as an
   unwrapped .timeline-icon would, against .timeline-track as usual. */
.timeline-icon-link {
    display: contents;
}
.timeline-icon-link .timeline-icon {
    cursor: pointer;
}
.timeline-icon-link .timeline-icon:hover {
    opacity: 0.7;
}
/* A clean circle around every icon (and the arrow), centered exactly on
   it -- a sibling element, not a border on the icon itself, since the
   icon's own box is a 16x16 <svg> and a border there would hug the
   glyph's bounding box rather than reading as a neat ring with breathing
   room around it. Fixed pixel diameter (not a percentage) for the same
   reason .timeline-icon itself is fixed-size: a true circle regardless of
   the track's fluid width, never an oval. Opaque background (matching
   .panel's own, so it blends into the row) is what makes
   .timeline-thread's line underneath look like it stops right at this
   ring's edge -- it doesn't actually stop there, it's just covered. Lower
   z-index than the icon on top of it (2) so the icon stays the visually
   dominant element at this point, the ring reading as a frame around it
   rather than competing with it. */
.timeline-icon-ring {
    position: absolute;
    top: 50%;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    border: 1.5px solid currentColor;
    background: var(--surface);
    transform: translate(-50%, -50%);
    z-index: 1;
}
.timeline-icon-text { color: var(--brand); }
.timeline-icon-call { color: #f97316; }
.timeline-icon-replied { color: var(--green); }
.timeline-icon-more { color: var(--text-muted); }
/* The "still going" arrow (see arrow_pct in routes/outbound_calls.py) --
   a lead whose cadence isn't finished gets one of these, positioned at
   its next known step's real time (or "now" if that's genuinely not
   known yet, e.g. mid-send), with the line extending on to it same as any
   other icon. A row whose cadence IS complete has no arrow at all, so the
   line -- never drawn past the icon list it's actually given -- simply
   ends at the final real icon instead. Neutral color (above) and smaller
   than a real event icon (below) so it never reads as an actual event;
   scoped to the svg specifically (not .timeline-icon-ring, which shares
   this same class only for its color) so its ring stays the normal ring
   size instead of shrinking to match. */
svg.timeline-icon.timeline-icon-more {
    width: 12px;
    height: 12px;
}

/* The "completed" badge (see dot.badge in routes/outbound_calls.py) -- a
   small, bold, green checkmark layered on the corner of a text or call
   icon to mean "text completed" (a reply came back) or "call completed"
   (it was answered), replacing what used to be two separate icons
   stacked/offset on top of each other at the same point. The base icon
   underneath keeps its own color unchanged (text stays blue, call stays
   orange) -- the badge is the only thing that's green, deliberately
   matching .timeline-icon-replied's green so "completed" reads as the
   same signal that color already carries elsewhere on this page.
   -badge-bg is a small opaque disc behind the checkmark glyph (same
   "opaque circle covers what's under it" trick as .timeline-icon-ring)
   so the badge reads as a clean layered mark instead of the base icon's
   own strokes showing through it. Both positioned identically to the
   base icon (left:X%; top:50%) then nudged to its upper-right corner via
   transform, so they move together with it as one unit; z-index above
   the base icon (2) so the badge is never the part that gets covered. */
.timeline-icon-badge-bg {
    position: absolute;
    top: 50%;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--surface);
    transform: translate(calc(-50% + 8px), calc(-50% - 8px));
    z-index: 2;
    pointer-events: none;
}
.timeline-icon-badge {
    position: absolute;
    top: 50%;
    width: 9px;
    height: 9px;
    fill: none;
    stroke: var(--green);
    stroke-width: 4px;
    stroke-linecap: round;
    stroke-linejoin: round;
    transform: translate(calc(-50% + 8px), calc(-50% - 8px));
    z-index: 3;
    pointer-events: none;
}

/* A same-day text + call that _assign_stacks (routes/outbound_calls.py)
   couldn't merge into one composite icon -- two distinct real events,
   rendered top and bottom instead of directly on top of each other. Fixed
   pixel `top` (not the usual 50%) overrides every element that shares
   this dot's x position -- the ring, the base icon, and (if this dot also
   carries a completed badge) the badge pair too, so the whole icon plus
   its badge moves as one unit exactly like the un-stacked case, just off
   the trunk. These two numbers are STACK_TOP_Y/STACK_BOTTOM_Y in
   routes/outbound_calls.py, kept numerically identical on purpose --
   that's what makes the connecting line's polyline (which uses those same
   two constants) land exactly on each stacked icon's actual center rather
   than needing a separate, easy-to-drift-out-of-sync source of truth.
   14px/46px, not perfectly symmetric-looking numbers, because they're
   solved from the ring's own fixed 26px diameter: each one sits exactly
   half a ring-diameter-plus-a-bit off center (top: 60/2 - 16, bottom:
   60/2 + 16), which is what actually guarantees the two 26px rings clear
   each other with a few pixels to spare instead of touching -- see
   .timeline-track's 60px height, sized for this same reason. Only ever
   two positions because _assign_stacks only ever produces pairs (see its
   own docstring on why 3+ doesn't arise here) -- a hypothetical third
   icon in the same cluster has nowhere else fixed to go and isn't
   supported. */
.timeline-icon.timeline-icon-stack-top,
.timeline-icon-ring.timeline-icon-stack-top,
.timeline-icon-badge-bg.timeline-icon-stack-top,
.timeline-icon-badge.timeline-icon-stack-top {
    top: 14px;
}
.timeline-icon.timeline-icon-stack-bottom,
.timeline-icon-ring.timeline-icon-stack-bottom,
.timeline-icon-badge-bg.timeline-icon-stack-bottom,
.timeline-icon-badge.timeline-icon-stack-bottom {
    top: 46px;
}

.touchpoint-link {
    color: var(--brand-text);
    font-weight: 600;
    text-decoration: none;
}

.touchpoint-link:hover {
    text-decoration: underline;
}

/* --- Page layout --- */

.page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 20px;
}

.page-header h1 {
    margin-bottom: 6px;
}

.badges {
    display: flex;
    gap: 6px;
}

.detail-grid {
    display: grid;
    /* Customer Info and Messages are capped narrow (a phone-width-ish
       proportion for Messages' stacked bubble thread, and Customer Info
       matches it so the row stays visually balanced) -- neither one's
       content (a short info list; a message thread that reflows fine at
       any width) needs more than that, and letting them grow with the
       viewport just left dead space either side of short content. Call
       Log takes 1fr, i.e. whatever's freed up, since its 5-column table
       is the one thing on this row that actually needs the room. minmax
       on it (not bare 1fr) keeps it from being crushed by the table's own
       content-based minimum before the wrap rules below even get a
       chance to apply. */
    grid-template-columns: minmax(240px, 300px) minmax(0, 1fr) minmax(240px, 300px);
    gap: 20px;
    margin-bottom: 20px;
    align-items: stretch;
}

/* Tall/narrow "iPhone Max" card shape: a fixed height (not just a minimum)
   is what makes "equal height" and "scroll internally, never truncate"
   both true at once -- content shorter than the card just leaves empty
   space below it; content taller scrolls within the card instead of
   pushing it (or the page) taller. Scoped to these three specifically,
   not the base .panel class used everywhere else in the app. */
.detail-grid > .panel {
    display: flex;
    flex-direction: column;
    height: 700px;
    overflow-y: auto;
}

/* Slightly smaller text (~10-15% down) specifically within these three
   cards, for a cleaner/denser look -- scoped to .detail-grid so the
   shared global label/textarea rules used by every other form in the
   app are untouched. Dropdowns (<select>) and buttons are deliberately
   excluded from this so they stay at normal, easily-clickable size. */
.detail-grid label {
    font-size: 0.75rem;
}

.detail-grid textarea {
    font-size: 0.8rem;
}

.info-list {
    display: grid;
    /* minmax(0, 1fr), not bare 1fr: a long unbroken value (an email
       address) otherwise forces this column past the narrow Lead Info
       card's width instead of wrapping within it. */
    grid-template-columns: auto minmax(0, 1fr);
    gap: 8px 16px;
    margin: 0;
}

.info-list dt {
    color: var(--text-muted);
    font-size: 0.75rem;
}

.info-list dd {
    margin: 0;
    overflow-wrap: break-word;
    font-size: 0.88rem;
}


/* --- Buttons --- */

.btn {
    display: inline-block;
    text-decoration: none;
    border-radius: 18px;
    padding: 9px 18px;
    font-size: 0.9rem;
    font-weight: 600;
    border: 1px solid transparent;
    cursor: pointer;
}

.btn-primary {
    background: var(--brand);
    color: #072522;
}

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

.btn-sm {
    padding: 4px 10px;
    font-size: 0.8rem;
}

/* --- Forms --- */

.form-panel {
    max-width: 640px;
}

.form-panel form {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

/* Appearance page: the sunrise box, the Default-view box and the Column
   View Edit box side by side (stacking on a narrow screen), in a single
   form with one Save. align-items: stretch keeps all three the same
   height/width regardless of how much each one holds. */
.appearance-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 20px;
    align-items: stretch;
    max-width: 1040px;
    margin-bottom: 16px;
}

.appearance-box {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.settings-section-title {
    font-family: 'Barlow Condensed', 'Inter', sans-serif;
    font-size: 1.1rem;
    margin: 0;
}

.settings-fieldset {
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 12px 14px;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* Business Profile > Store hours: 7-day open/close grid. */
.form-panel fieldset {
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 12px 14px;
    margin: 0;
}
.form-panel fieldset legend {
    font-weight: 600;
    padding: 0 6px;
}
.store-hours-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 10px;
}
.store-hours-table th,
.store-hours-table td {
    padding: 6px 8px;
    text-align: left;
    border-bottom: 1px solid var(--border);
}
.store-hours-table th:not(:first-child),
.store-hours-table td:not(:first-child) {
    text-align: center;
    white-space: nowrap;
}
.store-hours-table select {
    min-width: 7.5rem;
}

.settings-fieldset legend {
    padding: 0 6px;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
}

.form-row {
    display: flex;
    gap: 14px;
}

.form-row label {
    flex: 1;
}

label {
    display: flex;
    flex-direction: column;
    gap: 4px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.full-width {
    width: 100%;
}

.checkbox-label {
    flex-direction: row;
    align-items: center;
    gap: 8px;
    justify-content: flex-start;
}

input[type="text"], input[type="email"], input[type="datetime-local"],
input[type="number"], input[type="time"], input[type="url"],
select, textarea {
    font-family: inherit;
    font-size: 0.9rem;
    color: var(--text);
    padding: 8px 10px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--surface);
}

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

.form-error {
    background: transparent;
    border: 1px solid var(--hot-border);
    color: var(--hot);
    padding: 8px 12px;
    border-radius: 14px;
    font-size: 0.88rem;
    margin: 0 0 14px;
}

/* One-line explainer under an h1 for a form whose purpose isn't obvious
   from its title alone (e.g. leads/appointment_form.html's "Set
   Appointment" -- what it skips, not just what it does). */
.form-subtitle {
    color: var(--text-muted);
    font-size: 0.92rem;
    margin: -8px 0 18px;
    max-width: 60ch;
}

.form-success {
    background: transparent;
    border: 1px solid var(--green-border);
    color: var(--green-text);
    padding: 8px 12px;
    border-radius: 14px;
    font-size: 0.88rem;
    margin: 0 0 14px;
}

.nova-section {
    margin-bottom: 20px;
}

.nova-checkbox-row {
    flex-wrap: wrap;
    gap: 16px;
}

.nova-checkbox-row .checkbox-label {
    flex: 0 0 auto;
}

.inline-form {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.status-form {
    display: flex;
    gap: 6px;
    align-items: center;
}

/* --- Pipeline stats footer --- */
/* Fixed to the viewport bottom, so .pipeline-board below needs matching
   bottom spacing (see margin-bottom there and in the mobile breakpoint)
   or its last row of cards would sit hidden underneath this bar. */

.pipeline-stats {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 100;
    grid-template-columns: repeat(5, 1fr);
    margin: 0;
    background: var(--bg);
    border-top: 1px solid var(--border);
    box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.08);
    padding: 10px 24px;
}

.pipeline-stats .tile {
    align-items: center;
    text-align: center;
    padding: 10px 12px;
    gap: 2px;
}

.pipeline-stats .tile-value {
    font-size: 1.4rem;
}

.pipeline-stats .tile-label {
    font-size: 0.78rem;
}

/* --- Pipeline search --- */
/* Same "bubble" card look as .tile (background/border/radius/shadow),
   just slim and wide instead of tall. Width is calc(50% - 8px): with
   .tiles being a 4-column grid with a 16px gap, that's the exact width
   of 2 of those 4 columns plus the gap between them -- "2 boxes worth"
   at any viewport size, not a guessed pixel value. Specificity note: the
   descendant selector below (two classes + element/attribute)
   legitimately outranks the shared global input[type="text"] rule, so it
   overrides it without needing an ID hack. */

.pipeline-search-bar {
    width: calc(50% - 8px);
    margin: 0 auto 20px;
    display: flex;
    justify-content: center;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 18px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
    padding: 1px 16px;
}

.pipeline-search-input-wrap {
    position: relative;
    width: 100%;
}

.pipeline-search-input-wrap input[type="text"] {
    width: 100%;
    height: 28px;
    padding: 0 28px;
    border: none;
    border-radius: 18px;
    background: transparent;
    text-align: center;
}

.pipeline-search-input-wrap input[type="text"]:focus {
    outline: none;
}

.pipeline-search-clear {
    position: absolute;
    top: 50%;
    right: 4px;
    transform: translateY(-50%);
    border: none;
    background: none;
    padding: 4px;
    font-size: 1.05rem;
    line-height: 1;
    color: var(--text-muted);
    cursor: pointer;
}

.pipeline-search-clear:hover {
    color: var(--text);
}

/* --- Pipeline board --- */

/* Thin full-width divider between the summary tiles and the board itself,
   hinting that cards below are draggable between columns. margin-bottom
   (not just the .tiles bottom margin above it) is what pushes the columns
   down far enough that this doesn't feel cramped against either side. */
.pipeline-drag-hint {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0 0 28px;
}

/* Each side of the text is two equal-flex line segments, so the triple-arrow
   group dropped between them lands exactly at the halfway point between the
   text and the single arrow capping the far end of the line. */
.pipeline-drag-hint-line {
    flex: 1 1 auto;
    height: 1px;
    background: var(--border);
}

.pipeline-drag-hint-text {
    flex: 0 0 auto;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    white-space: nowrap;
}

/* Stack of three arrow glyphs shared by the drag hint banner and the card
   edge indicators below, so both read as the same "this can move
   horizontally" symbol. */
.arrow-lr-icon {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    line-height: 1;
    gap: 1px;
}

.arrow-lr-icon span {
    display: block;
    font-size: 0.7rem;
}

.pipeline-drag-hint-icon,
.pipeline-drag-hint-arrow-end {
    flex: 0 0 auto;
    color: var(--text-muted);
}

/* Banner variant of the shared arrow stack: laid out as a horizontal row
   instead of the vertical stack the card edges use. */
.pipeline-drag-hint-icon {
    flex-direction: row;
    gap: 3px;
}

.pipeline-drag-hint-arrow-end {
    font-size: 0.7rem;
    line-height: 1;
}

.pipeline-board {
    display: flex;
    gap: 16px;
    align-items: flex-start;
    /* Clears the fixed .pipeline-stats bar (one row tall on desktop) so its
       last row of cards never sits hidden underneath it. */
    margin-bottom: 100px;
}

.pipeline-column {
    flex: 1;
    min-width: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 18px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
    display: flex;
    flex-direction: column;
}

.pipeline-column-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 14px 16px;
    border-bottom: 1px solid var(--border);
    font-weight: 600;
}

.pipeline-count {
    background: var(--bg);
    color: var(--text-muted);
    border-radius: 999px;
    padding: 2px 9px;
    font-size: 0.8rem;
    font-weight: 600;
}

.pipeline-column-body {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 12px;
    min-height: 80px;
    border-radius: 0 0 18px 18px;
    transition: background-color 0.15s ease;
}

.pipeline-column-body.drag-over {
    background: var(--bg);
    outline: 2px dashed var(--brand);
    outline-offset: -6px;
}

/* Dedicated Pending Closed page (templates/pipeline_pending_closed.html)
   -- line-item rows, same table/row convention as the Customers page
   (.row-sold/.row-lost there): transparent fill, tinted text + a left
   accent bar, never a filled block. row-neutral is the base theme-blue
   tier (days 1-2); row-expiry-amber/red match the board card's own
   amber/red expiry tiers exactly, same urgency, same colors, just a row
   instead of a card. */
.row-neutral, .row-expiry-amber, .row-expiry-red {
    background: transparent;
}
.row-neutral { color: var(--brand); }
.row-expiry-amber { color: var(--attention); }
.row-expiry-red { color: var(--hot); }

.row-neutral td:first-child { box-shadow: inset 3px 0 var(--brand-border); }
.row-expiry-amber td:first-child { box-shadow: inset 3px 0 var(--attention-border); }
.row-expiry-red td:first-child { box-shadow: inset 3px 0 var(--hot-border); }

.row-neutral a, .row-expiry-amber a, .row-expiry-red a {
    color: inherit;
}

.pending-closed-row-actions {
    display: flex;
    gap: 8px;
    justify-content: flex-end;
}

.pipeline-empty {
    margin: 8px 0;
    text-align: center;
}

.pipeline-card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 12px;
    border-radius: 16px;
    /* Neutral fill for every card; the .card-* status class adds only a
       tinted border + text (colored-outline convention). */
    background: var(--surface);
    border: 1px solid var(--border);
    cursor: grab;
    font-size: 0.88rem;
}

.pipeline-card.dragging {
    opacity: 0.5;
}

/* (No longer emitted -- every Column View card is draggable now, matching
   Diagram View. Kept as a harmless no-op selector.) */
.pipeline-card-locked {
    cursor: default;
}

/* Pending Closed is the one column where inaction is itself a stuck state
   (see CLAUDE.md's guardrail) -- every lead here needs an explicit Sold/
   Lost call. A quick, phone-buzz-style jiggle draws the eye to that
   without suggesting the card is moving lanes or that something's wrong:
   mostly at rest, with a brief wobble every few seconds. transform-only
   (translateX + a slight rotate), so it never touches layout/spacing in
   the column -- covers both views' cards (.pipeline-card here, .lead-
   bubble in Diagram View's own Pending Closed box). */
@keyframes pipeline-pending-jiggle {
    0%, 92%, 100% { transform: translateX(0) rotate(0deg); }
    93% { transform: translateX(-3px) rotate(-1deg); }
    94.5% { transform: translateX(3px) rotate(1deg); }
    96% { transform: translateX(-2px) rotate(-0.7deg); }
    97.5% { transform: translateX(2px) rotate(0.7deg); }
    99% { transform: translateX(0) rotate(0deg); }
}

.pipeline-column-body[data-stage="closed"] .pipeline-card,
.pipeline-node-pending .lead-bubble {
    animation: pipeline-pending-jiggle 4.2s ease-in-out infinite;
}

/* Staggered so a whole column of cards doesn't buzz in lockstep. */
.pipeline-column-body[data-stage="closed"] .pipeline-card:nth-child(2n),
.pipeline-node-pending .lead-bubble:nth-child(2n) { animation-delay: 0.7s; }
.pipeline-column-body[data-stage="closed"] .pipeline-card:nth-child(3n),
.pipeline-node-pending .lead-bubble:nth-child(3n) { animation-delay: 1.5s; }
.pipeline-column-body[data-stage="closed"] .pipeline-card:nth-child(4n),
.pipeline-node-pending .lead-bubble:nth-child(4n) { animation-delay: 2.2s; }

@media (prefers-reduced-motion: reduce) {
    .pipeline-column-body[data-stage="closed"] .pipeline-card,
    .pipeline-node-pending .lead-bubble {
        animation: none;
    }
}

/* "Voicemail Attempts" and "Reached - no appointment set" widen to hold
   their three VM / FU sub-columns. */
.pipeline-column--wide {
    flex: 2.6;
}

.pipeline-subcols {
    display: flex;
    gap: 8px;
    align-items: flex-start;
}

/* Every VM / Attempt sub-column box, not just the last one -- outlined in
   the same amber ("yellow") tint uniformly. */
.pipeline-subcol {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 8px;
    background: var(--attention-bg);
    border: 1px solid var(--attention-border);
    border-radius: 12px;
}

.pipeline-subcol-label {
    font-family: 'Space Mono', 'Inter', monospace;
    font-size: 0.68rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--attention);
    text-align: center;
    padding: 2px 0 4px;
}

/* 'Reached' leads with no follow-up attempt logged yet (dash / unsettled)
   -- rendered below the FU 1/2/3 group, at the bottom of the column. */
.pipeline-subcol-loose {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-top: 12px;
}

/* --- "Reached - no appointment set" 3-faced channel roller ----------
   Call / Text / Email faces on a spinning triangular-prism roller, cycled
   by the header arrows (static/js/pipeline_fu_roller.js). Each face keeps
   the full FU 1/2/3 sub-column structure. */
.fu-roller .pipeline-column-header > span:first-child {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}
.fu-roller-badge {
    font-family: 'Space Mono', 'Inter', monospace;
    font-size: 0.62rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--brand-text);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 2px 8px;
}
.fu-roller-controls {
    display: inline-flex;
    align-items: center;
    gap: 10px;
}
.fu-roller-arrows {
    display: inline-flex;
    flex-direction: column;
    gap: 2px;
}
.fu-roller-arrow {
    display: inline-flex;
    padding: 1px 3px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    color: var(--text-muted);
    cursor: pointer;
    line-height: 0;
    transition: color 0.12s ease, background-color 0.12s ease, border-color 0.12s ease;
}
.fu-roller-arrow:hover {
    color: var(--brand-text);
    border-color: var(--brand);
    background: var(--surface-2);
}
.fu-roller-arrow svg { width: 15px; height: 15px; display: block; }
.fu-roller-empty { margin: 16px 0; text-align: center; }

.fu-roller-viewport {
    position: relative;
    min-height: 120px;
    overflow: hidden;
    perspective: 900px;
    perspective-origin: 50% 40%;
    transition: height 420ms cubic-bezier(0.4, 0.05, 0.2, 1);
}
.fu-roller-track {
    position: relative;
    transform-style: preserve-3d;
    min-height: inherit;
}
.fu-roller-face {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    backface-visibility: hidden;
    transform-origin: 50% 50%;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: transform 520ms cubic-bezier(0.4, 0.05, 0.2, 1), opacity 300ms ease;
}
/* Steady state -- the one face on screen. */
.fu-roller-face--active {
    position: relative;
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: none;
}
/* Mid-flip: both the outgoing and incoming face are laid out (absolute)
   and visible; --fu-dir (+1 next / -1 prev) flips the roll direction. */
.fu-roller-face.is-visible {
    visibility: visible;
    opacity: 1;
    pointer-events: none;
}
.fu-roller-face.is-entering {
    opacity: 0;
    transform: translateY(calc(var(--fu-dir, 1) * 42%)) rotateX(calc(var(--fu-dir, 1) * 90deg));
}
.fu-roller-face.is-leaving {
    opacity: 0;
    transform: translateY(calc(var(--fu-dir, 1) * -42%)) rotateX(calc(var(--fu-dir, 1) * -90deg));
}

@media (prefers-reduced-motion: reduce) {
    .fu-roller-face,
    .fu-roller-viewport { transition: none; }
}

/* Movable-direction indicators pinned to both edges of every card,
   echoing the banner icon so it's clear a card can travel either way. */
.pipeline-card-edge-icon {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    color: currentColor;
    opacity: 0.45;
    pointer-events: none;
}

.pipeline-card-edge-icon span {
    font-size: 0.5rem;
}

.pipeline-card-edge-left {
    left: 3px;
}

.pipeline-card-edge-right {
    right: 3px;
}

.pipeline-card-name a {
    color: inherit;
    font-weight: 600;
    text-decoration: none;
}

.pipeline-card-name a:hover {
    text-decoration: underline;
}

.pipeline-card-vehicle {
    opacity: 0.85;
}

/* Pipeline lead cards: colored-outline convention -- neutral (surface)
   fill, status-tinted border, status-coloured text. No filled blocks.
   card-neutral (theme blue, var(--brand) -- same "blue" this codebase
   already calls it elsewhere, see the Call Scheduler timeline's today
   line) is the one base color every card carries across its full journey
   except: card-unreached (Simple View's merged Voicemail-Attempts leads,
   flagged red since they've never actually been reached despite sitting
   in "Reached, no appointment set"), card-expiry-amber/red (Pending
   Closed's day-3/4 and day-5/6 expiry coloring -- days 1-2 use
   card-neutral), and card-closed (Sold/Lost, unchanged). */
.card-neutral {
    border-color: var(--brand-border);
    color: var(--brand);
}

.card-unreached {
    border-color: var(--hot-border);
    color: var(--hot);
}

.card-expiry-amber {
    border-color: var(--attention-border);
    color: var(--attention);
}

.card-expiry-red {
    border-color: var(--hot-border);
    color: var(--hot);
}

.card-closed {
    border-color: var(--green-border);
    color: var(--green-text);
}

.card-closed .pipeline-card-vehicle {
    color: var(--text-muted);
}

.sms-unread-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-left: 6px;
    border-radius: 50%;
    background: var(--brand);
    vertical-align: middle;
}

.pipeline-card-actions {
    display: flex;
    gap: 6px;
}

.pipeline-card-actions .btn {
    flex: 1;
    text-align: center;
}

.btn-sold {
    background: var(--gold);
    color: var(--gold-text);
    border-color: var(--gold);
}

.btn-lost {
    background: rgba(255, 255, 255, 0.15);
    color: #ffffff;
    border-color: rgba(255, 255, 255, 0.4);
}

/* Showed Up (Yes) -- an appointment-origin Pending Closed card's first
   question, before Sold/Lost even applies (see templates/pipeline.html's
   pcard macro). Green-leaning since it's the positive branch; pairs with
   the same amber .btn-no-show as the "No" answer. */
.btn-showed-yes {
    background: var(--green);
    color: #0B0C0E;
    border-color: var(--green);
}

/* No Show: same amber as "Reached - no appointment set" elsewhere on the
   board (--track-reached) -- a visual hint that pressing it sends the
   lead back there, and deliberately not red/danger-toned since this is a
   routine outcome, not an error. Appears alongside Sold/Lost in Pending
   Closed (flex:1 from .pipeline-card-actions handles 2 vs 3 buttons the
   same way), or alone on an Appointment Set card. */
.btn-no-show {
    background: var(--track-reached);
    color: #2B1B03;
    border-color: var(--track-reached);
}

/* Delete Lead -- small, low-key placement, but a SOLID red block so a
   destructive action never hides among the muted outline controls.
   Shared by the Columns cards, the Leads table and the customer page. */
.btn-delete-lead {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 9px;
    border: none;
    border-radius: 6px;
    background: var(--danger-solid);
    color: #fff;
    font: inherit;
    font-size: 0.68rem;
    font-weight: 600;
    line-height: 1.4;
    letter-spacing: 0.01em;
    cursor: pointer;
    text-decoration: none;
    transition: background 0.12s ease;
}
.btn-delete-lead:hover { background: var(--danger-solid-hover); }
.btn-delete-lead:focus-visible { outline: 2px solid var(--danger-solid); outline-offset: 2px; }

/* Larger variant for the standalone button at the bottom of a detail
   card (a 0.68rem pill on its own down there would read as an accident). */
.btn-delete-lead-lg {
    font-size: 0.85rem;
    padding: 8px 20px;
    border-radius: 8px;
}

/* Delete control pinned to the bottom-centre of a lead/customer detail
   card, under the lowest panel. */
.card-delete-footer {
    display: flex;
    justify-content: center;
    margin: 24px 0 8px;
}

/* --- Pipeline: bubble-cluster alternate view (see routes/pipeline.py,
   templates/pipeline_bubbles.html) -- the same active leads as the
   column board, laid out as a three-branch tree: a tall Prospects box on
   the left, three colour-coded branches (Appointment Set / Reached-no-
   appointment / Voicemail) fanning out from it, all converging into one
   Pending Closed box, which then splits to Sold and Lost. Flexbox lays
   it out; static/js/pipeline_bubble_connectors.js draws the arrows from
   the laid-out positions, static/js/pipeline_bubble_physics.js floats
   the lead chips inside each box, and static/js/pipeline_bubble_canvas.js
   makes the whole thing a pannable/zoomable sheet. */
.pipeline-bubbles-wrap {
    position: relative;
    height: 600px;
    /* Purely a clip region for the pan/zoom sheet -- no background,
       border, radius or shadow. The stage boxes are the only visible
       things; the page background (and the sunrise gradient behind it)
       shows straight through the gaps between them and under the
       connector lines. */
    overflow: hidden;
    background: transparent;
    cursor: grab;
    /* Clears the fixed .pipeline-stats bar at the bottom of the page, the
       same way .pipeline-board does -- without it the bottom of the sheet
       sits hidden behind that bar. */
    margin-bottom: 100px;
    /* Prevents the browser's own touch-scroll/pinch-zoom gestures from
       fighting the wheel/pointer handlers in pipeline_bubble_canvas.js,
       which implement pan/zoom themselves. */
    touch-action: none;
}

.pipeline-bubbles-wrap.panning {
    cursor: grabbing;
}

.pipeline-bubbles-canvas {
    position: absolute;
    top: 0;
    left: 0;
    width: max-content;
    height: max-content;
    transform-origin: 0 0;
    /* Fallback only -- static/js/pipeline_bubble_canvas.js overwrites
       this with the real fit-to-view transform as soon as the wrap is
       visible and measurable, then with whatever pan/zoom the user sets. */
    transform: scale(0.5);
}

/* The tree: Prospects | three stacked branches | Pending Closed | Sold/
   Lost. align-items:stretch is what lets the Prospects box grow to the
   full height of the branch stack beside it. */
.pipeline-bubbles-flow {
    display: flex;
    align-items: stretch;
    gap: 104px;
    padding: 72px 96px;
}

.pipeline-bubbles-tracks {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 34px;
    flex: none;
}

/* Sold + Lost, stacked and centred so Pending Closed's two outgoing
   lines fan symmetrically up-right and down-right. */
.pipeline-bubbles-end {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 60px;
    flex: none;
}

.pipeline-bubbles-connectors {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: visible;
    pointer-events: none;
    z-index: 1;
}

.pipeline-bubbles-connectors path {
    fill: none;
    stroke: var(--text-faint);
    stroke-width: 3px;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.pipeline-bubbles-connectors path.path-appt { stroke: var(--track-appt); }
.pipeline-bubbles-connectors path.path-reached { stroke: var(--track-reached); }
.pipeline-bubbles-connectors path.path-vm { stroke: var(--track-vm); }
.pipeline-bubbles-connectors path.path-sold { stroke: var(--green); }
.pipeline-bubbles-connectors path.path-lost {
    stroke: var(--text-muted);
    stroke-dasharray: 7 5;
}
/* The two Never Reached, Attempting to Reach exits (see EDGES in
   pipeline_bubble_connectors.js) -- white dotted up into Reached, solid
   red across and up into Lost. Distinct from path-vm (still used by the
   VM 1/2/3 inner-track lines) and from path-lost's own grey dash, which
   is a different connector (Pending Closed -> Lost). */
.pipeline-bubbles-connectors path.path-vm-reached {
    stroke: var(--text);
    stroke-dasharray: 5 5;
}
.pipeline-bubbles-connectors path.path-vm-lost { stroke: var(--danger-solid); }
.pipeline-bubbles-connectors path.path-inner {
    stroke-width: 2px;
    opacity: 0.5;
}

/* Heartbeat pulse travelling each connector (see makePulse() in
   pipeline_bubble_connectors.js). Same colour as its line, with a soft
   glow of the same colour. */
.pipeline-conn-pulse {
    fill: currentColor;
    color: var(--text-faint);
    filter: drop-shadow(0 0 3px currentColor);
}
.pipeline-conn-pulse.pulse-appt { color: var(--track-appt); }
.pipeline-conn-pulse.pulse-reached { color: var(--track-reached); }
.pipeline-conn-pulse.pulse-vm { color: var(--track-vm); }
.pipeline-conn-pulse.pulse-sold { color: var(--green); }
.pipeline-conn-pulse.pulse-lost { color: var(--text-muted); }
.pipeline-conn-pulse.pulse-vmreached { color: var(--text); }
.pipeline-conn-pulse.pulse-vmlost { color: var(--danger-solid); }

@media (prefers-reduced-motion: reduce) {
    .pipeline-conn-pulse { display: none; }
}

/* A box in the tree -- Prospects, a track, a slot, Pending Closed, Sold,
   Lost. Rounded rect, solid surface (an island on the dot field), a
   header label and a chip area below it. */
.pipeline-node {
    position: relative;
    flex: none;
    border-radius: 18px;
    border: 1.5px solid var(--border);
    background: var(--surface);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    box-shadow: 0 6px 22px -10px rgba(0, 0, 0, 0.8);
    overflow: hidden;
}

.pipeline-node-prospects {
    align-self: stretch;
    min-width: 392px;
    max-width: 424px;
}
.pipeline-node-prospects .pipeline-node-label { padding-left: 24px; padding-right: 24px; }

.pipeline-node-pending {
    align-self: center;
    min-width: 320px;
    max-width: 352px;
    min-height: 400px;
    border-color: var(--text-muted);
}

.pipeline-node-sold { border-color: var(--green); }
.pipeline-node-lost { border-color: var(--text-muted); }
.pipeline-node-sold,
.pipeline-node-lost { min-width: 300px; max-width: 328px; min-height: 250px; }

/* A branch. The track colour lives on the border + label; the tint on
   the body keeps the branch readable once lines have merged downstream. */
.pipeline-track { }
.pipeline-track--appt {
    border-color: var(--track-appt);
    min-width: 460px;
    min-height: 230px;
}
.pipeline-track--appt .pipeline-node-label { color: var(--track-appt); }
.pipeline-track--appt .pipeline-bubble-fill { background: var(--track-appt-bg); }

.pipeline-track--reached { border-color: var(--track-reached); }
.pipeline-track--reached .pipeline-node-label { color: var(--track-reached); }

.pipeline-track--vm { border-color: var(--track-vm); }
.pipeline-track--vm .pipeline-node-label { color: var(--track-vm); }

/* The 1/2/3 slots inside the Reached and Voicemail tracks -- a
   left-to-right row, each wide enough to hold several chips. */
.pipeline-track-slots {
    display: flex;
    gap: 22px;
    padding: 0 20px 20px;
}

.pipeline-slot {
    position: relative;
    flex: 1 1 0;
    min-width: 264px;
    min-height: 248px;
    border-radius: 12px;
    border: 1px solid var(--border);
    background: var(--surface-2);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.pipeline-track--reached .pipeline-slot { border-color: rgba(242, 169, 59, 0.35); }
.pipeline-track--reached .pipeline-slot .pipeline-bubble-fill { background: var(--track-reached-bg); }
.pipeline-track--vm .pipeline-slot { border-color: rgba(167, 139, 250, 0.35); }
.pipeline-track--vm .pipeline-slot .pipeline-bubble-fill { background: var(--track-vm-bg); }

/* Box header -- name + count pill. */
.pipeline-node-label {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    padding: 13px 20px 11px;
    font-family: 'Barlow Condensed', 'Inter', sans-serif;
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    border-bottom: 1px solid var(--border);
}
.pipeline-node-prospects .pipeline-node-label,
.pipeline-track > .pipeline-node-label { font-size: 1.18rem; }

.pipeline-slot-label {
    padding: 9px 12px 8px;
    font-size: 0.9rem;
    border-bottom: 1px dashed var(--border);
}

.pipeline-bubble-count {
    font-family: 'Space Mono', 'Inter', monospace;
    background: var(--bg);
    color: var(--text-muted);
    border-radius: 999px;
    padding: 1px 8px;
    font-size: 0.68rem;
    font-weight: 500;
    flex: none;
}

/* Chip area -- position:relative so the absolutely-positioned chips
   (placed every frame by static/js/pipeline_bubble_physics.js) bounce
   off ITS edges. No scroll: overflow past what fits is shown as a
   "+N more" pill instead (rendered by the template). */
.pipeline-bubble-fill {
    position: relative;
    flex: 1 1 auto;
    min-height: 156px;
    overflow: hidden;
}
.pipeline-node-prospects .pipeline-bubble-fill,
.pipeline-node-pending .pipeline-bubble-fill { min-height: 240px; }

.pipeline-node.drag-over,
.pipeline-slot.drag-over {
    outline: 2px dashed var(--brand);
    outline-offset: 3px;
}

/* Zero-lead state -- a clear label, centred, so an empty branch reads as
   "nothing here yet" rather than a broken/collapsed box. */
.pipeline-bubble-empty {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: 'Space Mono', 'Inter', monospace;
    font-size: 0.72rem;
    letter-spacing: 0.08em;
    color: var(--text-faint);
    pointer-events: none;
}

/* "+N more" -- pinned bottom-right, not part of the physics sim. */
.pipeline-bubble-overflow {
    position: absolute;
    right: 7px;
    bottom: 7px;
    z-index: 3;
    font-family: 'Space Mono', 'Inter', monospace;
    font-size: 0.62rem;
    font-weight: 500;
    color: var(--text-muted);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 2px 7px;
    pointer-events: none;
}

/* Lead chip -- floats inside its box under the physics sim. Quiet:
   secondary content, not a peer of the box. A real <button> (keyboard-
   focusable, a drag source, a hover/tap target for the popover). */
.lead-bubble {
    position: absolute;
    top: 0;
    left: 0;
    max-width: 120px;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    border-radius: 7px;
    border: 1px solid var(--brand-border);
    background: var(--bg);
    color: var(--brand);
    cursor: grab;
    padding: 4px 8px;
    font: inherit;
    text-align: left;
    overflow: hidden;
}

.lead-bubble[data-expiry="amber"] { border-color: var(--attention-border); }
.lead-bubble[data-expiry="red"] { border-color: var(--hot-border); }

.lead-bubble.dragging { opacity: 0.4; }

.lead-bubble:hover,
.lead-bubble:focus-visible {
    border-color: var(--text-muted);
    color: var(--text);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
    z-index: 4;
}


.lead-bubble-name,
.lead-bubble-vehicle {
    display: block;
    max-width: 104px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    pointer-events: none;
}

.lead-bubble-name { font-weight: 600; font-size: 0.62rem; color: var(--text); }
.lead-bubble-vehicle { font-size: 0.56rem; color: var(--text-faint); }

/* JS-positioned tooltip (see static/js/pipeline_bubbles.js) -- one shared
   element reused for every lead-bubble rather than one per bubble, shown/
   moved/hidden on hover, focus, or tap. */
.lead-bubble-popover {
    position: fixed;
    z-index: 50;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 10px 12px;
    font-size: 0.8rem;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
    pointer-events: none;
    max-width: 220px;
}

.lead-bubble-popover-name {
    font-weight: 700;
    margin-bottom: 2px;
}

.lead-bubble-popover-vehicle {
    color: var(--text-muted);
}

.lead-bubble-popover-link {
    display: block;
    margin-top: 6px;
    color: var(--brand-text);
    font-weight: 600;
    text-decoration: none;
    pointer-events: auto;
}

/* --- Lead source connectors --- */

.source-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 16px;
}

.source-card {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.source-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.source-card-header h2 {
    margin: 0;
}

.source-card-detail {
    margin: 0;
    font-size: 0.88rem;
    color: var(--text-muted);
}

.source-card-error {
    margin: 0;
    font-size: 0.85rem;
    color: var(--hot);
}

/* Row layout for the Lead Source / Inventory Source connector lists --
   same data as .source-card above, one connector per horizontal row
   inside a single bordered list instead of a grid of separate panels. */
.source-list {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    border-radius: 12px;
    overflow: hidden;
}

.source-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px 16px;
    padding: 14px 18px;
    background: var(--surface);
}

.source-row + .source-row {
    border-top: 1px solid var(--border);
}

.source-row-main {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 1 1 220px;
    min-width: 0;
}

.source-row-main h2 {
    margin: 0;
    font-size: 1rem;
}

.source-row-detail {
    flex: 2 1 240px;
    margin: 0;
    font-size: 0.85rem;
    color: var(--text-muted);
    min-width: 0;
}

.source-row-detail.empty {
    font-style: italic;
}

.source-row-error {
    flex-basis: 100%;
    margin: 0;
    font-size: 0.85rem;
    color: var(--hot);
}

.source-row-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-left: auto;
    flex-shrink: 0;
}

/* Per-connector toggle switch replacing the old Manage button -- a small
   Material-style track/thumb switch, its own class: an <a>
   when not connected yet (visually off, click opens the connect flow),
   a <button type="submit"> when connected (click pauses/resumes), or a
   disabled <button> for a coming_soon row. */
.source-toggle {
    flex-shrink: 0;
    position: relative;
    display: inline-block;
    width: 35px;
    height: 20px;
    padding: 0;
    border: none;
    border-radius: 999px;
    background: #2A2D33;
    cursor: pointer;
    text-decoration: none;
    transition: background-color 0.3s ease;
}

.source-toggle[aria-checked="true"] {
    background: var(--brand);
}

.source-toggle:disabled,
.source-toggle.disabled {
    cursor: not-allowed;
    opacity: 0.5;
}

.source-toggle-thumb {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: #6B7078;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
    transition: transform 0.3s ease, background-color 0.3s ease;
}

.source-toggle[aria-checked="true"] .source-toggle-thumb {
    background: #0B0C0E;
    transform: translateX(15px);
}

.source-toggle-form {
    display: inline-flex;
    flex-shrink: 0;
}

.hidden {
    display: none !important;
}

/* --- SMS conversation thread --- */

.sms-panel-header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 8px 12px;
    margin-bottom: 12px;
}

.sms-panel-header h2 {
    margin: 0;
}

.sms-search {
    /* Narrower Messages panel now (.detail-grid) -- flex/min-width: 0
       lets this shrink (or, with .sms-panel-header's flex-wrap, drop to
       its own line) instead of forcing the panel to overflow
       horizontally; max-width just keeps it from stretching too wide on
       its own line. */
    flex: 1 1 120px;
    min-width: 0;
    max-width: 200px;
}

/* ID selectors here, not just the classes above -- the general
   input[type="text"]/textarea rule (style.css ~line 467) carries an
   element-type match on top of its attribute selector, which out-specifies
   a bare class selector regardless of source order. IDs unambiguously win. */
#sms-search {
    font-size: 0.8rem;
}

.sms-thread {
    display: flex;
    flex-direction: column;
    gap: 10px;
    /* Fills whatever space is left in the tall .sms-panel card (header +
       compose form take fixed space above/below) rather than a fixed
       height of its own -- min-height: 0 is required here, not optional:
       flex items default to min-height: auto (content-based), which
       would otherwise stop this from ever shrinking enough to scroll. */
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 12px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 14px;
    margin-bottom: 12px;
}

.sms-bubble-row {
    display: flex;
}

.sms-bubble-row-highlight .sms-bubble {
    animation: highlight-flash 2.5s ease-out;
}

.row-highlight {
    animation: highlight-flash 2.5s ease-out;
}

@keyframes highlight-flash {
    0%, 40% { background-color: var(--gold-bg); }
    100% { background-color: transparent; }
}

.sms-bubble-row-in {
    justify-content: flex-start;
}

.sms-bubble-row-out {
    justify-content: flex-end;
}

.sms-bubble {
    max-width: 70%;
    padding: 8px 12px;
    border-radius: 14px;
    font-size: 0.78rem;
}

.sms-bubble-in {
    background: var(--brand-bg);
    color: var(--text);
    border-bottom-left-radius: 4px;
}

.sms-bubble-out {
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    border-bottom-right-radius: 4px;
}

.sms-bubble-text {
    white-space: pre-wrap;
    word-break: break-word;
}

.sms-bubble-meta {
    margin-top: 4px;
    font-size: 0.72rem;
    color: var(--text-muted);
}

.sms-compose {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.sms-compose-row {
    display: flex;
    gap: 8px;
    align-items: flex-end;
}

.sms-compose-row textarea {
    flex: 1;
    resize: vertical;
}

.sms-sender-input {
    max-width: 220px;
}

#sms-sender-name {
    font-size: 0.72rem;
}

@media (max-width: 720px) {
    .source-grid {
        grid-template-columns: 1fr;
    }
    .source-row {
        flex-direction: column;
        align-items: flex-start;
    }
    .source-row-actions {
        margin-left: 0;
    }
    .tiles {
        grid-template-columns: repeat(2, 1fr);
    }
    .pipeline-search-bar {
        width: 100%;
    }
    .pipeline-stats {
        grid-template-columns: repeat(3, 1fr);
        padding: 8px 12px;
    }
    .pipeline-stats .tile-value {
        font-size: 1.15rem;
    }
    .pipeline-stats .tile-label {
        font-size: 0.7rem;
    }
    .detail-grid {
        grid-template-columns: 1fr;
    }
    .detail-grid > .panel {
        /* The tall "iPhone Max" card shape is a desktop 3-column effect --
           stacked full-width on mobile, a fixed 700px box per card would
           mean 2100px of scrolling before reaching Activity History, so
           let each card size to its own content here instead. */
        height: auto;
        overflow-y: visible;
    }
    .sms-thread {
        flex: none;
        max-height: 420px;
    }
    .form-row {
        flex-direction: column;
    }
    .pipeline-board {
        flex-direction: column;
        /* Stats bar wraps to 2 rows here, so it's taller -- needs more clearance. */
        margin-bottom: 170px;
    }
    .pipeline-column {
        width: 100%;
    }
    .pipeline-subcols {
        flex-direction: column;
    }
}

/* --- Modal (Customers touchpoint log) --- */

.modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    z-index: 40;
}

.modal-panel {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: min(640px, calc(100% - 32px));
    max-height: min(600px, calc(100% - 64px));
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 18px;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
    z-index: 41;
    overflow: hidden;
}

/* Add Lead / Set Appointment popups (templates/pipeline.html) -- the same
   .modal-panel shell the Customers call-log modal uses, just a bit wider
   and taller so the existing 2-column form-row layout (leads/_fields.html)
   has room to breathe instead of feeling cramped. */
.modal-panel--form {
    width: min(760px, calc(100% - 32px));
    max-height: min(720px, calc(100% - 64px));
}

.modal-body form {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

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

.modal-header h2 {
    margin: 0;
}

.modal-close {
    background: none;
    border: none;
    font-size: 1.4rem;
    line-height: 1;
    color: var(--text-muted);
    cursor: pointer;
    padding: 4px;
}

.modal-body {
    padding: 16px 20px;
    overflow-y: auto;
}

/* --- Delete confirmation dialog (base.html + static/js/delete_confirm.js) --
   Centred, page-coloured box outlined in the teal accent (the folder-tab
   outline language); actions bottom-right -- solid-red Delete, teal
   outline Cancel. */
.confirm-modal {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    justify-content: center;
}
.confirm-modal.hidden { display: none !important; }

.confirm-modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
}

.confirm-modal-box {
    position: relative;
    width: min(440px, calc(100% - 32px));
    background: var(--bg);
    border: 1.5px solid var(--brand);
    border-radius: 16px;
    padding: 22px 22px 18px;
    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
}

.confirm-modal-msg {
    margin: 0 0 22px;
    font-size: 0.95rem;
    line-height: 1.5;
    color: var(--text);
}

.confirm-modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
}

.confirm-modal-delete,
.confirm-modal-proceed,
.confirm-modal-no-show {
    border: none;
    border-radius: 8px;
    padding: 8px 18px;
    color: #fff;
    font: inherit;
    font-weight: 700;
    cursor: pointer;
}

/* Email Log (lead detail page) -- reuses the .status pill convention
   (transparent fill, color-mix border, tinted text) for the sent/
   received indicator, and .table-scroll/.confirm-modal for the row list
   and the click-to-read modal, so this reads as one more native panel
   rather than a bolted-on widget. */
.email-log-table th:nth-child(1) { width: 18%; }
.email-log-table th:nth-child(2) { width: 56%; }
.email-log-table th:nth-child(3) { width: 26%; }

.email-log-row { cursor: pointer; }
.email-log-row:hover,
.email-log-row:focus-visible { background: var(--surface-2); }
.email-log-row:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

.email-subject-cell {
    color: var(--text);
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 0; /* forces the ellipsis to respect the column width above */
}

.email-badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.78rem;
    background: transparent;
    border: 1px solid;
    border-color: color-mix(in srgb, currentColor 45%, transparent);
}
.email-badge-outbound { color: var(--brand-text); }
.email-badge-inbound { color: var(--green-text); }

.email-view-box {
    width: min(560px, calc(100% - 32px));
    max-height: min(70vh, 620px);
    display: flex;
    flex-direction: column;
}

.email-view-close {
    position: absolute;
    top: 14px;
    right: 14px;
    width: 28px;
    height: 28px;
    border: none;
    background: transparent;
    color: var(--text-muted);
    font-size: 1.3rem;
    line-height: 1;
    cursor: pointer;
    border-radius: 6px;
}
.email-view-close:hover { color: var(--text); background: var(--surface-2); }

.email-view-meta {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0 26px 12px 0;
}
.email-view-when {
    font-size: 0.82rem;
    color: var(--text-muted);
}

.email-view-subject {
    margin: 0 26px 14px 0;
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--text);
    word-break: break-word;
}

.email-view-body {
    white-space: pre-wrap;
    word-break: break-word;
    color: var(--text);
    font-size: 0.92rem;
    line-height: 1.55;
    overflow-y: auto;
    padding-right: 4px;
}
.confirm-modal-delete { background: var(--danger-solid); }
.confirm-modal-delete:hover { background: var(--danger-solid-hover); }
/* Restore's primary action: same bold solid treatment as delete's, in
   the theme teal instead of red (--brand-dim so white text stays legible
   on it). */
.confirm-modal-proceed { background: var(--brand-dim); }
.confirm-modal-proceed:hover { background: var(--brand); color: #072522; }
.confirm-modal-proceed:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}
/* No Show isn't an error or a destructive delete -- it's a routine "send
   this back into cadence" outcome, so it gets its own tone (the same
   amber --track-reached already used for "Reached - no appointment set"
   elsewhere on the board) rather than borrowing delete's red. */
.confirm-modal-no-show { background: var(--track-reached); color: #2B1B03; }
.confirm-modal-no-show:hover { filter: brightness(0.92); }
.confirm-modal-no-show:focus-visible {
    outline: 2px solid var(--track-reached);
    outline-offset: 2px;
}

.confirm-modal-cancel {
    border: 1px solid var(--brand);
    border-radius: 8px;
    padding: 8px 18px;
    background: transparent;
    color: var(--brand);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
}
.confirm-modal-cancel:hover { background: var(--brand-bg); }

.confirm-modal-delete:focus-visible,
.confirm-modal-cancel:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

.table-scroll {
    /* No horizontal scrollbar -- the Call Log column is wide enough on
       its own now (.detail-grid gives it 1fr, whatever Customer Info /
       Messages don't need) that cell content wrapping (below) is enough
       to keep everything inside it; overflow-x: hidden is a hard
       guarantee against a horizontal scrollbar ever appearing even for
       an unusually long, unbreakable value. Still a direct child of a
       flex column (.detail-grid > .panel) -- min-width: 0 is still what
       lets it shrink to the panel's actual width instead of the table's
       content-based minimum pushing the panel wider. */
    overflow-x: hidden;
    min-width: 0;
}

.table-scroll table {
    table-layout: fixed;
    width: 100%;
}

.table-scroll th,
.table-scroll td {
    overflow-wrap: break-word;
}

/* table-layout: fixed splits width evenly across columns by default,
   which looks wrong for these five (a short word, a full date/time, a
   short duration, a short status word, a small button) -- explicit
   shares instead, in the same order as the header row in
   templates/customers/detail.html. */
.table-scroll th:nth-child(1) { width: 16%; }
.table-scroll th:nth-child(2) { width: 28%; }
.table-scroll th:nth-child(3) { width: 14%; }
.table-scroll th:nth-child(4) { width: 20%; }
.table-scroll th:nth-child(5) { width: 22%; }

.transcript-toggle {
    background: none;
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 2px 10px;
    font-size: 0.85rem;
    color: var(--text-muted);
    cursor: pointer;
}

.transcript-row td {
    background: var(--surface-muted, rgba(0, 0, 0, 0.03));
    padding: 12px;
}

.transcript-text {
    white-space: pre-wrap;
    word-break: break-word;
    font-family: inherit;
    font-size: 0.85rem;
    margin: 0;
    max-height: 240px;
    overflow-y: auto;
}

/* Message-thread rendering of a call transcript (lead detail's Activity
   History) -- same bubble language as .sms-bubble* (static/css/style.css's
   SMS thread) so a Nova phone conversation reads the same way a text
   conversation does: the caller's side tinted and right-aligned, Nova's
   side plain and left-aligned. */
.conversation-thread {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 12px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 14px;
    max-height: 320px;
    overflow-y: auto;
}

.conversation-row {
    display: flex;
}

.conversation-row-nova {
    justify-content: flex-end;
}

.conversation-row-caller {
    justify-content: flex-start;
}

.conversation-bubble {
    max-width: 70%;
    padding: 8px 12px;
    border-radius: 14px;
    font-size: 0.85rem;
}

.conversation-bubble-nova {
    background: var(--brand-bg);
    color: var(--text);
    border-bottom-right-radius: 4px;
}

.conversation-bubble-caller {
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    border-bottom-left-radius: 4px;
}

.conversation-text {
    white-space: pre-wrap;
    word-break: break-word;
}

.conversation-speaker {
    margin-top: 4px;
    font-size: 0.72rem;
    color: var(--text-muted);
}

/* Plain-language recap of the customer's side of a finished call (see
   routes/telephony.py's _summarize_customer_side), shown above the
   message thread it summarizes. */
.conversation-summary {
    padding: 10px 12px;
    background: transparent;
    border: 1px solid var(--attention-border);
    border-radius: 10px;
    margin-bottom: 10px;
}

.conversation-summary-label {
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--attention);
    margin-bottom: 4px;
}

.conversation-summary-text {
    margin: 0;
    font-size: 0.85rem;
    white-space: pre-wrap;
    word-break: break-word;
}

/* --- Customer notes --- */

.notes-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    margin-bottom: 12px;
}

.note-item {
    padding: 10px 12px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 14px;
}

.note-item-text {
    margin: 0 0 4px;
    font-size: 0.88rem;
    white-space: pre-wrap;
    overflow-wrap: break-word;
}

.note-item-meta {
    margin: 0;
    font-size: 0.72rem;
    color: var(--text-muted);
}
