/* ============================================================================
   Whacl house style
   ----------------------------------------------------------------------------
   The shared look, for every surface: the tenant app, the superadmin, and the
   sign-in pages. If a component appears on more than one of those, its rules
   belong here and nowhere else.

   These rules were lifted verbatim out of public/_layout.php, where they lived
   inline and were reachable only by the tenant app. The admin had grown a
   second copy and the auth pages a third, which is how they drifted apart --
   the same button defined three times cannot stay the same button.

   What does NOT belong here:
     - A shell. The tenant sidebar lives in app.css, the admin rail in
       admin.css, the centred sign-in card in auth.css. Those are genuinely
       different furniture around the same components.
     - Anything used by exactly one page. Page-specific CSS stays in that page.

   See docs/house-style.md for which class to reach for.
   ========================================================================= */

@font-face{font-family:'Inter';font-style:normal;font-weight:100 900;font-display:swap;src:url(/assets/fonts/inter-latin-var.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
/* Brand tokens, translated from docs/mockups/linear.css (the approved
   design, see docs/DESIGN_BUILD.md step 1). Old names are kept as aliases
   pointing at the new values so pages that reference them keep working. */
:root {
    --bg: #ffffff;
    --side: #fafafa;
    --glass: rgba(0,0,0,.03);
    --glass-2: rgba(0,0,0,.055);
    --glass-3: rgba(0,0,0,.08);
    --border: #e8e8e8;
    --border-2: #d4d4d4;
    --text: #1a1c1e;
    --text-2: #5f6368;
    --text-3: #8e9196;
    --ink: #1a1c1e;
    --ink-2: #2c2f33;
    --green: #0B815A;
    --green-2: #0f8f65;
    --green-ink: #0a6b4b;
    --green-soft: rgba(11,129,90,.09);
    --r: 8px;
    --r-lg: 12px;
    --sh: 0 1px 2px rgba(0,0,0,.04), 0 8px 24px -18px rgba(0,0,0,.18);
    --amber: #c2740c;
    --red: #b3261e;
    /* Aliases for the pre-redesign token names. */
    --canvas: var(--bg);
    --card: #fff;
    --rail: var(--side);
    --muted: var(--text-2);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
/* Shell-independent half of the body rule. Each surface adds its own layout:
   `body { display: flex }` in app.css, `.adm` in admin.css, `.auth-wrap` in
   auth.css. */
body {
    font: 13px/1.45 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
    background: var(--bg);
    color: var(--text);
    -webkit-font-smoothing: antialiased;
}
a { text-decoration: none; color: var(--text); }

/* ---------- Glass modal (shared) ---------- */
.modal-back {
    position: fixed; inset: 0; z-index: 50;
    background: rgba(30,28,24,.35);
    backdrop-filter: blur(6px) saturate(120%);
    -webkit-backdrop-filter: blur(6px) saturate(120%);
    display: none; align-items: center; justify-content: center;
    padding: 40px 24px;
}
.modal-back.open { display: flex; }
/* Opened from a plain <a href="#invite"> -- no JavaScript, and the back button
   closes them. This rule lived in admin.css, so every :target dialog on a
   tenant page (the whole of Team: invite, adjust, remove) was markup nothing
   could ever show. Both surfaces open modals from links; it belongs here. */
.modal-back:target { display: flex; }
.modal {
    background: rgba(255,255,255,.92);
    backdrop-filter: blur(28px) saturate(180%);
    -webkit-backdrop-filter: blur(28px) saturate(180%);
    border: 1px solid rgba(255,255,255,.7);
    border-radius: 18px;
    width: 100%; max-width: 560px;
    max-height: calc(100vh - 80px);
    display: flex; flex-direction: column;
    overflow: hidden;
}
.modal-head {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 16px 20px;
    border-bottom: 1px solid rgba(0,0,0,.06);
    flex-shrink: 0;
}
.modal-head .ttl { font-size: 14.5px; font-weight: 700; color: #111; }
.modal-head .sub { font-size: 11.5px; color: rgba(0,0,0,.58); margin-top: 2px; }
.modal-x {
    width: 30px; height: 30px; border-radius: 9px; border: none; cursor: pointer;
    background: rgba(0,0,0,.05); color: rgba(0,0,0,.68);
    display: flex; align-items: center; justify-content: center; flex-shrink: 0;
    font-size: 15px; line-height: 1; font-family: inherit;
    transition: background .12s, color .12s;
}
.modal-x:hover { background: rgba(0,0,0,.1); color: #111; }
/* One icon set (docs/PLAN.md #24): the modal-close glyph is this SVG, not the
   ✕ character -- a character renders with whatever the visitor's font ships,
   an SVG stroke matches the sidebar icon set exactly. Use inside any
   .modal-x/.alert-x-shaped close button:
     <svg class="icon-close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
   Pages that still emit a literal ✕ are theirs to update; this utility and
   snippet exist so every future one converts to the same markup. */
.icon-close { width: 14px; height: 14px; display: block; }
.modal-body { overflow-y: auto; flex: 1; }

/* ---------- Drawer (shared) ----------
   A right-side panel for editing one thing without leaving the list behind
   it -- AI Training's edit/upload/test drawers today. docs/DESIGN_PASS.md #6. */
.drawer-back {
    position: fixed; inset: 0; z-index: 50;
    background: rgba(0,0,0,.25);
    display: none;
}
.drawer-back.open { display: block; }
.drawer {
    position: fixed; top: 0; right: 0; bottom: 0; z-index: 51;
    width: 440px; max-width: 100%;
    background: #fff;
    border-left: 1px solid var(--border);
    border-radius: var(--r-lg) 0 0 var(--r-lg);
    display: flex; flex-direction: column;
}
.drawer-head {
    padding: 16px;
    border-bottom: 1px solid var(--border);
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    flex-shrink: 0;
}
.drawer-head .ttl { font-size: 13.5px; font-weight: 600; color: var(--ink); }
.drawer-body {
    padding: 16px;
    display: grid; gap: 14px;
    overflow-y: auto; flex: 1;
}
.drawer-body label { font-size: 12.5px; font-weight: 600; color: var(--muted); display: block; margin-bottom: 6px; }
.drawer-body small { display: block; margin-top: 4px; font-size: 12.5px; color: var(--muted); }
.drawer-foot {
    padding: 16px;
    border-top: 1px solid var(--border);
    display: flex; align-items: center; justify-content: flex-end; gap: 10px;
    flex-shrink: 0;
}
.drawer-foot .btn.danger { margin-right: auto; }
@media (max-width: 600px) {
    .drawer { width: 100%; }
}


/* ---------- Shared components ---------- */
.card,
.scard {
    background: var(--card);
    border-radius: 11px;
    border: 1px solid var(--border);
}
body.shell-float .card,
body.shell-float .scard { border-radius: var(--r-lg); box-shadow: var(--sh); }
.card.pad { padding: 16px 18px; }
/* docs/mockups/linear.css .card .hd/.bd: the header-strip + body shape the
   new inbox/dashboard pages (docs/DESIGN_BUILD.md steps 2-4) build with,
   alongside the existing .card-head/.card-sub pattern above. Scoped to
   body.shell-float -- .card is also the admin's card, which this redesign
   does not touch. */
body.shell-float .card {
    border-radius: var(--r-lg);
}
body.shell-float .card .hd { display: flex; align-items: center; justify-content: space-between; padding: 13px 16px; border-bottom: 1px solid var(--border); }
body.shell-float .card .hd h2 { font-size: 13.5px; font-weight: 600; margin: 0; }
body.shell-float .card .hd p { margin: 2px 0 0; color: var(--text-2); font-size: 12.5px; }
body.shell-float .card .bd { padding: 16px; }
body.shell-float .card .bd.tight { padding: 0; }

/* Level-3 "supporting module" per docs/DESIGN_SYSTEM.md's 3-level surface
   hierarchy: same shape as .card, flatter weight, so a browsing list (recent
   activity, past-profiled contacts) doesn't compete visually with a card
   that's actually asking for today's decision (alerts, review, pipeline).
   Additive only -- opt in per card, nothing existing changes. */
.card.soft { box-shadow: none; border-color: rgba(0,0,0,.06); }

/* .scard is the settings-page card: the same box, but carrying its own padding
   with the heading inline instead of in a bordered .card-head strip. Forms want
   one continuous padded area -- a divider under the title cuts a field group in
   half for no reason.

   It lived inline in whatsapp.php and settings.php, written out identically in
   both, which is the drift this file exists to end: check.php reads the four
   stylesheets and cannot see a component defined inside a page, so neither copy
   was guarded and either could have been edited alone. */
.scard { padding: 24px 26px; }
.scard-ttl {
    font-size: 15px; font-weight: 700; color: #111;
    margin-bottom: 4px;
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
/* .55 rather than the .45 both inline copies used, for the reason .card-sub
   documents below: this line carries whole sentences, and .45 lands near 3.5:1
   on white when body text needs 4.5:1. */
.scard-sub {
    font-size: 12.5px;
    line-height: 1.5;
    color: rgba(0, 0, 0, .55);
    margin-bottom: 18px;
}
.card-head {
    padding: 16px 18px;
    border-bottom: 1px solid rgba(0,0,0,.06);
    font-size: 14px; font-weight: 600; color: #111;
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.section-title { font-size: 13px; font-weight: 700; color: #111; margin-bottom: 12px; }

/* The quiet line under a card's heading: what this card is, in one sentence.
   Explanation belongs here or behind an .info-tip, not in a full-width banner
   -- a banner is for something that happened, and using one for standing text
   makes every screen shout at the person reading it.

   rgba(0,0,0,.55) rather than the .45 used for inline muted labels: this
   carries whole sentences, and .45 lands around 3.5:1 on white, under the
   4.5:1 needed for body text. */
.card-sub {
    padding: 0 18px 14px;
    margin-top: -4px;
    font-size: 12.5px;
    line-height: 1.5;
    color: rgba(0, 0, 0, .55);
}
.card-head + .card-sub { border-bottom: 1px solid var(--a-line, rgba(0,0,0,.06)); }

/* Wide content -- a table with many columns -- scrolls inside its own box
   rather than pushing the page sideways. A horizontally scrolling *page* is
   the thing to avoid; a scrolling table is fine and expected. */
.scroll-x { overflow-x: auto; }

.stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 18px; }
.stat-label {
    font-size: 12px; font-weight: 600; color: rgba(0,0,0,.58);
    letter-spacing: .02em; text-transform: uppercase;
}
.stat-num { font-size: 30px; font-weight: 800; color: #111; letter-spacing: -0.02em; }
/* The same tile with type that fits a word. .stat-num is 30px/800 with
   negative tracking, which is right for a figure and wrong for "3d ago" --
   set that way a phrase looks like a mistake beside a real number. Lived in
   admin.css until the channel pages needed it, which is the whole rule. */
.stat-word {
    margin-top: 4px;
    font-size: 20px; font-weight: 700; color: #111;
    letter-spacing: -.01em; line-height: 1.25;
}
.stat-trend { display: flex; align-items: center; gap: 4px; margin-top: 6px; font-size: 12px; font-weight: 600; }
.stat-trend.up { color: #16a34a; }
.stat-trend.flat { color: rgba(0,0,0,.58); }
.stat-trend svg { width: 11px; height: 11px; fill: none; stroke: currentColor; stroke-width: 2.4; }

table.list { width: 100%; border-collapse: collapse; }
table.list thead th {
    text-align: left; font-size: 11px; font-weight: 700;
    text-transform: uppercase; letter-spacing: .04em;
    color: rgba(0,0,0,.58);
    padding: 13px 18px;
    border-bottom: 1px solid rgba(0,0,0,.06);
}
table.list tbody td {
    padding: 13px 18px; font-size: 13px; font-weight: 500;
    color: rgba(0,0,0,.75);
    border-bottom: 1px solid rgba(0,0,0,.05);
}
table.list tbody tr:last-child td { border-bottom: none; }
table.list tbody tr:hover { background: rgba(0,0,0,.015); }
.muted { color: rgba(0,0,0,.58); }

/* ---------- Shared "detail page" header + filter bar ----------
   One look for every "back arrow + name + filters" row on the site
   (Customers' salesman/group view, AI Costs' per-message detail, …) —
   change it here once instead of per page. */
.detail-bar { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }
.detail-head { display: flex; align-items: center; gap: 12px; min-width: 0; }
.detail-back {
    width: 34px; height: 34px; border-radius: 9px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    background: rgba(0,0,0,.05); color: #111; transition: background .12s;
}
.detail-back:hover { background: rgba(0,0,0,.09); }
.detail-back svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2.2; }
.detail-title { font-size: 16px; font-weight: 700; color: #111; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.detail-sub { font-size: 12px; color: rgba(0,0,0,.58); margin-top: 2px; }

.gfilter { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Real vendor logos rendered where a stroked SVG icon would otherwise sit
   (AiProvider::iconHtml). object-fit keeps non-square source images from
   stretching into the square icon box the surrounding CSS gives them. */
.ai-icon-img { object-fit: contain; display: inline-block; }
/* min-width: 0 is load-bearing: a flex item's default min-width is its
   content size, which ignores overflow-x entirely -- without it, a tab strip
   too wide for its container still forces the container (and everything
   above it sharing the page) wider than the viewport instead of scrolling in
   its own lane. Every .gfilter-tab has white-space: nowrap, so with nothing
   to stop it, this is a page that scrolls sideways past its own header. */
/* Used to fade the scroll edges with two linear-gradient + two
   radial-gradient background layers (no JS). Gradients are banned from this
   file now (docs/PLAN.md #23), so the fade is gone; the border still marks
   the box's edge. */
.gfilter-tabs {
    display: flex; gap: 3px; padding: 4px; border: 1px solid var(--border); border-radius: 13px;
    max-width: 100%; min-width: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none;
    background-color: var(--canvas);
}
.gfilter-tabs::-webkit-scrollbar { display: none; }
.gfilter-tab {
    display: flex; align-items: center; gap: 5px; padding: 9px 16px; border-radius: 10px;
    font-family: inherit; font-size: 12.5px; font-weight: 700; color: rgba(0,0,0,.68);
    text-decoration: none; background: none; border: none; cursor: pointer;
    transition: all .12s; white-space: nowrap;
}
.gfilter-tab:hover { color: #111; }
.gfilter-tab.active { background: #fff; color: var(--ink); border: 1px solid var(--border); font-weight: 700; }
/* Date range: one pill-shaped control instead of two bare native inputs
   floating next to a dash — matches the .gfilter-tabs box next to it. */
.gfilter-dates {
    display: flex; align-items: center; gap: 8px;
    padding: 4px 6px; background: var(--canvas); border: 1px solid rgba(0,0,0,.08); border-radius: 13px;
}
.gfilter-dates input[type=date] {
    padding: 7px 9px; background: #fff; border: 1px solid rgba(0,0,0,.1); border-radius: 9px;
    font-family: inherit; font-size: 12.5px; font-weight: 600; color: #111; color-scheme: light;
}
.gfilter-dates input[type=date]:focus { outline: none; border-color: #111; box-shadow: 0 0 0 3px rgba(11,129,90,.25); }
.gfilter-dates span { font-size: 12px; font-weight: 600; color: rgba(0,0,0,.58); }
@media (max-width: 700px) { .gfilter { width: 100%; } }

/* Touch targets.
   These controls sit at roughly 26-29px tall, which is fine for a cursor and
   too small for a thumb. Raised to the 44px everyone's mobile guidelines ask
   for, but only where the input is actually coarse -- growing them on desktop
   would make a filter row of eight pills look like a row of buttons. Padding
   rather than height, so the label stays vertically centred. */
@media (pointer: coarse) {
    .gfilter-tab { min-height: 44px; padding-top: 12px; padding-bottom: 12px; }
    .btn.sm { min-height: 44px; padding-top: 11px; padding-bottom: 11px; }
    .switch-row { padding-top: 12px; padding-bottom: 12px; }
    /* The switch stays 38x22 -- .chswitch-track is inset:0 against it, so
       growing the box would stretch the pill itself. This grows only what a
       thumb has to land on. */
    .chswitch::after {
        content: ''; position: absolute; left: 50%; top: 50%;
        width: 44px; height: 44px; transform: translate(-50%, -50%);
    }
}

/* ---------- Shared "cs" custom dropdown ----------
   Used anywhere a native <select>'s open menu needs real styling (handoff
   status, campaign objective, …). One definition so every instance matches,
   and the panel is position:fixed (placed by JS from the trigger's
   bounding rect) so it always escapes a scrolling/clipped table or card
   instead of getting cut off at the container's edge. */
.cs { position: relative; display: inline-block; }
.cs-trigger {
    display: flex; align-items: center; gap: 6px;
    padding: 6px 10px; background: #fff; border: 1px solid rgba(0,0,0,.12); border-radius: 8px;
    font-family: inherit; font-size: 11.5px; font-weight: 600; color: #111; cursor: pointer;
    transition: border-color .12s, box-shadow .12s;
}
.cs-trigger:hover { border-color: rgba(0,0,0,.58); }
.cs.open .cs-trigger { border-color: #111; box-shadow: 0 0 0 3px rgba(11,129,90,.25); }
.cs-trigger svg { width: 12px; height: 12px; fill: none; stroke: rgba(0,0,0,.4); stroke-width: 2; flex-shrink: 0; transition: transform .15s; }
.cs.open .cs-trigger svg { transform: rotate(180deg); }
.cs-panel {
    position: fixed; z-index: 1000; min-width: 140px;
    background: #fff; border: 1px solid var(--border); border-radius: 12px;
    padding: 6px; display: none;
}
.cs.open .cs-panel { display: block; }
.cs-option {
    display: block; width: 100%; text-align: left; padding: 8px 10px; border-radius: 8px;
    font-family: inherit; font-size: 12.5px; font-weight: 500; color: rgba(0,0,0,.7);
    background: none; border: none; cursor: pointer; transition: background .1s, color .1s;
}
.cs-option:hover { background: rgba(0,0,0,.05); color: #111; }
.cs-option.sel { background: rgba(11,129,90,.08); color: #0B815A; font-weight: 700; }
.cs-lg .cs-trigger { padding: 8px 14px; font-size: 12.5px; border-radius: 9px; }
.cs-lg .cs-trigger svg { width: 13px; height: 13px; }
.cs-center { display: flex; justify-content: center; }
/* Full-width variant, for the places a native <select> used to sit inside a
   .field — matches the height and radius of the text inputs beside it. */
.cs-block { display: block; width: 100%; }
.cs-block .cs-trigger {
    width: 100%; justify-content: space-between; gap: 10px;
    padding: 11px 13px; font-size: 13px; font-weight: 500; border-radius: 10px;
    background: var(--canvas); border-color: rgba(0,0,0,.1);
}
.cs-block .cs-trigger:hover { background: #fff; }
.cs-block .cs-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cs-panel-scroll { max-height: 320px; overflow-y: auto; }
.cs-panel-searchable { padding: 6px; overflow: hidden; }
.cs-search {
    display: flex; align-items: center; gap: 8px; margin: 0 0 5px; padding: 0 9px;
    min-height: 38px; border: 1px solid rgba(0,0,0,.1); border-radius: 8px; background: var(--canvas);
    transition: border-color .12s, box-shadow .12s, background .12s;
}
.cs-search:focus-within { border-color: var(--green); box-shadow: 0 0 0 3px rgba(11,129,90,.18); background: #fff; }
.cs-search svg { width: 14px; height: 14px; flex: 0 0 14px; fill: none; stroke: rgba(0,0,0,.45); stroke-width: 2; stroke-linecap: round; }
.cs-search-input {
    width: 100%; min-width: 0; padding: 0; border: 0; outline: 0; background: transparent;
    color: #111; font: 500 12.5px/1.2 inherit;
}
.cs-search-input::-webkit-search-cancel-button { cursor: pointer; opacity: .55; }
/* ...but not on the inline combobox, where the field already carries a
   chevron: two glyphs in one control, one of which silently wipes what you
   typed, is a stray X sitting in the corner of the field. Its own rule, not
   grouped with ::-ms-clear -- one unknown pseudo-element in a selector list
   makes a browser drop the whole rule. */
.cs-inline-input::-webkit-search-cancel-button { display: none; }
.cs-inline-input::-webkit-search-decoration { display: none; }
.cs-options { overscroll-behavior: contain; }
.cs-custom-option { border-top: 1px solid rgba(0,0,0,.07); margin-top: 3px; padding-top: 10px; }
.cs-empty { margin: 0; padding: 10px; color: rgba(0,0,0,.5); font-size: 12px; text-align: center; }
/* Positioned against the .cs rather than the viewport — required whenever an
   ancestor (a modal's backdrop-filter, say) is the containing block for
   fixed-position children, which silently breaks viewport coordinates. */
.cs-panel-anchored { position: absolute; top: calc(100% + 6px); left: 0; right: auto; }

/* Calendar/date-range picker cells — the global shared design for every
   date filter in the app (AI Costs, Ads' Start/End dates), so a calendar
   looks and behaves the same wherever it shows up. Each page still sets
   its own .dp-pill/.dp-trigger/.dp-panel sizing (a topbar filter pill vs.
   a form field trigger are legitimately different contexts), but the
   calendar grid itself — the part users actually compare page to page —
   is one definition here. */
.dp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.dp-head-label { font-size: 12.5px; font-weight: 700; color: #111; }
.dp-hint { font-size: 10.5px; color: rgba(0,0,0,.58); margin: -4px 0 8px; }
.dp-nav { width: 22px; height: 22px; border-radius: 7px; border: none; background: none; cursor: pointer; display: flex; align-items: center; justify-content: center; color: rgba(0,0,0,.68); transition: background .1s; }
.dp-nav:hover { background: rgba(0,0,0,.06); color: #111; }
.dp-nav svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.dp-wds { display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px; }
.dp-days { display: grid; grid-template-columns: repeat(7, 1fr); }
.dp-wd { font-size: 9.5px; font-weight: 700; color: rgba(0,0,0,.58); text-align: center; padding: 3px 0; text-transform: uppercase; }
.dp-day { font-size: 12px; font-weight: 500; color: #111; text-align: center; padding: 5px 0; border: none; background: none; cursor: pointer; font-family: inherit; }
.dp-day:hover { background: rgba(0,0,0,.06); }
.dp-day.dp-muted { color: rgba(0,0,0,.25); }
.dp-day.dp-today { color: #0B815A; font-weight: 700; }
/* Not-arrived-yet days — a lighter grey than muted (previous/next month
   overflow) so "in this month but hasn't happened" reads as its own,
   distinct, non-interactive-looking state. */
.dp-day.dp-future { color: rgba(0,0,0,.58); }
.dp-day.dp-inrange { background: rgba(17,17,17,.08); }
.dp-day.dp-start, .dp-day.dp-end { background: #111; color: #fff; border-radius: 7px; }
.dp-day.dp-start.dp-inrange { border-radius: 7px 0 0 7px; }
.dp-day.dp-end.dp-inrange { border-radius: 0 7px 7px 0; }
.dp-clear { padding: 6px 0; font-size: 11.5px; font-weight: 600; color: rgba(0,0,0,.68); background: none; border: none; cursor: pointer; }
.dp-clear:hover { color: #111; }

/* vertical-align: middle, not the inline-block default of baseline. A pill has
   padding, so sitting it on the text baseline lifts it above the line it is
   supposed to be on -- next to plain text in the cell beside it, every badge
   in every table read as slightly too high. */
/* The badge carried its own green -- #15803d on rgba(22,163,74,.12) -- which
   is not the accent the rest of the app uses. Two greens a shade apart is
   worse than two obviously different ones: it reads as careless without
   anyone being able to name why. One green, #0B815A, everywhere.

   Fixed height and a dot slot so a row of them lines up and reads as one
   set of states rather than as differently-sized coloured pills. */
.badge {
    display: inline-flex; align-items: center; gap: 6px;
    height: 24px; padding: 0 11px; border-radius: 20px;
    font-size: 11.5px; font-weight: 650; vertical-align: middle;
    background: rgba(11,129,90,.1); color: #0B815A;
}
.badge::before {
    content: ''; width: 6px; height: 6px; border-radius: 50%;
    background: currentColor; flex-shrink: 0;
    /* Only the four channel states earn a dot; a role or a count does not. */
    display: none;
}
.badge.state::before { display: block; }
/* Same correction for anything else dropped inline into a table cell. */
table.list td img, table.list td svg { vertical-align: middle; }
.badge.dark { background: #111; color: #fff; }
.badge.gray { background: rgba(0,0,0,.055); color: rgba(0,0,0,.58); }
.badge.warn { background: rgba(224,138,30,.14); color: #c2740c; }
.badge.red { background: rgba(185,28,28,.09); color: #b91c1c; }
/* A column of badges that has to line up -- Yes/No, On/Off. A pill sizes to
   its content, so "Yes" and "No" come out different widths and a grid of them
   reads as ragged rather than as a matrix you can scan down. */
.badge.fixed { min-width: 46px; text-align: center; }

/* Small "?" hover tooltip — explains a stat/section without permanently
   taking up page space. Used across Settings, AI Costs, Dashboard, and
   Insights so the same explanation pattern reads consistently everywhere. */
.info-tip {
    position: relative; display: inline-flex; align-items: center; justify-content: center;
    width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0;
    background: rgba(0,0,0,.08); color: rgba(0,0,0,.68);
    font-size: 10px; font-weight: 700; cursor: help; margin-left: 5px; vertical-align: middle;
}
.info-tip:hover, .info-tip:focus { background: #111; color: #fff; outline: none; }
.info-tip:hover::after, .info-tip:focus::after {
    content: attr(data-tip);
    position: absolute; bottom: 130%; left: 0; transform: none;
    background: #111; color: #fff; padding: 8px 10px; border-radius: 9px;
    font-size: 10.5px; font-weight: 500; line-height: 1.4; white-space: pre-line;
    width: 230px; z-index: 30;
}
/* The tooltip opens upward, which is right almost everywhere and wrong at the
   top of a page -- there it opens into the header, or off the screen entirely.
   `.info-tip.down` opens below instead. CSS cannot flip this on its own, so the
   handful of tips that sit in a first-card title ask for it. */
.info-tip.down:hover::after, .info-tip.down:focus::after { bottom: auto; top: 130%; }

.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    padding: 10px 18px;
    background: var(--green); color: #fff;
    border: none; border-radius: 10px;
    font-family: inherit; font-size: 13px; font-weight: 600;
    cursor: pointer;
    transition: transform .1s, background .12s;
}
.btn:hover { background: #0a6f4e; transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
a.btn { color: #fff; }
a.btn:hover { color: #fff; }
.btn.ghost {
    background: #fff; color: var(--ink);
    border: 1px solid var(--border); box-shadow: none;
}
.btn.ghost:hover { background: var(--canvas); color: #111; }
/* .sm for a button sitting in a card head or a table row, where the full
   38px control pushes the row taller than the text it belongs to. .danger for
   the one that destroys something. Both were admin-only until Team needed
   them; a second surface means they move here rather than get copied. */
.btn.sm { padding: 6px 11px; font-size: 12px; }
.btn.danger { background: #fff; color: var(--red); border: 1px solid rgba(179,38,30,.3); }
.btn.danger:hover { background: var(--canvas); }
/* house.js adds this to any submit button while its form's real round trip
   is in flight -- most forms here still do a full POST, so the wait
   between click and the next page painting was previously silent. */
.btn-spin {
    width: 13px; height: 13px; flex-shrink: 0;
    border: 2px solid rgba(255,255,255,.35); border-top-color: #fff;
    border-radius: 50%; animation: btnSpin .7s linear infinite;
}
.btn.ghost .btn-spin { border-color: rgba(0,0,0,.2); border-top-color: rgba(0,0,0,.6); }
@keyframes btnSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .btn-spin { animation: none; opacity: .5; }
}
/* house.js's confirm-typed-name gate -- dead until the name matches, then
   pops live. */
.btn.confirm-locked { opacity: .4; cursor: not-allowed; box-shadow: none; }
.btn.confirm-pop { animation: confirmPop .3s cubic-bezier(.34,1.56,.64,1); }
@keyframes confirmPop { 0%, 100% { transform: scale(1); } 45% { transform: scale(1.045); } }
@media (prefers-reduced-motion: reduce) {
    .btn.confirm-pop { animation: none; }
}
/* Outlined destructive -- Disconnect, not Delete. Solid red is for the thing
   with no undo; disconnecting a channel is reversible in a click, and drawing
   it as loudly as "drop this database" is what made every channel page invent
   its own quieter version instead: two of them were .btn.ghost with an inline
   colour override and the third was a .retry-link, three treatments of one
   verb. This is that button, defined once. */
.btn.ghost.danger {
    background: rgba(255,255,255,.7); color: #b91c1c;
    border: 1px solid rgba(185,28,28,.3); box-shadow: none;
}
.btn.ghost.danger:hover { background: #fff; color: #991b1b; border-color: rgba(185,28,28,.55); }
/* Each channel's own brand colour, so its connect action reads as an
   Instagram/Messenger action rather than another generic dark button.
   Gradients and box-shadows are banned from this file (docs/PLAN.md #23) --
   the .ig logo class keeps the Instagram gradient, this button does not, so
   each is a flat brand colour instead. */
.btn.ig-btn { background: #d62976; }
.btn.ig-btn:hover { background: #b81f63; }
.btn.ms-btn { background: #0866FF; }
.btn.ms-btn:hover { background: #0a53cc; }

/* New shell only (docs/DESIGN_BUILD.md step 1, docs/mockups/linear.css
   .btn/.btn.primary/.btn.sm/.btn.ghost): the frozen App Review tenant never
   gets body.shell-float, so its buttons -- including login.php's -- keep the
   green fill above untouched. */
body.shell-float .btn {
    background: #fff; color: var(--text);
    border: 1px solid var(--border-2); border-radius: var(--r);
    padding: 0 11px; height: 30px;
    font-size: 12.5px; font-weight: 500;
}
body.shell-float .btn:hover { background: #f6f6f7; transform: none; }
body.shell-float a.btn, body.shell-float a.btn:hover { color: var(--text); }
/* check.php's style contract (docs/PLAN.md #23) bans gradients and shadows
   outside a narrow allowlist, which the mockup's .btn.primary gradient and
   raised shadow do not fit -- flat var(--ink) instead. Noted under "Open"
   in docs/DESIGN_BUILD.md. */
body.shell-float .btn.primary {
    background: var(--ink);
    border-color: var(--ink); color: #fff; font-weight: 600;
    box-shadow: 0 1px 0 rgba(255,255,255,.12) inset, 0 4px 12px -8px rgba(0,0,0,.5);
}
body.shell-float .btn.primary:hover { background: var(--ink-2); }
body.shell-float a.btn.primary, body.shell-float a.btn.primary:hover { color: #fff; }
body.shell-float .btn.sm { height: 26px; padding: 0 9px; font-size: 12px; }
body.shell-float .btn.ghost { background: transparent; border-color: transparent; color: var(--text-2); box-shadow: none; }
body.shell-float .btn.ghost:hover { background: var(--glass); color: var(--text); }
body.shell-float .btn.danger { background: #fff; color: var(--red); border-color: rgba(179,38,30,.3); }
body.shell-float .btn.danger:hover { background: var(--glass); }
body.shell-float .btn.ghost.danger { background: transparent; color: #b91c1c; border: 1px solid rgba(185,28,28,.3); }
body.shell-float .btn.ghost.danger:hover { background: rgba(185,28,28,.06); color: #991b1b; border-color: rgba(185,28,28,.55); }
body.shell-float .btn-spin { border-color: rgba(0,0,0,.15); border-top-color: rgba(0,0,0,.5); }
body.shell-float .btn.primary .btn-spin { border-color: rgba(255,255,255,.35); border-top-color: #fff; }

.field { margin-bottom: 15px; }
.field label { display: block; font-size: 12px; font-weight: 600; color: rgba(0,0,0,.68); margin-bottom: 6px; }
.field small { font-weight: 500; color: rgba(0,0,0,.58); }
/* Every text-like input, by exclusion rather than by list.
   This was a list of four types, and `email` was not one of them -- so Team's
   "Their email" rendered as a raw browser input in the middle of a styled form
   while nothing looked wrong in the markup. Listing types means the next `tel`
   or `url` field breaks the same way; excluding the three that must keep their
   native rendering means it cannot happen again. */
.field input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=hidden]),
.field textarea, .field select {
    width: 100%; padding: 10px 12px;
    background: #fff;
    border: 1px solid var(--border);
    border-radius: 8px;
    color: #111; font-family: inherit; font-size: 13.5px; font-weight: 500;
    outline: none;
    transition: border-color .12s, box-shadow .12s;
}
.field input:focus, .field textarea:focus, .field select:focus {
    border-color: var(--green);
    box-shadow: 0 0 0 3px rgba(11,129,90,.25);
    background: #fff;
}
.field select {
    appearance: none; -webkit-appearance: none; -moz-appearance: none;
    padding-right: 36px; cursor: pointer;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 12px center; background-size: 14px;
}
.field input[type=date] { color-scheme: light; cursor: pointer; }
.field input[type=date]::-webkit-calendar-picker-indicator {
    cursor: pointer; opacity: .45; filter: invert(0);
    padding: 4px; border-radius: 6px; margin-right: -4px;
    transition: opacity .12s, background .12s;
}
.field input[type=date]::-webkit-calendar-picker-indicator:hover { opacity: .8; background: rgba(0,0,0,.06); }
.mini-input, .mini-select {
    padding: 8px 11px; background: var(--canvas);
    border: 1px solid rgba(0,0,0,.12); border-radius: 9px;
    color: #111; font-family: inherit; font-size: 12.5px; font-weight: 500;
    outline: none; transition: border-color .12s, box-shadow .12s;
}
.mini-input:focus, .mini-select:focus { border-color: #111; box-shadow: 0 0 0 3px rgba(11,129,90,.25); background: #fff; }
.mini-select {
    appearance: none; -webkit-appearance: none; -moz-appearance: none;
    padding-right: 30px; cursor: pointer;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 9px center; background-size: 13px;
}
.field textarea { resize: vertical; }

.avatar-sm {
    width: 30px; height: 30px; border-radius: 50%;
    background: var(--canvas);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 12px; font-weight: 700; color: #333;
    flex-shrink: 0;
}

.empty {
    padding: 52px 24px; text-align: center;
    color: rgba(0,0,0,.58); font-size: 13.5px; font-weight: 500;
    display: flex; flex-direction: column; align-items: center;
}
.empty svg { width: 34px; height: 34px; stroke: rgba(0,0,0,.2); fill: none; margin-bottom: 12px; }


@media (max-width: 1100px) {
    .stat-grid { grid-template-columns: repeat(2, minmax(0,1fr)); }
}

/* Monospace runs: ids, hostnames, tokens. Used by the admin, the tenant
   Connect page and the sign-in pages, so it belongs here -- it was
   previously defined only in the design stylesheet, which meant the tenant
   app referenced a class nothing served to it defined. */
.mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 12px; }


/* ---------- Shared components that were stranded ----------
   These came out of the design stylesheet and, when it was split, landed in
   auth.css -- which only the sign-in pages load. So the tenant app and the
   admin could not use an .alert or a .btn-link at all, and any page needing
   one hand-rolled it with inline styles instead. That is exactly the drift
   house.css exists to prevent, reappearing one level down.

   Everything below is used by more than one surface. .badge is deliberately
   not repeated: house.css already defines it, and one definition is the
   entire point. */

.btn-link {
    background: none; border: none; padding: 0; cursor: pointer;
    font-family: inherit; font-size: 12.5px; font-weight: 600; color: #0B815A;
}
.btn-link:hover { text-decoration: underline; }
/* ---------- Badges ---------- */
.modal-foot { display: flex; justify-content: flex-end; gap: 10px; padding: 0 20px 20px; }
/* ---------- Alert / failure states --------------------------------------
   The brief is explicit that these are product surface, not a red toast:
   every one states what happened, what it means for the business right now,
   and the one action that fixes it. Same anatomy every time so a client
   learns to read them.  */
.alert {
    display: flex; align-items: center; gap: 13px; padding: 15px 17px;
    border-radius: 14px; border: 1px solid; background: #fff;
}
.alert + .alert { margin-top: 12px; }
.alert-ico {
    width: 32px; height: 32px; border-radius: 9px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
}
.alert-ico svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 2; }
.alert-main { flex: 1; min-width: 0; }
.alert-ttl { font-size: 13.5px; font-weight: 700; color: #111; }
.alert-txt { font-size: 12.5px; color: rgba(0,0,0,.6); line-height: 1.6; margin-top: 5px; }
.alert-txt b { color: rgba(0,0,0,.8); font-weight: 600; }
.alert-acts { display: flex; align-items: center; gap: 10px; margin-top: 13px; flex-wrap: wrap; }
.alert-acts .btn { padding: 8px 15px; font-size: 12.5px; }
.alert.danger { border-color: rgba(0,0,0,.1); background: rgba(220,38,38,.035); }
.alert.danger .alert-ico { background: rgba(220,38,38,.11); color: #b91c1c; }
.alert.warn { border-color: rgba(224,138,30,.35); background: rgba(224,138,30,.045); }
.alert.warn .alert-ico { background: rgba(224,138,30,.15); color: #c2740c; }
.alert.info { border-color: rgba(0,0,0,.1); background: var(--canvas); }
.alert.info .alert-ico { background: rgba(0,0,0,.06); color: rgba(0,0,0,.68); }
.alert.ok { border-color: rgba(22,163,74,.3); background: rgba(22,163,74,.04); }
.alert.ok .alert-ico { background: rgba(22,163,74,.13); color: #15803d; }
/* admin/tenant.php's own suspended notice only -- .alert.info elsewhere is
   too generic a class to animate globally, this fades in on its own. */
.tenant-suspended-notice { animation: tenantSuspendedIn .4s ease .1s both; }
@keyframes tenantSuspendedIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
    .tenant-suspended-notice { animation: none; }
}

/* ---------- Any of the above, as a toast ---------------------------------
   A one-off confirmation ("testing is now active", "Facebook connected",
   "Settings saved") used whichever of .alert/.saved-bar/.err-bar fit and
   rendered in flow, at the top -- so every action pushed everything below it
   down. .toast is a positioning modifier, not a new surface: it works on any
   of the three, floating the same markup over the bottom-right corner
   instead, so nothing on the page moves. Only opt in banners that confirm an
   action just taken. A standing fact -- "this workspace is suspended", a
   validation error still attached to the field it is about -- stays in flow
   with no .toast, because it is true for as long as the page is open, not
   for a few seconds after a click. */
.toast {
    position: fixed; right: 22px; bottom: 22px; z-index: 90; max-width: 360px;
    opacity: 0; transform: translateY(10px) scale(.98); pointer-events: none;
    transition: opacity .22s ease, transform .22s ease;
}
.toast.show { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
/* A toast holding a one-time invite link needs room for the link field and
   Copy button, and must not clear itself before it's been copied -- see
   house.js, which skips the auto-dismiss timer for [data-persist]. */
.toast[data-persist] { max-width: 420px; }
.alert-x {
    background: none; border: none; padding: 2px; cursor: pointer; color: rgba(0,0,0,.4);
    flex-shrink: 0; align-self: flex-start; display: flex; align-items: center; justify-content: center;
}
.alert-x:hover { color: rgba(0,0,0,.65); }
.alert-x svg { width: 13px; height: 13px; stroke: currentColor; stroke-width: 2; fill: none; }
@media (max-width: 640px) {
    .toast { left: 16px; right: 16px; bottom: 16px; max-width: none; }
}

/* ---------- "Saved." ----------------------------------------------------
   The thin green strip the tenant app shows after a save. An .alert is the
   wrong instrument for this: an alert states what happened, what it means and
   what to do about it, which is three things too many for confirming that a
   dropdown you just changed did in fact change. This says one thing quietly
   and sits next to the control that caused it.

   It was written inline in settings.php, ai-training.php, facebook.php and
   instagram.php -- four copies of the same nine lines, which is the drift
   house.css exists to end. Defined once here; those pages can drop their
   copies as they are next touched. */
.saved-bar {
    display: flex; align-items: center; gap: 9px;
    background: #e8f5ef; border: 1px solid rgba(11,129,90,.3);
    border-radius: 11px; padding: 11px 15px;
    font-size: 13px; font-weight: 600; color: #0B815A;
}
.saved-bar svg { width: 15px; height: 15px; flex-shrink: 0; }
/* The tick strokes itself on rather than just appearing -- Notify.php's own
   icon path is ~20 units long, so 20 is exact rather than a guess. */
.saved-bar svg path { stroke-dasharray: 20; stroke-dashoffset: 20; animation: checkDraw .34s cubic-bezier(.2,.7,.3,1) .15s forwards; }
@keyframes checkDraw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
    .saved-bar svg path { animation: none; stroke-dashoffset: 0; }
}
/* Whacl\Notify's wrapper around the message (and, on the one case that has
   one, the invite-link copy box below it) -- lets a bar built from PHP stack
   two things vertically without fighting .saved-bar/.err-bar's own
   align-items:center. */
.notify-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }

/* ---------- The red counterpart -------------------------------------------
   Same instrument as .saved-bar, opposite news. It was inline in facebook.php
   and instagram.php with identical declarations, which is how .scard ended up
   in four copies with one of them already drifted. */
.err-bar {
    display: flex; align-items: center; gap: 9px;
    background: #fbe9e7; border: 1px solid rgba(192,57,43,.3);
    border-radius: 11px; padding: 11px 15px;
    font-size: 13px; font-weight: 600; color: #b3261e;
}
.err-bar svg { width: 15px; height: 15px; flex-shrink: 0; }

/* ---------- Channel status ------------------------------------------------
   The three channel pages all answer the same question -- is this connected,
   and is it actually delivering -- and each drew its own answer differently.
   One set of components so WhatsApp, Facebook and Instagram read as three
   views of one thing rather than three pages built in three sittings.

   .conn is the inline dot-and-word. .status-grid holds the fact tiles, which
   carry a label, a value with a dot, and a line of plain English underneath
   saying what to do about it -- that third line is the whole point: a red dot
   with no sentence is what sent someone looking for the fault at Meta. */
.conn { display: flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 600; margin-top: 4px; }
.conn .dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
.conn.on { color: #0B815A; }
.conn.on .dot { background: #0B815A; }
.conn.off { color: #c2740c; }
.conn.off .dot { background: #e08a1e; }

.status-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
/* Three facts rather than two or four. Two-across leaves the third tile alone
   on its own row next to a tile-sized hole, which is what the Facebook and
   Instagram pages looked like once each grew a delivery chip. A modifier
   rather than auto-fit: these tiles carry a sentence, and letting the count
   float with the viewport made the same card read differently on two
   screens. */
.status-grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }

/* The same tile, standing on the page ground instead of inside a card.
   .status-chip is inset -- a pale box on white, which is right when it sits
   in a .scard and wrong the moment it does not: a grey box on a grey page
   inside a white card is three surfaces deep and reads as muddy. This is the
   Dashboard's own stat card, which is what these tiles became when the
   channel pages stopped wrapping them.

   Only the surface changes. The label, value and sentence inside are
   .status-chip's, because they are the same three things either way. */
.status-chip.tile {
    padding: 18px 20px;
    background: #fff;
    border-color: var(--border);
}

/* A section title with something aligned opposite it -- a range, a count, a
   link. .section-title alone is a block, so a caller wanting the pair wrote
   its own flex row every time. */
.section-row {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 12px; margin-bottom: 12px;
}
.section-row .section-title { margin-bottom: 0; }
.status-chip { padding: 16px 18px; border: 1px solid rgba(0,0,0,.06); border-radius: 12px; background: var(--canvas); }
.status-chip .t { font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: rgba(0,0,0,.58); margin-bottom: 8px; }
.status-chip .v { font-size: 15px; font-weight: 800; color: #111; display: flex; align-items: center; gap: 9px; }
/* Wraps rather than ellipsing: this line is a sentence telling someone what
   is wrong, and a truncated instruction is worse than none. */
.status-chip .sub { font-size: 12px; font-weight: 500; color: rgba(0,0,0,.58); margin-top: 6px; line-height: 1.45; }
.dot-ok { width: 9px; height: 9px; border-radius: 50%; background: #0B815A; display: inline-block; flex-shrink: 0; }
.dot-bad { width: 9px; height: 9px; border-radius: 50%; background: #c0392b; display: inline-block; flex-shrink: 0; }
/* Amber, for "half of this works". Distinct from .dot-bad on purpose --
   credentials saved but nothing routing is not the same as nothing at all,
   and rendering both red is what made the difference invisible. */
.dot-warn { width: 9px; height: 9px; border-radius: 50%; background: #e08a1e; display: inline-block; flex-shrink: 0; }

/* Both variants collapse together — .cols-3 has to be named explicitly or its
   own rule above stays more specific than this one and three tiles stay three
   tiles on a phone.

   1000px, not 1100: the channel pages run the full width of the main column
   now, and at 1100 the third tile was still dropping to its own row on an
   ordinary laptop — the exact hole this variant exists to close. */
@media (max-width: 1000px) { .status-grid.cols-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 760px) { .status-grid, .status-grid.cols-3 { grid-template-columns: 1fr; } }
.nav-sep { height: 1px; background: rgba(0,0,0,.07); margin: 9px 12px; }
.sw input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; z-index: 2; }
.sw i {
    position: absolute; inset: 0; border-radius: 20px; background: rgba(0,0,0,.18);
    transition: background .15s; pointer-events: none;
}
.sw i::after {
    content: ''; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px;
    border-radius: 50%; background: #fff;
    transition: transform .15s;
}
.sw input:checked + i { background: #0B815A; }
.sw input:checked + i::after { transform: translateX(18px); }
.sw input:focus-visible + i { box-shadow: 0 0 0 3px rgba(11,129,90,.25); }

/* ---------- Table rows that describe a person ----------
   An avatar, a name and a quiet second line. Used by Team and by anything else
   that lists people; the alternative is every such table inventing its own
   flex row inline, which is how the last set of pages ended up mismatched. */
.person { display: flex; align-items: center; gap: 11px; flex-wrap: nowrap; }
.person-txt { min-width: 0; }
.person-name {
    display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
    font-weight: 600; color: #111;
}
.person-sub { display: block; font-size: 11.5px; color: rgba(0,0,0,.58); margin-top: 2px; }

/* One action per row, and the rest as links. A row of filled buttons makes
   every option look equally likely, which is how a destructive one gets
   clicked by accident. */
/* Three actions of different weight (Adjust, Reset/Resend, Remove) sitting
   right against each other with nothing but a gap between them read as a
   pile rather than a row -- the same three links with no divider look
   scaffolded regardless of the gap size. A hairline between them is the
   whole fix; the gap only needed to make room for it. */
.row-actions { display: inline-flex; align-items: center; justify-content: flex-end; }
.row-actions form { display: inline; }
.row-actions > a, .row-actions > form {
    padding-left: 16px; border-left: 1px solid rgba(0, 0, 0, .1);
    margin-left: 16px;
}
.row-actions > a:first-child, .row-actions > form:first-child {
    padding-left: 0; border-left: none; margin-left: 0;
}
.row-form { display: flex; align-items: center; gap: 8px; margin: 0; }
.btn-link.danger { color: #b91c1c; }
.btn-link.danger:hover { color: #991b1b; }
.small { font-size: 11.5px; }

/* A checkbox and its label, sized so the whole row is the target rather than
   the 13px box alone. */
.check-row {
    display: flex; align-items: center; gap: 11px;
    padding: 9px 0; font-size: 13.5px; cursor: pointer;
    min-height: 40px;
}
.check-row input { width: 16px; height: 16px; flex-shrink: 0; accent-color: #111; cursor: pointer; }

/* Picking one thing out of a list of real things -- a Facebook Page, an ad
   account -- where the choice has consequences and the id matters as much as
   the name. .check-row is for settings; this is a decision, so the whole row
   is a bordered target that fills in when selected rather than a bare radio
   with text beside it. */
.pick-row {
    display: flex; align-items: center; gap: 12px;
    padding: 13px 15px; cursor: pointer;
    border: 1px solid rgba(0,0,0,.1); border-radius: 12px;
    background: #fff; transition: border-color .12s, background .12s, box-shadow .12s;
}
/* Air between rows. Flush against each other they read as one striped
   block rather than as two things you are choosing between. */
.pick-row + .pick-row { margin-top: 10px; }
.pick-row:hover { border-color: rgba(0,0,0,.22); }
.pick-row input { width: 17px; height: 17px; flex-shrink: 0; accent-color: #111; cursor: pointer; }
.pick-row:has(input:checked) {
    border-color: #111; background: rgba(17,17,17,.03);
}
.pick-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pick-name { font-weight: 700; font-size: 14px; color: #111; }
.pick-meta { font-size: 11.5px; color: rgba(0,0,0,.5); }
/* Closed accounts still list and still answer with zeroes, so they stay
   pickable and stay legible -- just visibly not the obvious choice. */
/* The Meta Business a group of ad accounts belongs to. Sticky, because the
   picker scrolls inside the dialog and a heading that leaves the top takes the
   answer to "whose accounts am I looking at" with it. */
.pick-group {
    position: sticky; top: 0; z-index: 1;
    display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
    margin: 14px 0 8px; padding: 6px 2px;
    background: #fff;
    color: #111; font-size: 12px; font-weight: 750; letter-spacing: .04em; text-transform: uppercase;
}
.pick-group:first-child { margin-top: 0; }
.pick-group-n { color: rgba(0,0,0,.5); font-size: 11px; font-weight: 600; letter-spacing: 0; text-transform: none; }
.pick-row-off .pick-name { color: rgba(0,0,0,.62); }

/* Meta's own delivery numbers, above the funnel and visibly not part of it.
   The funnel counts what reached the inbox; this counts what Meta served.
   Tagging the strip is cheaper than explaining the difference in a sentence
   nobody reads, and it is the one place ads_read's output is shown whole. */
.meta-perf {
    display: flex; align-items: center; flex-wrap: wrap; gap: 12px 26px;
    margin: 0 0 20px; padding: 15px 20px;
    border: 1px solid rgba(0,0,0,.07); border-radius: 14px;
    background: rgba(0,0,0,.022);
}
.meta-perf-tag {
    padding: 4px 9px; border-radius: 999px;
    background: #111; color: #fff;
    font-size: 10px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
}
.meta-perf-figs { display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px 26px; }
.meta-perf-fig { display: flex; align-items: baseline; gap: 6px; font-size: 12px; color: rgba(0,0,0,.52); }
.meta-perf-fig b { font-size: 19px; font-weight: 700; color: #111; letter-spacing: -.02em; }

/* The short "here is what this button will actually do" list that sits above
   a consequential action. Onboarding's WhatsApp step had one hand-rolled; it
   is the clearest screen in setup, so the Meta screens use its shape. */
.pick-note {
    margin-top: 18px; padding: 15px 17px;
    border: 1px solid rgba(0,0,0,.07); border-radius: 12px;
    background: rgba(0,0,0,.015);
}
.pick-note-ttl { font-size: 12.5px; font-weight: 700; color: #111; margin-bottom: 9px; }
.pick-note-list { margin: 0; padding-left: 17px; display: flex; flex-direction: column; gap: 7px; }
.pick-note-list li { font-size: 13px; line-height: 1.5; color: rgba(0,0,0,.62); }

/* Connect WhatsApp, in whichever shell it was reached through. The action
   reads down the left and the scope down the right, and the same markup
   folds to one column on the setup wizard's narrow panel because the second
   track will not fit there. auto-fit rather than a breakpoint: the shell,
   not the viewport, is what differs between the two placements. */
.connect-grid {
    display: grid; gap: 20px; align-items: start;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}
.connect-side { display: flex; flex-direction: column; gap: 14px; }

/* The dashboard shell hands this page the full canvas. Left alone, a 29px
   title and one button stretch across all of it and read as a page missing
   its content. */
.connect-page { max-width: 1000px; }

/* Plumbing a client cannot act on, kept on the page for whoever set it up
   and folded away for everyone else. Open it and it reads as a note; closed
   it is one quiet line rather than a paragraph of configuration sitting
   above the button it explains. */
.staff-note {
    margin-top: 18px; border: 1px solid rgba(0,0,0,.07);
    border-radius: 12px; background: rgba(0,0,0,.015);
}
.staff-note > summary {
    display: flex; align-items: center; gap: 9px;
    padding: 11px 15px; cursor: pointer; list-style: none;
    font-size: 12.5px; font-weight: 600; color: rgba(0,0,0,.55);
}
.staff-note > summary::-webkit-details-marker { display: none; }
.staff-note > summary::after {
    content: ''; margin-left: auto;
    width: 7px; height: 7px; border-right: 1.6px solid rgba(0,0,0,.35);
    border-bottom: 1.6px solid rgba(0,0,0,.35); transform: rotate(45deg) translate(-2px,-2px);
    transition: transform .15s ease;
}
.staff-note[open] > summary::after { transform: rotate(-135deg) translate(-2px,-2px); }
.staff-note-body {
    padding: 0 15px 14px; font-size: 12.5px; line-height: 1.6;
    color: rgba(0,0,0,.6); overflow-wrap: anywhere;
}

/* A real switch, not a checkbox styled to look like one -- .check-row's
   native box reads as a form field to fill in; this reads as a setting
   that's on or off, which is what Paused/Priority/Active actually are.
   Still a genuine <input type=checkbox> underneath (opacity:0, not
   display:none), so it submits with the form and needs no JS to work.
   Promoted from ai-training.php, which had its own copy of exactly this --
   any page that needs an on/off switch uses this one now. */
.chswitch { position: relative; display: inline-block; width: 38px; height: 22px; flex-shrink: 0; }
.chswitch input { opacity: 0; width: 0; height: 0; position: absolute; }
.chswitch-track { position: absolute; inset: 0; background: rgba(0,0,0,.15); border-radius: 12px; transition: background .15s; }
/* A small overshoot on landing rather than a flat glide -- every switch in
   the product picks this up for free. */
.chswitch-knob { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: left .34s cubic-bezier(.34,1.56,.64,1); }
@media (prefers-reduced-motion: reduce) {
    .chswitch-knob { transition: none; }
}
.chswitch input:checked + .chswitch-track { background: #0B815A; }
.chswitch input:checked + .chswitch-track .chswitch-knob { left: 19px; }
.chswitch input:focus-visible + .chswitch-track { outline: 2px solid #111; outline-offset: 2px; }

/* A .chswitch paired with a label and (optionally) a description -- the row
   .check-row was for, but with a real switch. Whole row is the click
   target, same reasoning as .check-row: nobody should have to land on the
   16px control itself. */
.switch-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; cursor: pointer; }
.switch-row-body { flex: 1; min-width: 0; }
.switch-row-label { font-size: 13.5px; font-weight: 700; color: #111; }
.switch-row-sub { margin-top: 2px; font-size: 11.5px; line-height: 1.45; color: rgba(0,0,0,.58); }
/* Optional icon badge in front of the label -- what a switch-row is *for*,
   at a glance, when the row is one of several stacked together (module
   toggles) rather than a single well-labelled setting. */
.switch-row-ico {
    width: 32px; height: 32px; border-radius: 9px; flex-shrink: 0;
    background: var(--canvas); border: 1px solid rgba(0,0,0,.06);
    display: flex; align-items: center; justify-content: center;
    color: rgba(0,0,0,.6);
}
.switch-row-ico svg { width: 17px; height: 17px; stroke-width: 1.9; }

/* Two columns of switch-rows instead of one long list -- for a set of
   toggles long enough that a single column runs well past the fold (module
   switches). Each row keeps its own hairline; the grid only changes how
   many sit side by side. */
.switch-row-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 24px; row-gap: 2px; }
@media (max-width: 620px) {
    .switch-row-grid { grid-template-columns: 1fr; }
}

/* Sticky "you have unsaved changes" footer for a long settings form.
   On a page tall enough that the Save button scrolls out of sight (AI
   Training), a plain button at the bottom leaves no way to tell staged
   edits from ones that already took effect. This stays in view and says
   which of the two you are looking at. */
.save-bar {
    position: sticky; bottom: 0; z-index: 20;
    display: flex; align-items: center; gap: 14px;
    margin-top: 4px; padding: 13px 18px;
    background: #fff; border: 1px solid rgba(0,0,0,.09); border-radius: 14px;
}
.save-bar-txt { flex: 1; min-width: 0; font-size: 12.5px; color: rgba(0,0,0,.5); }
/* Only once something has actually changed does this read as a call to
   action -- an always-loud Save bar is background noise by the second visit. */
.save-bar.dirty { border-color: rgba(11,129,90,.4); }
.save-bar.dirty .save-bar-txt { color: #111; font-weight: 600; }
.save-bar:not(.dirty) .btn { opacity: .55; }

/* A key/value line in a "here is what you are about to do" review list --
   campaign-new.php's Review & Create step, and campaigns.php's send-confirm
   dialog, which reviews the same campaign right before it becomes real
   messages. */
.review-row {
    display: flex; justify-content: space-between; font-size: 13.5px; padding: 10px 0;
    border-bottom: 1px solid rgba(0,0,0,.06);
}
.review-row:last-child { border-bottom: none; }
.review-row .k { color: rgba(0,0,0,.5); }
.review-row .v { font-weight: 600; color: #111; }

.modal-body.pad { padding: 18px 20px; }
.modal-copy { font-size: 13px; line-height: 1.6; color: rgba(0,0,0,.6); margin: 0; }
.modal-head .modal-x { margin-left: auto; text-decoration: none; }
.danger-ico {
    width: 32px; height: 32px; border-radius: 9px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    background: rgba(220,38,38,.1); color: #b91c1c;
}
.danger-ico svg { width: 17px; height: 17px; }

/* Read-only value beside a Copy button -- invite links, webhook URLs. */
.copy-row { display: flex; gap: 10px; margin-top: 10px; }
.copy-row input {
    flex: 1; min-width: 0;
    padding: 10px 12px;
    background: var(--canvas); border: 1px solid rgba(0,0,0,.12); border-radius: 10px;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: #111;
}
.copy-row .btn { flex-shrink: 0; }
/* Flashed by house.js after a copy -- the clipboard write is invisible by
   nature, so the button itself has to say it landed. */
.copy-row .btn.copied { color: #0B815A; border-color: rgba(11,129,90,.35); animation: copyPop .28s cubic-bezier(.34,1.56,.64,1); }
@keyframes copyPop { 0%, 100% { transform: scale(1); } 45% { transform: scale(1.06); } }
@media (prefers-reduced-motion: reduce) {
    .copy-row .btn.copied { animation: none; }
}

.avatar-sm {
    width: 30px; height: 30px; border-radius: 50%;
    background: var(--canvas); color: #333;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 11.5px; font-weight: 700; flex-shrink: 0;
}

/* The search box in a top bar. Lived in app.css, so the admin could not use
   it and rendered a bare unstyled input instead -- the same stranding that
   put .alert in auth.css. Both surfaces have a top bar; this belongs here. */
.top-search {
    display: flex; align-items: center; gap: 8px;
    background: rgba(255,255,255,.6);
    border: 1px solid rgba(255,255,255,.7);
    border-radius: 10px;
    padding: 10px 16px;
    width: 340px;
}
.top-search svg { width: 15px; height: 15px; fill: none; stroke: rgba(0,0,0,.4); stroke-width: 2; flex-shrink: 0; }
.top-search input {
    flex: 1; border: none; background: transparent; outline: none;
    font-family: inherit; font-size: 13px; color: #111; min-width: 0;
}
.top-search input::placeholder { color: rgba(0,0,0,.58); }

/* An address field with its domain suffix shown inline. Signup uses it and
   so does the admin creating a workspace -- the same decision, made twice. */
.slug-input {
    display: flex; align-items: center;
    background: var(--canvas); border: 1px solid rgba(0,0,0,.12); border-radius: 10px;
    transition: border-color .12s, box-shadow .12s, background .12s;
    overflow: hidden;
}
.slug-input:focus-within { border-color: #111; box-shadow: 0 0 0 3px rgba(11,129,90,.25); background: #fff; }
.slug-input input {
    flex: 1; min-width: 0; border: none; background: transparent; outline: none;
    padding: 11px 4px 11px 13px;
    font-family: inherit; font-size: 13.5px; font-weight: 600; color: #111;
    text-align: right;
}
.slug-tail {
    padding: 11px 13px 11px 0; flex-shrink: 0;
    font-size: 13.5px; font-weight: 500; color: rgba(0,0,0,.58);
}
.slug-input.ok  { border-color: rgba(22,163,74,.5); }
.slug-input.bad { border-color: rgba(185,28,28,.5); }

/* The designed empty state: a quiet icon, a title, and one line saying why
   it is empty and what would fill it. "No data" tells nobody anything. */
.empty-ico {
    width: 52px; height: 52px; border-radius: 15px; margin-bottom: 15px;
    background: rgba(0,0,0,.045);
    display: flex; align-items: center; justify-content: center;
}
/* margin-bottom comes from the bare `.empty svg` rule above, which is meant
   for an icon standing on its own. Inside .empty-ico the box is already doing
   the spacing, and the inherited margin pushed the glyph off-centre -- it sat
   visibly high in its rounded square. */
.empty-ico svg { width: 24px; height: 24px; margin: 0; fill: none; stroke: rgba(0,0,0,.35); stroke-width: 1.7; }
.empty-ttl { font-size: 14.5px; font-weight: 700; color: #111; }
.empty-txt { font-size: 13px; color: rgba(0,0,0,.68); line-height: 1.6; margin-top: 7px; max-width: 42ch; }
.empty-acts { margin-top: 17px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }

/* The Meta mark is a solid shape, not a line icon. Stated here too so it
   cannot be inherited into an outline by any rule that decides button
   glyphs are strokes. */
.meta-mark, .btn svg.meta-mark { fill: currentColor; stroke: none; }

/* ---------- Support ticket screenshots ----------------------------------
   In house.css and not app.css because both surfaces draw them: the client's
   own Support popup and superadmin's triage popup on Platform Health, and
   the admin pages load house.css + admin.css only. A copy in each sheet is
   the drift this file exists to end -- admin.css carries nothing but the
   larger size it wants.

   Fixed box so a portrait phone shot and a landscape desktop one line up as
   one row rather than stepping down the popup. */
.tkt-shots { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
.tkt-shot {
    display: block; width: 62px; height: 46px; border-radius: 7px; overflow: hidden;
    border: 1px solid rgba(0,0,0,.1); background: var(--canvas);
}
.tkt-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tkt-shot:hover { border-color: rgba(0,0,0,.4); }

/* ============================================================================
   New shell (docs/DESIGN_BUILD.md / docs/mockups/linear.css)
   ----------------------------------------------------------------------------
   Everything below is additive and only reachable from body.shell-float
   pages (or, for genuinely new class names nothing already uses, plain
   additions) -- the frozen App Review tenant and the admin never render any
   of it. See docs/DESIGN_BUILD.md step 1.
   ========================================================================= */

/* .setup/.prog/.steps/.step/.ck: the tenant's onboarding-progress card,
   built by step 3 on the Dashboard and AI Training. New class names, so
   this is a plain addition rather than something to scope. */
.setup { display: flex; align-items: center; gap: 16px; padding: 16px; }
.prog { flex: 1; }
.prog .row { display: flex; justify-content: space-between; margin-bottom: 8px; }
.prog strong { font-weight: 600; }
.prog span { color: var(--text-2); }
.prog .track { height: 4px; border-radius: 2px; background: var(--glass-3); overflow: hidden; }
.prog .track i { display: block; height: 100%; background: var(--ink); }
.steps { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--border); }
.step { padding: 12px 16px; border-right: 1px solid var(--border); display: flex; gap: 9px; align-items: flex-start; }
.step:last-child { border-right: 0; }
.step .ck { width: 15px; height: 15px; border-radius: 50%; border: 1.5px solid var(--border-2); flex: none; margin-top: 2px; position: relative; }
.step .ck.done { background: var(--ink); border-color: var(--ink); }
.step .ck.done::after { content: ""; position: absolute; left: 4px; top: 1px; width: 3px; height: 7px; border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg); }
.step strong { display: block; font-weight: 500; font-size: 12.5px; }
.step span { color: var(--text-3); font-size: 12px; }

/* .badge, restyled to docs/mockups/linear.css's .chip values -- same class
   names (item 9: "map the existing .badge* classes onto these values"),
   scoped to body.shell-float since .badge is also the admin's badge. */
body.shell-float .badge {
    border-radius: 5px; padding: 2px 7px; font-size: 11.5px; font-weight: 500;
    background: var(--green-soft); color: var(--green-ink);
}
body.shell-float .badge.gray { background: var(--glass-2); color: var(--text-2); }
body.shell-float .badge.red { background: rgba(240,68,56,.12); color: #b42318; }
body.shell-float .badge.warn { background: rgba(247,144,9,.14); color: #93370d; }
body.shell-float .badge.dark { background: var(--ink); color: #fff; }

/* .gfilter-tab(s), restyled to docs/mockups/linear.css's .tabs/.tab values
   (item 11: "map .gfilter-tab onto .tab"), scoped to body.shell-float since
   conversations.php (frozen) and the admin also use .gfilter-tab. */
body.shell-float .gfilter-tabs { gap: 2px; padding: 0; border: none; background: none; }
body.shell-float .gfilter-tab { padding: 5px 9px; border-radius: var(--r); color: var(--text-2); font-weight: 500; font-size: 12.5px; }
body.shell-float .gfilter-tab.active { background: var(--ink); color: #fff; border: none; font-weight: 500; }

/* Toolbar search field (docs/mockups/linear.css .input) -- distinct from
   .field input (the form control), this is the compact table/card toolbar
   search box. Scoped to body.shell-float since .field input above already
   covers the frozen shell's forms (login.php included). */
body.shell-float .toolbar-input {
    height: 30px; border: 1px solid var(--border-2); border-radius: var(--r);
    background: #fff; display: flex; align-items: center; gap: 8px;
    padding: 0 9px; color: var(--text-3); flex: 1; max-width: 300px; margin-left: auto;
}
body.shell-float .toolbar-input input {
    border: none; outline: none; background: none; font: inherit;
    color: var(--text); width: 100%;
}

/* Plain inputs outside a .field (docs/mockups/linear.css .input's element
   selectors) -- .field input above already covers form fields, this covers
   an inline filter/search box in the new shell. */
body.shell-float input[type="text"]:not(.field *),
body.shell-float input[type="search"]:not(.field *),
body.shell-float input[type="email"]:not(.field *),
body.shell-float select:not(.field *),
body.shell-float textarea:not(.field *) {
    height: 30px; padding: 0 9px;
    border: 1px solid var(--border-2); border-radius: var(--r);
    background: #fff; color: var(--text); font: inherit; font-size: 13px;
}

/* table/th/td (docs/mockups/linear.css): the new shell's plain table look,
   for pages built without the admin's table.list. Scoped to body.shell-float
   so table.list (admin, and conversations.php's frozen list) is untouched. */
body.shell-float table.plain { width: 100%; border-collapse: collapse; }
body.shell-float table.plain th {
    font-size: 11.5px; font-weight: 500; color: var(--text-3); text-align: left;
    padding: 8px 16px; border-bottom: 1px solid var(--border);
}
body.shell-float table.plain td {
    padding: 11px 16px; border-bottom: 1px solid var(--border); vertical-align: middle;
}
body.shell-float table.plain tr:last-child td { border-bottom: 0; }
body.shell-float table.plain tr:hover td { background: var(--glass); }
body.shell-float table.plain td .name { font-weight: 500; }
body.shell-float table.plain td .meta { color: var(--text-3); font-size: 12px; }

/* Review of Step 2 (docs/DESIGN_BUILD.md): the staff-impersonation and
   payment-due banners, as one thin line each instead of a slab, so two
   notices read as two lines rather than two boxes. New shell only -- the
   frozen tenant keeps .imp-bar/.alert.warn exactly as before. */
.notice-line {
    display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    padding: 5px 26px; min-height: 28px;
    background: #fff4e5; color: var(--text-2); font-size: 12.5px; line-height: 1.4;
    border-bottom: 1px solid var(--border);
}
.notice-line b { font-weight: 600; color: var(--text); }
.notice-line a { margin-left: auto; flex-shrink: 0; font-weight: 600; color: var(--text); text-decoration: underline; }

/* docs/DESIGN_BUILD.md step 3 / docs/mockups/linear.css: components the
   four rebuilt pages need. New class names, so plain additions. */
.ph { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
.ph h1 { font-size: 22px; font-weight: 600; margin: 0; letter-spacing: -.02em; }
.ph p { margin: 6px 0 0; color: var(--text-2); max-width: 560px; }
.grid { display: grid; grid-template-columns: minmax(0,1fr) 330px; gap: 14px; align-items: start; }
/* Not .stack -- that name is already auth.css's vertical-form-field
   spacer; check.php's cross-file duplicate check catches exactly this. */
.col-stack { display: grid; gap: 14px; }
@media (max-width: 980px) { .grid { grid-template-columns: 1fr; } }
.toolbar { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); }
.input { height: 30px; border: 1px solid var(--border-2); border-radius: var(--r); background: #fff; display: flex; align-items: center; gap: 8px; padding: 0 9px; color: var(--text-3); flex: 1; max-width: 300px; margin-left: auto; }
.fico { width: 28px; height: 28px; border-radius: var(--r); background: var(--glass-2); border: 1px solid var(--border); display: grid; place-items: center; color: var(--text-2); font-size: 12px; }
.dots { color: var(--text-3); letter-spacing: 2px; }
.tabs { display: flex; gap: 2px; }
.tab { padding: 5px 9px; border-radius: var(--r); color: var(--text-2); font-weight: 500; font-size: 12.5px; cursor: pointer; }
.tab.on { background: var(--ink); color: #fff; }
/* Not .chips -- that name is already auth.css's; same cross-file check. */
.chip-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.chip-row .c { border: 1px solid var(--border-2); border-radius: 999px; padding: 4px 9px; font-size: 12px; color: var(--text-2); background: #fff; }
.chatin { display: flex; gap: 8px; margin-top: 14px; }
.chatin .input { flex: 1; max-width: none; margin: 0; }
.chatin .go { width: 30px; height: 30px; border-radius: var(--r); background: var(--ink); color: #fff; display: grid; place-items: center; flex: none; }
.krow { display: flex; align-items: center; gap: 10px; padding: 9px 16px; border-bottom: 1px solid var(--border); }
.krow:last-child { border-bottom: 0; }
.krow .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.krow b { width: 70px; font-weight: 500; }
.krow .n { color: var(--text-2); width: 22px; font-variant-numeric: tabular-nums; }
.krow span:last-child { color: var(--text-3); font-size: 12px; }
.note { padding: 11px 16px; border-top: 1px solid var(--border); color: var(--text-3); font-size: 12px; }
.help { padding: 14px 16px; border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--side); display: flex; align-items: center; gap: 12px; }
.help strong { display: block; font-weight: 600; }
.help span { color: var(--text-2); font-size: 12px; }
body.shell-float .pager { display: flex; justify-content: space-between; padding: 9px 16px; border-top: 1px solid var(--border); color: var(--text-3); font-size: 12px; }
