/* Hallmark · pre-emit critique: P5 H5 E5 S4 R5 V4 */
/* Hallmark · component: responsive wizard progress + contained actions · genre: modern-minimal · tone: operational · palette: portal tokens · macrostructure: existing wizard preserved · contrast: pass (40–41) · tokens: pass (48) · responsive/mobile: pass (34, 49–57) · slop: pass */
/*
 * ═══════════════════════════════════════════════════════════
 *  1confirmed client portal
 * ═══════════════════════════════════════════════════════════
 *
 * The portal's own rules. The tokens they reference live in tokens.css, which
 * the layout links immediately before this file — see that file's header for
 * why it is a separate link and not an @import.
 */


/* ═══════════════════════════════════════════════════════════
   BASE
   ═══════════════════════════════════════════════════════════ */

@layer base {
    html,
    body {
        /* clip, not hidden — hidden would break the sticky rail and topbar */
        overflow-x: clip;
    }

    html {
        background: var(--color-paper);
        color: var(--color-ink);
        font-family: var(--font-sans);
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
    }

    body {
        min-height: 100vh;
        font-size: 14px;
        line-height: 1.55;
    }

    h1, h2, h3, h4 {
        font-family: var(--font-display);
        font-style: normal;
        font-weight: 600;
        letter-spacing: -0.02em;
        /* long campaign names must break inside the word, not overflow */
        overflow-wrap: anywhere;
        min-width: 0;
    }

    /* Every figure on the page aligns in its column. */
    .cl-num,
    table,
    .cl-figure__value,
    .cl-stage__count,
    .cl-ladder__count,
    .cl-ladder__rate {
        font-variant-numeric: tabular-nums;
    }

    /* Instant, always. A ring that fades in leaves keyboard users blind
       for the length of the transition. */
    *:focus-visible {
        outline: 2px solid var(--color-accent);
        outline-offset: 2px;
        border-radius: var(--radius-xs);
    }

    ::selection {
        background: var(--color-accent);
        color: var(--color-paper-raised);
    }

    /*
     * Styled scrollbars are for a mouse only.
     *
     * Styling ::-webkit-scrollbar opts out of the overlay scrollbar, so on
     * Chrome for Android this drew a permanent 8px grey bar down the right of
     * .cl-main and took those 8px out of the layout: every page sat 8px off
     * centre, with a wider gutter on the right than the left. A touch screen
     * keeps the platform's own overlay bar, which takes no width.
     */
    @media (hover: hover) and (pointer: fine) {
        ::-webkit-scrollbar { width: 8px; height: 8px; }
        ::-webkit-scrollbar-track { background: transparent; }
        ::-webkit-scrollbar-thumb {
            background: var(--color-rule);
            border-radius: var(--radius-pill);
        }
        ::-webkit-scrollbar-thumb:hover { background: var(--color-muted-soft); }
    }

    [x-cloak] { display: none !important; }
}

/* ═══════════════════════════════════════════════════════════
   SHELL — rail left, canvas right
   ═══════════════════════════════════════════════════════════ */

/*
|--------------------------------------------------------------------------
| The shell scrolls its content, not itself
|--------------------------------------------------------------------------
|
| The rail used to be `position: sticky` inside a page that scrolled normally,
| and it *did* stick — but its height was calc(100vh - banner), so the moment
| the banner scrolled away the rail ended 48px above the bottom of the screen
| and the page showed through beneath it. Measured: pinned at top 0 with its
| bottom edge at 852 in a 900px viewport. That gap is what read as the sidebar
| scrolling with the page.
|
| Shrinking the rail for a banner is only correct while the banner is on
| screen, and no static height can be right in both states. So the page stops
| scrolling instead: the viewport holds the banner, the rail and the topbar,
| and .cl-main is the only thing with anywhere to go.
|
| This is why body is a flex column rather than the shell carrying
| `calc(100vh - var(--cl-banner-h))`: a banner of any height, or none at all,
| lays out correctly with no arithmetic and no custom property to keep in sync.
|
| Everything else on <body> is fixed or zero-height — the alert stack, the
| dialogs, the scrim — so nothing else becomes a flex item.
|
| The height is the *dynamic* viewport where the browser has one. On a phone
| 100% resolves against a box that can be taller than what is on screen while
| the browser's address bar is showing, so the bottom of .cl-main — the Back
| and Send bar at the end of a wizard — sat under the browser's toolbar and
| could not be scrolled into view. 100dvh is the height actually visible.
*/
html {
    height: 100%;
}

body {
    display: flex;
    flex-direction: column;
    height: 100%;
    height: 100dvh;
    overflow: hidden;
}

.cl-shell {
    display: flex;
    /* flex-1 with min-height: 0 — without the min-height a flex child refuses
       to shrink below its content and the shell grows past the viewport again,
       which is the whole bug returning by another route. */
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

.cl-canvas {
    flex: 1;
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.cl-main {
    flex: 1;
    min-width: 0;
    /* The page's one scroller. The topbar is its sibling above, not inside it,
       so it stays put without needing to be sticky. */
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior-y: contain;
    /*
     * A flex column so the colophon can be pushed to the bottom on a short
     * page — see its margin-top below. Column with the default stretch is what
     * block layout already did for these children, so nothing above it moves.
     */
    display: flex;
    flex-direction: column;
    /* Generous top, tighter bottom — the rhythm is deliberately uneven. */
    padding: var(--space-xl) clamp(1rem, 4vw, 2.5rem) var(--space-lg);
    width: 100%;
    /*
     * The source capped this at 1440px and left-aligned it, which strands
     * dead space down the right edge of anything wider (220px at 1920).
     * The cap is kept only as a very high ceiling so text lines can't run
     * absurdly long on ultrawide, and `margin-inline: auto` centres what is
     * left instead of pinning it left.
     */
    max-width: 2200px;
    margin-inline: auto;
}

/* ═══════════════════════════════════════════════════════════
   N3 · SIDE-RAIL
   The rail is not a link list. It carries the engine's state — the
   stage strip at its foot is the answer to "is it running?", which is
   the only question a client asks between logins.
   ═══════════════════════════════════════════════════════════ */

.cl-rail {
    width: var(--rail-w);
    flex-shrink: 0;
    /*
     * Static and full height. It no longer needs to stick, because nothing it
     * sits in scrolls — the shell is exactly the viewport minus the banner and
     * the rail simply fills it.
     */
    height: 100%;
    display: flex;
    flex-direction: column;
    background: var(--color-paper-raised);
    border-inline-end: 1px solid var(--color-rule);
    z-index: var(--z-sticky);
}

.cl-rail__brand {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    padding: 12px 18px 17px;
    text-decoration: none;
}



/* The "by Abweb" co-brand — shown only when the global setting is on, the
   same condition the Vue sidebar applies. */
.cl-rail__byline {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    align-self: flex-end;
    /* The endorsement closes against the wordmark's trailing edge. */
    margin-inline-start: 0;
    margin-inline-end: 18px;
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.025em;
    color: var(--color-muted);
}

.cl-rail__byline img { width: 14px; height: 14px; }

.cl-rail__kind {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-muted);
}

.cl-rail__nav {
    flex: 1;
    overflow-y: auto;
    padding: var(--space-xs) var(--space-xs) var(--space-md);
    display: flex;
    flex-direction: column;
    gap: var(--space-3xs);

    /*
     * The navigation is a quiet surface, so its scrollbar is too: one hairline,
     * one colour, always the same.
     *
     * It used to fade in on hover with a transition. `scrollbar-color` is not
     * an animatable property — browsers jump straight to the new value — so
     * what that produced was the platform's own dark scrollbar for a moment
     * and then a grey one, which is the flicker rather than a fade. Nothing
     * here changes state, so there is nothing to flicker.
     */
    scrollbar-width: thin;
    scrollbar-color: var(--color-rule) transparent;
}

/*
 * Safari, which has no scrollbar-color. Chrome supports both and lets
 * scrollbar-color win, so these rules only ever apply where the standard
 * property does not — which is why they must describe the same scrollbar
 * rather than a second design.
 */
.cl-rail__nav::-webkit-scrollbar { width: 4px; }
.cl-rail__nav::-webkit-scrollbar-track { background: transparent; }

.cl-rail__nav::-webkit-scrollbar-thumb {
    background: var(--color-rule);
    border-radius: var(--radius-pill);
}


.cl-rail__group {
    padding: var(--space-md) var(--space-sm) var(--space-2xs);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-muted);
}

.cl-rail__link {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-xs) var(--space-sm);
    border-radius: var(--radius-sm);
    color: var(--color-ink-soft);
    font-size: 13.5px;
    font-weight: 500;
    text-decoration: none;
    /* named properties only — never transition-all */
    transition: background-color var(--dur-short) var(--ease-out),
                color var(--dur-short) var(--ease-out);
}

.cl-rail__link svg {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    stroke-width: 1.75;
    color: var(--color-muted);
    transition: color var(--dur-short) var(--ease-out);
}

.cl-rail__link:hover {
    background: var(--color-paper-sunken);
    color: var(--color-ink);
}

.cl-rail__link:hover svg { color: var(--color-ink-soft); }

.cl-rail__link:active { background: var(--color-paper-tint); }

.cl-rail__link[aria-disabled='true'] {
    opacity: 0.55;
    cursor: not-allowed;
    pointer-events: none;
}

/* Transparent even under the pointer: the highlight is the marker below, and
   the link's own hover wash would only show round its edges. */
.cl-rail__link.is-active {
    background: transparent;
    color: var(--color-accent);
    font-weight: 700;
}

.cl-rail__link.is-active svg { color: var(--color-accent); }

/*
 * The current page's highlight — wash and bar — drawn by an element of its
 * own inside the active link rather than by the link.
 *
 * It is a separate element so that moving between pages can carry it: the
 * page handover in components/motion.css names it, and it glides from the
 * item you left to the one you chose instead of blinking out on one and on
 * at the other. Behind the label (the link isolates, so -1 stays inside it).
 */
.cl-rail__link { isolation: isolate; }

.cl-rail__marker {
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background: var(--color-accent-wash);
    pointer-events: none;
}

/* The filled-bar indicator — the rail's one accent flourish. */
.cl-rail__marker::before {
    content: '';
    position: absolute;
    inset-inline-start: calc(var(--space-xs) * -1);
    top: 50%;
    transform: translateY(-50%);
    width: 3px;
    height: 18px;
    /* Rounded only on the side that faces the link. */
    border-start-start-radius: 0;
    border-start-end-radius: var(--radius-xs);
    border-end-end-radius: var(--radius-xs);
    border-end-start-radius: 0;
    background: var(--color-accent);
}

.cl-rail__count {
    margin-inline-start: auto;
    font-family: var(--font-display);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    color: var(--color-muted);
}

.cl-rail__link.is-active .cl-rail__count { color: var(--color-accent); }

/* ── Engine strip — the rail's reason to exist ─────────────── */

.cl-rail__engine {
    margin: 0 var(--space-xs);
    padding: var(--space-sm);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-md);
    background: var(--color-paper);
    background: var(--color-paper);
}

/* The accent dot is the landing mockup's "● PIPELINE AGENT LIVE" tell,
   reused for the one thing in the portal that is genuinely live. */
.cl-eyebrow-dot {
    width: 7px;
    height: 7px;
    flex-shrink: 0;
    border-radius: var(--radius-pill);
    background: var(--color-accent);
}

.cl-rail__engine-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-xs);
    margin-bottom: var(--space-sm);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-muted);
}

.cl-rail__ticks {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 3px;
}

.cl-rail__tick {
    height: 3px;
    border-radius: var(--radius-pill);
    background: var(--color-rule);
}

/* A lit tick is progress made. While sending it is warm — the run is still
   moving; once finished the whole strip turns green. */
.cl-rail__tick.is-lit { background: var(--color-warm); }
.cl-rail__engine[data-state='finished'] .cl-rail__tick.is-lit { background: var(--color-live); }

.cl-rail__engine-title {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.cl-rail__engine[data-state='finished'] .cl-eyebrow-dot { background: var(--color-live); }

/* While a run is live the dot breathes, so the strip reads as "now" without
   any polling behind it. */
.cl-rail__engine[data-state='sending'] .cl-eyebrow-dot {
    background: var(--color-warm-ink);
    animation: cl-pulse 1.8s var(--ease-out) infinite;
}

@keyframes cl-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
    .cl-rail__engine[data-state='sending'] .cl-eyebrow-dot { animation: none; }
}

.cl-rail__engine-note {
    margin-top: var(--space-xs);
    font-size: 11.5px;
    color: var(--color-muted);
}

/* ── Rail foot ─────────────────────────────────────────────── */

.cl-rail__foot {
    padding: var(--space-sm) var(--space-xs) var(--space-md);
    border-top: 1px solid var(--color-rule-soft);
    margin-top: var(--space-sm);
}

.cl-rail__account {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    width: 100%;
    min-width: 0;
    overflow: hidden;
    padding: var(--space-xs) var(--space-sm);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    text-align: start;
    cursor: pointer;
    color: inherit;
    font: inherit;
    transition: background-color var(--dur-short) var(--ease-out);
}

.cl-rail__account:hover { background: var(--color-paper-sunken); }

.cl-rail__avatar {
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    display: grid;
    place-items: center;
    border-radius: var(--radius-pill);
    background: var(--color-ink);
    color: var(--color-paper-raised);
    font-family: var(--font-display);
    font-size: 12px;
    font-weight: 700;
}

.cl-rail__who {
    min-width: 0;
    flex: 1;
    overflow: hidden;
}

.cl-rail__name {
    display: block;
    max-width: 100%;
    font-size: 12.5px;
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cl-rail__mail {
    display: block;
    max-width: 100%;
    font-size: 11px;
    color: var(--color-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ── Mobile: the rail becomes a sheet ──────────────────────── */

.cl-rail__scrim {
    display: none;
    position: fixed;
    inset: 0;
    background: var(--color-scrim);
    z-index: calc(var(--z-sticky) - 1);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-mid) var(--ease-out);
}

@media (max-width: 63.99rem) {
    .cl-rail {
        position: fixed;
        inset-inline-start: 0;
        top: 0;
        /* Fixed to the viewport rather than filling the shell, so it says its
           own height rather than inheriting the base rule's 100%. Dynamic
           where supported, for the same reason as body: 100vh ran under the
           phone browser's toolbar and hid the foot of the menu. */
        height: 100vh;
        height: 100dvh;
        transform: translateX(-100%);
        transition: transform var(--dur-mid) var(--ease-out);
        box-shadow: var(--shadow-sheet);
    }

    /* translateX has no logical form, and the rail changes sides: parked
       off the right edge in Arabic rather than off the left. */
    [dir="rtl"] .cl-rail { transform: translateX(100%); }

    .cl-rail.is-open { transform: translateX(0); }

    .cl-rail__scrim { display: block; }
    .cl-rail__scrim.is-open { opacity: 1; pointer-events: auto; }
}

/* ═══════════════════════════════════════════════════════════
   TOPBAR
   ═══════════════════════════════════════════════════════════ */

/*
 * Two sticky-at-top-0 elements exist on every screen — this bar and the
 * rail. They never collide, because the rail occupies its own column to the
 * left of the canvas rather than sitting above it, and the z-order settles
 * it regardless: the rail holds --z-sticky (200), this bar --z-raised (10),
 * so the navigation always out-paints the content chrome.
 */
/* The bar's height is a token because the alert region has to sit under it;
   two places knowing the same number independently is how they drift. */
:root { --topbar-h: 60px; }

.cl-topbar {
    position: sticky;
    top: 0;
    z-index: var(--z-raised);
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-sm) clamp(1rem, 4vw, 2.5rem);
    min-height: var(--topbar-h);
    background: color-mix(in oklab, var(--color-paper) 86%, transparent);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--color-rule-soft);
}

/* Every control in the bar is one square, so the burger, the bell and the
   account trigger line up on both edges instead of on their text baselines. */
:root { --topbar-control: 34px; }

.cl-topbar__burger {
    display: none;
    width: var(--topbar-control);
    height: var(--topbar-control);
    /* Fixed, like the controls at the other end. It is a flex item with
       nothing inside that can shrink, so on a 360px phone with the upgrade
       chip in the bar it gave up 6px of its width before the page's name
       gave up any, and stood narrower than the bell. */
    flex-shrink: 0;
    place-items: center;
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    background: var(--color-paper-raised);
    cursor: pointer;
    color: var(--color-ink-soft);
    transition: background-color var(--dur-short) var(--ease-out);
}

.cl-topbar__burger:hover { background: var(--color-paper-sunken); }

@media (max-width: 63.99rem) {
    .cl-topbar__burger { display: grid; }
}

.cl-crumbs {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    min-width: 0;
    font-size: 12.5px;
    color: var(--color-muted);
}

.cl-crumbs a {
    color: var(--color-muted);
    text-decoration: none;
    white-space: nowrap;
    transition: color var(--dur-short) var(--ease-out);
}

.cl-crumbs a:hover { color: var(--color-accent); }

.cl-crumbs__here {
    color: var(--color-ink);
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cl-crumbs__sep { color: var(--color-muted); }

.cl-topbar__end {
    margin-inline-start: auto;
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    /* The crumb gives up its width first; these are all fixed-size controls
       and there is nothing in them that can usefully shrink. */
    flex-shrink: 0;
}

/* ── Credit balance ───────────────────────────────────────────────────────
   A readout, not a control: the Vue chip opens a purchase modal the portal
   has no equivalent for yet, and a button that does nothing is worse than a
   number that just sits there. Zero is a real balance and is coloured, not
   hidden — an account that cannot send should be able to see why from any
   page. */

.cl-topbar__credit {
    /* Anchors the visually-hidden unit at phone width. */
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    height: var(--topbar-control);
    padding: 0 var(--space-sm);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    background: var(--color-paper-raised);
    font-size: 12.5px;
    line-height: 1;
    white-space: nowrap;
}

.cl-topbar__credit-mark {
    width: 15px;
    height: 15px;
    flex-shrink: 0;
    color: var(--color-accent);
}

/*
 * While the balance is counting to a new figure. The chip lifts for the half
 * second the number is moving, so a change that happens after a send is seen
 * rather than merely rendered — the count alone is easy to miss when the eye
 * is still on the button that was just pressed.
 */
.cl-topbar__credit.is-moving {
    border-color: var(--credit-tone, var(--color-accent));
    background: var(--credit-wash, var(--color-accent-wash));
}

.cl-topbar__credit.is-moving .cl-topbar__credit-value { color: var(--credit-tone, var(--color-accent)); }

/*
 * Which way it moved. Spending is the logo's orange (#A75726, 5.2:1 under
 * white text, 4.7:1 on its own wash); topping up is the brand green. The
 * tone is set by credit-motion.js for as long as the change is on screen,
 * and the chip, its ring, the token and the receipt all read it.
 */
.cl-topbar__credit[data-tone='spend'] {
    --credit-tone: var(--color-warm-ink);
    --credit-wash: var(--color-warm-wash);
}

.cl-topbar__credit[data-tone='gain'] {
    --credit-tone: var(--color-accent);
    --credit-wash: var(--color-accent-wash);
}

/*
    The way from the balance to where it went.

    A square icon button beside the credit chip, matched to its height and
    border so the two read as one control with two halves — the number, and the
    story behind it. Deliberately quiet: it is the second thing anyone looks at,
    never the first.
*/
.cl-topbar__ledger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--topbar-control);
    block-size: var(--topbar-control);
    margin-inline-start: calc(var(--space-2xs) * -1 + 2px);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    background: var(--color-paper-raised);
    color: var(--color-muted);
    text-decoration: none;
}
.cl-topbar__ledger svg { inline-size: 15px; block-size: 15px; stroke-width: 1.75; }
.cl-topbar__ledger:hover { color: var(--color-ink-2); border-color: var(--color-rule-track); }
.cl-topbar__ledger:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }

@media (prefers-reduced-motion: reduce) {
    .cl-topbar__credit { transition: none; }
}

.cl-topbar__credit-value {
    font-family: var(--font-display);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--color-ink);
}

.cl-topbar__credit-unit { color: var(--color-muted); }

.cl-topbar__credit.is-empty {
    border-color: var(--color-warn-line);
    background: var(--color-warn-wash);
}

/*
 * The upgrade chip.
 *
 * The credit chip's shape — same height, radius, border and type — so the
 * header keeps one grammar rather than gaining a second kind of control. The
 * warn wash is the same one an empty balance wears, which is the point: both
 * say "something here needs attention", and a reader who has learned one has
 * learned the other.
 *
 * Quieter than the balance on purpose. The number is what the sender came to
 * read; this is what we would like them to notice, and an offer that shouts on
 * every page for months stops being read at all.
 */
.cl-topbar__upgrade {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    height: var(--topbar-control);
    padding: 0 var(--space-sm);
    border: 1px solid var(--color-accent);
    /* A pill, like every other call to action in the product. The chips beside
       it are readouts and keep the softer rectangle; the difference is the
       point. */
    border-radius: var(--radius-pill);
    background: var(--color-accent);
    color: var(--color-accent-ink);
    font-size: 12.5px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    box-shadow: var(--shadow-lift);
    transition: background-color var(--dur-short) var(--ease-out),
                border-color var(--dur-short) var(--ease-out),
                transform var(--dur-short) var(--ease-out);
}

.cl-topbar__upgrade-mark {
    width: 15px;
    height: 15px;
    flex-shrink: 0;
    /* Inherits the chip's ink. The star is part of the label, not a separate
       marker sitting on a surface of its own. */
    color: currentColor;
}

.cl-topbar__upgrade:hover {
    background: var(--color-accent-hover);
    border-color: var(--color-accent-hover);
    transform: translateY(-1px);
}

.cl-topbar__upgrade:active { transform: translateY(0); }

/*
 * Three beats on arrival, then still.
 *
 * The chip has to be found on a page somebody opened to do something else, and
 * a filled pill in a bar of quiet readouts is most of that on its own. The ring
 * is the rest of it — but only three times: a header that pulses forever stops
 * being noticed within a day and starts being resented within a week, which is
 * the opposite of what an offer is for. `both` holds the final frame, so the
 * chip settles flat rather than snapping back mid-pulse.
 */
@media (prefers-reduced-motion: no-preference) {
    .cl-topbar__upgrade {
        animation: cl-upgrade-beat 2.4s var(--ease-out) 3 both;
    }
}

@keyframes cl-upgrade-beat {
    0%, 55%, 100% { box-shadow: var(--shadow-lift), 0 0 0 0 color-mix(in oklab, var(--color-accent) 40%, transparent); }
    25% { box-shadow: var(--shadow-lift), 0 0 0 6px color-mix(in oklab, var(--color-accent) 0%, transparent); }
}

/*
 * Nothing left to send with.
 *
 * The mark and the figure go amber together with the chip's own border and
 * wash — one state, said three ways, because a balance of zero is the one
 * number in the header that changes what you can do next.
 *
 * This block was broken by a bad paste: a duplicate of the `.is-moving` rules
 * above had landed *inside* this selector list, comment and all. The comment is
 * stripped when the sheet is parsed, so the second selector resolved to
 * `.cl-topbar__credit.is-empty .cl-topbar__credit.is-moving` — a chip inside
 * itself, matching nothing — and the closing
 * `.cl-topbar__credit-value { color: warn }` was left unscoped, which painted
 * *every* balance amber instead of only an empty one.
 */
.cl-topbar__credit.is-empty .cl-topbar__credit-mark,
.cl-topbar__credit.is-empty .cl-topbar__credit-value { color: var(--color-warn); }

/* ── The balance, moving ──────────────────────────────────────────────────
   What a send costs, carried to the balance and counted off it — the whole
   sentence is told in resources/js/portal/credit-motion.js. These are its
   pieces: the figure's one-cell grid, the odometer laid over it, the hit,
   the receipt under the chip, and the token thrown from the button.

   Every piece is a picture. The real figure stays in the chip's text, only
   hidden from sight while the wheels turn over it. */

.cl-topbar__credit-figure {
    display: inline-grid;
    /* Figures read left to right in every language the portal speaks, and the
       odometer lines up with the figure under it by its ones wheel. */
    direction: ltr;
    unicode-bidi: isolate;
}

.cl-topbar__credit-figure > * { grid-area: 1 / 1; }

/* Hidden, not removed: it keeps its width and its place in the accessible
   name while the odometer turns on top of it. */
.cl-topbar__credit-value.is-veiled { visibility: hidden; }

.cl-odo {
    justify-self: end;
    display: inline-flex;
    align-items: flex-start;
    height: 1em;
    line-height: 1;
}

/* One window a digit high onto a strip of digits. */
.cl-odo__wheel {
    display: block;
    height: 1em;
    overflow: hidden;
}

.cl-odo__strip { display: block; }

/* Each wheel brings its own length (--odo-roll): the ones wheel turns
   longest, so the figure settles from the left and the last digit lands
   last, like a counter coming to rest. */
.cl-odo.is-rolling .cl-odo__strip {
    transition: transform var(--odo-roll, 1s) var(--ease-out);
}

.cl-odo__cell {
    display: block;
    height: 1em;
    line-height: 1;
    text-align: center;
}

/* A separator the figure loses on the way — 1,005 down to 985 — fades out
   as its wheel rolls away to nothing. */
.cl-odo__sep { transition: opacity var(--dur-mid) var(--ease-out); }
.cl-odo__sep[data-shown='false'] { opacity: 0; }

/* The landing. The chip takes the token like a hand catching it: in, out
   past where it started, and back. */
.cl-topbar__credit.is-hit {
    animation: cl-credit-hit calc(var(--dur-mid) * 2.4) var(--ease-out);
}

@keyframes cl-credit-hit {
    0% { transform: none; }
    16% { transform: scale(0.86, 0.92); }
    42% { transform: scale(1.08); }
    68% { transform: scale(0.98); }
    100% { transform: none; }
}

/* One ring out from the chip as it lands — the ring the upgrade chip beats
   with, once, in the colour of the change. */
.cl-topbar__credit.is-hit::after {
    content: '';
    position: absolute;
    inset: -1px;
    border-radius: inherit;
    pointer-events: none;
    /* As long as the squash: the class goes when that ends, and the ring
       must be gone by then rather than cut off. */
    animation: cl-credit-ring calc(var(--dur-mid) * 2.4) var(--ease-out) forwards;
}

@keyframes cl-credit-ring {
    from { box-shadow: 0 0 0 0 color-mix(in oklab, var(--credit-tone, var(--color-accent)) 45%, transparent); }
    to { box-shadow: 0 0 0 12px color-mix(in oklab, var(--credit-tone, var(--color-accent)) 0%, transparent); }
}

/* The receipt: "−20" under the chip for a moment after the wheels start, for
   whoever looked up late. Orange for a cost, green for a top-up. */
.cl-topbar__credit-receipt {
    position: absolute;
    top: calc(100% + var(--space-2xs));
    inset-inline: 0;
    width: max-content;
    margin-inline: auto;
    padding: var(--space-3xs) var(--space-xs);
    border-radius: var(--radius-pill);
    background: var(--color-warm-ink);
    color: var(--color-paper-raised);
    font-family: var(--font-display);
    font-size: 11px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1.3;
    direction: ltr;
    unicode-bidi: isolate;
    pointer-events: none;
    animation: cl-credit-receipt 1.9s var(--ease-out) forwards;
}

.cl-topbar__credit-receipt.is-gain {
    background: var(--color-accent);
    color: var(--color-accent-ink);
}

@keyframes cl-credit-receipt {
    0% { opacity: 0; transform: translateY(-6px) scale(0.8); }
    14% { opacity: 1; transform: none; }
    72% { opacity: 1; transform: none; }
    100% { opacity: 0; transform: translateY(8px); }
}

/* The token — the cost in flight from the button to the balance. Orange
   for what was spent, green for what was added. */
.cl-credit-token {
    position: fixed;
    /* Physical on purpose: the script places it by transform from viewport
       rectangles, and those are physical in every language. */
    top: 0;
    left: 0;
    z-index: var(--z-toast);
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    padding: var(--space-2xs) var(--space-sm) var(--space-2xs) var(--space-xs);
    border-radius: var(--radius-pill);
    background: var(--color-warm-ink);
    color: var(--color-paper-raised);
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    white-space: nowrap;
    box-shadow: var(--shadow-soft);
    pointer-events: none;
    direction: ltr;
    unicode-bidi: isolate;
}

.cl-credit-token svg { width: 14px; height: 14px; flex-shrink: 0; }

.cl-credit-token.is-gain {
    background: var(--color-accent);
    color: var(--color-accent-ink);
}

/* The script does not play any of this for a reader who asked for less
   motion; this covers a hit already under way when the setting changes. */
@media (prefers-reduced-motion: reduce) {
    .cl-topbar__credit.is-hit,
    .cl-topbar__credit.is-hit::after { animation: none; }
}

/* ── Notifications ────────────────────────────────────────────────────────
   A link to the page, not a second copy of the list. The count is carried by
   the link's accessible name, so the badge is decorative and can be styled
   for glance-value alone. */

.cl-topbar__bell {
    position: relative;
    display: grid;
    place-items: center;
    width: var(--topbar-control);
    height: var(--topbar-control);
    flex-shrink: 0;
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    background: var(--color-paper-raised);
    color: var(--color-ink-soft);
    transition:
        background-color var(--dur-short) var(--ease-out),
        color var(--dur-short) var(--ease-out);
}

.cl-topbar__bell svg { width: 17px; height: 17px; }

.cl-topbar__bell:hover { background: var(--color-paper-sunken); color: var(--color-ink); }

.cl-topbar__bell[aria-current='page'] {
    border-color: var(--color-accent-line);
    background: var(--color-accent-wash);
    color: var(--color-accent);
}

.cl-topbar__count {
    position: absolute;
    top: -6px;
    inset-inline-end: -6px;
    min-width: 17px;
    height: 17px;
    padding: 0 var(--space-2xs);
    display: grid;
    place-items: center;
    border-radius: var(--radius-pill);
    background: var(--color-halt);
    color: var(--color-accent-ink);
    font-family: var(--font-display);
    font-size: 10px;
    font-weight: 600;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    /* Cut out of the button rather than drawn on it, so the badge reads as a
       separate object at a glance. */
    box-shadow: 0 0 0 2px var(--color-paper-raised);
}

/* ── Account menu ─────────────────────────────────────────────────────────
   The bar's one full control, and its only visual anchor. Everything the Vue
   header opened on hover opens on click here — hover is not an interaction
   on a touch screen, and the Vue version's 100ms close timer made it a race
   on a trackpad too. */

.cl-topmenu { position: relative; }

.cl-topmenu__trigger {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    height: var(--topbar-control);
    padding-block: 0;
    padding-inline: var(--space-2xs) var(--space-xs);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    background: var(--color-paper-raised);
    color: var(--color-ink);
    transition:
        background-color var(--dur-short) var(--ease-out),
        border-color var(--dur-short) var(--ease-out);
}

.cl-topmenu__trigger:hover { background: var(--color-paper-sunken); }

.cl-topmenu__trigger[aria-expanded='true'] {
    border-color: var(--color-accent-line);
    background: var(--color-accent-wash);
}

.cl-topmenu__avatar {
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    flex-shrink: 0;
    border-radius: var(--radius-xs);
    background: var(--color-accent);
    color: var(--color-accent-ink);
    font-family: var(--font-display);
    font-size: 12px;
    font-weight: 600;
    line-height: 1;
}

.cl-topmenu__name {
    font-size: 13px;
    font-weight: 500;
    /* A long first name truncates rather than pushing the bar wider. */
    max-width: 10ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cl-topmenu__caret {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    color: var(--color-muted);
    transition: transform var(--dur-short) var(--ease-out);
}

.cl-topmenu__trigger[aria-expanded='true'] .cl-topmenu__caret { transform: rotate(180deg); }

.cl-topmenu__panel {
    position: absolute;
    top: calc(100% + var(--space-xs));
    inset-inline-end: 0;
    z-index: var(--z-dropdown);
    /* Never wider than the viewport it is pinned to the edge of. */
    width: min(250px, calc(100vw - 2 * var(--space-md)));
    padding: var(--space-2xs);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-md);
    background: var(--color-paper-raised);
    box-shadow: var(--shadow-soft);
    /* The entrance is a CSS animation, not an Alpine transition: x-show flips
       display, which restarts the animation on its own, and the panel is
       never left half-faded if a close interrupts an open. */
    animation: cl-topmenu-in var(--dur-mid) var(--ease-spring);
    /* It grows out of the trigger it hangs from, at the end edge. */
    transform-origin: top right;
}

[dir="rtl"] .cl-topmenu__panel { transform-origin: top left; }

@keyframes cl-topmenu-in {
    from { opacity: 0; transform: translateY(-8px) scale(0.96); }
    to { opacity: 1; transform: none; }
}

.cl-topmenu__head {
    display: flex;
    flex-direction: column;
    gap: var(--space-3xs);
    padding: var(--space-xs) var(--space-sm) var(--space-sm);
    margin-bottom: var(--space-2xs);
    border-bottom: 1px solid var(--color-rule-soft);
}

.cl-topmenu__head-name {
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 600;
    color: var(--color-ink);
    overflow-wrap: anywhere;
}

.cl-topmenu__head-mail {
    font-size: 11.5px;
    line-height: 1.35;
    color: var(--color-muted);
    overflow-wrap: anywhere;
}

.cl-topmenu__form { display: block; }

.cl-topmenu__item {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    width: 100%;
    padding: var(--space-xs) var(--space-sm);
    border-radius: var(--radius-sm);
    color: var(--color-ink-soft);
    font-size: 13px;
    text-align: start;
    transition:
        background-color var(--dur-short) var(--ease-out),
        color var(--dur-short) var(--ease-out);
}

/* :focus, not :focus-visible — the menu moves focus itself when it opens,
   and an item that holds focus invisibly is an item nobody can find again. */
.cl-topmenu__item:hover,
.cl-topmenu__item:focus {
    background: var(--color-paper-sunken);
    color: var(--color-ink);
    outline: none;
}

.cl-topmenu__icon {
    width: 15px;
    height: 15px;
    flex-shrink: 0;
    color: var(--color-muted);
}

.cl-topmenu__item:hover .cl-topmenu__icon,
.cl-topmenu__item:focus .cl-topmenu__icon { color: var(--color-accent); }

.cl-topmenu__away {
    width: 12px;
    height: 12px;
    margin-inline-start: auto;
    flex-shrink: 0;
    color: var(--color-muted);
}

.cl-topmenu__sep {
    height: 1px;
    margin: var(--space-2xs) var(--space-xs);
    background: var(--color-rule-soft);
}

/* ── Language switcher ──────────────────────────────────────────────────────
   A radio group, not menu items: the two choices are one answer to one
   question, and arrowing onto them as if they were separate destinations is
   what `role="none"` on the labels prevents. The rest of the menu keeps its
   keyboard behaviour; Tab reaches these. */

.cl-topmenu__group {
    padding: var(--space-2xs) var(--space-xs) var(--space-xs);
}

.cl-topmenu__caption {
    display: block;
    padding-inline: var(--space-2xs);
    margin-bottom: var(--space-2xs);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-muted);
}

.cl-topmenu__langs {
    display: flex;
    gap: var(--space-3xs);
}

.cl-topmenu__lang {
    display: flex;
    align-items: center;
    gap: var(--space-3xs);
    flex: 1;
    padding: var(--space-2xs) var(--space-xs);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-sm);
    font-size: 12.5px;
    color: var(--color-ink-soft);
    cursor: pointer;
    transition:
        background-color var(--dur-short) var(--ease-out),
        border-color var(--dur-short) var(--ease-out),
        color var(--dur-short) var(--ease-out);
}

.cl-topmenu__lang:hover {
    background: var(--color-paper-sunken);
    color: var(--color-ink);
}

/* The input carries the focus ring for the whole control: it is what the
   keyboard actually lands on, and hiding it outright would leave a radio
   group nobody can see themselves moving through. */
.cl-topmenu__lang-input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.cl-topmenu__lang:focus-within {
    outline: 2px solid var(--color-accent);
    outline-offset: 1px;
}

.cl-topmenu__lang.is-on {
    border-color: var(--color-accent);
    background: var(--color-accent-wash, var(--color-paper-sunken));
    color: var(--color-ink);
    font-weight: 600;
}

.cl-topmenu__lang-label { flex: 1; }

.cl-topmenu__lang-tick {
    width: 13px;
    height: 13px;
    flex-shrink: 0;
    color: var(--color-accent);
    opacity: 0;
}

.cl-topmenu__lang.is-on .cl-topmenu__lang-tick { opacity: 1; }

.cl-topmenu__lang-go {
    width: 100%;
    margin-top: var(--space-2xs);
    padding: var(--space-2xs);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-sm);
    font-size: 12px;
    color: var(--color-ink-soft);
}

.cl-topmenu__item--halt { color: var(--color-halt); }
.cl-topmenu__item--halt .cl-topmenu__icon { color: var(--color-halt); }

.cl-topmenu__item--halt:hover,
.cl-topmenu__item--halt:focus {
    background: var(--color-halt-wash);
    color: var(--color-halt);
}

.cl-topmenu__item--halt:hover .cl-topmenu__icon,
.cl-topmenu__item--halt:focus .cl-topmenu__icon { color: var(--color-halt); }

/* ── The bar at phone width ───────────────────────────────────────────────
   Below 40rem the account trigger drops to its avatar and the bar loses the
   vertical padding it does not need; below 30rem the word "credits" is taken
   out of the layout but kept in the accessibility tree, so the balance still
   reads as "1,240 credits" when it looks like "1,240". */

@media (max-width: 40rem) {
    /* The bar keeps its 52px; the controls take 2px more of it each. At 34px
       the burger, the bell and the account were under the 36px a thumb can
       hit without aiming, on the one row of controls every page shares. */
    :root {
        --topbar-h: 52px;
        --topbar-control: 36px;
    }

    /* No vertical padding: the min-height centres the row, and two 8px pads
       around a 36px control would make the bar 53px against the 52 the
       alerts and the notification panel are placed by. */
    .cl-topbar {
        gap: var(--space-xs);
        padding-block: 0;
    }

    .cl-topmenu__name,
    .cl-topmenu__caret { display: none; }

    .cl-topmenu__trigger {
        width: var(--topbar-control);
        padding: 0;
        justify-content: center;
    }

    /* 36px rows are comfortable with a pointer and small with a thumb. */
    .cl-topmenu__item { padding-block: var(--space-sm); }

    /* The language choices were 28px chips under 44px rows, the smallest
       targets in the menu and the ones that reload the page when hit. */
    .cl-topmenu__lang { min-height: 40px; }
}

@media (max-width: 30rem) {
    .cl-topbar__credit { padding: 0 var(--space-xs); }

    /*
     * The way to where the balance went moves into the top-up dialog.
     *
     * Five controls beside the page's name left the name 74px at 360 — "New
     * messa…", "Auto Broadc…" on most pages, and "Overv…" on an account that
     * also shows the upgrade chip. The history button is the quietest of the
     * five, and the balance it sits beside opens the dialog that now carries
     * the same link (buy-credits.blade.php), so it is one tap further away
     * rather than gone.
     */
    .cl-topbar__ledger { display: none; }

    .cl-topbar__upgrade { padding: 0 var(--space-xs); }
    .cl-topbar__upgrade-text { display: none; }

    .cl-topbar__credit-unit {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
}

@media (prefers-reduced-motion: reduce) {
    .cl-topbar__bell,
    .cl-topmenu__trigger,
    .cl-topmenu__caret,
    .cl-topmenu__item { transition: none; }

    .cl-topmenu__panel { animation: none; }
}

/* ═══════════════════════════════════════════════════════════
   Ft2 · COLOPHON — one line, hairline above
   ═══════════════════════════════════════════════════════════ */

/*
 * Bottom of the page, never bottom of the screen.
 *
 * `margin-top: auto` inside the scrolling column takes up whatever space is
 * left, so on a short page the colophon sits on the bottom edge instead of
 * floating halfway up under three cards — and on a long one it simply follows
 * the content and scrolls out of sight like everything else.
 *
 * Deliberately not position: sticky. Pinned to the viewport it would spend
 * every screen of every page repeating a copyright line, and the space a
 * colophon is worth is the space it takes when you have finished reading.
 */
.cl-colophon {
    /*
     * `margin-top: auto` fills spare room on short pages, but resolves to zero
     * once the page is long enough to scroll. Keep a real closing beat in that
     * second case so the last card never becomes the footer's top edge.
     */
    --cl-colophon-clearance: clamp(var(--space-lg), 4vw, var(--space-xl));
    position: relative;
    margin-top: auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs) var(--space-sm);
    padding: calc(var(--cl-colophon-clearance) + var(--space-md)) clamp(1rem, 4vw, 2.5rem) var(--space-lg);
    border-top: 0;
    font-size: 11.5px;
    color: var(--color-muted);
}

.cl-colophon::before {
    content: "";
    position: absolute;
    inset-block-start: var(--cl-colophon-clearance);
    inset-inline: 0;
    border-block-start: 1px solid var(--color-rule-soft);
}

/* ═══════════════════════════════════════════════════════════
   SECTION HEAD — stacked, single column, never tag-left
   ═══════════════════════════════════════════════════════════ */

.cl-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-sm) var(--space-lg);
    margin-bottom: var(--space-md);
}

.cl-head__text { min-width: 0; }

.cl-head__title {
    font-size: 17px;
    font-weight: 600;
    letter-spacing: -0.02em;
}

.cl-head__sub {
    margin-top: 2px;
    font-size: 12.5px;
    color: var(--color-muted);
}

.cl-head__end { margin-inline-start: auto; }

/* Top-aligned rather than on the text's baseline. A button that opens with
   an icon takes its baseline from the bottom of the icon, not from its words,
   so beside a words-only button it sat 4px higher — "Rename" above "Back to
   saved lists", "Export bookings" above "All calendars". */
.cl-head__end > .cl-btn { vertical-align: top; }

.cl-page-title {
    font-size: clamp(1.5rem, 1.1rem + 1.4vw, 2rem);
    font-weight: 700;
    letter-spacing: -0.04em;
    line-height: 1.1;
}

.cl-page-lede {
    margin-top: var(--space-2xs);
    font-size: 13.5px;
    color: var(--color-muted);
    max-width: 62ch;
}

/* On a phone the head is already stacked, so pushing the action to the far
   edge leaves it floating in space with nothing to align to. Bring it back
   to the text's own margin. */
@media (max-width: 40rem) {
    .cl-head__end { margin-inline-start: 0; }

    /* Two or more buttons side by side were spaced by the word space between
       the tags — 4px, under the 8 a thumb needs between neighbours — and with
       nothing at all between the lines once they wrapped. A row with a real
       gap, both ways. Only where the buttons are the direct children: a page
       that wraps its own group keeps its own layout. */
    .cl-head__end:has(> .cl-btn + .cl-btn) {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-xs);
    }
}

/* ═══════════════════════════════════════════════════════════
   PANEL — the single containment layer. Nothing bordered nests
   inside anything else bordered; card-in-card is the tell.
   ═══════════════════════════════════════════════════════════ */

.cl-panel {
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-rest);
}

.cl-panel__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-md) var(--space-sm);
    border-bottom: 1px solid var(--color-rule-soft);
}

/*
 * A head with nothing under it does not need a rule under it.
 *
 * These panels are a title and a switch, and the body appears only once the
 * switch is on. Collapsed, the head's own bottom border sat a hair above the
 * panel's border and the card ended in a double line. Alpine hides the body
 * with an inline display:none, so it is still a child and :last-child cannot
 * see it — the state is named on the panel instead.
 */
.cl-panel.is-collapsed > .cl-panel__head { border-bottom: 0; }

.cl-panel__title {
    font-size: 13.5px;
    font-weight: 600;
    letter-spacing: -0.01em;
}

.cl-panel__note {
    font-size: 11.5px;
    color: var(--color-muted);
}

.cl-panel__end { margin-inline-start: auto; }

/*
 * One panel per screen carries the landing hero's long soft shadow — the
 * schematic, which is this page's equivalent of the site's hero mockup.
 * Applying it to every panel would flatten the hierarchy back out; the
 * site has exactly one card floating that high, and so does the portal.
 */
.cl-panel--hero {
    box-shadow: var(--shadow-soft);
    border-color: var(--color-rule-soft);
}

/* Padding varies by content: dense bodies get less than airy ones. */
.cl-panel__body { padding: var(--space-md); }
.cl-panel__body--airy { padding: var(--space-lg) var(--space-md); }
.cl-panel__body--flush { padding: 0; }

.cl-panel__foot {
    padding: var(--space-sm) var(--space-md);
    border-top: 1px solid var(--color-rule-soft);
    font-size: 12px;
    color: var(--color-muted);
}

/* ═══════════════════════════════════════════════════════════
   THE SCHEMATIC — four agent stages, drawn as one instrument
   ═══════════════════════════════════════════════════════════
   Each stage owns a heavy top rule; the gaps between rules are the
   handoffs, and the drop-off chip sits in the gap. Read left to right
   it is the engine: Search → Enrich → Profile → Send.

   This replaces the row of four identical stat cards. The cards told
   you four numbers; the schematic tells you where the work is.
*/

.cl-schematic {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-lg);
    list-style: none;
    margin: 0;
    padding: 0;
}

.cl-stage {
    position: relative;
    padding-top: var(--space-sm);
    border-top: 2px solid var(--color-rule-track);
    min-width: 0;
}

.cl-stage.is-done { border-top-color: var(--color-muted); }
.cl-stage.is-running { border-top-color: var(--color-accent); }
.cl-stage.is-halted { border-top-color: var(--color-halt); }

.cl-stage__mark {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    margin-bottom: var(--space-2xs);
}

.cl-stage__index {
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.06em;
    color: var(--color-muted);
}

.cl-stage__dot {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--color-rule-track);
    flex-shrink: 0;
}

.cl-stage.is-done .cl-stage__dot { background: var(--color-muted); }
.cl-stage.is-halted .cl-stage__dot { background: var(--color-halt); }

/* Motion 1 of 3 — the only thing on the page that moves on its own.
   A still portal can't tell you the engine is running; this can. */
.cl-stage.is-running .cl-stage__dot {
    background: var(--color-accent);
    animation: cl-breathe 2.4s var(--ease-in-out) infinite;
}

@keyframes cl-breathe {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

.cl-stage__name {
    font-size: 14px;
    font-weight: 600;
    letter-spacing: -0.015em;
}

.cl-stage__blurb {
    margin-top: 2px;
    font-size: 11.5px;
    line-height: 1.45;
    color: var(--color-muted);
    min-height: 2.9em;
}

.cl-stage__figure {
    display: flex;
    align-items: baseline;
    gap: var(--space-2xs);
    margin-top: var(--space-sm);
}

.cl-stage__count {
    font-family: var(--font-display);
    font-size: 26px;
    font-weight: 700;
    letter-spacing: -0.04em;
    line-height: 1;
}

.cl-stage__unit {
    font-size: 11px;
    color: var(--color-muted);
}

.cl-stage__state {
    margin-top: var(--space-xs);
    font-size: 11px;
    color: var(--color-muted);
}

/* The handoff chip. Absolute inside the gap on wide screens, inline on
   narrow ones — one element, repositioned, so screen readers hear it
   exactly once. */
.cl-stage__handoff {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    color: var(--color-muted);
    /* Matches the panel it sits in, so the chip reads as a gap in the track
       rather than a grey tab laid on top of it. */
    background: var(--color-paper-raised);
    padding: 0 var(--space-2xs);
    white-space: nowrap;
}

@media (min-width: 64rem) {
    .cl-stage__handoff {
        position: absolute;
        top: -0.62rem;
        /* Centre the chip in the gap: its own leading edge starts at the
           stage's trailing edge, then it slides back by half its width. */
        inset-inline-start: 100%;
        margin-inline-start: calc(var(--space-lg) / 2);
        transform: translateX(-50%);
        /* The next stage is a later sibling, so its top rule would otherwise
           paint over this chip and strike the text through. */
        z-index: var(--z-raised);
    }

    /* translateX has no logical form. The chip pulls back by half its own
       width to land on the gap's centre; mirrored, it pulls the other way. */
    [dir="rtl"] .cl-stage__handoff { transform: translateX(50%); }

    .cl-stage:last-child .cl-stage__handoff { display: none; }
}

@media (max-width: 63.99rem) {
    .cl-schematic { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .cl-stage__blurb { min-height: 0; }
    .cl-stage__handoff { margin-top: var(--space-xs); padding: 0; }
}

@media (max-width: 40rem) {
    .cl-schematic { grid-template-columns: minmax(0, 1fr); gap: var(--space-md); }
}

/* ═══════════════════════════════════════════════════════════
   THE LADDER — the lead funnel, drawn as it actually behaves
   ═══════════════════════════════════════════════════════════
   Proportional bars narrowing down the page, with the conversion to
   the next step sitting on the hairline between rows. A donut would
   have hidden the one thing that matters: where the drop is.
*/

.cl-ladder {
    list-style: none;
    margin: 0;
    padding: 0;
}

.cl-ladder__step {
    display: grid;
    grid-template-columns: 11ch minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-xs) 0;
    border-bottom: 1px solid var(--color-rule-soft);
}

.cl-ladder__step:last-child { border-bottom: 0; }

.cl-ladder__label {
    font-size: 12px;
    font-weight: 500;
    color: var(--color-ink-soft);
}

.cl-ladder__track {
    height: 10px;
    border-radius: var(--radius-xs);
    background: var(--color-paper-sunken);
    overflow: hidden;
}

.cl-ladder__fill {
    display: block;
    height: 100%;
    width: var(--fill, 0%);
    /* The last step is often 1–2% of the head of the funnel, which rounds to
       a bar you cannot see. A floor keeps the narrowing honest — the step is
       tiny, not missing. */
    min-width: 4px;
    border-radius: var(--radius-xs);
    background: var(--color-ink-soft);
}

/* Tone names come from the service (warm · live · halt), not from 1leado's
   original vocabulary — the old `replied`/`converted` selectors matched
   nothing, so every bar rendered neutral.

   The funnel reads warm at the top (the audience you started with), neutral
   through the middle, green at the interaction step and red at the loss. */
.cl-ladder__step[data-tone='warm'] .cl-ladder__fill { background: var(--color-warm); }
.cl-ladder__step[data-tone='live'] .cl-ladder__fill { background: var(--color-live); }
.cl-ladder__step[data-tone='halt'] .cl-ladder__fill { background: var(--color-halt); }

.cl-ladder__count {
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 600;
    text-align: end;
    min-width: 6ch;
}

.cl-ladder__rate {
    grid-column: 2 / -1;
    justify-self: start;
    font-size: 10.5px;
    color: var(--color-muted);
    padding-bottom: var(--space-2xs);
}

@media (max-width: 40rem) {
    .cl-ladder__step { grid-template-columns: minmax(0, 1fr) auto; }
    .cl-ladder__label { grid-column: 1 / -1; }
    .cl-ladder__rate { grid-column: 1 / -1; }
}

/* ═══════════════════════════════════════════════════════════
   FIGURES — a hairline-separated strip, not a row of cards
   ═══════════════════════════════════════════════════════════ */

.cl-figures {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-lg);
    background: var(--color-paper-raised);
    box-shadow: var(--shadow-rest);
    overflow: hidden;
}

.cl-figure {
    padding: var(--space-md);
    border-inline-end: 1px solid var(--color-rule-soft);
    min-width: 0;
}

.cl-figure:last-child { border-inline-end: 0; }

.cl-figure__value {
    font-family: var(--font-display);
    font-size: 28px;
    font-weight: 700;
    letter-spacing: -0.04em;
    line-height: 1;
    display: block;
}

.cl-figure__label {
    display: block;
    margin-top: var(--space-xs);
    font-size: 12px;
    font-weight: 500;
    color: var(--color-ink-soft);
}

.cl-figure__note {
    display: block;
    margin-top: 1px;
    font-size: 11px;
    color: var(--color-muted);
}

.cl-figure--accent .cl-figure__value { color: var(--color-accent); }
.cl-figure--warm .cl-figure__value { color: var(--color-warm); }

/* Phones get a fixed 2×2 rather than auto-fit: the strip's dividers only
   read correctly when the component knows its own column count. */
@media (max-width: 40rem) {
    .cl-figures { grid-template-columns: repeat(2, minmax(0, 1fr)); }

    .cl-figure {
        border-inline-end: 1px solid var(--color-rule-soft);
        border-bottom: 1px solid var(--color-rule-soft);
    }

    .cl-figure:nth-child(2n) { border-inline-end: 0; }
    .cl-figure:nth-last-child(-n + 2) { border-bottom: 0; }
}

/* ═══════════════════════════════════════════════════════════
   BADGES — colour is never the only signal; each carries a word
   ═══════════════════════════════════════════════════════════ */

.cl-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    padding: var(--space-3xs) var(--space-xs);
    border-radius: var(--radius-pill);
    border: 1px solid var(--color-rule);
    background: var(--color-paper-sunken);
    color: var(--color-ink-soft);
    font-size: 11px;
    font-weight: 500;
    line-height: 1.5;
    white-space: nowrap;
}

.cl-badge__dot {
    width: 5px;
    height: 5px;
    border-radius: var(--radius-pill);
    background: currentColor;
    flex-shrink: 0;
}

.cl-badge--live {
    background: var(--color-live-wash);
    border-color: var(--color-live-line);
    color: var(--color-live);
}

.cl-badge--warn {
    background: var(--color-warn-wash);
    border-color: var(--color-warn-line);
    color: var(--color-warn);
}

.cl-badge--halt {
    background: var(--color-halt-wash);
    border-color: var(--color-halt-line);
    color: var(--color-halt);
}

.cl-badge--accent {
    background: var(--color-accent-wash);
    border-color: var(--color-accent-line);
    color: var(--color-accent);
}

.cl-badge--warm {
    background: var(--color-warm-wash);
    border-color: var(--color-warm-line);
    color: var(--color-warm-ink);
}

/* ═══════════════════════════════════════════════════════════
   CONTROLS — eight states each
   ═══════════════════════════════════════════════════════════ */

.cl-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
    padding: var(--space-xs) var(--space-md);
    min-height: 36px;
    border: 1px solid var(--color-rule);
    /* Pill, like every control on the landing page. */
    border-radius: var(--radius-pill);
    background: var(--color-paper-raised);
    color: var(--color-ink);
    font-family: var(--font-sans);
    font-size: 13px;
    font-weight: 600;
    /* a control label is a one-line object — it never wraps */
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    /* The transform comes back on the spring: a press goes in at once (see
       :active) and the button rises out of it with a little give. */
    transition: background-color var(--dur-short) var(--ease-out),
                border-color var(--dur-short) var(--ease-out),
                color var(--dur-short) var(--ease-out),
                box-shadow var(--dur-short) var(--ease-out),
                transform var(--dur-mid) var(--ease-spring);
}

.cl-btn svg { width: 15px; height: 15px; flex-shrink: 0; stroke-width: 1.75; }

/*
 * Hover is for a pointer that can hover, and never for a disabled button.
 *
 * A touch screen leaves :hover on whatever was tapped last. Tap Continue in
 * the wizard and the next step's Continue, disabled until that step is done,
 * sat under the finger with the primary hover on it: a lifted green pill with
 * the grey disabled label on top, which looked ready and did nothing. Under
 * a mouse the same rules beat the disabled colours too, being later at the
 * same weight. The exclusion is in :where() so it adds no weight, and a page
 * that restyles a button's hover still wins exactly as it did.
 */
@media (hover: hover) and (pointer: fine) {
    .cl-btn:hover:where(:not(:disabled, [aria-disabled='true'])) {
        background: var(--color-paper-sunken);
        border-color: var(--color-rule-track);
    }
}

/* Pressed in, immediately: the shorter duration belongs to the state being
   entered, so the press is a tick and the release keeps the spring above. */
.cl-btn:active {
    transform: scale(0.96);
    transition-duration: var(--dur-tick);
}

.cl-btn:disabled,
.cl-btn[aria-disabled='true'] {
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
}

/* A faded brand colour reads as broken rather than as unavailable, so a
   disabled button drops out of the palette entirely. */
.cl-btn--primary:disabled,
.cl-btn--primary[aria-disabled='true'] {
    background: var(--color-paper-sunken);
    border-color: var(--color-rule);
    color: var(--color-muted-soft);
}

.cl-btn--ghost:disabled { opacity: 0.45; }

.cl-btn[data-state='loading'] { color: var(--color-muted); cursor: progress; }
.cl-btn[data-state='error'] { border-color: var(--color-halt-line); color: var(--color-halt); }
.cl-btn[data-state='success'] { border-color: var(--color-live-line); color: var(--color-live); }

/*
 * Busy — the request is out and the button is waiting on it.
 *
 * One spinner for every button, drawn by the button itself and keyed on the
 * aria-busy the templates already publish for screen readers, so the state
 * has a single source. Sign in, create the account, send the reset link,
 * send the message: each said "Sending…" in words and nothing moved, which
 * on a slow line read as a page that had stopped.
 *
 * A button that draws its own spinner — an `#i-spinner` icon, or an element
 * whose class says spin — keeps it and is not given a second one.
 *
 * A busy primary keeps its colour. A disabled one drops out of the palette
 * (above) because it cannot be pressed; a busy one is not unavailable, it is
 * working, and turning grey at the moment of pressing read as a failure.
 */
.cl-btn[aria-busy='true'] { cursor: progress; }

.cl-btn[aria-busy='true']:not(:has([href='#i-spinner'], [class*='spin']))::after {
    content: '';
    flex-shrink: 0;
    width: 14px;
    height: 14px;
    border: 2px solid currentColor;
    border-inline-end-color: transparent;
    border-radius: 50%;
    animation: cl-spin 700ms linear infinite;
}

.cl-btn--primary[aria-busy='true']:is(:disabled, [aria-disabled='true']) {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-accent-ink);
    box-shadow: none;
}

/*
 * The primary action is accent indigo, not ink.
 *
 * The first pass made it black, which is a perfectly good button and the
 * wrong one here: on the landing page the indigo pill IS the call to
 * action — "Join waitlist", "Find leads" — so a client arriving in the
 * portal already reads indigo as "this is the thing to press". Spending
 * that learned association on a black button wastes it.
 */
.cl-btn--primary {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-accent-ink);
    box-shadow: var(--shadow-lift);
}

/* Hover and disabled: see .cl-btn:hover above. */
@media (hover: hover) and (pointer: fine) {
    .cl-btn--primary:hover:where(:not(:disabled, [aria-disabled='true'])) {
        background: var(--color-accent-hover);
        border-color: var(--color-accent-hover);
        /* The site's CTA lifts 2px on hover; the portal's does the same. */
        transform: translateY(-1.5px);
    }
}

/* Not on a disabled one either: this comes after `.cl-btn:disabled` at the
   same weight, so it pressed in a button that had just said it would not. */
.cl-btn--primary:active:where(:not(:disabled, [aria-disabled='true'])) { transform: scale(0.96); }

.cl-btn--quiet {
    background: transparent;
    border-color: transparent;
    color: var(--color-muted);
}

@media (hover: hover) and (pointer: fine) {
    .cl-btn--quiet:hover:where(:not(:disabled, [aria-disabled='true'])) {
        background: var(--color-paper-sunken);
        color: var(--color-ink);
    }
}

.cl-btn--sm { min-height: 28px; padding: var(--space-2xs) var(--space-sm); font-size: 12px; }

/* Below the typography breakpoint every hit target reaches 44px. */
@media (max-width: 40rem) {
    .cl-btn { min-height: 44px; padding-inline: var(--space-sm); }
    .cl-btn--sm { min-height: 44px; }
}

/* A typographic link CTA — a word, a rule, an arrow. No box. */
.cl-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    color: var(--color-accent);
    font-size: 12.5px;
    font-weight: 500;
    white-space: nowrap;
    text-decoration: none;
    border-bottom: 1px solid var(--color-accent-line);
    transition: border-color var(--dur-short) var(--ease-out);
}

.cl-link:hover { border-bottom-color: var(--color-accent); }

/* ── Filter chips — radio tabs that don't scroll-jump ──────── */

.cl-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2xs);
}

.cl-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    padding: var(--space-2xs) var(--space-sm);
    min-height: 30px;
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-pill);
    background: var(--color-paper-raised);
    color: var(--color-ink-soft);
    font-size: 12px;
    font-weight: 500;
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--dur-short) var(--ease-out),
                border-color var(--dur-short) var(--ease-out),
                color var(--dur-short) var(--ease-out);
}

.cl-chip:hover { background: var(--color-paper-sunken); }
.cl-chip:active { transform: translateY(1px); }

.cl-chip:disabled,
.cl-chip[aria-disabled='true'] {
    opacity: 0.55;
    cursor: not-allowed;
    transform: none;
}

/* Selected chip matches the landing page's use-case selector: a light
   accent wash with accent text and an accent hairline — not a black fill. */
.cl-chip.is-on {
    background: var(--color-accent-wash);
    border-color: var(--color-accent-line);
    color: var(--color-accent);
}

.cl-chip__n {
    font-family: var(--font-display);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    color: var(--color-muted);
}

.cl-chip.is-on .cl-chip__n { color: var(--color-accent); }

@media (max-width: 40rem) {
    .cl-chip { min-height: 44px; }
}

/* ── Search field ──────────────────────────────────────────── */

.cl-field {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    padding: 0 var(--space-sm);
    min-height: 36px;
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    background: var(--color-paper-raised);
    box-shadow: var(--shadow-rest);
    transition: border-color var(--dur-short) var(--ease-out),
                box-shadow var(--dur-short) var(--ease-out);
}

.cl-field:focus-within {
    border-color: var(--color-accent);
    /* The hero's field glows on focus; so does this one. */
    box-shadow: 0 0 0 3px var(--color-accent-wash);
}

.cl-field svg { width: 15px; height: 15px; color: var(--color-muted); flex-shrink: 0; }

.cl-field input {
    border: 0;
    outline: 0;
    background: transparent;
    font: inherit;
    font-size: 13px;
    color: var(--color-ink);
    min-width: 0;
    width: 100%;
}

.cl-field input::placeholder { color: var(--color-muted); }

/* The field shares the button's base height on every viewport — a 34px
   input beside a 44px button is the most common form-tuning slip.

   After the base rule, not before it: same specificity, so whichever comes
   last wins, and above the base rule this lost and left every phone search
   box at 36px. The input fills the box top to bottom too. The wrapper is a
   div, not a label, so a tap on its padding above or below a 27px input
   focused nothing. */
@media (max-width: 40rem) {
    .cl-field { min-height: 44px; }
    .cl-field input { align-self: stretch; }
}

/* ═══════════════════════════════════════════════════════════
   FORM CONTROLS
   ═══════════════════════════════════════════════════════════
   Inputs are where an almost-right interface gives itself away, so the
   rules here are strict:

     · border-width never changes between states — only colour and the
       focus ring move, because a 1px→2px border on focus shifts the
       whole row by a pixel
     · the focus ring is an outline, never a border
     · every field reserves its helper line, so an appearing error does
       not push the rest of the form down the page
     · inputs share the buttons' height, on every viewport
*/

.cl-form {
    display: grid;
    gap: var(--space-lg);
}

.cl-fieldset {
    display: grid;
    gap: var(--space-md);
    border: 0;
    padding: 0;
    margin: 0;
    min-width: 0;
}

.cl-legend {
    font-family: var(--font-display);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: -0.02em;
    padding: 0;
    margin-bottom: var(--space-2xs);
}

.cl-legend__note {
    display: block;
    margin-top: 2px;
    font-family: var(--font-sans);
    font-size: 12px;
    font-weight: 400;
    letter-spacing: 0;
    color: var(--color-muted);
}

.cl-grid-2 {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-md);
}

@media (max-width: 40rem) {
    .cl-grid-2 { grid-template-columns: minmax(0, 1fr); }
}

.cl-label {
    display: block;
    margin-bottom: var(--space-2xs);
    font-size: 12.5px;
    font-weight: 600;
    color: var(--color-ink-soft);
}

.cl-label__req {
    color: var(--color-accent);
    margin-inline-start: 2px;
}

.cl-input,
.cl-select,
.cl-textarea {
    width: 100%;
    min-height: 40px;
    padding: var(--space-xs) var(--space-sm);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    background: var(--color-paper-raised);
    color: var(--color-ink);
    font-family: var(--font-sans);
    font-size: 13.5px;
    line-height: 1.5;
    /* Held at rest so activating the ring cannot change the geometry. */
    outline: 2px solid transparent;
    outline-offset: 1px;
    transition: border-color var(--dur-short) var(--ease-out),
                box-shadow var(--dur-short) var(--ease-out);
}

.cl-textarea {
    min-height: 92px;
    resize: vertical;
}

.cl-select {
    appearance: none;
    padding-inline-end: var(--space-xl);
    /* Chevron drawn as a background gradient pair — no icon font, no image. */
    background-image:
        linear-gradient(45deg, transparent 50%, currentColor 50%),
        linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position:
        calc(100% - 18px) calc(50% + 2px),
        calc(100% - 13px) calc(50% + 2px);
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}

/* The chevron is two background tiles measured from the trailing edge, and
   background-position has no logical form — so the pair is remeasured from
   the other edge rather than rewritten. The mark is untouched: a chevron
   that means "expand" points down and reads the same either way. */
[dir="rtl"] .cl-select {
    background-position:
        13px calc(50% + 2px),
        18px calc(50% + 2px);
}

.cl-input::placeholder,
.cl-textarea::placeholder { color: var(--color-muted); }

/*
 * Fields whose contents are never in the reader's language.
 *
 * An email address, a URL, a phone number and a password are left-to-right
 * runs in every locale — there is no Arabic spelling of "name@company.com".
 * Left to inherit `dir="rtl"` the caret starts at the right, the placeholder
 * is right-aligned, and the bidi algorithm moves the punctuation as you type:
 * "name@company.com" becomes "company.com@name" and "+212 600…" puts its plus
 * on the wrong end. Every character is there and the field is unusable.
 *
 * `text-align: start` resolves against the `direction` set on the same
 * element, so this both pins the run and puts it back on the left — the two
 * have to move together or the field is LTR text hugging the right edge.
 *
 * Deliberately not applied to `type="text"` or `search`: a contact's name and
 * a search term are exactly the places someone types Arabic.
 *
 * Selected by type rather than by class. The portal has at least three input
 * classes — `.cl-input` on the app's forms, `.cl-auth__input` on the four auth
 * surfaces, and the ones intl-tel-input writes itself — and keying on the
 * class means the rule is correct on the pages someone remembered and absent
 * on the rest. This sheet is only ever loaded by the portal, so the bare
 * selector reaches nothing else.
 */
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="url"],
input[type="number"] {
    direction: ltr;
    text-align: start;
}

.cl-input:hover,
.cl-select:hover,
.cl-textarea:hover { border-color: var(--color-rule-track); }

.cl-input:focus-visible,
.cl-select:focus-visible,
.cl-textarea:focus-visible {
    border-color: var(--color-accent);
    outline-color: var(--color-accent);
}

.cl-input:disabled,
.cl-select:disabled,
.cl-textarea:disabled {
    opacity: 0.55;
    cursor: not-allowed;
    background: var(--color-paper-sunken);
}

.cl-input[aria-invalid='true'],
.cl-select[aria-invalid='true'],
.cl-textarea[aria-invalid='true'] {
    border-color: var(--color-halt);
    background: var(--color-halt-wash);
}

/* Reserved whether or not there is anything to say, so a validation error
   appearing does not reflow the form beneath it. */
.cl-hint {
    display: block;
    min-height: 1lh;
    margin-top: var(--space-2xs);
    font-size: 11.5px;
    line-height: 1.4;
    color: var(--color-muted);
}

.cl-hint--error {
    color: var(--color-halt);
    font-weight: 500;
}

/* ── Multi-select as a chip field ──────────────────────────── */

.cl-checks {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2xs);
}

.cl-check {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    padding: var(--space-2xs) var(--space-sm);
    min-height: 34px;
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-pill);
    background: var(--color-paper-raised);
    font-size: 12.5px;
    font-weight: 500;
    cursor: pointer;
    transition: background-color var(--dur-short) var(--ease-out),
                border-color var(--dur-short) var(--ease-out),
                color var(--dur-short) var(--ease-out);
}

.cl-check:hover { background: var(--color-paper-sunken); }

/* The input stays in normal flow at zero size rather than absolutely
   positioned — an absolutely-positioned checked input makes the browser
   scroll the page to it on every click. */
.cl-check input {
    width: 0;
    height: 0;
    opacity: 0;
    margin: 0;
    padding: 0;
    border: 0;
    appearance: none;
}

.cl-check:has(input:checked) {
    background: var(--color-accent-wash);
    border-color: var(--color-accent-line);
    color: var(--color-accent);
}

.cl-check:has(input:focus-visible) {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

.cl-check:has(input:disabled) {
    opacity: 0.55;
    cursor: not-allowed;
}

@media (max-width: 40rem) {
    .cl-check { min-height: 44px; }

    /* Not .cl-textarea: it already has 92px, and listing it here lowered it
       to 44 on a phone — two rows, with a three-line placeholder cut off
       after the second. */
    .cl-input,
    .cl-select { min-height: 44px; }
}

/* ── Picker — searchable multi-select ──────────────────────── */

.cl-picker { position: relative; }

.cl-picker__control { position: relative; }

.cl-picker__count {
    font-size: 11px;
    color: var(--color-accent);
    font-weight: 600;
    white-space: nowrap;
    flex-shrink: 0;
}

.cl-picker__list {
    position: absolute;
    top: calc(100% + var(--space-2xs));
    left: 0;
    right: 0;
    /* Above the fields below it, beneath the rail and any sheet. */
    z-index: var(--z-dropdown);
    max-height: 264px;
    overflow-y: auto;
    padding: var(--space-2xs);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-soft);
}

.cl-picker__opt {
    display: block;
    width: 100%;
    padding: var(--space-xs) var(--space-sm);
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    text-align: start;
    font: inherit;
    font-size: 13px;
    color: var(--color-ink);
    cursor: pointer;
    transition: background-color var(--dur-short) var(--ease-out);
}

.cl-picker__opt:hover { background: var(--color-paper-sunken); }

.cl-picker__opt.is-on {
    background: var(--color-accent-wash);
    color: var(--color-accent);
    font-weight: 600;
}

.cl-picker__opt-label { display: block; }

.cl-picker__opt-hint {
    display: block;
    margin-top: 1px;
    font-size: 11px;
    font-weight: 400;
    color: var(--color-muted);
}

.cl-picker__empty,
.cl-picker__more {
    padding: var(--space-sm);
    margin: 0;
    font-size: 12px;
    color: var(--color-muted);
}

.cl-picker__more {
    border-top: 1px solid var(--color-rule-soft);
}

.cl-picker__chosen {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2xs);
    margin-top: var(--space-xs);
}

.cl-chip__x {
    border: 0;
    background: transparent;
    padding-block: 0;
    padding-inline: var(--space-2xs) 0;
    margin: 0;
    font: inherit;
    font-size: 14px;
    line-height: 1;
    color: currentColor;
    opacity: 0.6;
    cursor: pointer;
    transition: opacity var(--dur-short) var(--ease-out);
}

.cl-chip__x:hover { opacity: 1; }

/* ── Source cards — icon, name, provider, tick ─────────────────
   A pill was the wrong shape once these carried three things. Cards on a
   fluid grid: they line up, they wrap predictably, and the icon has room
   to sit beside the text instead of above it. */

.cl-sources {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
    gap: var(--space-xs);
}

.cl-source {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-md);
    background: var(--color-paper-raised);
    cursor: pointer;
    min-width: 0;
    transition: background-color var(--dur-short) var(--ease-out),
                border-color var(--dur-short) var(--ease-out);
}

.cl-source:hover { background: var(--color-paper-sunken); }

/* In flow at zero size — an absolutely-positioned checked input makes the
   browser scroll the page to it on every click. */
.cl-source input {
    width: 0;
    height: 0;
    opacity: 0;
    margin: 0;
    padding: 0;
    border: 0;
    appearance: none;
}

.cl-source__mark {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    flex-shrink: 0;
    border-radius: var(--radius-sm);
    background: var(--color-paper-sunken);
    color: var(--color-ink-soft);
    transition: background-color var(--dur-short) var(--ease-out),
                color var(--dur-short) var(--ease-out);
}

.cl-source__mark svg { width: 17px; height: 17px; }

/* An uploaded logo is contained, never cropped — these are wordmarks as
   often as they are squares. */
.cl-source__mark img {
    width: 22px;
    height: 22px;
    object-fit: contain;
}

.cl-source__text { min-width: 0; flex: 1; }

.cl-source__name {
    display: block;
    font-size: 12.5px;
    font-weight: 600;
    line-height: 1.3;
    /* Long provider names must wrap inside the card, not widen the grid. */
    overflow-wrap: anywhere;
}

.cl-source__hint {
    display: block;
    margin-top: 2px;
    font-size: 10.5px;
    color: var(--color-muted);
    overflow-wrap: anywhere;
}

.cl-source__tick {
    display: grid;
    place-items: center;
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    border-radius: var(--radius-pill);
    background: var(--color-accent);
    color: var(--color-accent-ink);
    opacity: 0;
    transition: opacity var(--dur-short) var(--ease-out);
}

.cl-source__tick svg { width: 11px; height: 11px; }

.cl-source:has(input:checked) {
    background: var(--color-paper-raised);
    border-color: var(--color-accent);
}

.cl-source:has(input:checked) .cl-source__mark {
    background: var(--color-paper-sunken);
    color: var(--color-accent);
}

.cl-source:has(input:checked) .cl-source__name { color: var(--color-ink); }
.cl-source:has(input:checked) .cl-source__hint { color: var(--color-muted); }
.cl-source:has(input:checked) .cl-source__tick { opacity: 1; }

.cl-source:has(input:focus-visible) {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

.cl-source:has(input:disabled) {
    opacity: 0.55;
    cursor: not-allowed;
}

@media (max-width: 40rem) {
    .cl-sources { grid-template-columns: minmax(0, 1fr); }
}

/* ── Form actions ──────────────────────────────────────────── */

.cl-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
    padding-top: var(--space-xs);
}

.cl-actions__note {
    font-size: 11.5px;
    color: var(--color-muted);
    margin-inline-start: auto;
}

@media (max-width: 40rem) {
    .cl-actions__note { margin-inline-start: 0; width: 100%; }
    /* Stack form actions before translated labels run out of room. */
    .cl-actions { min-inline-size: 0; }
    .cl-actions > .cl-btn { flex: 1 0 100%; inline-size: 100%; padding-inline: var(--space-sm); }
}

/* ═══════════════════════════════════════════════════════════
   TABLE
   ═══════════════════════════════════════════════════════════ */

.cl-scroll { overflow-x: auto; }

.cl-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
}

.cl-table th {
    padding: var(--space-xs) var(--space-md);
    text-align: start;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-muted);
    border-bottom: 1px solid var(--color-rule-soft);
    white-space: nowrap;
}

.cl-table td {
    padding: var(--space-sm) var(--space-md);
    border-bottom: 1px solid var(--color-rule-soft);
    vertical-align: middle;
}

.cl-table tbody tr:last-child td { border-bottom: 0; }

/* Motion 2 of 3 — one signal on hover. Colour only: no lift, no scale,
   no shadow. Four simultaneous hover effects is the generated tell. */
.cl-table tbody tr {
    transition: background-color var(--dur-short) var(--ease-out);
}

.cl-table tbody tr:hover { background: var(--color-paper-sunken); }

.cl-table__name {
    font-weight: 500;
    color: var(--color-ink);
    text-decoration: none;
}

.cl-table__name:hover { color: var(--color-accent); }

.cl-table__sub {
    margin-top: 1px;
    font-size: 11.5px;
    color: var(--color-muted);
}

.cl-table__num {
    font-family: var(--font-display);
    font-weight: 600;
    text-align: end;
}

/* ── Runs that carry their own direction ──────────────────────────────────
   A table holds data, and data is not written in the interface's language: a
   contact is called whatever they are called, and a phone number, an address,
   a token and a template variable are left-to-right runs whatever the page
   around them is doing.

   Left alone in an Arabic page the bidi algorithm reorders them, and it does
   it to the *edges* of the run — so "+212 600 000 000" renders as
   "000 000 600 212+" and "name@company.com" loses its dot to the front. Every
   character is present and the string is still wrong.

   `plaintext` is `dir="auto"` as a style rule: each cell takes its direction
   from its own first strong character, so an Arabic name reads right-to-left
   and the number beside it reads left-to-right, without either being declared
   in the markup. It is set on the cell rather than the row because the two
   have to be able to disagree.

   `isolate` on the narrower classes below: those hold one value that is always
   LTR, so they are pinned rather than sniffed, and isolation stops them
   dragging neighbouring punctuation into their own run. */

.cl-table td,
.cl-dt td { unicode-bidi: plaintext; }

.cl-table__num,
.cl-dt__num,
[data-ltr] {
    direction: ltr;
    unicode-bidi: isolate;
}

/* Numbers stay end-aligned in both directions: pinning the run to LTR must
   not drag the column back to the left in Arabic. */
.cl-table__num, .cl-dt__num { text-align: end; }

/* ── Score meter — a lead's fit, drawn ─────────────────────── */

.cl-score {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
}

.cl-score__track {
    width: 46px;
    height: 4px;
    border-radius: var(--radius-pill);
    background: var(--color-paper-sunken);
    overflow: hidden;
}

.cl-score__fill {
    display: block;
    height: 100%;
    width: var(--fill, 0%);
    border-radius: var(--radius-pill);
    background: var(--color-accent);
}

.cl-score__n {
    font-family: var(--font-display);
    font-size: 12px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--color-ink-soft);
}

/* ── Row actions ───────────────────────────────────────────── */

.cl-row-actions {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
}

/* ═══════════════════════════════════════════════════════════
   THE DECISION — approve, or reject with a reason
   ═══════════════════════════════════════════════════════════ */

.cl-decide {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--space-lg);
}

.cl-decide__reject {
    flex: 1;
    min-width: min(340px, 100%);
}

.cl-decide__row {
    display: flex;
    gap: var(--space-xs);
}

.cl-decide__row .cl-input { flex: 1; min-width: 0; }

@media (max-width: 40rem) {
    .cl-decide { flex-direction: column; gap: var(--space-md); }
    .cl-decide__row { flex-direction: column; }
}

/* ═══════════════════════════════════════════════════════════
   THREAD — a conversation, not a chat app
   ═══════════════════════════════════════════════════════════
   Inbound and outbound differ by alignment, fill and one restrained accent
   edge. No tails, avatars or delivery ticks: this is a record of what was
   said, read after the fact, not a live chat window.
*/

.cl-thread {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--space-md);
}

.cl-thread__row {
    display: grid;
    justify-items: end;
    max-width: 82%;
    margin-inline-start: auto;
}

.cl-thread__row.is-in {
    justify-items: start;
    margin-inline-start: 0;
    margin-inline-end: auto;
}

.cl-thread__bubble {
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--color-rule);
    border-inline-end: 2px solid var(--color-accent);
    border-radius: var(--radius-md);
    background: var(--color-paper-raised);
    color: var(--color-ink);
    font-size: 13px;
    line-height: 1.55;
    /* A pasted URL or a long word must wrap rather than widen the bubble. */
    overflow-wrap: anywhere;
}

.cl-thread__row.is-in .cl-thread__bubble {
    border-inline-end: 1px solid var(--color-rule);
    background: var(--color-paper-sunken);
    color: var(--color-ink);
}

.cl-thread__meta {
    margin-top: var(--space-2xs);
    font-size: 11px;
    color: var(--color-muted);
}

/* ═══════════════════════════════════════════════════════════
   FACTS — a definition list, set as one
   ═══════════════════════════════════════════════════════════ */

.cl-facts {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-2xs) var(--space-md);
    margin: 0;
    font-size: 12.5px;
}

.cl-facts dt {
    color: var(--color-muted);
    white-space: nowrap;
}

.cl-facts dd {
    margin: 0;
    color: var(--color-ink);
    overflow-wrap: anywhere;
}

/* ═══════════════════════════════════════════════════════════
   ACTIVITY LOG — a log, set like one
   ═══════════════════════════════════════════════════════════ */

.cl-log {
    list-style: none;
    margin: 0;
    padding: 0;
}

.cl-log__row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-sm);
    padding: var(--space-sm) 0;
    border-bottom: 1px solid var(--color-rule-soft);
}

.cl-log__row:last-child { border-bottom: 0; }

.cl-log__when {
    font-family: var(--font-display);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    color: var(--color-muted);
    padding-top: 1px;
    white-space: nowrap;
}

.cl-log__what { font-size: 12.5px; line-height: 1.5; min-width: 0; }

.cl-log__who { font-weight: 500; }

.cl-log__meta { font-size: 11.5px; color: var(--color-muted); margin-top: 1px; }

/* ═══════════════════════════════════════════════════════════
   QUOTA METER
   ═══════════════════════════════════════════════════════════ */

.cl-meter__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-sm);
    margin-bottom: var(--space-xs);
}

.cl-meter__value {
    font-family: var(--font-display);
    font-size: 20px;
    font-weight: 700;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
}

.cl-meter__cap { font-size: 12px; color: var(--color-muted); }

.cl-meter__track {
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--color-paper-sunken);
    overflow: hidden;
}

.cl-meter__fill {
    display: block;
    height: 100%;
    width: var(--fill, 0%);
    border-radius: var(--radius-pill);
    background: var(--color-accent);
}

.cl-meter--warn .cl-meter__fill { background: var(--color-warn); }

.cl-meter__note {
    margin-top: var(--space-xs);
    font-size: 11.5px;
    color: var(--color-muted);
}

/* ═══════════════════════════════════════════════════════════
   EMPTY STATE — the one place the portal speaks
   ═══════════════════════════════════════════════════════════ */

.cl-empty {
    padding: var(--space-xl) var(--space-md);
    text-align: center;
}

.cl-empty__line {
    font-family: var(--font-accent);
    font-style: normal;
    font-size: 22px;
    line-height: 1.3;
    letter-spacing: -0.01em;
    color: var(--color-ink);
    max-width: 34ch;
    margin-inline: auto;
}

.cl-empty__note {
    margin-top: var(--space-xs);
    font-size: 12.5px;
    color: var(--color-muted);
    max-width: 46ch;
    margin-inline: auto;
}

.cl-empty__act { margin-top: var(--space-md); }

/* ═══════════════════════════════════════════════════════════
   ALERT — a banner that states, then instructs
   ═══════════════════════════════════════════════════════════ */

.cl-alert {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--color-rule);
    border-inline-start-width: 1px;
    border-radius: var(--radius-md);
    background: var(--color-paper-raised);
    font-size: 12.5px;
    line-height: 1.5;
}

.cl-alert svg { width: 16px; height: 16px; flex-shrink: 0; margin-top: 1px; }

.cl-alert strong { font-weight: 600; }

.cl-alert--warn {
    background: var(--color-warn-wash);
    border-color: var(--color-warn-line);
    color: var(--color-warn);
}

.cl-alert--halt {
    background: var(--color-halt-wash);
    border-color: var(--color-halt-line);
    color: var(--color-halt);
}

.cl-alert--accent {
    background: var(--color-paper-sunken);
    border-color: var(--color-rule);
    color: var(--color-ink-soft);
}

.cl-alert--accent > svg { color: var(--color-accent); }

/* ═══════════════════════════════════════════════════════════
   PAGE GRIDS — asymmetric on purpose. Equal thirds is the tell.
   ═══════════════════════════════════════════════════════════ */

.cl-split {
    display: grid;
    grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
    gap: var(--space-lg);
    align-items: start;
}

.cl-split--even {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}

/*
 * Panels packed into columns, each starting where the one above it ended.
 *
 * A two-column grid is only ever right when both halves are the same height,
 * and dashboard panels never are: a five-bar funnel beside a one-line quota
 * leaves a column of nothing, and a panel with no partner leaves half a row.
 * A column set has no rows to leave empty — the flow simply continues.
 *
 * `columns: 26rem 2` rather than a count plus a media query: it takes two
 * columns when there is room for two 26rem ones and one when there is not, so
 * the breakpoint is the content width itself rather than a viewport guess that
 * has to be re-guessed every time the rail changes. Two at most, because these
 * panels hold charts and a third column starves them.
 *
 * `break-inside: avoid` is what keeps a panel whole; without it a column break
 * lands mid-panel and cuts a card in half down the page.
 */
.cl-mason {
    columns: 26rem 2;
    column-gap: var(--space-lg);

    /*
     * No margin of its own. The panels' spacing hangs off their top edges and a
     * column set truncates a leading margin at the start of each column
     * fragment, so the first panel in every column contributes nothing and the
     * stack's own 24px is the only thing between this section and its
     * neighbours.
     *
     * Both of the obvious alternatives were measured and both are wrong.
     * margin-bottom on the panels plus a negative margin-bottom here — the
     * usual masonry trick — leaves 16px before the next section rather than 24:
     * a trailing margin is truncated at a column break too, so the negative
     * value eats real space instead of leftover. Keeping the leading margins
     * and cancelling one here closes the gap above to 0 for the same reason,
     * since there is no leading margin left to cancel.
     */
}

.cl-mason > * {
    break-inside: avoid;
    margin-top: var(--space-lg);
}

/*
 * The first panel carries no leading margin, because in one-column mode nothing
 * takes it away.
 *
 * At two columns a column set truncates the leading margin at the start of each
 * fragment, so the panels heading both columns contribute nothing and the gap
 * above the section is the stack's own 24px. At one column there is no
 * fragmentation to truncate anything, the first panel keeps its 24px, and it
 * lands on top of the stack's — measured at 48px against 24 everywhere else.
 * Zeroing it explicitly is a no-op in the two-column case and the fix in the
 * one-column case.
 */
.cl-mason > :first-child { margin-top: 0; }

/* The stat-box gap rather than the section gap, for a split that belongs to the
   row above it rather than starting a new band. Named like .cl-stack--tight,
   which does the same job vertically. */

.cl-stack {
    display: grid;
    gap: var(--space-lg);
}

/*
 * Grid items default to min-width: auto, which means a child refuses to
 * shrink below its own min-content width. One wide data table inside a
 * panel inside a stack is therefore enough to push the entire page wider
 * than the viewport — the table's own overflow-x never gets a chance to
 * engage, because its ancestors grew to fit it instead of clipping it.
 *
 * This one line is what keeps .cl-scroll working on a phone.
 */
.cl-stack > * { min-width: 0; }

.cl-stack--tight { gap: var(--space-md); }

@media (max-width: 63.99rem) {
    .cl-split,
    .cl-split--even { grid-template-columns: minmax(0, 1fr); }
}

/* ═══════════════════════════════════════════════════════════
   PIPELINE CARDS — a list of runs, not a grid of tiles
   ═══════════════════════════════════════════════════════════ */

.cl-run {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-sm) var(--space-md);
    padding: var(--space-md);
    border-bottom: 1px solid var(--color-rule-soft);
    text-decoration: none;
    color: inherit;
    transition: background-color var(--dur-short) var(--ease-out);
}

.cl-run:last-child { border-bottom: 0; }
.cl-run:hover { background: var(--color-paper-sunken); }

.cl-run__name {
    font-family: var(--font-display);
    font-size: 14.5px;
    font-weight: 600;
    letter-spacing: -0.02em;
}

.cl-run__meta {
    margin-top: 2px;
    font-size: 11.5px;
    color: var(--color-muted);
}

.cl-run__end {
    display: flex;
    align-items: center;
    gap: var(--space-md);
}

.cl-run__stat { text-align: end; }

.cl-run__stat-value {
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1.2;
}

.cl-run__stat-label {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-muted);
}

@media (max-width: 40rem) {
    .cl-run { grid-template-columns: minmax(0, 1fr); }
    .cl-run__end { justify-content: flex-start; }
}

/* ═══════════════════════════════════════════════════════════
   BOOKINGS
   ═══════════════════════════════════════════════════════════ */

.cl-booking {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-sm) var(--space-md);
    border-bottom: 1px solid var(--color-rule-soft);
}

.cl-booking:last-child { border-bottom: 0; }

.cl-booking__when {
    text-align: center;
    padding-inline-end: var(--space-md);
    border-inline-end: 1px solid var(--color-rule-soft);
    min-width: 52px;
}

.cl-booking__day {
    font-family: var(--font-display);
    font-size: 19px;
    font-weight: 700;
    line-height: 1;
    font-variant-numeric: tabular-nums;
}

.cl-booking__month {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--color-muted);
    margin-top: 2px;
}

.cl-booking__who { font-size: 13px; font-weight: 500; min-width: 0; }
.cl-booking__meta { font-size: 11.5px; color: var(--color-muted); margin-top: 1px; }

@media (max-width: 40rem) {
    .cl-booking { grid-template-columns: auto minmax(0, 1fr); }
    .cl-booking > :last-child { grid-column: 2; }
}

/* ═══════════════════════════════════════════════════════════
   NOTIFICATIONS
   ═══════════════════════════════════════════════════════════ */

.cl-notice {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-sm) var(--space-md);
    border-bottom: 1px solid var(--color-rule-soft);
    text-decoration: none;
    color: inherit;
    transition: background-color var(--dur-short) var(--ease-out);
}

.cl-notice:last-child { border-bottom: 0; }
.cl-notice:hover { background: var(--color-paper-sunken); }

.cl-notice__mark { flex-shrink: 0; }

.cl-notice__body { min-width: 0; }

.cl-notice__title {
    display: block;
    font-size: 13px;
    font-weight: 500;
}

.cl-notice__meta {
    display: block;
    margin-top: 1px;
    font-size: 11.5px;
    color: var(--color-muted);
}

.cl-notice__when {
    font-size: 11px;
    color: var(--color-muted);
    white-space: nowrap;
}

@media (max-width: 40rem) {
    .cl-notice { grid-template-columns: auto minmax(0, 1fr); }
    .cl-notice__when { grid-column: 2; }
}

/* ═══════════════════════════════════════════════════════════
   SWITCH — a preference, not a form field
   ═══════════════════════════════════════════════════════════ */

.cl-toggle-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-lg);
}

.cl-toggle-row__title {
    font-size: 13.5px;
    font-weight: 600;
}

.cl-toggle-row__note {
    margin-top: var(--space-2xs);
    font-size: 12px;
    line-height: 1.55;
    color: var(--color-muted);
    max-width: 62ch;
}

.cl-switch {
    position: relative;
    display: inline-flex;
    align-items: center;
    /* The track and its label were touching: the switch is an inline-flex row
       and had no gap, so "Send a second message later" started against the
       edge of the toggle and read as part of it. */
    gap: var(--space-xs);
    flex-shrink: 0;
    cursor: pointer;
}

/*
 * The words beside the switch.
 *
 * This class was in the markup with no rule behind it, so the label inherited
 * whatever the panel head happened to be setting — which next to a bold title
 * meant it read at the same weight as the heading it was explaining.
 */
.cl-switch__label {
    color: var(--color-ink-soft);
    font-size: 13.5px;
    font-weight: 500;
    line-height: 1.4;
}

/* In flow at zero size — an absolutely positioned checked input makes the
   browser scroll to it on activation. */
.cl-switch input {
    width: 0;
    height: 0;
    opacity: 0;
    margin: 0;
    appearance: none;
}

/* But as a flex item it still has the row's gap after it, so a switch whose
   input comes first sat 8px in from the edge its heading and note line up
   on. The margin takes that gap back. */
.cl-switch > input:first-child { margin-inline-end: calc(var(--space-xs) * -1); }

.cl-switch__track {
    display: block;
    width: 42px;
    height: 24px;
    padding: 3px;
    border-radius: var(--radius-pill);
    background: var(--color-paper-sunken);
    border: 1px solid var(--color-rule);
    transition: background-color var(--dur-short) var(--ease-out),
                border-color var(--dur-short) var(--ease-out);
}

.cl-switch__thumb {
    position: relative;
    display: block;
    width: 16px;
    height: 16px;
    border-radius: var(--radius-pill);
    background: var(--color-paper-raised);
    box-shadow: var(--shadow-rest);
    /* transform, not a margin — a layout property would reflow per frame. */
    transition: transform var(--dur-short) var(--ease-out);
}

.cl-switch:has(input:checked) .cl-switch__track {
    background: var(--color-accent);
    border-color: var(--color-accent);
}

.cl-switch:has(input:checked) .cl-switch__thumb {
    transform: translateX(18px);
}

/* translateX has no logical form. Mirrored, the thumb travels the other
   way along its track. */
[dir="rtl"] .cl-switch:has(input:checked) .cl-switch__thumb {
    transform: translateX(-18px);
}

.cl-switch:has(input:focus-visible) .cl-switch__track {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

/* A switch whose precondition is not met. Dimmed and inert, but still readable —
   the note beside it explains what to do, and a control the reader cannot see is
   a control they cannot act on. */
.cl-switch.is-disabled { cursor: not-allowed; opacity: .55; }
.cl-switch.is-disabled .cl-switch__label { color: var(--color-muted); }



.cl-switch:has(input:disabled) {
    opacity: 0.55;
    cursor: not-allowed;
}

/* The same compact feature header is used in both message builders. */
.cl-toggle-panel__head {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: var(--space-sm);
}

.cl-toggle-panel__copy {
    display: grid;
    min-width: 0;
    gap: var(--space-2xs);
}

.cl-toggle-panel__copy .cl-panel__note {
    max-width: 64ch;
    margin: 0;
    line-height: 1.5;
}

.cl-toggle-panel__control {
    width: auto;
    min-width: 0;
    min-block-size: var(--size-hit);
    max-width: 100%;
    flex-shrink: 1;
    justify-self: start;
}

.cl-toggle-panel__control .cl-switch__track {
    flex: 0 0 42px;
}

.cl-toggle-panel__control .cl-switch__label {
    min-width: 0;
    overflow-wrap: break-word;
}

/* Optional wizard features -------------------------------------------------
 * One segmented choice group, not three cards. The native checkbox keeps each
 * complete segment as its accessible target; layout alone changes by width. */
.cl-feature-chooser {
    display: grid;
    gap: var(--space-sm);
    margin-block-start: var(--space-lg);
    padding-block-start: var(--space-lg);
    border-block-start: 1px solid var(--color-rule);
    container: feature-chooser / inline-size;
}

.cl-feature-chooser__head {
    display: grid;
    max-width: 64ch;
    gap: var(--space-3xs);
}

.cl-feature-chooser__title {
    margin: 0;
    color: var(--color-ink);
    font-size: 15px;
    font-weight: 700;
    line-height: 1.35;
}

.cl-feature-chooser__note {
    margin: 0;
    color: var(--color-muted);
    font-size: 13px;
    line-height: 1.5;
}

.cl-feature-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    min-width: 0;
    max-width: 100%;
    gap: 0;
    overflow: clip;
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-md);
    background: var(--color-paper-raised);
}

.cl-feature-option {
    position: relative;
    isolation: isolate;
    display: grid;
    grid-template-columns: 4.5rem minmax(0, 1fr) auto;
    grid-template-areas: "art copy toggle";
    align-items: center;
    min-width: 0;
    width: 100%;
    max-width: 100%;
    min-height: 5rem;
    gap: var(--space-xs);
    padding: var(--space-xs) var(--space-sm);
    color: var(--color-ink);
    background: var(--color-paper-raised);
    border: 0;
    border-radius: 0;
    cursor: pointer;
    transition: background-color var(--dur-short) var(--ease-out);
}

.cl-feature-option:not([style*="display: none"]) ~ .cl-feature-option:not([style*="display: none"]) {
    border-block-start: 1px solid var(--color-rule);
}

.cl-feature-option.is-selected {
    background: var(--color-accent-wash);
    box-shadow: inset 0 0 0 1px var(--color-accent);
}

.cl-feature-option.is-disabled {
    opacity: 0.58;
    cursor: not-allowed;
}

.cl-feature-option:not(.is-disabled):active {
    background: var(--color-paper-2);
}

/* An unavailable switch must not fade its actionable connection help. */
.cl-feature-option--setup.is-disabled {
    opacity: 1;
}

.cl-feature-option--setup.is-disabled .cl-feature-option__art,
.cl-feature-option--setup.is-disabled .cl-feature-option__toggle {
    opacity: .58;
}

.cl-feature-option__setup {
    justify-self: start;
    margin-top: var(--space-3xs);
    max-width: 100%;
    white-space: normal;
}

.cl-feature-option.is-active:not(.is-disabled) {
    background: var(--color-paper-2);
}

.cl-feature-option[data-state='loading'] {
    cursor: progress;
    background: var(--color-paper-2);
}

.cl-feature-option[data-state='error'] {
    background: var(--color-halt-wash);
    box-shadow: inset 0 0 0 1px var(--color-halt-line);
}

.cl-feature-option[data-state='success'] {
    background: var(--color-live-wash);
    box-shadow: inset 0 0 0 1px var(--color-live-line);
}

.cl-feature-option__input {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

.cl-feature-option:has(.cl-feature-option__input:focus-visible),
.cl-feature-option.is-focus {
    outline: 2px solid var(--color-accent);
    outline-offset: -2px;
}

.cl-feature-option__art {
    grid-area: art;
    display: grid;
    place-items: center;
    inline-size: 4.5rem;
    block-size: 3.25rem;
    min-width: 0;
    overflow: clip;
    contain: paint;
    background: transparent;
    border: 0;
}

.cl-feature-visual {
    display: block;
    width: min(100%, calc(var(--space-2xl) * 5));
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    aspect-ratio: 80 / 27;
    object-fit: contain;
    overflow: hidden;
}

/* The diagrams describe a directional sequence. Mirror their geometry in RTL,
   then counter-mirror the few embedded labels so glyphs remain readable. */
[dir="rtl"] .cl-feature-visual {
    transform: scaleX(-1);
    transform-box: fill-box;
    transform-origin: center;
}

[dir="rtl"] .cl-feature-visual text {
    transform: scaleX(-1);
    transform-box: fill-box;
    transform-origin: center;
}

.cl-feature-visual__device {
    fill: var(--color-paper-raised);
    stroke: var(--color-ink-soft);
    stroke-width: 2.25;
    stroke-linejoin: round;
}

.cl-feature-visual__screen {
    fill: var(--color-paper-sunken);
    stroke: var(--color-rule-track);
    stroke-width: 1.25;
}

.cl-feature-visual__message {
    fill: var(--color-paper-raised);
    stroke: var(--color-rule-track);
    stroke-width: 1.25;
}

.cl-feature-visual__accent-fill {
    fill: var(--color-accent);
}

.cl-feature-visual__accent-wash {
    fill: var(--color-accent-wash);
    stroke: var(--color-accent-line);
    stroke-width: 1.25;
}

.cl-feature-visual__danger-fill {
    fill: var(--color-halt);
}

.cl-feature-visual__stage {
    fill: var(--color-accent-wash);
    stroke: var(--color-accent-line);
    stroke-width: 1;
}

.cl-feature-visual__stage-label,
.cl-feature-visual__micro-label {
    fill: var(--color-ink-soft);
    font-family: inherit;
    font-size: 8px;
    font-weight: 750;
}

.cl-feature-visual__stage-label,
.cl-feature-visual__micro-label--accent {
    fill: var(--color-accent);
}

.cl-feature-visual__micro-label {
    font-size: 6.5px;
    letter-spacing: -0.02em;
}

.cl-feature-visual__line,
.cl-feature-visual__grid,
.cl-feature-visual__ticks,
.cl-feature-visual__accent-stroke,
.cl-feature-visual__flow,
.cl-feature-visual__arrow,
.cl-feature-visual__bar,
.cl-feature-visual__check,
.cl-feature-visual__cross,
.cl-feature-visual__sim-chip {
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.cl-feature-visual__line,
.cl-feature-visual__grid,
.cl-feature-visual__ticks {
    stroke: var(--color-muted-soft);
    stroke-width: 2;
}

.cl-feature-visual__grid {
    stroke: var(--color-rule-track);
    stroke-width: 1;
}

.cl-feature-visual__line--strong {
    stroke: var(--color-ink-soft);
    stroke-width: 2.5;
}

.cl-feature-visual__accent-stroke,
.cl-feature-visual__flow,
.cl-feature-visual__arrow {
    stroke: var(--color-accent);
    stroke-width: 2.5;
}

.cl-feature-visual__accent-stroke--strong {
    stroke-width: 3;
}

.cl-feature-visual__flow {
    stroke-dasharray: 5 7;
    animation: cl-feature-flow 3.2s linear infinite;
}

.cl-feature-visual__bar {
    stroke: var(--color-accent);
    stroke-width: 5;
}

.cl-feature-visual__check,
.cl-feature-visual__cross {
    stroke: var(--color-accent-ink);
    stroke-width: 1.7;
}

.cl-feature-visual__sim-chip {
    stroke: var(--color-accent);
    stroke-width: 1.5;
}

.cl-feature-visual__pulse,
.cl-feature-visual__status {
    transform-box: fill-box;
    transform-origin: center;
    animation: cl-feature-pulse 3s var(--ease-out) infinite;
}

.cl-feature-visual__status--two { animation-delay: 400ms; }
.cl-feature-visual__status--three { animation-delay: 800ms; }
.cl-feature-visual__pulse--late { animation-delay: 1.1s; }

.cl-feature-visual__clock-hand {
    transform-box: view-box;
    transform-origin: 160px 48px;
    animation: cl-feature-clock 4.8s var(--ease-out) infinite alternate;
}

.cl-feature-visual__object--report,
.cl-feature-visual__object--later,
.cl-feature-visual__object--sms {
    transform-box: fill-box;
    transform-origin: center;
    animation: cl-feature-arrive 4.2s var(--ease-out) infinite;
}

/* The fallback diagram is an explanation, not ambient decoration. Keeping its
   three states still makes the failed WhatsApp → wait → SMS path readable. */
.cl-feature-visual__flow--static {
    animation: none;
}

@keyframes cl-feature-flow {
    to { stroke-dashoffset: -24; }
}

@keyframes cl-feature-pulse {
    0%, 55%, 100% { opacity: 0.76; transform: scale(0.94); }
    20% { opacity: 1; transform: scale(1.08); }
}

@keyframes cl-feature-clock {
    0%, 20% { transform: rotate(-18deg); }
    70%, 100% { transform: rotate(42deg); }
}

@keyframes cl-feature-arrive {
    0%, 12% { opacity: 0.84; transform: translateY(2px); }
    28%, 100% { opacity: 1; transform: translateY(0); }
}

.cl-feature-option__copy {
    grid-area: copy;
    display: grid;
    min-width: 0;
    gap: var(--space-3xs);
}

.cl-feature-option__copy strong {
    font-size: 13.5px;
    font-weight: 650;
    line-height: 1.35;
}

.cl-feature-option__copy small {
    color: var(--color-muted);
    font-size: 11.5px;
    line-height: 1.45;
    overflow-wrap: break-word;
}

.cl-feature-option__copy .cl-feature-option__unavailable {
    color: var(--color-halt);
}

.cl-feature-option__toggle {
    grid-area: toggle;
    display: block;
    align-self: center;
}

.cl-feature-option__input:checked ~ .cl-feature-option__toggle .cl-switch__track {
    background: var(--color-accent);
    border-color: var(--color-accent);
}

.cl-feature-option__input:checked ~ .cl-feature-option__toggle .cl-switch__thumb {
    transform: translateX(18px);
}

[dir="rtl"] .cl-feature-option__input:checked ~ .cl-feature-option__toggle .cl-switch__thumb {
    transform: translateX(-18px);
}

.cl-feature-option[data-state='loading'] .cl-switch__track {
    background: var(--color-accent-wash);
    border-color: var(--color-accent-line);
}

.cl-feature-option[data-state='loading'] .cl-switch__thumb::after {
    content: "";
    position: absolute;
    inset: 3px;
    border: 2px solid var(--color-rule-track);
    border-block-start-color: var(--color-accent);
    border-radius: var(--radius-pill);
    animation: cl-feature-option-loading 800ms linear infinite;
}

.cl-feature-option[data-state='error'] .cl-switch__track {
    background: var(--color-halt-wash);
    border-color: var(--color-halt-line);
}

.cl-feature-option[data-state='error'] .cl-switch__thumb::after,
.cl-feature-option[data-state='success'] .cl-switch__thumb::after {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    font-size: 11px;
    font-weight: 800;
    line-height: 1;
}

.cl-feature-option[data-state='error'] .cl-switch__thumb::after {
    content: "!";
    color: var(--color-halt);
}

.cl-feature-option[data-state='success'] .cl-switch__track {
    background: var(--color-live-wash);
    border-color: var(--color-live-line);
}

.cl-feature-option[data-state='success'] .cl-switch__thumb::after {
    content: "✓";
    color: var(--color-live);
}

@keyframes cl-feature-option-loading {
    to { transform: rotate(1turn); }
}

.cl-feature-option.is-hover:not(.is-disabled):not([data-state='loading']):not([data-state='error']):not([data-state='success']) {
    background: var(--color-paper-sunken);
}

@media (hover: hover) and (pointer: fine) {
    .cl-feature-option:not(.is-disabled):not([data-state='loading']):not([data-state='error']):not([data-state='success']):hover {
        background: var(--color-paper-sunken);
    }
}

@media (min-width: 48rem) {
    .cl-feature-grid {
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
    }

    .cl-feature-option {
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas:
            "art art"
            "copy toggle";
        grid-template-rows: 5.75rem auto;
        align-items: start;
        min-height: 10.25rem;
        gap: var(--space-xs) var(--space-sm);
        padding: var(--space-sm);
    }

    .cl-feature-option:not([style*="display: none"]) ~ .cl-feature-option:not([style*="display: none"]) {
        border-block-start: 0;
        border-inline-start: 1px solid var(--color-rule);
    }

    .cl-feature-option__art {
        inline-size: 100%;
        block-size: 5.75rem;
    }

    .cl-feature-option__toggle {
        align-self: start;
        margin-block-start: var(--space-3xs);
    }
}

/* Auto Broadcast keeps all optional paths in one compact, scannable row when
   its content column has room. The breakpoint follows the chooser itself, not
   the viewport, because the portal rail can materially change usable width. */
.cl-feature-grid--compact {
    inline-size: min(100%, calc(var(--space-3xl) * 12));
    gap: var(--space-xs);
    overflow: visible;
    border: 0;
    border-radius: 0;
    background: transparent;
}

.cl-feature-option.cl-feature-option--compact {
    grid-template-columns: 3.5rem minmax(0, 1fr) auto;
    grid-template-areas: "art copy toggle";
    grid-template-rows: auto;
    align-items: center;
    min-height: 5.25rem;
    max-inline-size: calc(var(--space-3xl) * 4);
    gap: var(--space-xs);
    padding: var(--space-xs) var(--space-sm);
    border: var(--rule-hair) solid var(--color-rule);
    border-radius: var(--radius-md);
}

.cl-feature-grid--compact .cl-feature-option:not([style*="display: none"]) ~ .cl-feature-option:not([style*="display: none"]) {
    border-block-start: var(--rule-hair) solid var(--color-rule);
    border-inline-start: var(--rule-hair) solid var(--color-rule);
}

.cl-feature-option--compact .cl-feature-option__art {
    justify-self: start;
    inline-size: 3.5rem;
    block-size: 3.25rem;
}

.cl-feature-option--compact .cl-feature-option__toggle {
    align-self: center;
    margin-block-start: 0;
}

@container feature-chooser (min-width: 34rem) {
    .cl-feature-grid--compact {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@container feature-chooser (min-width: 44rem) {
    .cl-feature-grid--compact {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media (prefers-reduced-motion: reduce) {
    .cl-feature-visual__flow,
    .cl-feature-visual__pulse,
    .cl-feature-visual__status,
    .cl-feature-visual__clock-hand,
    .cl-feature-visual__object--report,
    .cl-feature-visual__object--later,
    .cl-feature-visual__object--sms,
    .cl-feature-option[data-state='loading'] .cl-switch__thumb::after {
        animation: none;
    }
}

.cl-feature-step {
    container: feature-step / inline-size;
}

/* The step itself is the only card. Results, Follow-up and SMS keep their
   shared panel structure for behaviour, but do not draw a second card inside
   that surface. */
.cl-feature-step > .cl-toggle-panel {
    inline-size: 100%;
    margin: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
}

.cl-feature-step > .cl-toggle-panel > .cl-toggle-panel__head {
    padding: 0 0 var(--space-sm);
}

.cl-feature-step > .cl-toggle-panel.is-collapsed > .cl-toggle-panel__head {
    padding-block-end: 0;
}

.cl-feature-step > .cl-toggle-panel > .cl-panel__body {
    padding: var(--space-md) 0 0;
}

.cl-feature-step .cl-toggle-panel__control .cl-switch__label {
    white-space: nowrap;
    overflow-wrap: normal;
}

@container feature-step (min-width: 40rem) {
    .cl-toggle-panel__head {
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: center;
    }

    .cl-toggle-panel__control {
        justify-self: end;
    }
}

@media (max-width: 40rem) {
    .cl-toggle-row { flex-direction: column; gap: var(--space-sm); }
}

/* ═══════════════════════════════════════════════════════════
   MOTION 3 of 3 — one orchestrated entrance, once, on load
   ═══════════════════════════════════════════════════════════
   After this the page is simply there. Nothing re-animates on scroll.

   The sections rise far enough to read as arriving — 6px was below what
   anybody notices — and settle with the house ease-out, one stagger step
   apart. The page-to-page handover in components/motion.css fades the old
   page out over the top of this, so the two read as one movement.

   `backwards`, not `both`. A fill that outlives the animation leaves a
   transform and an opacity applied to the section for the life of the page,
   which makes it a containing block for anything `position: fixed` inside it
   and a stacking context for everything else — the trap the lightbox, the
   wizard and the stat-box help each had to be rescued from. `backwards`
   still holds the first frame through the stagger delay, then lets go.
*/

.cl-enter {
    animation: cl-rise var(--dur-enter) var(--ease-out) backwards;
}

.cl-enter-2 { animation-delay: calc(var(--dur-stagger) * 1); }
.cl-enter-3 { animation-delay: calc(var(--dur-stagger) * 2); }
.cl-enter-4 { animation-delay: calc(var(--dur-stagger) * 3); }

@keyframes cl-rise {
    from { opacity: 0; transform: translateY(16px) scale(0.985); }
    to { opacity: 1; transform: none; }
}

/* ═══════════════════════════════════════════════════════════
   REDUCED MOTION — spatial motion collapses, state survives
   ═══════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    /* The running stage still has to read as running — it keeps its
       accent fill, it just stops breathing. */
    .cl-stage.is-running .cl-stage__dot {
        animation: none;
        opacity: 1;
    }
}

/* ═══════════════════════════════════════════════════════════
   UTILITIES — small, named, and few
   ═══════════════════════════════════════════════════════════ */

.cl-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}


/* ── Utility shims (the cloned markup uses a handful of tailwind utils) ── */
.mb-6 { margin-bottom: 1.5rem; }
.w-full { width: 100%; }
.inline-flex { display: inline-flex; }
.items-center { align-items: center; }
.gap-1\.5 { gap: 0.375rem; }

/* ═══════════════════════════════════════════════════════════════════════
   AUTH — sign in / create account

   Same surface language as the portal: warm-cool paper, one centred panel
   on the soft hero shadow, pill controls, DM Sans body with a Space Grotesk
   title. The panel is the only thing on the page, so it — not a rail —
   carries the elevation.
   ═══════════════════════════════════════════════════════════════════════ */

.cl-auth {
    min-height: 100vh;
    display: grid;
    place-items: center;
    padding: var(--space-lg) var(--space-md);
    background: var(--color-paper);
}

.cl-auth__panel {
    width: 100%;
    max-width: 420px;
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-soft);
    padding: var(--space-xl) var(--space-lg) var(--space-lg);
}

.cl-auth__brand {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    margin-bottom: var(--space-lg);
}

.cl-auth__wordmark {
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 700;
    letter-spacing: -0.02em;
}

.cl-auth__title {
    font-family: var(--font-display);
    font-size: 24px;
    font-weight: 700;
    letter-spacing: -0.03em;
    line-height: 1.15;
}

.cl-auth__lede {
    margin-top: var(--space-2xs);
    font-size: 13px;
    color: var(--color-muted);
}

.cl-auth__form {
    display: grid;
    gap: var(--space-md);
    margin-top: var(--space-lg);
}

.cl-auth__label {
    display: block;
    margin-bottom: var(--space-2xs);
    font-size: 12.5px;
    font-weight: 500;
    color: var(--color-ink-soft);
}

/* One control shape across the portal: pill, 1px border in every state.
   Border-width never changes on focus — that would shift the row by a pixel. */
.cl-auth__input {
    width: 100%;
    min-height: 44px;
    padding: 0 var(--space-sm);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule);
    /* Soft rectangle — see the note on .cl-dt__input. */
    border-radius: var(--radius-sm);
    font-family: var(--font-sans);
    font-size: 14px;
    color: var(--color-ink);
    transition: border-color var(--dur-short) var(--ease-out);
}

.cl-auth__input::placeholder { color: var(--color-muted); }
.cl-auth__input:hover { border-color: var(--color-rule-track); }

/* The ring appears instantly — a ring that fades in leaves keyboard users
   with no indicator for the length of the transition. */
.cl-auth__input:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
    border-color: var(--color-accent);
}

.cl-auth__input[aria-invalid="true"] { border-color: var(--color-halt); }

/* Errors are text, never a red border alone. */
.cl-auth__error {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    margin-top: var(--space-2xs);
    font-size: 12.5px;
    color: var(--color-halt);
}

.cl-auth__hint {
    margin-top: var(--space-2xs);
    font-size: 12px;
    color: var(--color-muted);
}

.cl-auth__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    flex-wrap: wrap;
}

.cl-auth__check {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 12.5px;
    color: var(--color-ink-soft);
}

.cl-auth__check input { accent-color: var(--color-accent); width: 15px; height: 15px; }

.cl-auth__submit { width: 100%; }

.cl-auth__foot {
    margin-top: var(--space-lg);
    padding-top: var(--space-md);
    border-top: 1px solid var(--color-rule-soft);
    font-size: 13px;
    color: var(--color-muted);
    text-align: center;
}

.cl-auth__foot a {
    color: var(--color-accent);
    font-weight: 500;
    text-decoration: none;
}

.cl-auth__foot a:hover { text-decoration: underline; }

.cl-auth__sep {
    display: block;
    margin-top: var(--space-lg);
    font-size: 11.5px;
    color: var(--color-muted-soft);
    text-align: center;
}

/* ═══════════════════════════════════════════════════════════════════════
   DASHBOARD — stat boxes, sparklines, overview chart, cost table

   Added for parity with the Vue dashboard. Charts are inline SVG whose
   geometry is computed server-side in SeriesData, so there is no charting
   library and no API call.
   ═══════════════════════════════════════════════════════════════════════ */

.cl-statboxes {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr));
    gap: var(--space-md);
}

.cl-statbox {
    position: relative;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-sm);
    min-width: 0;
    padding: var(--space-md);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-rest);
    /* Not `overflow: hidden` — the sparkline's tooltip has to be able to
       leave the box. The warm rail below is rounded on its own instead. */
}

.cl-statbox__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.cl-statbox__label {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3xs, 4px);
    font-size: 12px;
    color: var(--color-muted);
}

/*
    "How would you know that?" — answered beside the figure that claims it.

    The bubble is positioned against .cl-statbox rather than against the icon,
    so it hangs from the card's own edge instead of from wherever in the label
    the icon happens to land. That works because .cl-statbox is the nearest
    positioned ancestor and, deliberately, is not overflow:hidden — the same
    property the sparkline's tooltip relies on.
*/
.cl-help { display: inline-flex; }

.cl-help__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: none;
    color: var(--color-muted);
    cursor: pointer;
    /* A 12px glyph is under the 24px minimum a finger can reliably hit, so the
       button keeps its target and the icon just sits in the middle of it. */
    inline-size: 24px;
    block-size: 24px;
    margin-inline: -6px;
    border-radius: var(--radius-sm);
}
.cl-help__btn svg { inline-size: 13px; block-size: 13px; }

/* 24px is a pointer's minimum, not a thumb's. On a phone the target grows to
   40px around the same glyph — invisibly, so the label beside it does not
   move. Relative on the button only: the bubble is its sibling and still
   hangs from the card. */
@media (max-width: 40rem) {
    .cl-help__btn { position: relative; }

    .cl-help__btn::after {
        content: "";
        position: absolute;
        inset: -8px;
    }
}
.cl-help__btn:hover { color: var(--color-ink-2); }
.cl-help__btn:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 1px;
    color: var(--color-ink-2);
}

.cl-help__bubble {
    position: absolute;
    /* Inside the card's own layer — the card is what rises, below. A higher
       value here would be trapped by it and buy nothing. */
    z-index: var(--z-raised);
    inline-size: max-content;
    max-inline-size: min(300px, 80vw);
    padding: var(--space-sm);
    font-size: 11.5px;
    line-height: 1.5;
    text-align: start;
    /*
        Dark, where the rest of the portal is paper.
        
        A tooltip is not part of the page it explains — it is a thing that
        appeared over it, and the inversion says so before a word is read. It
        also settles the layering visually: a pale panel on a pale card needs
        its border to be believed, a dark one does not.
    */
    color: var(--color-paper-raised);
    background: var(--color-ink);
    border: 0;
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-lift, var(--shadow-rest));
}

/*
    Above the card by preference — it opens into the gap between rows rather
    than over the row underneath. A card in the top row has no gap above it, so
    the component measures on open and flips; see place() in stat-boxes.
*/
.cl-help__bubble--above { inset-block-end: calc(100% + 6px); }
.cl-help__bubble--below { inset-block-start: calc(100% + 6px); }

/*
    Which edge it hangs from, decided by measurement rather than by position in
    the DOM. `:last-child` was the first answer and it was wrong: in an
    `auto-fit` grid the last card is the rightmost one only when the row is full,
    so a card mid-row threw its bubble leftward out of the content area.
*/
.cl-help__bubble--start { inset-inline-start: 0; }
.cl-help__bubble--end { inset-inline-end: 0; }

/*
    Lift the whole card while its bubble is open.

    The card, not the bubble, and above the rail rather than merely above its
    neighbours. Three things sit over this bubble otherwise: the sibling cards,
    which paint after it in tree order; the sticky page chrome, which the bubble
    now opens into since it hangs above the row; and the rail, at --z-sticky.
    Giving the card a z-index makes it a stacking context, so whatever the
    bubble asks for inside is capped by the card's own level — which is why the
    lift belongs here and has to clear --z-sticky in one step.

    Only while open, so a resting dashboard keeps the flat stacking it had.

    Keyed on `aria-expanded`, which the button already publishes for assistive
    tech, so the state has one source rather than a class mirroring a binding.
*/
.cl-statbox:has(.cl-help__btn[aria-expanded="true"]) {
    z-index: calc(var(--z-sticky) + 1);
}

/*
    And lift the row, not only the card inside it.

    `.cl-statboxes` is rendered with `.cl-enter`, whose `animation: … both`
    leaves an animation applying transform and opacity — which makes the row a
    stacking context, so the card's z-index above is resolved *within the row*
    and a later sibling row paints over the whole thing regardless. This is the
    lift that actually clears the row below; the one above orders the cards
    within it.
*/
.cl-statboxes:has(.cl-help__btn[aria-expanded="true"]) {
    position: relative;
    z-index: calc(var(--z-sticky) + 1);
}

.cl-statbox__value {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-size: 26px;
    font-weight: 700;
    letter-spacing: -0.04em;
    line-height: 1.05;
}

/* The audience figures. Warm marks "who you reached" so it reads apart from
   the green delivery figures; --color-warm-ink rather than --color-warm
   because this is text and has to clear 4.5:1 (it sits at 5.21:1). */
.cl-statbox--warm .cl-statbox__value { color: var(--color-warm-ink); }

/* The chart's tooltip is absolutely positioned inside this wrapper, so the
   wrapper must not clip it and the box has to stack above its neighbours
   while it is showing. */
.cl-statbox__spark { overflow: visible; }
.cl-statbox:hover { z-index: var(--z-raised); }
.cl-statbox .apexcharts-canvas { overflow: visible; }
.cl-statbox .apexcharts-tooltip { z-index: var(--z-dropdown); }

/*
 * The audience figures carry a warm edge, drawn as the card's own leading border.
 *
 * Not as a pseudo-element. A separate 3px bar cannot follow a 20px corner: run
 * it full height and the corners curve away from it at both ends, inset it and
 * it either still crosses the curve or floats free of the edge it belongs to.
 * A border has no such problem — border-radius shapes it by construction, so
 * the colour turns the corner exactly as the card does.
 *
 * Every box carries the 3px leading border, so they all share one geometry and
 * the content sits on the same line across the row; only the colour changes.
 * On the others it is the same hairline colour as the rest of their border,
 * which is why it reads as no marker at all.
 */
.cl-statbox {
    border-inline-start-width: 3px;
    /* The delivery figures, and their green sparkline. */
    border-inline-start-color: var(--color-live);
}

/*
 * The audience figures. --color-warm-ink rather than --color-warm: the lighter
 * brand orange sits at 2.49:1 on white, which is fine for a wash but too faint
 * for a mark someone is meant to read as a category. This is also the exact
 * colour of the value above it and of the line beside it, so the three agree.
 */
.cl-statbox--warm { border-inline-start-color: var(--color-warm-ink); }

/* Something that needs attention rather than reading — an account with no
   subscription. Same rail, same grammar, third colour. */
.cl-statbox--warn { border-inline-start-color: var(--color-warn); }

/*
 * A card carrying prose and an action instead of a number and a sparkline.
 *
 * The full row, not two columns of it. At one column the sentence wraps to four
 * lines and the button drops beneath it; at two the card is the right shape but
 * the row it lands on keeps a hole beside it — seven figures and a two-wide
 * card never divide evenly into a grid whose column count changes with the
 * viewport, so there is always a gap, and it is always at the end where it
 * reads as something failed to load.
 *
 * Spanning the row removes the arithmetic. It also suits the content: badge and
 * sentence at one end, the action at the other, which is what the card already
 * lays out and what the extra width finally gives it room to do.
 */
.cl-statbox--wide { grid-column: 1 / -1; }

/* The badge line. Its own rule rather than .cl-statbox__value, which is 28px
   display type sized for a numeral and would swallow a pill whole. */
.cl-statbox__state { display: flex; margin: 2px 0; }

/*
 * The offer bar: subscription, on an account that has none.
 *
 * ── Why a bar and not a card ────────────────────────────────────────────
 * It spans the whole row, so shaped like a card it is 160px of mostly nothing
 * before the reader reaches a single figure. One line of content, centred, with
 * the action at the far end: it leads the page without costing it a screenful.
 *
 * ── Why ink and not brand green ─────────────────────────────────────────
 * Green was tried three ways — wash, flat and gradient — and every one of them
 * lost to the page. This dashboard is already green: the rails on six cards,
 * the sparklines, the "New message" button, the links. One more green surface
 * is more of the same thing, however dark it is, and a gradient only makes it a
 * more elaborate version of the same thing.
 *
 * Ink is the one value the page does not already use as a surface, so the bar
 * separates from everything around it without a second colour entering the
 * scheme. It also frees the accent: on a near-black ground the brand green can
 * be the button, which is the one place on this bar attention is actually
 * wanted. Dark surface, light words, one green control — the contrast does the
 * work that decoration was failing to do.
 */
.cl-offer {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    border-radius: var(--radius-lg);
    background: var(--color-ink);
    color: var(--color-paper-raised);
    box-shadow: var(--shadow-rest);
}

/* The mark does the work three lines of copy would otherwise have to. */
.cl-offer__mark {
    flex: none;
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border-radius: var(--radius-pill);
    /* Green, on the one dark surface where green is not competing with six
       other greens for the eye. */
    background: color-mix(in oklab, var(--color-accent) 30%, transparent);
    color: color-mix(in oklab, var(--color-accent) 55%, var(--color-paper-raised));
}

.cl-offer__mark svg { width: 18px; height: 18px; }

/*
 * Headline and detail on one line where there is room, two where there is not.
 * `baseline` rather than `center`, so two type sizes sit on one line instead of
 * floating against each other's midpoints.
 */
.cl-offer__text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 2px var(--space-xs);
}

.cl-offer__title {
    font-family: var(--font-display);
    font-size: 17px;
    font-weight: 700;
    letter-spacing: -0.01em;
    line-height: 1.2;
}

/* Steps back from the headline by lightness rather than by a second colour —
   one ink, two weights of it. Held at 70% of white on this ground, which clears
   4.5:1 comfortably; below about 55% it does not. */
.cl-offer__note {
    font-size: 12.5px;
    line-height: 1.4;
    color: color-mix(in oklab, var(--color-paper-raised) 70%, var(--color-ink));
}

/*
 * The action is the only green on the bar, which is the point of putting the
 * bar on ink: nothing else here competes with it.
 */
.cl-offer__cta {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    min-height: 36px;
    padding: 0 var(--space-md);
    border-radius: var(--radius-pill);
    background: var(--color-accent);
    color: var(--color-accent-ink);
    font-family: var(--font-sans);
    font-size: 13px;
    font-weight: 700;
    white-space: nowrap;
    text-decoration: none;
    transition: transform var(--dur-short) var(--ease-out),
                background-color var(--dur-short) var(--ease-out);
}

.cl-offer__cta svg { width: 15px; height: 15px; }

/* The chevron points along the reading direction, so it turns with it. */
[dir='rtl'] .cl-offer__cta svg { transform: scaleX(-1); }

.cl-offer__cta:hover {
    background: color-mix(in oklab, var(--color-accent) 82%, var(--color-paper-raised));
    transform: translateY(-1px);
}

.cl-offer__cta:active { transform: translateY(0); }

/*
 * At phone width the bar stops being a row.
 *
 * Side by side in a 335px column the headline wraps to two lines and the
 * sentence to four, all against a button holding 120px of the width hostage.
 * Stacked, the words get the full column and the button sits under them where a
 * thumb expects it.
 */
@media (max-width: 40rem) {
    .cl-offer {
        flex-wrap: wrap;
        padding: var(--space-md);
    }

    .cl-offer__text { flex-basis: 100%; }
    .cl-offer__cta { width: 100%; justify-content: center; }
}

/* The account-wide qualifier, when a single number is selected. On its own line
   so it reads as a footnote to the sentence rather than as part of it.

   No opacity. It inherits --color-muted, which is chosen to clear 4.5:1 on
   paper and has nothing to spare — 0.85 of it lands at 4.4:1, which is a
   footnote nobody with low vision can read. Position carries "secondary" here;
   the colour is already as light as it is allowed to be. */
.cl-statbox__aside { display: block; }

.cl-statbox__note {
    font-size: 11.5px;
    color: var(--color-muted);
    line-height: 1.35;
    overflow-wrap: anywhere;
    white-space: normal;
}

/* ── Charts (ApexCharts) ──────────────────────────────────────────────
   The library draws the marks; these rules only place its canvas and calm
   its default chrome so it sits inside a portal panel. */

/* The trim is logical even though the chart is not. `dir="ltr"` sits on the
   canvas inside this wrapper, not on the wrapper — ApexCharts keeps drawing
   left to right, while the wrapper still swaps sides with the card's flex row.
   So the 8px it gives back belongs to whichever edge it has landed against. */
.cl-statbox__spark { flex-shrink: 0; margin-inline-end: -8px; }

.cl-chart .apexcharts-tooltip {
    border: 1px solid var(--color-rule) !important;
    border-radius: var(--radius-sm) !important;
    /* --shadow-md was never defined, so this declaration was invalid and the
       tooltip floated over the chart with no elevation at all. --shadow-rest
       is the small-object shadow; --shadow-soft is sized for a sheet. */
    box-shadow: var(--shadow-rest) !important;
}

.cl-chart .apexcharts-tooltip-title {
    background: var(--color-paper-sunken) !important;
    border-bottom: 1px solid var(--color-rule-soft) !important;
    font-weight: 600 !important;
}

.apexcharts-legend-text { color: var(--color-muted) !important; }

/* ── State panels (subscription, best send time, account) ─────────────── */

.cl-state { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-xs); }
.cl-state__note { font-size: 12.5px; color: var(--color-muted); }

.cl-state__figure {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-size: 28px;
    font-weight: 700;
    letter-spacing: -0.04em;
    line-height: 1;
}

.cl-state__figure--sm { font-size: 18px; letter-spacing: -0.02em; }

/* ── Cost table ───────────────────────────────────────────────────────── */

.cl-table-wrap { width: 100%; max-width: 100%; overflow-x: auto; }
/*
 * A numeric column's heading, ranged with the figures under it.
 *
 * Qualified past the element selector on purpose. `.cl-table th` above sets
 * `text-align: start` and outranks a lone class — 0,1,1 against 0,1,0 — so this
 * modifier has never once moved a heading since it was written. Every numeric
 * column in the portal has been rendering its label hard left and its figures
 * hard right: the Auto Broadcast list, the broadcast contacts table, the
 * pricing bands and the cost table all carry the class and none of them obeyed
 * it.
 *
 * The `td` half is belt and braces — `.cl-table__num` already ends its cells —
 * so a column that ranges its heading ranges its body too, whichever class it
 * happens to use.
 */
.cl-table th.cl-table__right,
.cl-table td.cl-table__right { text-align: end; }

.cl-table__num {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    text-align: end;
}

.cl-sr-only {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

/* x-cloak keeps the readout hidden until Alpine has initialised, so the
   month tooltip never flashes on first paint. */
[x-cloak] { display: none !important; }

/* ═══════════════════════════════════════════════════════════════════════
   DATATABLE — simple-datatables, themed

   The library owns the behaviour and emits its own toolbar and pager; these
   rules dress that markup in the portal's language rather than replacing it.
   ═══════════════════════════════════════════════════════════════════════ */

.cl-dt {
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-rest);
    overflow: hidden;

    /*
     * Never shrink. A table is as tall as its rows.
     *
     * `.cl-main` is the page's scroller *and* a flex column, so a table placed
     * directly in it is a flex item. `overflow: hidden` above makes this a
     * scroll container, and the flexbox rule is that a scroll container's
     * automatic minimum size is 0 rather than its content — so it was free to
     * be squashed to whatever space was left, and being `overflow: hidden` it
     * then clipped its own rows. `.cl-main` meanwhile saw nothing overflowing
     * and reported scrollHeight === clientHeight, so the page would not scroll
     * to them either. Twenty-five rows rendered, eight reachable.
     *
     * It only ever showed on the pages that put .cl-dt straight into .cl-main
     * — broadcasts and the message log. Contacts and SMS nest theirs inside a
     * wrapper, which is the flex item instead, which is why those two looked
     * fine and made this hard to see.
     */
    flex: 0 0 auto;
}

/* A table that is a panel's body — the calendar's booking activity — is
   already inside a card. With its own border a pixel inside the panel's it
   drew a double edge down both sides and a second set of corners under the
   panel's heading, and on a phone the row cards made a third. The panel is
   the frame; the table keeps only its toolbar and footer rules. Where it
   ends the panel it still clips its rows to the panel's rounded foot. */
.cl-panel > .cl-dt {
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
}

.cl-panel > .cl-dt:last-child {
    border-end-start-radius: calc(var(--radius-lg) - 1px);
    border-end-end-radius: calc(var(--radius-lg) - 1px);
}

/* ── Toolbar (top) and footer (bottom) ────────────────────────────────── */

.cl-dt__top,
.cl-dt__bottom {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
}

.cl-dt__top { border-bottom: 1px solid var(--color-rule-soft); }
.cl-dt__bottom { border-top: 1px solid var(--color-rule-soft); }

/* Nothing to page through. A search that matches nothing leaves Grid.js's
   pager holding only its two arrows, both disabled, and no count above them:
   a strip of empty footer with "‹" and "›" adrift in it, under a sentence
   that already says there is nothing here. The footer goes with them. A
   pager with even one page number keeps it. */
.cl-dt__bottom:not(:has(.gridjs-summary)):has(.gridjs-pages > :last-child:nth-child(2)) {
    display: none;
}

.cl-dt__input,
.gridjs-search-input {
    min-height: 38px;
    min-width: 240px;
    padding: 0 var(--space-sm);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule);
    /* Soft rectangle, not a pill. Capsule-shaped text fields read as 2015;
       controls that hold text want a radius that matches the panels around
       them. Buttons keep the pill — that is the brand's shape, not the
       form's. */
    border-radius: var(--radius-sm);
    font-family: var(--font-sans);
    font-size: 13px;
    color: var(--color-ink);
}

.cl-dt__input::placeholder,
.gridjs-search-input::placeholder { color: var(--color-muted); }
.cl-dt__input:focus-visible,
.gridjs-search-input:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.cl-dt__select {
    min-height: 34px;
    padding: 0 var(--space-xs);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    font-family: var(--font-sans);
    font-size: 13px;
    color: var(--color-ink);
    cursor: pointer;
}

.cl-dt__summary { font-size: 12px; color: var(--color-muted); }

/* The table scrolls inside its own frame — the page never scrolls sideways. */
.cl-dt__container { width: 100%; overflow-x: auto; }

/* ── Grid.js structure ────────────────────────────────────────────────── */

/* Grid.js ships no styles of its own here — its theme is deliberately not
   imported, so the portal's own table styling is the only styling. These rules
   map the library's structural classes onto it. */

/* Numeric columns align to the trailing edge. Carried as a data attribute
   rather than a class, so it lands on both the header and the cell without
   displacing Grid.js's own classes. */

/*
 * Counted columns are centred — header and values together.
 *
 * They used to be end-aligned, which lines digits up but strands a one-digit
 * value at the far right of a long header: the "1" under DELIVERED sat against
 * the word's last letter and the two did not read as a pair, even though their
 * right edges matched to the pixel. These columns hold small counts rather than
 * amounts being compared down the column, so reading as a pair is worth more
 * than the digits queueing up.
 *
 * The sort caret is pulled out of the centring with a negative end margin. It
 * is an inline 6px triangle with a 5px margin sitting after the label, so
 * without this the *label plus caret* is what gets centred and the label itself
 * sits eleven pixels left of the numbers under it. `gridjs-th-sort` is a class
 * on the <th> itself, not on a wrapper inside it.
 */
.cl-table [data-align='center'] { text-align: center; }

.cl-table th[data-align='center'] .gridjs-th-content { display: inline-block; }

.cl-table th[data-align='center'].gridjs-th-sort .gridjs-sort {
    margin-inline-end: -11px;
}

.gridjs-wrapper {
    width: 100%;
    /* The table scrolls inside its own frame; the page never scrolls sideways. */
    overflow-x: auto;
    overscroll-behavior-x: contain;
}

.gridjs-table { width: 100%; border-collapse: collapse; }

/*
 * A table wider than the phone says that it is.
 *
 * The frame scrolls sideways, and a phone draws no scrollbar until it is
 * already being scrolled — so the broadcasts table ended exactly at the edge
 * of the screen after ID and NAME and read as a two-column table, while
 * contacts and the message log cut the phone number through its digits, which
 * reads as the layout breaking rather than as more columns. The edge with
 * more beyond it fades out instead, driven by the frame's own scroll
 * position: the trailing edge at the start, both in the middle, the leading
 * edge at the end, and neither when the table fits (an inactive timeline
 * applies nothing). The same fade the landing's journey strip uses.
 *
 * The cost table and the other plain wrappers scroll the same way and get the
 * same edge. Not the card tables: they do not scroll on a phone at all.
 *
 * Only where scroll-driven animation exists; elsewhere the table is as before.
 */
@property --cl-dt-fade-start {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}

@property --cl-dt-fade-end {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}

@keyframes cl-dt-edge-fade {
    0%   { --cl-dt-fade-start: 0px;  --cl-dt-fade-end: 40px; }
    6%   { --cl-dt-fade-start: 40px; }
    94%  { --cl-dt-fade-end: 40px; }
    100% { --cl-dt-fade-start: 40px; --cl-dt-fade-end: 0px; }
}

@supports (animation-timeline: scroll()) {
    @media (max-width: 40rem) {
        .cl-dt:not(.cl-dt--cards) .gridjs-wrapper,
        .cl-table-wrap {
            --cl-dt-fade-to: right;
            -webkit-mask-image: linear-gradient(to var(--cl-dt-fade-to), transparent, #000 var(--cl-dt-fade-start), #000 calc(100% - var(--cl-dt-fade-end)), transparent);
            mask-image: linear-gradient(to var(--cl-dt-fade-to), transparent, #000 var(--cl-dt-fade-start), #000 calc(100% - var(--cl-dt-fade-end)), transparent);
            animation: cl-dt-edge-fade linear both;
            animation-timeline: scroll(self inline);
        }

        /* A gradient's direction is physical, and the scroll starts at the
           right in Arabic: the start of the timeline is the right-hand edge. */
        [dir="rtl"] .cl-dt:not(.cl-dt--cards) .gridjs-wrapper,
        [dir="rtl"] .cl-table-wrap { --cl-dt-fade-to: left; }
    }
}

/* The search box sits in the toolbar rather than filling it. */
.gridjs-search { flex: 0 1 auto; }

/* Tabular figures so "1 – 15" and "16 – 30" occupy the same space. */
.gridjs-summary {
    font-size: 12px;
    color: var(--color-muted);
    font-variant-numeric: tabular-nums;
}

/* ── Loading skeleton ─────────────────────────────────────────────────────
   The same shimmer the Vue tables use (components/shimmer/SkeletonTable.vue):
   a sweeping gradient over placeholder bars, so the table keeps its shape
   while a page is in flight instead of collapsing and jumping the layout.

   Grid.js does not put a class on the container while loading, so the
   component adds `is-loading` itself. */

.cl-dt__skeleton { width: 100%; overflow-x: hidden; }

.cl-dt__skeleton td { padding: var(--space-xs) var(--space-sm); }

/* On a phone the skeleton keeps two columns. Every column of a ten-column
   table squeezed into 330px left each bar a few pixels wide, so the loading
   table was a field of grey hairlines — more like a broken page than one on
   its way. Two bars a row is the shape a phone shows either way: the first
   columns of a table that scrolls, or label and value in a card. */
@media (max-width: 40rem) {
    .cl-dt__skeleton td:nth-child(n + 3) { display: none; }
    .cl-dt__skeleton td:first-child { width: 38%; }
}

.cl-skeleton-bar {
    display: block;
    height: 20px;
    margin: 5px 0;
    border-radius: var(--radius-xs);
    background: linear-gradient(
        90deg,
        var(--color-paper-sunken) 25%,
        var(--color-rule) 50%,
        var(--color-paper-sunken) 75%
    );
    background-size: 200% 100%;
    /* Eased rather than the browser's default curve, which lurched at each
       end of the sweep; slower, so it reads as waiting, not as alarm. */
    animation: cl-shimmer 1.8s var(--ease-in-out) infinite;
}

@keyframes cl-shimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* background-position has no logical form, so the sweep is reversed rather
   than restated — it should run with the text, not against it. */
[dir="rtl"] .cl-skeleton-bar { animation-direction: reverse; }

/* The skeleton and the table swap places. The skeleton is a sibling of the
   grid, never inside it — Grid.js diffs its own DOM and will fight anything
   inserted there. */
.cl-dt:not(.is-loading) .cl-dt__skeleton { display: none; }
.cl-dt.is-loading .gridjs-wrapper,
.cl-dt.is-loading .gridjs-footer { visibility: hidden; }
.cl-dt.is-loading .cl-dt__mount { height: 0; overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
    .cl-skeleton-bar { animation: none; }
}

/* ── Sortable headers ─────────────────────────────────────────────────── */

.gridjs-th-sort { cursor: pointer; user-select: none; }
.gridjs-th-sort:hover { color: var(--color-ink); }

.gridjs-th-sort .gridjs-th-content { display: inline-block; }

/* A quiet caret that only shows on the sorted column, rather than the
   library's default arrows on every header. */
.gridjs-sort {
    display: inline-block;
    width: 0;
    height: 0;
    margin-inline-start: 5px;
    border: 3px solid transparent;
    background: none;
    padding: 0;
    cursor: pointer;
    vertical-align: middle;
}

.gridjs-sort-neutral { border-top-color: var(--color-muted-soft); opacity: 0; }
.gridjs-th-sort:hover .gridjs-sort-neutral { opacity: 0.5; }

.gridjs-sort-asc {
    border-top-color: transparent;
    border-bottom-color: var(--color-accent);
    opacity: 1;
}

.gridjs-sort-desc { border-top-color: var(--color-accent); opacity: 1; }

.gridjs-th:has(.gridjs-sort-asc),
.gridjs-th:has(.gridjs-sort-desc) { color: var(--color-accent); }

/* ── Pager ────────────────────────────────────────────────────────────── */

/* Grid.js's pager grows a second ellipsis as soon as you leave the first few
   pages — "‹ 1 2 3 … 65 ›" becomes "‹ 1 … 3 4 5 … 65 ›" — which moved the whole
   row sideways every time someone paged. Reserving the widest arrangement
   keeps it still. Nine slots is the most the truncating pager produces at the
   default pagerDelta of 2; the width is derived so it stays correct if the
   button size changes. */
.cl-pager {
    /* The reserved slot is wider than a button so a two- or three-digit page
       number cannot push the row past the width set aside for it — that was a
       1px drift in the arrows every time the last page number grew a digit. */
    --pager-slot: 34px;
    --pager-gap: 2px;
    --pager-slots: 9;

    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

.cl-pager .gridjs-pages {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--pager-gap);
    min-width: calc(
        var(--pager-slots) * var(--pager-slot) +
        (var(--pager-slots) - 1) * var(--pager-gap)
    );
}

/* Previous and next are pinned to the ends of that reserved width, so the two
   controls someone actually aims at never move as the page numbers between
   them change shape. */
.cl-pager .gridjs-pages > :first-child { margin-inline-end: auto; }
.cl-pager .gridjs-pages > :last-child { margin-inline-start: auto; }

.cl-pager__page {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 30px;
    height: 30px;
    padding: 0 var(--space-xs);
    border: 0;
    border-radius: var(--radius-pill);
    background: none;
    font-family: var(--font-sans);
    font-size: 12.5px;
    color: var(--color-ink-soft);
    cursor: pointer;
}

.cl-pager__page:hover:not(:disabled) { background: var(--color-paper-sunken); }

.cl-pager__page.is-current {
    background: var(--color-accent);
    color: var(--color-accent-ink);
    font-weight: 600;
}

.cl-pager__page:disabled { color: var(--color-muted-soft); cursor: default; }

.cl-pager__page:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* ── The pager on a phone ─────────────────────────────────────────────────
   The reservation above is right on a desktop and unaffordable below it. Nine
   slots is 322px, which is wider than the whole viewport at 320 and wider than
   the table card at every phone width up to about 480 — so "next page" was
   laid out at x=393 on a 312px screen and could only be reached by scrolling
   the table sideways. On a screen whose entire content is a paged list, the
   control that turns the page was off it.

   The reservation is dropped rather than shrunk. Grid.js renders seven buttons
   here, about 250px, which fits inside the card at 320 unaided; reserving room
   for an arrangement that may never appear is what pushed it off. If a ninth
   slot does appear, the row now wraps instead of overflowing.

   Prev and next keep the full 44px target. They are the two controls a thumb
   aims at — the numbers are a convenience, and giving all seven 44px would
   need 308px that a 320px screen does not have. */
@media (max-width: 29.99rem) {
    .cl-pager {
        flex-wrap: wrap;
        row-gap: var(--space-xs);
        justify-content: center;
    }

    .cl-pager .gridjs-pages {
        min-width: 0;
        width: 100%;
        flex-wrap: wrap;
        row-gap: var(--space-2xs);
    }

    /* The pager is the footer's only child, and a flex item sized to its
       content: the 100% above was 100% of the count line, not of the card. A
       long count ("Showing 1 – 15 of 260 messages") hid that by filling the
       row; a short one ("Showing 1 – 2 of 2 rows") left "›" stranded
       mid-card with the page number off-centre. */
    .cl-dt__bottom > .cl-pager { flex-grow: 1; }
}

/* Prev and next reach the 44px this sheet already asks of every other control
   below 40rem (see .cl-btn at 2008). Only those two: seven 44px buttons need
   308px, which a 320px screen does not have, and the numbers are a
   convenience — these two are what a thumb actually aims at. They stay pinned
   to the ends of the row by the auto margins above. */
@media (max-width: 40rem) {
    .cl-pager .gridjs-pages > :first-child,
    .cl-pager .gridjs-pages > :last-child {
        min-width: 44px;
        height: 44px;
    }

    /*
     * Fit the busiest arrangement on one line.
     *
     * Away from the first pages the pager reads "‹ 1 … 4 5 6 … 18 ›", which
     * was 314px against the 294 the card has at 360 — so "›" wrapped onto a
     * line of its own under the numbers, the one control a thumb is aiming
     * for, stranded in the corner. The ellipses are not buttons anyone can
     * press and need only the width of the glyph, and the numbers give up
     * the side padding a two-digit page never needed: 290px, with room for a
     * three-digit page count.
     *
     * The numbers keep their 30px disc, which is what marks the current page,
     * and take the row's height as an invisible target: 40px to aim at.
     */
    .cl-pager__page {
        position: relative;
        padding-inline: var(--space-2xs);
    }

    .cl-pager__page::after {
        content: "";
        position: absolute;
        inset: -5px 0;
    }

    .cl-pager .gridjs-spread { min-width: 18px; }

    /* One page, nothing to turn: both arrows are disabled, and pinned to the
       ends of a full-width row they were two faint 6px marks in the corners
       of the footer that read as stray glyphs. They keep their room, so the
       page number stays centred and the footer keeps its height. */
    .cl-pager .gridjs-pages:has(> :first-child:disabled):has(> :last-child:disabled) > :disabled {
        visibility: hidden;
    }
}

/* Hover only where there is a pointer to hover with. On a touch screen the
   tapped arrow kept its grey disc after the next page had loaded, as if it
   were still pressed. */
@media (hover: none) {
    .cl-pager__page:hover:not(:disabled):not(.is-current) { background: none; }
}


/* ── Dense rows ───────────────────────────────────────────────────────── */

.cl-table--dense { font-size: 12.5px; white-space: nowrap; }
.cl-table--dense th,
.cl-table--dense td { padding: var(--space-xs) var(--space-sm); }

.cl-table__muted { color: var(--color-muted); }

/* A cell that points at what it names — Credit activity's "What for". Reads as
   a link only on approach: a table of underlines is a table nobody can scan. */
.cl-table__link {
    color: var(--color-ink-2);
    text-decoration: none;
    border-block-end: 1px solid var(--color-rule-track);
}
.cl-table__link:hover,
.cl-table__link:focus-visible {
    color: var(--color-accent);
    border-block-end-color: currentColor;
}
.cl-table__mono { font-family: var(--font-display); font-variant-numeric: tabular-nums; }

/* Stage icons belong to the headings; rows keep only the receipt time or
   waiting text so long recipient lists stay easy to scan. */
.cl-delivery-heading {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    white-space: nowrap;
}
.cl-delivery-heading svg { width: 15px; height: 15px; flex: 0 0 15px; }

.cl-delivery {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding-block: 2px;
    white-space: nowrap;
    color: var(--color-muted);
}

.cl-delivery__dot {
    flex: 0 0 6px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-accent);
}

.cl-delivery__detail { font-size: 13px; line-height: 1.4; font-variant-numeric: tabular-nums; }
.cl-delivery--recorded .cl-delivery__detail { color: var(--color-ink-soft); }

/* A short public handle that copies its complete UUID. It is a genuine button
   for keyboard access, but reads like a compact table value until approached. */
.cl-table__uuid {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 4px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--color-muted);
    font: inherit;
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    cursor: copy;
}

.cl-table__uuid svg {
    width: 12px;
    height: 12px;
    opacity: 0;
    transition: opacity var(--dur-short) var(--ease-out);
}

.cl-table__uuid:hover,
.cl-table__uuid:focus-visible {
    color: var(--color-accent);
}

.cl-table__uuid:hover svg,
.cl-table__uuid:focus-visible svg { opacity: 1; }

.cl-table__uuid:focus-visible {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.cl-table__name {
    display: block;
    max-width: 26ch;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: 500;
    color: var(--color-ink);
}

.cl-table__sub {
    display: block;
    max-width: 22ch;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 11px;
    color: var(--color-muted);
}

.cl-table__sub--strong { color: var(--color-ink-soft); font-size: 12px; }

/* Long free text: one clipped line, the whole value in the tooltip. Clipped
   here rather than in PHP so the full string stays selectable and searchable. */
.cl-table__clip {
    display: block;
    max-width: 30ch;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ── Opt-in table cards on a phone ───────────────────────────────────────
   Dense operational tables keep the usual contained horizontal scroller.
   A short five-column history can opt into cards instead: the same Grid.js
   component, search, sort, loading and actions, with each column label repeated
   beside its value. `data-label` is emitted only when `mobileCards` is true. */

.cl-dt--cards .cl-dt__top,
.cl-dt--cards .cl-dt__bottom {
    align-items: stretch;
}

.cl-dt--cards .gridjs-search,
.cl-dt--cards .gridjs-search-input {
    min-inline-size: 0;
    inline-size: 100%;
}

.cl-dt--cards .cl-dt__container,
.cl-dt--cards .gridjs-wrapper {
    overflow-x: clip;
}

.cl-dt--cards .gridjs-table,
.cl-dt--cards .gridjs-tbody {
    display: block;
    inline-size: 100%;
}

.cl-dt--cards .gridjs-thead {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.cl-dt--cards .gridjs-tbody {
    display: grid;
    gap: var(--space-sm);
    padding: var(--space-sm);
}

.cl-dt--cards .gridjs-tbody .gridjs-tr {
    display: grid;
    padding: var(--space-sm);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-input);
    background: var(--color-paper-raised);
}

.cl-dt--cards .gridjs-tbody .gridjs-td {
    display: grid;
    grid-template-columns: minmax(6.5rem, 0.45fr) minmax(0, 1fr);
    gap: var(--space-sm);
    min-inline-size: 0;
    padding: var(--space-xs) 0;
    border-block-end: 1px solid var(--color-rule-soft);
    text-align: start;
    white-space: normal;
}

.cl-dt--cards .gridjs-tbody .gridjs-td::before {
    content: attr(data-label);
    color: var(--color-muted);
    font-size: var(--text-xs);
    font-weight: 600;
}

/* Phone only, like the caps below: it outranks the desktop block's cell
   border, so above 40rem it took the rule out from under the last column of
   every row and the table read as one column short of a grid. */
@media (max-width: 40rem) {
    .cl-dt--cards .gridjs-tbody .gridjs-td:last-child {
        border-block-end: 0;
    }
}

/* Phone only. Unlike the rules around it this one had no counterpart in the
   desktop block below, so a table that opted into cards lost its 26ch / 22ch
   / 30ch caps on a desktop too: names and notes ran to full length there and
   pushed every column after them sideways — no longer "the same table" from
   40rem up. */
@media (max-width: 40rem) {
    .cl-dt--cards .cl-table__clip,
    .cl-dt--cards .cl-table__sub,
    .cl-dt--cards .cl-table__name {
        max-inline-size: 100%;
        min-inline-size: 0;
    }

    /* A card cell wraps, but the clip kept its ellipsis, so a value with a
       run longer than the column broke at its punctuation and each line that
       still overflowed was cut on its own: "https://example.com/sprin…" over
       "utm_source=whatsapp&u…" over "2026". The long run breaks inside itself
       instead and the value reads whole, like the message bodies beside it —
       a phone has no hover to show the rest. */
    .cl-dt--cards .cl-table__clip { overflow-wrap: anywhere; }
}

.cl-dt--cards .cl-table__progress {
    min-inline-size: 0;
}

.cl-dt--cards .cl-table__progress-head {
    align-items: stretch;
    flex-direction: column;
    min-inline-size: 0;
}

.cl-dt--cards .cl-table__progress-detail {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    min-inline-size: 0;
}

.cl-dt--cards .cl-table__progress-detail > span {
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cl-dt--cards .cl-table__actions {
    justify-content: flex-start;
}

/* The empty and error rows hold one sentence across every column and carry
   no label, so as a card they drew an empty label column and pushed "No
   contacts match your search" into the right-hand half of a bordered box that
   looked like a blank record. One centred line, no box. Phone only: above
   40rem the row is a table row again and keeps its own alignment. */
@media (max-width: 40rem) {
    .cl-dt--cards .gridjs-tbody .gridjs-tr:has(.gridjs-message) {
        border-color: transparent;
        background: transparent;
    }

    .cl-dt--cards .gridjs-tbody .gridjs-message {
        display: block;
        text-align: center;
        color: var(--color-muted);
    }

    .cl-dt--cards .gridjs-tbody .gridjs-message::before { content: none; }

    /* The copyable ID is a button with 4px of padding round its text, which
       in a card set it 4px in from every other value in the column. Pulled
       back so the text lines up and the hover wash still has its margin. */
    .cl-dt--cards .cl-table__uuid { margin-inline-start: -4px; }

    /* It is also the first control in every card, and 23px tall. The target
       grows to 37px around it, inside the cell's own padding, so the ID still
       reads as a value and the card keeps its height. */
    .cl-dt--cards .cl-table__uuid { position: relative; }

    .cl-dt--cards .cl-table__uuid::after {
        content: "";
        position: absolute;
        inset: -7px 0;
    }

    /* Row actions are 44px targets round a 14px icon, so the first icon sat
       15px in from every other value in the column, and below its label.
       The group is pulled back by the difference and the label centred on
       it. */
    .cl-dt--cards .cl-table__actions { margin-inline-start: calc((14px - 44px) / 2); }
    .cl-dt--cards .gridjs-tbody .gridjs-td:has(.cl-table__actions) { align-items: center; }
}

/*
 * The card in Arabic: every label on the right.
 *
 * Each cell is dir="auto" (datatable.js says why), and as a card the cell is
 * the grid that holds the label and the value, so the grid followed the value:
 * a cell whose value had no Arabic in it — a phone number, an ID, a date, a
 * Latin name — put its label on the left, and the card zig-zagged down its
 * rows. The cell takes the page's direction for its layout; the value keeps
 * the direction its own text gave it, so "+212 643 600 683" and "10% off
 * (Summer)" still read the right way round, and it sits against its label
 * like an Arabic value does. Row actions have no text to take a direction
 * from and follow the page.
 *
 * Only where :dir() is understood. Without it the second rule cannot undo the
 * first, and the values would be reordered rather than the labels moved.
 */
@supports selector(:dir(ltr)) {
    @media (max-width: 40rem) {
        [dir="rtl"] .cl-dt--cards .gridjs-tbody .gridjs-td { direction: rtl; }

        [dir="rtl"] .cl-dt--cards .gridjs-tbody .gridjs-td:dir(ltr):not(:has(.cl-table__actions)) > * {
            direction: ltr;
            justify-self: start;
            /* The label's side, in a box that runs left to right. */
            text-align: end;
        }
    }
}

@media (min-width: 40.0625rem) {
    .cl-dt--cards .cl-dt__top,
    .cl-dt--cards .cl-dt__bottom {
        align-items: center;
    }

    .cl-dt--cards .gridjs-search { inline-size: auto; }

    .cl-dt--cards .gridjs-search-input {
        min-inline-size: 240px;
        inline-size: auto;
    }

    .cl-dt--cards .cl-dt__container,
    .cl-dt--cards .gridjs-wrapper {
        overflow-x: auto;
    }

    .cl-dt--cards .gridjs-table { display: table; }

    .cl-dt--cards .gridjs-thead {
        position: static;
        inline-size: auto;
        block-size: auto;
        margin: 0;
        overflow: visible;
        clip-path: none;
        white-space: normal;
    }

    .cl-dt--cards .gridjs-tbody {
        display: table-row-group;
        padding: 0;
    }

    .cl-dt--cards .gridjs-tbody .gridjs-tr {
        display: table-row;
        padding: 0;
        border: 0;
        border-radius: 0;
        background: transparent;
    }

    /* The reset above outranks the table's row hover, so a card table lost
       it on a desktop. Restated, so it is the same table there in use too. */
    .cl-dt--cards .gridjs-tbody .gridjs-tr:hover { background: var(--color-paper-sunken); }

    .cl-dt--cards .gridjs-tbody .gridjs-td {
        display: table-cell;
        padding: var(--space-xs) var(--space-sm);
        border-block-end: 1px solid var(--color-rule-soft);
        text-align: start;
        white-space: nowrap;
    }

    .cl-dt--cards .gridjs-tbody .gridjs-td[data-align='center'] {
        text-align: center;
    }

    /* The plain table's last row has no rule under it (the footer draws its
       own); the card rule above outranks that one, so it is restated here. */
    .cl-dt--cards .gridjs-tbody .gridjs-tr:last-child .gridjs-td {
        border-block-end: 0;
    }

    .cl-dt--cards .gridjs-tbody .gridjs-td::before { display: none; }

    .cl-dt--cards .cl-table__progress { min-inline-size: 9rem; }

    .cl-dt--cards .cl-table__progress-head {
        align-items: center;
        flex-direction: row;
    }

    .cl-dt--cards .cl-table__progress-detail { display: flex; }

    .cl-dt--cards .cl-table__progress-detail {
        flex: 1 1 0;
        justify-content: flex-end;
        overflow: hidden;
    }

    .cl-dt--cards .cl-table__actions { justify-content: center; }
}

/* A row's merge data, as pairs rather than as raw JSON. The row stays one line
   tall — the pairs scroll sideways inside the cell instead of growing it, so a
   contact carrying eight fields does not make every other row tall to match. */
/*
 * The data cell.
 *
 * Still a compact strip — the column is one of seven and cannot be the width
 * of the widest reply anybody has ever sent. What changed is that it no longer
 * pretends to be the whole answer: it is focusable, it says how many fields it
 * is holding, and hovering or focusing it opens the tooltip with all of them.
 *
 * It used to scroll horizontally with `scrollbar-width: none`, so a row
 * carrying six fields showed two and gave no sign the other four existed.
 * Clipped now, with a fade at the end, because a fade reads as "more" where a
 * hard cut reads as "that is all there is".
 */
.cl-table__pairs {
    display: flex;
    gap: 4px;
    max-width: 34ch;
    overflow: hidden;
    cursor: pointer;
    border-radius: 6px;
    /* The fade. Transparent for the last few characters, so the strip trails
       off instead of ending mid-word. */
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2.5ch), transparent 100%);
    mask-image: linear-gradient(to right, #000 calc(100% - 2.5ch), transparent 100%);
}

/* A gradient's direction is physical. A strip that runs right to left starts
   at the right and runs out at the left, so the fade belongs on the left
   edge: drawn to the right it dimmed the first pair and cut the last one
   mid-word. :dir(), not [dir="rtl"] on the page: a Grid.js cell is
   dir="auto", so on an Arabic page a strip of Latin keys still runs left to
   right and keeps the fade above. */
.cl-table__pairs:dir(rtl) {
    -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 2.5ch), transparent 100%);
    mask-image: linear-gradient(to left, #000 calc(100% - 2.5ch), transparent 100%);
}

.cl-table__pairs:hover .cl-table__pair,
.cl-table__pairs:focus-visible .cl-table__pair {
    background: var(--color-paper-raised);
    border-color: var(--color-rule);
}

.cl-table__pairs:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

.cl-table__pair {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    padding: 2px 6px;
    border-radius: 5px;
    border: 1px solid transparent;
    background: var(--color-paper-sunken);
    white-space: nowrap;
    transition: background-color .12s ease, border-color .12s ease;
}

@media (prefers-reduced-motion: reduce) {
    .cl-table__pair { transition: none; }
}

.cl-table__pair-key { font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--color-muted); }
.cl-table__pair-val { font-size: 11.5px; color: var(--color-ink); }

.cl-table__actions { display: flex; align-items: center; justify-content: center; gap: 4px; }

/* ── Interaction-rate dial ────────────────────────────────────────────── */

.cl-rate { display: inline-flex; align-items: center; gap: 6px; }
.cl-rate__ring { width: 20px; height: 20px; transform: rotate(-90deg); flex-shrink: 0; }
.cl-rate__track { fill: none; stroke: var(--color-rule); stroke-width: 3; }
.cl-rate__fill { fill: none; stroke: var(--color-accent); stroke-width: 3; stroke-linecap: round; }

.cl-rate__value {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    font-size: 12px;
}

/* ── Queued-work progress ────────────────────────────────────────────────
   A shared table cell for tasks that move from queued to ready. The badge
   names the state; the bar and figures make its amount scannable without
   asking colour to carry meaning. */

.cl-table__progress {
    display: grid;
    gap: var(--space-2xs);
    min-inline-size: 9rem;
}

.cl-table__progress-head,
.cl-table__progress-detail {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
}

.cl-table__progress-head {
    justify-content: space-between;
}

.cl-table__progress-detail {
    color: var(--color-muted);
    font-family: var(--font-display);
    font-size: var(--text-2xs);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.cl-table__progress-detail b {
    color: var(--color-ink-soft);
    font-weight: 600;
}

.cl-table__progress-track {
    display: block;
    block-size: var(--space-2xs);
    overflow: hidden;
    border-radius: var(--radius-pill);
    background: var(--color-rule-track);
}

.cl-table__progress-fill {
    display: block;
    inline-size: 100%;
    block-size: 100%;
    border-radius: inherit;
    background: var(--color-accent);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--dur-mid) var(--ease-out);
}

[dir="rtl"] .cl-table__progress-fill { transform-origin: right center; }
.cl-table__progress.is-live .cl-table__progress-fill { background: var(--color-live); }
.cl-table__progress.is-warn .cl-table__progress-fill { background: var(--color-warn); }
.cl-table__progress.is-halt .cl-table__progress-fill { background: var(--color-halt); }

/* ── Row actions ──────────────────────────────────────────────────────── */

.cl-act {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: var(--radius-pill);
    color: var(--color-muted);
    background: none;
    border: 0;
    cursor: pointer;
    transition: background-color var(--dur-short) var(--ease-out),
                color var(--dur-short) var(--ease-out);
}

.cl-act svg { width: 14px; height: 14px; }

/* Hover only where there is a pointer to hover with, as on the buttons and
   the pager: on a touch screen the tapped edit or delete kept its grey disc
   after the tap, as if it were still pressed. */
@media (hover: hover) and (pointer: fine) {
    .cl-act:hover { background: var(--color-paper-sunken); color: var(--color-ink); }
}

/* ── Row actions on a phone ───────────────────────────────────────────────
   26px is comfortable for a mouse and poor for a thumb, and on the table
   screens these are the only per-row controls there are — edit, delete, open,
   download the QR. The icon keeps its 14px; only the target grows, so the rows
   do not start reading as heavier.

   This has to sit after the base rule, not with the other mobile blocks
   earlier in the sheet: same specificity, so whichever comes last wins. */
@media (max-width: 40rem) {
    .cl-act {
        width: 44px;
        height: 44px;
    }

    /* Tuned for 26px controls; at 44 they would otherwise run together. */
    .cl-table__actions { gap: var(--space-2xs); }
}
.cl-act:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }

.cl-act--live { color: var(--color-live); }
@media (hover: hover) and (pointer: fine) {
    .cl-act--halt:hover { background: var(--color-halt-wash); color: var(--color-halt); }
}
.cl-act--busy { color: var(--color-accent); cursor: default; }
.cl-act--marker { color: var(--color-muted); cursor: default; }

/* The sending indicator is the one moving thing in the table. */
.cl-act--busy svg { animation: cl-spin 1.6s linear infinite; }

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

@media (prefers-reduced-motion: reduce) {
    .cl-act--busy svg { animation: none; }
    .cl-table__progress-fill { transition-duration: var(--dur-micro); }
}

/* ── Action feedback ──────────────────────────────────────────────────── */

/* ── Alerts ───────────────────────────────────────────────────────────────
   One floating region, top right, above everything. Messages used to sit at
   the bottom of the table, which on a page of fifteen rows meant the reader
   had already scrolled past the place the news appeared. */

.cl-alerts {
    position: fixed;
    top: calc(var(--topbar-h) + var(--space-sm));
    inset-inline-end: var(--space-md);
    /* Above the rail and the sticky table header; below nothing else. */
    z-index: var(--z-toast);

    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    width: min(380px, calc(100vw - 2 * var(--space-md)));

    /* The region is always in the DOM so assistive tech is already watching
       it; it must never swallow clicks meant for the page beneath. */
    pointer-events: none;
}

.cl-alert {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    padding-block: var(--space-sm);
    /* More at the leading edge, which is where the 3px tone border is. */
    padding-inline: var(--space-md) var(--space-sm);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule);
    border-inline-start: 3px solid var(--color-muted-soft);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-soft), var(--shadow-rest);
    pointer-events: auto;
}

.cl-alert__mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 20px;
    height: 20px;
    margin-top: 1px;
    border-radius: var(--radius-pill);
}

.cl-alert__mark svg { width: 13px; height: 13px; }

.cl-alert__text {
    flex: 1;
    margin: 0;
    font-size: 13px;
    line-height: 1.45;
    color: var(--color-ink);
}

.cl-alert__close {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--color-muted);
    cursor: pointer;
}

.cl-alert__close svg { width: 14px; height: 14px; }
.cl-alert__close:hover { background: var(--color-paper-sunken); color: var(--color-ink); }
.cl-alert__close:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* Tones are the portal's, so an alert reads as part of the same system as the
   badges and the funnel rather than as a browser dialog. */
.cl-alert--live { border-inline-start-color: var(--color-live); }
.cl-alert--live .cl-alert__mark { background: var(--color-live-wash); color: var(--color-live); }

.cl-alert--halt { border-inline-start-color: var(--color-halt); }
.cl-alert--halt .cl-alert__mark { background: var(--color-halt-wash); color: var(--color-halt); }

/* Enter from the edge it lives on, and land on the spring: a toast is the
   portal answering something the reader just did, so it should arrive with
   the same energy as the press that caused it. It leaves faster than it
   came, accelerating away — nobody waits for a message to finish going. */
.cl-alert--entering {
    animation: cl-alert-in calc(var(--dur-mid) * 1.6) var(--ease-spring) both;
}

.cl-alert--leaving {
    animation: cl-alert-out var(--dur-short) var(--ease-in) both;
}

@keyframes cl-alert-in {
    from { opacity: 0; transform: translateX(28px) scale(0.94); }
    40% { opacity: 1; }
    to { opacity: 1; transform: none; }
}

@keyframes cl-alert-out {
    from { opacity: 1; }
    to { opacity: 0; transform: translateX(20px) scale(0.96); }
}

/* translateX has no logical form, and the edge it lives on moves: in Arabic
   the region sits at the left, so the slide has to arrive from the left. */
@keyframes cl-alert-in-rtl {
    from { opacity: 0; transform: translateX(-28px) scale(0.94); }
    40% { opacity: 1; }
    to { opacity: 1; transform: none; }
}

@keyframes cl-alert-out-rtl {
    from { opacity: 1; }
    to { opacity: 0; transform: translateX(-20px) scale(0.96); }
}

[dir="rtl"] .cl-alert--entering { animation-name: cl-alert-in-rtl; }
[dir="rtl"] .cl-alert--leaving { animation-name: cl-alert-out-rtl; }

@media (prefers-reduced-motion: reduce) {
    /* The mirrored pair is named too: `[dir="rtl"] .cl-alert--entering` above
       outranks a bare class, so without it the RTL animation would survive
       the opt-out. */
    .cl-alert--entering,
    .cl-alert--leaving,
    [dir="rtl"] .cl-alert--entering,
    [dir="rtl"] .cl-alert--leaving { animation: none; }
}

@media (max-width: 640px) {
    /* On a phone the rail's topbar is the only fixed chrome; the alerts sit
       just under it and span the width. */
    .cl-alerts {
        left: var(--space-sm);
        right: var(--space-sm);
        width: auto;
    }

    /* The dismiss button is 24px, which a thumb misses as often as it hits.
       The target grows around it rather than the button itself, so the alert
       keeps its height and its quiet corner mark. */
    .cl-alert__close { position: relative; }

    .cl-alert__close::after {
        content: "";
        position: absolute;
        inset: -8px;
    }
}

/* ── Tabs (broadcast / follow-up contacts) ────────────────────────────── */

.cl-tabs { display: flex; gap: 4px; margin-bottom: var(--space-md); }

.cl-tabs__tab {
    padding: var(--space-2xs) var(--space-md);
    border-radius: var(--radius-pill);
    font-size: 13px;
    font-weight: 500;
    color: var(--color-muted);
    text-decoration: none;
}

.cl-tabs__tab:hover { background: var(--color-paper-sunken); color: var(--color-ink); }

.cl-tabs__tab.is-active {
    background: var(--color-accent);
    color: var(--color-accent-ink);
}

.cl-sr-only {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

[x-cloak] { display: none !important; }


.cl-dt__input:focus-visible,
.cl-dt__select:focus-visible,
.cl-auth__input:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 1px;
    border-color: var(--color-accent);
}

.cl-dt__input:hover,
.cl-dt__select:hover { border-color: var(--color-rule-track); }


/* ── Footer ───────────────────────────────────────────────────────────────
   Mirrors the Vue footer: copyright left, page links beside it, separated by
   a hairline from the content above. */

.cl-colophon__copy { font-size: 12.5px; color: var(--color-muted); }

.cl-colophon__links {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs) var(--space-md);
}

/* One underline, drawn by text-decoration only. An earlier rule also drew a
   border-bottom here, so a hovered link showed two lines. */
.cl-colophon__links a {
    font-size: 12.5px;
    color: var(--color-ink-soft);
    text-decoration: none;
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
    transition: color var(--dur-short) var(--ease-out);
}

.cl-colophon__links a:hover {
    color: var(--color-accent);
    text-decoration: underline;
}


/* ═══════════════════════════════════════════════════════════
   NEW MESSAGE WIZARD
   ═══════════════════════════════════════════════════════════ */

/*
 * The wizard, with room either side of it.
 *
 * The canvas gives every page a gutter of clamp(1rem, 4vw, 2.5rem), which caps
 * at 40px — enough for a table that wants the width, not enough for a five-step
 * form that runs edge to edge on a wide monitor. So the whole wizard is given a
 * measure and centred: the stepper, the step card and the Back/Send bar all
 * stop at the same place, and the space appears on both sides rather than
 * being taken out of one panel.
 *
 * 76rem is measured against the widest thing inside it, the template grid on
 * step 2. That grid is four fixed tracks, so narrowing the page never costs it
 * a column, it only makes each card narrower. Measured at this width a template
 * card is 284px and its name still sets on one line.
 */
/*
 * The wizard fills the pane, so its bar can reach the floor.
 *
 * .cl-main is a flex column, so without flex: 1 the wizard is only as tall as
 * its step — and on a short step, like the language choice, the sticky bar has
 * nothing to stick to and simply sits wherever the content happens to end,
 * halfway up an empty pane. Filling the pane gives it a bottom to find.
 *
 * min-height: 0 for the usual reason: a flex child that will not shrink below
 * its content turns a long step into a pane that grows instead of scrolling.
 */
.cl-wizard {
    display: flex;
    flex-direction: column;
    /*
     * Grows to fill a short step, never shrinks below its content.
     *
     * It was `flex: 1; min-height: 0`, which let .cl-main squeeze the wizard
     * to the height of the screen while its steps overflowed the box. The
     * pane's bottom padding then sat at the end of that squeezed box, halfway
     * up the page, instead of under Back and Next — so the bar ended flush
     * with the bottom edge and, on a phone, under the browser's toolbar.
     */
    flex: 1 0 auto;
    gap: var(--space-lg);
    width: 100%;
    max-width: 76rem;
    margin-inline: auto;
}

/* The page entrance must release its transform after it finishes; otherwise
   it becomes the containing block for the mobile fixed confirmation dock. */
.cl-wizard.cl-enter { animation-fill-mode: none; }

/* ── Stepper ──────────────────────────────────────────────────────────── */

/* Small panes show the one decision that matters now. The full navigation is
   not squeezed, wrapped, or made horizontally scrollable; it returns at the
   content breakpoint where all labels have room. */
.cl-step-status {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-xs);
    min-width: 0;
    padding: var(--space-sm);
    border: var(--rule-hair) solid var(--color-rule-soft);
    border-radius: var(--radius-lg);
    background: var(--color-paper-raised);
    box-shadow: var(--shadow-rest);
}

.cl-step-status__dot {
    display: grid;
    place-items: center;
    width: calc(var(--space-lg) + var(--space-2xs));
    height: calc(var(--space-lg) + var(--space-2xs));
    border-radius: var(--radius-pill);
    background: var(--color-accent);
    color: var(--color-accent-ink);
    font-family: var(--font-display);
    font-size: var(--text-xs);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.cl-step-status__copy {
    min-width: 0;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: var(--text-sm);
    line-height: 1.2;
}

.cl-step-status__copy strong {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cl-step-status__position {
    color: var(--color-muted);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.cl-step-status__rail {
    grid-column: 1 / -1;
    display: block;
    height: var(--rule-fine);
    overflow: clip;
    border-radius: var(--radius-pill);
    background: var(--color-rule-soft);
}

.cl-step-status__fill {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: inherit;
    background: var(--color-accent);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--dur-short) var(--ease-out);
}

[dir="rtl"] .cl-step-status__fill { transform-origin: right center; }

.cl-steps {
    display: none;
    gap: var(--space-xs);
    margin: 0;
    padding: var(--space-sm);
    list-style: none;
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-rest);
}

.cl-steps__item { flex: 1 1 0; min-width: 0; }

/* What the list says while the path is still undecided — see the note on
   isStepVisible(). Dashed, because a solid rule here would read as the edge
   of a step that does not exist yet. */
.cl-steps__pending {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    min-height: 40px;
    padding-inline: var(--space-sm);
    border-inline-start: 1px dashed var(--color-rule);
    color: var(--color-muted);
    font-size: 12.5px;
    text-wrap: pretty;
}

.cl-step {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    width: 100%;
    padding: var(--space-xs) var(--space-sm);
    border: 0;
    border-radius: var(--radius-md);
    background: none;
    font-family: var(--font-sans);
    font-size: 12.5px;
    color: var(--color-muted);
    text-align: start;
    cursor: pointer;
}

.cl-step:hover:not(:disabled) { background: var(--color-paper-sunken); }
.cl-step:disabled { cursor: default; opacity: 0.55; }
.cl-step:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.cl-step__dot {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 24px;
    height: 24px;
    border: 1px solid var(--color-rule-control);
    border-radius: var(--radius-pill);
    font-size: 11px;
    font-weight: 700;
}

.cl-step__tick { width: 12px; height: 12px; display: none; }

.cl-step.is-current { color: var(--color-ink); font-weight: 600; }
.cl-step.is-current .cl-step__dot {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-accent-ink);
}

.cl-step.is-done { color: var(--color-ink-soft); }
.cl-step.is-done .cl-step__dot {
    background: var(--color-live-wash);
    border-color: var(--color-live);
    color: var(--color-live);
}
.cl-step.is-done .cl-step__num { display: none; }
.cl-step.is-done .cl-step__tick { display: block; }

.cl-step__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (min-width: 64rem) {
    .cl-step-status { display: none; }

    .cl-steps {
        display: flex;
        flex-wrap: nowrap;
    }
}

@media (prefers-reduced-motion: reduce) {
    .cl-step-status__fill { transition: none; }
}

/* ── Step panels ──────────────────────────────────────────────────────── */

/* A step is a panel, like every other surface in the portal — the content
   used to float on the page background, which is why it read as unfinished
   next to the dashboard. */
.cl-wstep {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    padding: var(--space-md);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-rest);
    /* Each step arrives rather than appearing, so moving through the wizard
       reads as movement. */
    animation: cl-step-in calc(var(--dur-enter) * 0.8) var(--ease-out) backwards;
}

/* `backwards` for the reason given at .cl-enter: a step holds pickers and
   sheets that are positioned against the viewport. */
@keyframes cl-step-in {
    from { opacity: 0; transform: translateY(14px) scale(0.99); }
    to { opacity: 1; transform: none; }
}

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

.cl-wstep__head {
    max-width: 60ch;
    padding-bottom: var(--space-sm);
    border-bottom: 1px solid var(--color-rule-soft);
}

/* Panels nested inside a step are already on a raised surface, so they drop
   their own shadow and read as sections rather than as stacked cards. */
.cl-wstep .cl-panel {
    background: none;
    border-color: var(--color-rule-soft);
    box-shadow: none;
}

/* The page already carries a title and the stepper already says where you
   are; a third heading the size of the page title just competed with it. */
.cl-wstep__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: 16px;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--color-ink);
}

/* These headings receive focus only to announce a completed step change to a
   screen reader. They are not controls, so the global interactive focus ring
   would draw a misleading green input-like boundary around their text. */
.cl-wizard :is(.cl-wstep__title, .cl-panel__title)[tabindex="-1"]:focus-visible {
    outline: none;
}

.cl-wstep__lede { margin: 2px 0 0; font-size: 12.5px; color: var(--color-muted); }

.cl-wstep__subtitle {
    margin: 0 0 var(--space-sm);
    font-size: 13px;
    font-weight: 600;
    color: var(--color-ink-soft);
}

.cl-wstep__section { margin-top: var(--space-md); }

.cl-wstep__toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    flex-wrap: wrap;
}

.cl-wstep__count { font-size: 12px; color: var(--color-muted); }

/* ── Template cards ───────────────────────────────────────────────────────
   A card is the message, not a picture of one: the WhatsApp components are
   rendered as WhatsApp will render them, and the template's image only stands
   in when there are no components to draw. */

/* A grid, not CSS columns.

   This was `column-count: 4` — masonry, copied from the Vue grid, so cards of
   wildly different lengths could each keep their natural height. It packed
   wrongly and it read wrongly:

   - `column-fill: balance` divides by height, not by count, and every card is
     `break-inside: avoid`, so the balancer cannot split at its target height —
     it grows the column until whole cards fit and overshoots. Two templates
     both landed in column 1 with three columns empty beside them; three
     templates went 2 + 1 + 0.
   - Columns flow down and then across, so on a list ranked by a search the
     second-best match sat *underneath* the best one rather than beside it.
     A picker is read left to right.

   What the columns bought was ragged card heights, and `align-items: start` is
   what keeps that: a card is still exactly as tall as its own content. The one
   thing given up is that a short card no longer has the next card slide up
   beneath it — it leaves whitespace until the next row. That is the whole cost,
   and it is cheaper than a picker that hides its second result. */
/*
 * Masonry, as CSS columns do it.
 *
 * Templates are wildly different lengths, and a row grid either clips the long
 * ones or pads the short ones with dead space. Columns let every card keep its
 * natural height and pack against the one above it — the Pinterest layout,
 * which is the right shape for browsing a set of unequal things.
 *
 * This was briefly a row grid and then a row grid with capped previews. Both
 * were wrong: capping the preview to make the cards match hides the end of the
 * message, and the message is the thing being chosen between.
 *
 * ## Below five cards it is not masonry, it is a row
 *
 * column-fill defaults to `balance`, and balance under-fills: measured here,
 * two cards land in one column and three or four land in two, however many
 * columns are asked for. It is not a height problem — two cards of *identical*
 * height do it too — and capping column-count does not help, because the
 * browser is already choosing to use fewer columns than it was given.
 *
 * The only thing that splits them is `column-fill: auto`, which needs a fixed
 * height nobody can know here.
 *
 * So below five cards the grid stops being a column context and becomes a row
 * of tracks. Nothing is lost: four cards are a single row, and a single row is
 * what masonry would have produced if it worked. Everything from five up gets
 * real masonry, which is the case that actually matters — this list is
 * normally twenty templates.
 */
.cl-tgrid {
    column-count: 4;
    column-gap: var(--space-md);
}

/*
 * The sparse case: a row of tracks, not columns.
 *
 * `column-count: initial` is what actually leaves the multi-column formatting
 * context — without it the grid properties are ignored, because an element
 * cannot be both. The card's column-specific margin goes too; the grid's own
 * gap does that job.
 */
.cl-tgrid.is-sparse {
    column-count: initial;
    display: grid;
    /*
     * Four tracks, not one per card.
     *
     * Sizing the tracks by how many cards there are made a single result
     * stretch the full width of the pane — a 1250px card carrying one message,
     * which looks like a bug and reads like a different component. The track
     * count is a property of the viewport, not of the search result: one card
     * takes the first of four and the other three stay empty, so a card is the
     * same width whether the filter returns one or twenty.
     */
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-md);
    align-items: start;
}

.cl-tgrid.is-sparse .cl-tcard { margin-bottom: 0; }

/*
 * The loading skeleton: the shape the cards will land in, filled edge to edge.
 *
 * A row of tracks, never the column context above, and for the reason set out
 * there — `column-fill: balance` under-fills. Six equal-height ghosts packed
 * themselves two-to-a-column into three columns and left the right of the pane
 * empty, which reads as a grid that has finished loading and found three
 * things rather than one that is still working.
 *
 * Masonry earns nothing here in any case: every ghost is the same height, so
 * columns and rows would be indistinguishable if balance behaved. What matters
 * is that the placeholder occupies the width the real grid is about to.
 */
.cl-tgrid--loading {
    column-count: initial;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-md);
    align-items: start;
}

.cl-tgrid--loading .cl-tcard { margin-bottom: 0; }

/*
 * Full rows only.
 *
 * Twelve ghosts are rendered and the surplus is hidden here rather than
 * counted in the template, because the number that fills a row is a property
 * of the viewport and the viewport is only known to CSS. Twelve divides by
 * four, three, two and one, so every breakpoint below ends on a complete row;
 * a ragged last row is the same "it has finished and found five" misreading in
 * a smaller form.
 */
.cl-tgrid--loading > *:nth-child(n + 9) { display: none; }

@media (max-width: 1500px) {
    .cl-tgrid--loading { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .cl-tgrid--loading > *:nth-child(n + 7) { display: none; }
}
@media (max-width: 1100px) {
    .cl-tgrid--loading { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .cl-tgrid--loading > *:nth-child(n + 7) { display: none; }
}
@media (max-width: 700px) {
    .cl-tgrid--loading { grid-template-columns: minmax(0, 1fr); }
    .cl-tgrid--loading > *:nth-child(n + 4) { display: none; }
}

@media (max-width: 1500px) {
    .cl-tgrid { column-count: 3; }
    .cl-tgrid.is-sparse { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 1100px) {
    .cl-tgrid { column-count: 2; }
    .cl-tgrid.is-sparse { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 700px) {
    .cl-tgrid { column-count: 1; }
    .cl-tgrid.is-sparse { grid-template-columns: minmax(0, 1fr); }
}

.cl-tcard {
    position: relative;
    /* inline-flex + break-inside is what keeps a card whole inside a column;
       a block-level card gets split across the column boundary. */
    display: inline-flex;
    width: 100%;
    margin-bottom: var(--space-md);
    break-inside: avoid;
    flex-direction: column;
    padding: 0;
    overflow: hidden;
    background: var(--color-paper-raised);
    border: 2px solid var(--color-rule);
    border-radius: 14px;
    font-family: var(--font-sans);
    text-align: start;
    cursor: pointer;
    transition: border-color var(--dur-short) var(--ease-out),
                box-shadow var(--dur-short) var(--ease-out);
}

/*
 * ── The API reference link ────────────────────────────────────────────────
 *
 * A quiet pill that says what it is. It sits on template cards, on the
 * customise step beside the fields it explains, and in the template table.
 *
 * Warm rather than accent: green is this product's "go" — the send button, the
 * selected card, a delivered message — and a link to documentation is not an
 * action on the message being composed. The warm family is what the identity
 * already spends on labels and figures.
 */
.cl-tdoc {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 9px;
    border: 1px solid var(--color-warm-line);
    border-radius: var(--radius-pill);
    background: var(--color-warm-wash);
    color: var(--color-warm-ink);
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-decoration: none;
    white-space: nowrap;
    transition: background var(--dur-short) var(--ease-out),
                border-color var(--dur-short) var(--ease-out);
}

.cl-tdoc:hover {
    border-color: var(--color-warm);
    background: color-mix(in oklab, var(--color-warm-wash) 70%, var(--color-warm) 30%);
}

.cl-tdoc:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.cl-tdoc svg { width: 12px; height: 12px; flex: 0 0 auto; }

.cl-tdoc--compact { padding: 4px; }

/* In the template table the chip follows the name with nothing between them,
   so a name that fit on one line ran straight into it: "OTP{}". They are two
   targets, the name opening the preview and the chip the docs in a new tab,
   and a thumb aimed at the end of one landed on the other. The space is the
   name's, not the chip's, so a chip that wraps under a long name still lines
   up with it instead of starting 8px in. */
.cl-table__tpl:has(+ .cl-tdoc),
.cl-table__strong:has(+ .cl-tdoc) { margin-inline-end: var(--space-xs); }

/* The customise step's own row for it — above the heading, aligned to the
   start of the measure, so it reads as a note about this step. */
.cl-wstep__doc {
    margin: 0 0 var(--space-sm);
}

/*
 * On a template card the whole surface is a <button>, so this cannot be nested
 * inside it — it is a sibling laid over the card's top corner. The wrapper
 * below owns the positioning the card used to.
 */
.cl-tcard-wrap {
    position: relative;
    display: inline-block;
    width: 100%;
    margin-bottom: var(--space-md);
    break-inside: avoid;
}

.cl-tcard-wrap .cl-tcard { margin-bottom: 0; break-inside: auto; }

/*
 * Always drawn, never revealed on hover.
 *
 * It was hover-only on the first pass, on the reasoning that a badge on every
 * tile turns a card grid into a grid of badges. That was the wrong trade: a
 * control nobody knows is there is a control nobody uses, and this one is the
 * whole point of the page for the person integrating. Hover also does not
 * exist on a touch screen, and this list is used on phones.
 */
.cl-tcard-wrap .cl-tdoc {
    position: absolute;
    inset-block-start: 8px;
    inset-inline-start: 8px;
    z-index: 2;
}

.cl-tcard:hover { border-color: var(--color-rule-track); box-shadow: var(--shadow-rest); }
.cl-tcard:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.cl-tcard.is-active { border-color: var(--color-accent); box-shadow: var(--shadow-lift); }

.cl-tcard--ghost {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    padding: var(--space-md);
    cursor: default;
}

/* Selection tick and price sit over the preview. */
.cl-tcard__check {
    position: absolute;
    top: 10px;
    inset-inline-start: 10px;
    z-index: 1;
    display: none;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: var(--radius-pill);
    background: var(--color-accent);
    color: var(--color-accent-ink);
}

.cl-tcard__check svg { width: 13px; height: 13px; }
.cl-tcard.is-active .cl-tcard__check { display: flex; }

.cl-tcard__credit {
    position: absolute;
    top: 10px;
    inset-inline-end: 10px;
    z-index: 1;
    padding: 3px 9px;
    border-radius: var(--radius-pill);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule);
    font-size: 11px;
    font-weight: 600;
    color: var(--color-ink-soft);
}

/* No max-height: the whole message shows. Clipping it was only needed to keep
   a row grid even, and the masonry layout removes that reason.

   The message sits on white, like a message does. On the sunken grey it read
   as disabled — the words are the thing being chosen, so they carry full ink
   rather than the muted tone used for supporting text. */
.cl-tcard__preview {
    display: block;
    background: var(--color-paper-raised);
    border-bottom: 1.5px solid var(--color-rule);
}

.cl-tcard__image { display: block; }
.cl-tcard__image img { width: 100%; display: block; }

.cl-tcard__info { display: flex; flex-direction: column; gap: var(--space-xs); padding: var(--space-md); }
.cl-tcard__name { font-size: 14px; font-weight: 600; color: var(--color-ink); line-height: 1.35; }

.cl-tcard__vars { display: flex; flex-direction: column; gap: 4px; }

.cl-tcard__vars-label {
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--color-muted);
}

.cl-tcard__tags { display: flex; flex-wrap: wrap; gap: 4px; }

/* Template tags are small rectangles, not the portal's pill chips — a card can
   carry a dozen of them and pills at that density read as noise.

   Three tones, three meanings, the same split the Vue grid makes:
     accent  a variable that changes per row
     warm    a global, the same for every contact
     cool    data the template catches on the way back */
.cl-tag {
    /*
     * One line, clipped, with the whole name in the tooltip.
     *
     * These were allowed to wrap, which was better than the flex row that
     * stretched them — but a name like "Reviews Platform Name (Trustpilot,
     * Google Reviews…)" then became a two-line block taller than the template
     * name above it, and in a masonry grid that one card sets the rhythm for
     * the column. A tag is a label for scanning, not prose: it is worth
     * knowing the variable is there and roughly what it is, and the full text
     * is one hover away.
     */
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
    padding: 3px 8px;
    border-radius: var(--radius-xs);
    font-size: 11px;
    font-weight: 500;
    line-height: 1.45;
    text-align: start;
    /* Variable names arrive from templates and can be a single long token;
       breaking anywhere is better than overflowing the card. */
    overflow-wrap: anywhere;
    background: var(--color-accent-wash);
    color: var(--color-accent);
}

.cl-tag--global { background: var(--color-warm-wash); color: var(--color-warm-ink); }
.cl-tag--catch {
    border: 1px solid var(--color-rule);
    background: var(--color-paper-sunken);
    color: var(--color-ink-soft);
}


/* Global variables and caught data read differently from row variables. */
.cl-chip--global {
    background: var(--color-warm-wash);
    border-color: var(--color-warm-line);
    color: var(--color-warm-ink);
}

/* ── Forms ────────────────────────────────────────────────────────────── */

.cl-form__row { display: flex; flex-wrap: wrap; gap: var(--space-md); align-items: flex-end; }
.cl-form__row > .cl-fld { flex: 1 1 220px; }

/* Named cl-fld, not cl-field: the cloned stylesheet already uses .cl-field
   for a bordered input *wrapper* (icon + input, vertically centred). Reusing
   that name wrapped every label in a box and centred it — which is exactly
   what it looked like. */
.cl-fld { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.cl-fld--narrow { max-width: 180px; }
/* Grows with the bar rather than stopping at 320px — see the filter-bar
   note below, where the three fields are sized to share the row. */
.cl-fld--search { flex: 1.5 1 11rem; max-inline-size: 30rem; }

.cl-fld__label {
    display: flex;
    align-items: baseline;
    gap: 6px;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--color-ink-soft);
}

.cl-fld__required { color: var(--color-halt); }
.cl-fld__hint { font-weight: 400; font-size: 11.5px; color: var(--color-muted); }
.cl-fld__note { font-size: 11.5px; color: var(--color-muted); }

/* The same semantic identity used while mapping a spreadsheet follows the
   field into New Message. A neutral badge keeps meaning separate from green
   actions/success and red validation. */
.cl-variable-label {
    align-items: center;
    gap: var(--space-xs);
    min-width: 0;
}

.cl-variable-label__icon {
    display: grid;
    flex: 0 0 auto;
    place-items: center;
    width: 28px;
    height: 28px;
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-xs);
    background: var(--color-paper-sunken);
    color: var(--color-ink-soft);
}

.cl-variable-label__icon svg { width: 15px; height: 15px; }
.cl-variable-label__name { min-width: 0; overflow-wrap: anywhere; }

.cl-fld:has(.is-invalid) .cl-variable-label__icon {
    border-color: var(--color-halt-line);
    background: var(--color-halt-wash);
    color: var(--color-halt);
}

.cl-fld:has(:disabled) .cl-variable-label__icon { opacity: 0.55; }

/*
|--------------------------------------------------------------------------
| Best-time preview
|--------------------------------------------------------------------------
|
| The sample shown under "send at each contact's best time": a few of the
| sender's own numbers beside the hour each would go out at.
|
| `min-height` is the point of the block. The contents arrive after a request,
| and three states of different heights pass through the same box — loading, a
| sentence, a list. Without a floor the wizard step reflows twice while the
| sender is reading it, and the field below jumps under the cursor. The floor is
| the tallest thing that can land there, so nothing moves after the switch.
|
| Times are `tabular-nums` so the column of hours lines up rather than shimmering
| as the digits change width.
*/
.cl-bt {
    min-height: 84px;
    margin-top: var(--space-2xs);
    padding: var(--space-xs) var(--space-sm);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-sm);
    background: var(--color-paper-sunken);
}

.cl-bt__msg,
.cl-bt__lead,
.cl-bt__foot {
    margin: 0;
    font-size: 11.5px;
    line-height: 1.5;
    color: var(--color-muted);
}

.cl-bt__lead { margin-bottom: var(--space-2xs); }
.cl-bt__foot { margin-top: var(--space-2xs); }

.cl-bt__list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: 2px;
}

.cl-bt__row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-sm);
    font-size: 12.5px;
}

/* The number is data, not prose: it keeps its direction in Arabic. */
.cl-bt__phone {
    color: var(--color-ink-2);
    font-variant-numeric: tabular-nums;
}

.cl-bt__time {
    font-weight: 600;
    color: var(--color-ink);
    font-variant-numeric: tabular-nums;
}

/* The clock the hour belongs to — present, but never louder than the hour. */
.cl-bt__zone {
    margin-inline-start: var(--space-2xs);
    font-size: 11px;
    font-weight: 500;
    color: var(--color-muted);
}

/*
    The choice for contacts we have no send time for.

    Sits above the examples panel and borrows nothing from it: the panel reports,
    this asks. A plain fieldset on the surface rather than a second sunken box —
    two nested tinted rectangles under one switch reads as a settings screen.
*/
.cl-btf {
    margin: var(--space-xs) 0 0;
    padding: 0;
    border: 0;
}

.cl-btf__title {
    padding: 0;
    font-size: 12px;
    font-weight: 600;
    color: var(--color-ink);
}

.cl-btf__lede,
.cl-btf__note {
    margin: 2px 0 0;
    font-size: 11.5px;
    line-height: 1.5;
    color: var(--color-muted);
}

.cl-btf__opts {
    display: grid;
    gap: var(--space-2xs);
    margin-top: var(--space-xs);
}

.cl-btf__opt {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2xs);
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--color-ink-2);
    cursor: pointer;
}

/* Nudged onto the first line of a label that wraps to two. */
.cl-btf__opt input { margin-block-start: 2px; flex: none; }
.cl-fld__error {
    font-size: 11.5px;
    line-height: 1.45;
    color: var(--color-halt);
}

/* A remote template file is still being inspected. This is progress, not a
   validation failure, so it stays neutral and never borrows the red error
   treatment. */
.cl-fld__verify {
    display: inline-flex;
    align-items: center;
    align-self: flex-start;
    gap: var(--space-2xs);
    min-height: 17px;
    font-size: 11.5px;
    line-height: 1.45;
    color: var(--color-accent);
}

.cl-fld__verify-spinner {
    width: 13px;
    height: 13px;
    flex: 0 0 auto;
    border: 2px solid var(--color-rule-track);
    border-block-start-color: currentColor;
    border-radius: 50%;
    animation: cl-spin 800ms linear infinite;
}

@media (prefers-reduced-motion: reduce) {
    .cl-fld__verify-spinner { animation: none; }
}

/* Only the additions the wizard needs; .cl-input, .cl-chip, .cl-switch,
   .cl-form and .cl-panel all come from the portal's own components. */
.cl-input--file { padding: var(--space-xs); }
select.cl-input { cursor: pointer; }
.cl-input.is-invalid { border-color: var(--color-halt); }
.cl-form { gap: var(--space-md); }

/* ── Segmented control ────────────────────────────────────────────────── */

.cl-segment {
    display: inline-flex;
    padding: 3px;
    gap: 2px;
    background: var(--color-paper-sunken);
    border-radius: var(--radius-md);
}

.cl-segment__btn {
    padding: 6px 14px;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    font-family: var(--font-sans);
    font-size: 12.5px;
    color: var(--color-muted);
    cursor: pointer;
}

.cl-segment__btn.is-active {
    background: var(--color-paper-raised);
    color: var(--color-ink);
    font-weight: 600;
    box-shadow: var(--shadow-rest);
}

.cl-segment__btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* 31px tall from its padding, under a thumb. On a phone it takes the 40px
   the other two-way switch (.cl-seg__opt) has there. */
@media (max-width: 40rem) {
    .cl-segment__btn { min-height: 40px; }
}

/* ── File drop ────────────────────────────────────────────────────────── */

.cl-drop {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-lg);
    border: 1px dashed var(--color-rule-control);
    border-radius: var(--radius-lg);
    background: var(--color-paper-sunken);
    cursor: pointer;
}

.cl-drop:hover { border-color: var(--color-accent); }
.cl-drop.is-filled {
    border-style: solid;
    border-color: var(--color-accent);
    background: var(--color-paper-raised);
}

.cl-drop__icon { color: var(--color-muted); }
.cl-drop__icon svg { width: 24px; height: 24px; }
.cl-drop__text { display: flex; flex-direction: column; gap: 2px; font-size: 13px; color: var(--color-ink); }
.cl-drop__text span { font-size: 11.5px; color: var(--color-muted); }

/* ── Readout ──────────────────────────────────────────────────────────── */

.cl-readout {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-xs) var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    background: var(--color-live-wash);
    border: 1px solid var(--color-live-line);
    border-radius: var(--radius-md);
}

.cl-readout__figure { font-family: var(--font-display); font-size: 18px; font-weight: 700; color: var(--color-live); }
.cl-readout__label { font-size: 12.5px; color: var(--color-ink-soft); }
.cl-readout__cols { display: flex; flex-wrap: wrap; gap: 4px; margin-inline-start: auto; }

/* ── Mapping ──────────────────────────────────────────────────────────── */

/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V4
 * Hallmark · component: semantic mapping grid · genre: modern-minimal
 * tone: operational · macrostructure: responsive mapping list
 * states: default · hover · focus · active · disabled · loading · error · success
 * contrast: pass (40–41) · slop: pass (42–49) · mobile: pass (34, 49, 50–57)
 */
.cl-map {
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
    min-width: 0;
}

.cl-map__status {
    display: inline-flex;
    align-items: center;
    align-self: flex-start;
    gap: var(--space-xs);
    min-height: 32px;
    padding: var(--space-2xs) var(--space-sm);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-pill);
    background: var(--color-paper-sunken);
    color: var(--color-ink-soft);
    font-size: 11.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.cl-map__status svg,
.cl-map__suggestion svg {
    width: 14px;
    height: 14px;
    flex: none;
}

.cl-map__status.is-complete {
    border-color: var(--color-live-line);
    background: var(--color-live-wash);
    color: var(--color-live);
}

.cl-map__head {
    display: none;
    color: var(--color-muted);
    font-size: 11px;
    font-weight: 600;
}

.cl-map__row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: center;
    gap: var(--space-xs);
    min-width: 0;
    padding: var(--space-sm) var(--space-xs);
    border: 1px solid transparent;
    border-block-end-color: var(--color-rule-soft);
    border-radius: var(--radius-sm);
    background: var(--color-paper-raised);
    color: var(--color-ink);
    transition:
        background-color var(--dur-short) var(--ease-out),
        border-color var(--dur-short) var(--ease-out),
        transform var(--dur-tick) var(--ease-out);
}

.cl-map__from {
    display: grid;
    grid-template-columns: 36px minmax(0, 1fr);
    align-items: center;
    gap: var(--space-xs);
    min-width: 0;
}

.cl-map__field-icon {
    display: grid;
    place-items: center;
    width: 36px;
    height: 36px;
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-sm);
    background: var(--color-paper-sunken);
    color: var(--color-ink-soft);
}

.cl-map__field-icon svg { width: 17px; height: 17px; }
.cl-map__field-icon--small { width: 30px; height: 30px; border-radius: var(--radius-xs); }
.cl-map__field-icon--small svg { width: 15px; height: 15px; }

.cl-map__identity {
    display: flex;
    flex-direction: column;
    gap: var(--space-3xs);
    min-width: 0;
}

.cl-map__name {
    min-width: 0;
    overflow-wrap: anywhere;
    font-size: 13px;
    font-weight: 600;
    line-height: 1.35;
    color: var(--color-ink);
}

.cl-map__var {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 11px;
    color: var(--color-muted);
}

.cl-map__arrow {
    display: none;
    align-items: center;
    gap: var(--space-2xs);
    color: var(--color-muted-soft);
}

.cl-map__arrow span {
    width: 12px;
    height: 1px;
    background: var(--color-rule-track);
}

.cl-map__arrow svg { width: 14px; height: 14px; }
[dir="rtl"] .cl-map__arrow svg { transform: rotate(180deg); }

.cl-map__to {
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
    min-width: 0;
}

.cl-map__suggestion {
    display: inline-flex;
    align-items: center;
    align-self: flex-start;
    gap: var(--space-2xs);
    color: var(--color-accent);
    font-size: 11px;
    font-weight: 600;
    white-space: nowrap;
}

.cl-map__global-label {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    min-width: 0;
}

/* The icon is a fixed tile, not a share of the row. As a flex item it could
   shrink, and a long field name ("Reviews Platform Name (Trustpilot, Google
   Reviews…)") took width from it: an 18×30 sliver between 30×30 squares. */
.cl-map__global-label > .cl-map__field-icon { flex: none; }

/* Alpine renders global template values as consecutive fields after the
   mapped-column list. Give each label its own breathing room so the semantic
   icon does not appear attached to the rule/control immediately above it. */
.cl-fld:has(> .cl-map__global-label) {
    padding-block-start: var(--space-xs);
}

/* On a phone the name, the asterisk and "same for every contact" read as one
   line of text that wraps as text. As three flex items each wrapped in its
   own column: "Card 2 / image" beside "same for / every / contact". */
@media (max-width: 40rem) {
    .cl-map__global-label .cl-fld__label { display: block; }
}

.cl-map__row.is-complete .cl-map__field-icon,
.cl-map__row.is-suggested .cl-map__field-icon {
    border-color: var(--color-rule-soft);
    background: var(--color-paper-sunken);
    color: var(--color-ink-soft);
}

.cl-map__row.is-error {
    border-color: var(--color-halt-line);
    background: var(--color-halt-wash);
    color: var(--color-ink);
}

.cl-map__row.is-error .cl-map__field-icon {
    border-color: var(--color-halt-line);
    background: var(--color-paper-raised);
    color: var(--color-halt);
}

.cl-map__row:focus-within,
.cl-map__row.is-focus {
    outline: 2px solid var(--color-focus);
    outline-offset: 2px;
}

.cl-map__row.is-active { transform: translateY(1px); }

.cl-map__row:has(select:disabled),
.cl-map__row.is-disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.cl-map__row.is-loading .cl-map__field-icon { color: var(--color-ink-soft); }

@media (hover: hover) and (pointer: fine) {
    .cl-map__row:hover,
    .cl-map__row.is-hover { background: var(--color-paper-sunken); }
}

@media (min-width: 40rem) {
    .cl-map__head,
    .cl-map__row {
        grid-template-columns: minmax(0, 0.92fr) 28px minmax(0, 1.08fr);
        column-gap: var(--space-sm);
    }

    .cl-map__head {
        padding: var(--space-xs);
    }

    .cl-map__arrow { display: flex; }
}

@media (prefers-reduced-motion: reduce) {
    .cl-map__row { transition-duration: var(--dur-micro); }
    .cl-map__row.is-active { transform: none; }
}

/* ── Switch ───────────────────────────────────────────────────────────── */


/* ── Picked template strip ────────────────────────────────────────────── */

.cl-picked {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-lg);
}

.cl-picked__thumb {
    position: relative;
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    overflow: hidden;
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-sm);
    background: var(--color-paper-sunken);
    color: var(--color-muted);
}
.cl-picked__thumb svg { width: 18px; height: 18px; }
.cl-picked__thumb img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.cl-picked__text { display: flex; flex-direction: column; min-width: 0; }
.cl-picked__label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--color-muted); }
.cl-picked__name { font-size: 13.5px; font-weight: 600; color: var(--color-ink); }
.cl-picked .cl-btn { margin-inline-start: auto; }

/* ── Customise workspace ─────────────────────────────────────────────── */

.cl-wstep--customise { container: customise-step / inline-size; }

.cl-wcustom {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    min-width: 0;
    overflow: hidden;
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-lg);
}

.cl-wcustom__fields-head {
    max-width: none;
    padding: var(--space-md);
    background: var(--color-paper-raised);
}

.cl-wcustom__fields {
    min-width: 0;
    padding: var(--space-md);
    background: var(--color-paper-raised);
}

.cl-wcustom__form {
    align-content: start;
    height: 100%;
}

.cl-wcustom__empty {
    display: grid;
    min-height: 16rem;
    place-items: center;
}

.cl-wcustom__preview {
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: var(--color-paper-sunken);
    border-top: 1px solid var(--color-rule-soft);
}

.cl-wcustom__preview-head {
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
    padding: var(--space-md);
    border-bottom: 1px solid var(--color-rule-soft);
}

.cl-wcustom__preview-title {
    min-width: 0;
    margin: 0;
    overflow-wrap: anywhere;
    font-family: var(--font-display);
    font-size: 15px;
    font-style: normal;
    font-weight: 700;
    color: var(--color-ink);
}

.cl-wcustom__preview-note {
    max-width: 54ch;
    margin: 0;
    font-size: 12px;
    line-height: 1.5;
    color: var(--color-muted);
}

.cl-wcustom__preview-body {
    display: grid;
    /* One column the width of the panel. Left implicit, the column took the
       bubble's min-content, and a carousel's card rail is wider than a phone:
       the bubble ran past the panel and its text was cut mid-word. The review
       stage below says the same. */
    grid-template-columns: minmax(0, 1fr);
    flex: 1;
    min-width: 0;
    place-items: start center;
    padding: var(--space-md);
}

.cl-wa--customise {
    width: 100%;
    max-width: 28rem;
    margin-inline: auto;
    border-color: var(--color-rule);
    box-shadow: var(--shadow-rest);
}

/*
 * The broadcast branch of the customise step: a stack of separate decisions.
 *
 * Contacts, then the column mapping, then where the results go, then the
 * follow-up. The wrapper was a plain block, so the only thing between "2
 * contacts ready" and "Match your columns" was the two panels' borders meeting
 * — and further down, "Results spreadsheet" and "Follow-up" sat flush against
 * each other and read as one control with two switches in it.
 *
 * The same gap the mapping grid already uses between its own two cards, so the
 * rhythm down the step is one measure rather than two.
 */
.cl-wbroadcast {
    display: grid;
    gap: var(--space-lg);
}

.cl-wbroadcast-map {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: var(--space-lg);
    min-width: 0;
}

.cl-wbroadcast-map > .cl-panel { min-width: 0; }

.cl-wcustom__preview--broadcast {
    min-width: 0;
    overflow: hidden;
    border: 1px solid var(--color-live-line);
    border-radius: var(--radius-lg);
}

@container customise-step (min-width: 52rem) {
    /*
     * Two columns, and a height that follows what is in them.
     *
     * This used to hold a floor of clamp(32rem, 70dvh, 42rem) — up to 672px —
     * so that the workspace read as a workspace. But the number of fields is
     * the template's, not the layout's: an OTP template has one input and a
     * four-line bubble, and the floor left roughly three hundred empty pixels
     * under both columns. A card that is mostly nothing looks like a card that
     * failed to load.
     *
     * So the rows size to content and the taller column sets the height. The
     * small floor is only there to stop a card being shorter than the preview
     * bubble it contains; every real template already clears it, and the
     * no-fields case has its own floor on .cl-wcustom__empty.
     */
    .cl-wcustom {
        grid-template-columns: minmax(0, 1fr) minmax(20rem, 0.92fr);
        grid-template-rows: auto minmax(0, 1fr);
        min-height: 18rem;
    }

    .cl-wcustom__fields-head {
        grid-column: 1;
        grid-row: 1;
    }

    .cl-wcustom__fields {
        grid-column: 1;
        grid-row: 2;
    }

    .cl-wcustom__preview {
        grid-column: 2;
        grid-row: 1 / span 2;
        border-top: 0;
        border-inline-start: 1px solid var(--color-rule-soft);
    }

    .cl-wcustom.is-previewless {
        grid-template-columns: minmax(0, 1fr);
        min-height: 0;
    }

    .cl-wbroadcast-map {
        grid-template-columns: minmax(0, 1fr) minmax(20rem, 0.92fr);
    }

    .cl-wbroadcast-map.is-previewless {
        grid-template-columns: minmax(0, 1fr);
    }

    .cl-wbroadcast-map .cl-wcustom__preview--broadcast {
        grid-column: auto;
        grid-row: auto;
        border: 1px solid var(--color-rule-soft);
    }
}

/* ── Review ───────────────────────────────────────────────────────────── */

/* Hallmark · genre: modern-minimal · tone: utilitarian · anchor: brand green
 * macrostructure: preview-first checkout · body: asymmetric 60/40 · action: persistent checkout dock
 * button: solid · image: inline preview · reveal: details-only
 * pre-emit critique: P5 H5 E5 S5 R5 V5 · contrast: pass (40–41)
 * slop: pass (42–49) · mobile: pass (34, 49, 50–57)
 */

/* Step four changes mode: it confirms a send instead of collecting another
   answer. Removing the wizard card here prevents a third containment layer;
   the preview and decision summary become the two intentional surfaces. */
.cl-wstep--review {
    container: review-step / inline-size;
    gap: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    animation: none;
}

.cl-wreview {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: var(--space-lg);
    width: 100%;
    max-width: 42rem;
    margin-inline: auto;
}

/* When Meta has no direct-message preview for this template, the decision
   summary is the review step. Let that single surface use the same content
   measure as the full checkout instead of leaving it as a narrow orphan card. */
.cl-wreview.is-previewless {
    max-width: 72rem;
}

.cl-wreview__section {
    min-width: 0;
    overflow: hidden;
    border-radius: var(--radius-lg);
}

.cl-wreview__preview {
    background: var(--color-paper-sunken);
    border: 1px solid var(--color-rule);
}

.cl-wreview__summary {
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule);
    box-shadow: var(--shadow-rest);
}

.cl-wreview__section-head {
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
    padding: var(--space-sm) var(--space-md);
    border-bottom: 1px solid var(--color-rule-soft);
}

.cl-wreview__preview .cl-wreview__section-head {
    border-color: var(--color-rule-soft);
}

.cl-wreview__section-title {
    min-width: 0;
    margin: 0;
    overflow-wrap: anywhere;
    font-family: var(--font-display);
    font-size: 16px;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--color-ink);
}

.cl-wreview__section-note {
    max-width: 54ch;
    margin: 0;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--color-muted);
}

.cl-wreview__preview-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    padding: var(--space-sm);
}

.cl-wreview__preview-stage {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    place-items: start center;
    min-width: 0;
}

.cl-wreview__ready-title {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
}

.cl-wreview__ready-mark {
    display: inline-grid;
    place-items: center;
    flex: 0 0 auto;
    width: 24px;
    height: 24px;
    border-radius: var(--radius-pill);
    background: var(--color-live-wash);
    border: 1px solid var(--color-live-line);
    color: var(--color-live);
}

.cl-wreview__ready-mark svg { width: 13px; height: 13px; }

/*
 * Sent. The mark is the one thing on the review that changes at the moment
 * the message goes, so it is what answers the press: it fills with the live
 * tone and pops on the spring, and a ring goes out from it once. Nothing
 * loops — the heading beside it already says "Sent" in words.
 */
.cl-wreview__ready-mark { position: relative; }

.cl-wreview__ready-mark.is-sent {
    background: var(--color-live);
    border-color: var(--color-live);
    color: var(--color-paper-raised);
    animation: cl-sent-pop calc(var(--dur-mid) * 2) var(--ease-spring) backwards;
}

.cl-wreview__ready-mark.is-sent::after {
    content: '';
    position: absolute;
    inset: -1px;
    border: 2px solid var(--color-live);
    border-radius: inherit;
    opacity: 0;
    animation: cl-sent-ring calc(var(--dur-long) * 2.4) var(--ease-out) var(--dur-tick);
    pointer-events: none;
}

@keyframes cl-sent-pop {
    from { transform: scale(0.4); }
    to { transform: none; }
}

@keyframes cl-sent-ring {
    from { opacity: 0.7; transform: scale(1); }
    to { opacity: 0; transform: scale(2.4); }
}

/* Recipient first, then the message it applies to, then quieter delivery
   context. These are facts, not cards; one rule and weight change are enough. */
.cl-review-facts {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0 var(--space-md);
}

.cl-review-fact {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: var(--space-xs);
    padding: var(--space-sm) 0;
    border-bottom: 1px solid var(--color-rule-soft);
}

.cl-review-fact dt {
    min-width: 0;
    font-size: 12px;
    color: var(--color-muted);
}

.cl-review-fact dd {
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
    min-width: 0;
    margin: 0;
    text-align: start;
}

.cl-review-fact dd strong {
    min-width: 0;
    overflow-wrap: anywhere;
    font-size: 13px;
    line-height: 1.45;
    color: var(--color-ink);
}

.cl-review-fact dd > span,
.cl-review-fact__meta {
    font-size: 11.5px;
    line-height: 1.45;
    color: var(--color-muted);
}

.cl-review-fact__meta {
    display: inline-flex;
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: var(--space-2xs);
}

.cl-review-fact--recipient {
    padding-block: var(--space-md);
}

.cl-review-fact--recipient dt {
    color: var(--color-accent);
    font-weight: 600;
}

.cl-review-fact--recipient dd strong {
    font-family: var(--font-display);
    font-size: 17px;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
}

.cl-review-fact--quiet dd strong {
    color: var(--color-ink-soft);
    font-weight: 500;
}

/* Rows left out of the send: quiet in weight, but not in colour. */
.cl-review-fact--skip dd strong { color: var(--color-halt); }

.cl-review-cost {
    display: flex;
    flex-direction: column;
    gap: var(--space-2xs);
    margin: var(--space-md);
    padding: var(--space-sm);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-md);
    background: var(--color-paper-sunken);
    color: var(--color-ink);
}

.cl-review-cost__label {
    font-size: 12px;
    font-weight: 600;
    color: var(--color-ink-soft);
}

.cl-review-cost__total {
    display: flex;
    align-items: baseline;
    gap: var(--space-xs);
    margin: 0;
}

.cl-review-cost__total strong {
    font-family: var(--font-display);
    font-size: 28px;
    line-height: 1;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--color-ink);
}

.cl-review-cost__total span {
    font-size: 13px;
    font-weight: 600;
    color: var(--color-ink-soft);
}

.cl-review-cost__note,
.cl-review-cost__warning {
    margin: 0;
    font-size: 11.5px;
    line-height: 1.5;
}

.cl-review-cost__note { color: var(--color-muted); }
.cl-review-cost__warning { margin-top: var(--space-xs); color: var(--color-warm-ink); }

.cl-wreview__summary > :is(.cl-errors, .cl-note) {
    margin: 0 var(--space-md) var(--space-md);
}

/*
 * What the template asks back for, behind a disclosure.
 *
 * It was a bare <summary> — small bold text with no container, no caret and
 * no open state — so it read as a stray label that had lost its panel rather
 * than as something you could press. It is a control, and now looks like one:
 * a full-width row that reveals a bordered panel, with a caret that turns.
 */
.cl-wreview__details {
    width: 100%;
    /* The same measure as .cl-wreview .cl-wa below: it describes that
       message, and a panel wider than the thing it is about reads as a
       separate section rather than a footnote to it. */
    max-width: 25.5rem;
    margin-inline: auto;
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-md);
    background: var(--color-paper-raised);
    overflow: hidden;
}

/* The browser's own triangle, in both spellings. */
.cl-wreview__details-summary::-webkit-details-marker { display: none; }
.cl-wreview__details-summary { list-style: none; }

.cl-wreview__details-summary {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    /* 44px: this is a touch target before it is a label. */
    min-height: 44px;
    padding: var(--space-xs) var(--space-md);
    font-size: 12.5px;
    font-weight: 600;
    color: var(--color-ink-soft);
    cursor: pointer;
    user-select: none;
}

.cl-wreview__details-caret {
    width: 14px;
    height: 14px;
    flex: none;
    color: var(--color-muted);
    transition: transform .15s ease;
}

.cl-wreview__details[open] .cl-wreview__details-caret { transform: rotate(90deg); }

/* How many fields are behind it, so opening is a decision rather than a
   guess. */
.cl-wreview__details-count {
    margin-inline-start: auto;
    min-width: 1.25rem;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--color-paper-sunken);
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    color: var(--color-muted);
    text-align: center;
}

@media (hover: hover) and (pointer: fine) {
    .cl-wreview__details-summary:hover { background: var(--color-paper-sunken); color: var(--color-ink); }
    .cl-wreview__details-summary:hover .cl-wreview__details-caret { color: var(--color-accent); }
}

.cl-wreview__details-summary:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: -2px;
}

.cl-wreview__details-body {
    padding: var(--space-sm) var(--space-md) var(--space-md);
    border-top: 1px solid var(--color-rule-soft);
}

.cl-wreview__details-label {
    margin: 0 0 var(--space-2xs);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-muted);
}

.cl-wreview__details-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}

@media (prefers-reduced-motion: reduce) {
    .cl-wreview__details-caret { transition: none; }
}

@container review-step (max-width: 30rem) {
    .cl-wreview__section-head,
    .cl-wreview__preview-body,
    .cl-review-facts { padding-inline: var(--space-md); }

    .cl-review-cost { margin-inline: var(--space-md); }
}

/* While the review itself is one wide column, facts can use a compact label
   rail. Once the preview and summary split, the summary becomes narrow again
   and returns to the stacked reading order above. */
@container review-step (min-width: 40rem) and (max-width: 51.99rem) {
    .cl-review-fact {
        grid-template-columns: minmax(7rem, 0.6fr) minmax(0, 1.4fr);
        gap: var(--space-md);
    }
}

@container review-step (min-width: 52rem) {
    .cl-wreview {
        grid-template-columns: minmax(0, 1.45fr) minmax(18rem, 0.85fr);
        align-items: stretch;
        gap: var(--space-lg);
        /*
         * Full width, like every other step.
         *
         * .cl-wstep--review strips the card, so this grid *is* the step's
         * visible width — and at 72rem it sat four rem inside the cards on
         * steps 1 to 3 and inside the Back/Next bar below it. The wizard
         * already caps the column at 76rem; a second cap here only made the
         * steps disagree with each other.
         *
         * The 42rem on the base rule stays: that is the one-column layout on
         * a narrow screen, where it is a reading measure rather than a
         * mismatch.
         */
        max-width: none;
    }

    .cl-wreview__section { height: 100%; }

    .cl-wreview__summary {
        display: flex;
        flex-direction: column;
    }

    .cl-review-facts { flex: 1 0 auto; }

    /*
     * The bubble is as tall as its message.
     *
     * It carried a min-height of up to 28rem, to keep the two decision
     * surfaces the same size. But that height lands inside the bubble, so a
     * short template — a greeting and two buttons — drew a message that ended
     * a third of the way down and then several hundred pixels of empty bubble
     * under it, which reads as content that failed to load.
     *
     * The columns stay level without it: .cl-wreview__section is height: 100%
     * just above, so the two cards still match. Only the message inside stops
     * being stretched to fill one.
     */

    .cl-wreview.is-previewless {
        grid-template-columns: minmax(0, 1fr);
        max-width: none;
    }
}

.cl-summary { display: flex; flex-direction: column; gap: 0; margin: 0; }

.cl-summary__row {
    display: grid;
    grid-template-columns: minmax(6.5rem, 0.7fr) minmax(0, 1.3fr);
    align-items: start;
    gap: var(--space-md);
    padding: var(--space-sm) 0;
    border-bottom: 1px solid var(--color-rule-soft);
}

.cl-summary__row dt { min-width: 0; font-size: 12.5px; color: var(--color-muted); }
.cl-summary__row dd {
    min-width: 0;
    margin: 0;
    overflow-wrap: anywhere;
    font-size: 13px;
    font-weight: 600;
    color: var(--color-ink);
    text-align: end;
    unicode-bidi: plaintext;
}

/* Cost is one final verification row, not a promotional card. */
.cl-cost {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: baseline;
    gap: var(--space-xs);
    margin-top: var(--space-sm);
    padding-top: var(--space-md);
    border-top: 0;
}

.cl-cost__label { min-width: 0; font-size: 12px; color: var(--color-muted); }
.cl-cost__value {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-size: 20px;
    font-weight: 700;
    color: var(--color-accent);
}

.cl-cost__unit-strong { color: var(--color-ink-soft); font-weight: 600; }
.cl-cost__unit { font-size: 12.5px; color: var(--color-muted); }
.cl-cost__note { grid-column: 1 / -1; font-size: 11.5px; color: var(--color-muted); }

@container review-step (max-width: 30rem) {
    .cl-summary__row { grid-template-columns: minmax(0, 1fr); gap: var(--space-2xs); }
    .cl-summary__row dd { text-align: start; }
}

/* ── WhatsApp preview ─────────────────────────────────────────────────── */

.cl-wa {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    max-width: 340px;
    padding: var(--space-sm);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-md);
}

/* The send review has enough measure to show a real message line length. The
   template grid keeps the narrower phone-sized bubble it was designed for. */
.cl-wreview .cl-wa {
    width: 100%;
    max-width: 25.5rem;
    margin-inline: auto;
    padding: var(--space-xs);
    border-color: var(--color-rule);
    box-shadow: var(--shadow-rest);
}

/* A media header is evidence, not the focal illustration of this screen. */
.cl-wreview .cl-slot {
    aspect-ratio: 4 / 1;
}

.cl-wreview .cl-slot__engine { width: min(14%, 3rem); }

.cl-wreview .cl-wa__header { font-size: 12.75px; line-height: 1.45; }
.cl-wreview .cl-wa__body { font-size: 12.5px; line-height: 1.5; }
.cl-wreview .cl-wa__footer { font-size: 11px; line-height: 1.45; }

/* Inside a grid card the preview fills the card and carries no frame of its
   own — the card is the frame. */
.cl-wa--card {
    max-width: none;
    /* Extra room at the top so the price badge and the selection tick sit
       clear of the first line rather than on top of it. */
    padding: 42px 16px 16px;
    background: var(--color-paper-raised);
    border: 0;
    border-radius: 0;
    font-size: 13px;
    line-height: 1.6;
}

.cl-wa__head { display: flex; flex-direction: column; gap: 4px; }

.cl-wa__media {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: var(--space-md);
    border-radius: var(--radius-sm);
    background: var(--color-paper-sunken);
    font-size: 11.5px;
    color: var(--color-muted);
}

.cl-wa__media svg { width: 16px; height: 16px; }
.cl-wa__header { font-size: 13.5px; color: var(--color-ink); }
.cl-wa__body {
    margin: 0;
    font-size: 13px;
    line-height: 1.6;
    color: var(--color-ink);
    white-space: pre-wrap;
    word-break: break-word;
}
.cl-wa__footer { margin: 0; font-size: 11.5px; color: var(--color-muted); }
.cl-wa__header { font-size: 13.5px; color: var(--color-ink); font-weight: 700; }
.cl-wa__image { width: 100%; border-radius: var(--radius-sm); }
.cl-wreview .cl-wa__image { max-height: 9rem; object-fit: cover; }

.cl-wa__header-media {
    width: 100%;
    margin-bottom: var(--space-xs);
    overflow: hidden;
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    background: var(--color-paper-sunken);
}

.cl-wa__header-image,
.cl-wa__header-video {
    display: block;
    width: 100%;
    max-height: 16rem;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    background: var(--color-paper-sunken);
}

.cl-wreview :is(.cl-wa__header-image, .cl-wa__header-video) {
    max-height: 10rem;
}

.cl-wa__header-document {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    min-height: 5rem;
    padding: var(--space-md);
    color: var(--color-ink);
    text-decoration: none;
}

.cl-wa__header-document:hover { background: var(--color-paper-raised); }

.cl-wa__header-document > svg {
    flex: 0 0 auto;
    width: 24px;
    height: 24px;
    color: var(--color-accent);
}

.cl-wa__header-document > span {
    min-width: 0;
    overflow-wrap: anywhere;
    font-size: 12px;
    font-weight: 600;
}

.cl-wa__header-document--detected {
    min-height: 6.25rem;
    padding: var(--space-sm) var(--space-md);
    background: linear-gradient(135deg, color-mix(in srgb, var(--color-paper-sunken) 92%, var(--color-accent) 8%), var(--color-paper-raised));
    transition: background-color 140ms ease, box-shadow 140ms ease;
}

.cl-wa__header-document--detected:hover {
    background: var(--color-paper-raised);
    box-shadow: inset 0 0 0 1px var(--color-rule-control);
}

.cl-wa__header-document--detected:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: -2px;
}

.cl-wa__header-document--detected > .cl-wa__header-document-art {
    display: grid;
    place-items: center;
    flex: 0 0 3.25rem;
    width: 3.25rem;
    height: 4rem;
}

.cl-wa__header-document-art img {
    display: block;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.cl-wa__header-document--detected > .cl-wa__header-document-copy {
    display: flex;
    flex: 1 1 auto;
    min-width: 0;
    flex-direction: column;
    gap: 3px;
}

.cl-wa__header-document-copy strong {
    overflow: hidden;
    font-size: 12.5px;
    font-weight: 700;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cl-wa__header-document-copy small {
    color: var(--color-muted);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.02em;
}

.cl-wa__header-document .cl-wa__header-document-open {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    color: var(--color-muted);
}

/* WhatsApp's formatting vocabulary. These elements are
   emitted only by whatsapp-text.js after both template text and values have
   been escaped. */
.cl-wa :is(.cl-wa__header, .cl-wa__body, .cl-wa__footer) code {
    padding-inline: var(--space-3xs);
    border-radius: var(--radius-xs);
    background: var(--color-paper-sunken);
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 0.94em;
}

.cl-wa :is(.cl-wa__header, .cl-wa__body, .cl-wa__footer) em { font-style: italic; }
.cl-wa__list-mark {
    display: inline-block;
    min-width: 1.15em;
    color: var(--color-ink-soft);
    font-variant-numeric: tabular-nums;
    font-weight: 700;
}

.cl-wa__quote {
    padding-inline-start: var(--space-xs);
    border-inline-start: 2px solid var(--color-rule-control);
    color: var(--color-ink-soft);
}

.cl-wa__link {
    color: var(--color-accent);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
}

/* A value the sender supplied, not words from the template. The customer
   receives plain text — this tint exists only so the person reviewing can
   see at a glance which parts of the sentence they filled in.

   The warm family, so it reads as the numeral in the animated logo rather
   than as the accent: the accent already carries links here, and `code`
   already carries the sunken paper tint, so a third mark needs its own hue
   to stay tellable from the other two. */
.cl-wa__fill {
    padding: 1px 5px;
    border-radius: var(--radius-xs);
    background: var(--color-warm-wash);
    box-shadow: inset 0 -2px 0 var(--color-warm);
    color: var(--color-warm-ink);
    font-weight: 600;
    /* Both marks keep their shape when a long value wraps mid-sentence. */
    box-decoration-break: clone;
    -webkit-box-decoration-break: clone;
}

/* The other half of the same story: a variable with nothing in it yet. An
   empty outline rather than a fill, so it reads as a slot still waiting for
   a value — the shape says it before the words do. */
.cl-wa__hole {
    padding: 1px 5px;
    border-radius: var(--radius-xs);
    border: 1.5px dashed var(--color-rule-track);
    background: transparent;
    color: var(--color-ink-soft);
    font-weight: 600;
    box-decoration-break: clone;
    -webkit-box-decoration-break: clone;
}

/* ── Naming the field behind a mark ──────────────────────────────────────
   The browser's own title tooltip waits about a second, long enough to feel
   unrelated to the word under the cursor. This one is immediate. */
:is(.cl-wa__fill, .cl-wa__hole)[data-label] {
    position: relative;
    cursor: help;
}

:is(.cl-wa__fill, .cl-wa__hole)[data-label]::after,
:is(.cl-wa__fill, .cl-wa__hole)[data-label]::before {
    position: absolute;
    left: 50%;
    z-index: 4;
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: opacity 120ms var(--ease-out, ease), translate 120ms var(--ease-out, ease);
}

/* The bubble. */
:is(.cl-wa__fill, .cl-wa__hole)[data-label]::after {
    content: attr(data-label);
    bottom: calc(100% + 9px);
    translate: -50% 3px;
    padding: 7px 11px;
    border-radius: var(--radius-sm, 8px);
    background: var(--color-ink);
    color: var(--color-paper);
    font-size: 12.5px;
    font-weight: 500;
    line-height: 1.4;
    letter-spacing: 0;
    white-space: nowrap;
    box-shadow: var(--shadow-soft);
}

/* The caret, so it points at the word rather than floating above it. */
:is(.cl-wa__fill, .cl-wa__hole)[data-label]::before {
    content: "";
    bottom: calc(100% + 4px);
    translate: -50% 3px;
    border: 5px solid transparent;
    border-top-color: var(--color-ink);
}

:is(.cl-wa__fill, .cl-wa__hole)[data-label]:hover::after,
:is(.cl-wa__fill, .cl-wa__hole)[data-label]:hover::before,
:is(.cl-wa__fill, .cl-wa__hole)[data-label]:focus-visible::after,
:is(.cl-wa__fill, .cl-wa__hole)[data-label]:focus-visible::before {
    opacity: 1;
    visibility: visible;
    translate: -50% 0;
}

/* ── The recipient reel ──────────────────────────────────────────────────
   Under a broadcast preview: which of the uploaded rows the bubble above is
   standing in for. The reel plays on its own so the mapped words swap while
   the template's own words hold still — which is the whole explanation of
   what a variable is, without a word of instruction. */

.cl-wreel {
    display: flex;
    align-items: center;
    gap: var(--space-3xs);
    margin-top: var(--space-xs);
    padding-top: var(--space-xs);
    border-top: 1px solid var(--color-rule-soft);
}

.cl-wreel__btn {
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-xs);
    background: transparent;
    color: var(--color-ink-soft);
    cursor: pointer;
    transition: background 120ms var(--ease-out, ease), color 120ms var(--ease-out, ease);
}

.cl-wreel__btn:hover { background: var(--color-paper-sunken); color: var(--color-ink); }
.cl-wreel__btn svg { width: 15px; height: 15px; }

/* 26px with 2px between them is a pointer's target. On a phone the same
   glyphs get 40px and room between them, as the table's row actions do. */
@media (max-width: 40rem) {
    .cl-wreel { gap: var(--space-2xs); }
    .cl-wreel__btn { width: 40px; height: 40px; }
}

.cl-wreel__who {
    margin: 0 0 0 var(--space-2xs);
    font-size: 11.5px;
    font-weight: 500;
    color: var(--color-warm-ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cl-wreel__count {
    margin-inline-start: var(--space-2xs);
    color: var(--color-ink-soft);
    font-variant-numeric: tabular-nums;
}

/* Only what actually arrived types itself in. The renderer splits a changed
   value at the last character it shares with the previous one, so typing a
   letter types that letter while the rest stands still, and a paste — or a
   step to another recipient — shares nothing and arrives whole.

   clip-path rather than width: the span keeps its final size throughout, so
   nothing around it reflows while the characters appear. `--chars` counts
   only what is arriving, so one letter takes one beat and an address takes
   many. */
.cl-wa__typed {
    display: inline-block;
    vertical-align: bottom;
    white-space: pre-wrap;
    /* A fixed cadence first, for engines that reject var() inside steps();
       the two lines below refine it where they are understood, and are simply
       dropped where they are not. */
    animation: cl-wa-type 260ms steps(8, end) both;
    animation-duration: calc(var(--chars, 6) * 34ms);
    animation-timing-function: steps(var(--chars, 6), end);
}

@keyframes cl-wa-type {
    from { clip-path: inset(0 100% 0 0); }
    to   { clip-path: inset(0 0 0 0); }
}

/* Asked for less motion: the reel is paused in JavaScript and the swap loses
   its movement here, so stepping by hand still reads but nothing animates. */
@media (prefers-reduced-motion: reduce) {
    .cl-wa__typed { animation: none; }
    .cl-wreel__btn { transition: none; }
}

/* ── A location header ───────────────────────────────────────────────────
   The one header with nothing to show and everything to say: WhatsApp draws a
   map card with the place's name and address stacked under it. There is no map
   to draw here — we have coordinates, not tiles — so the pin stands in for it
   at the size the card would be, and the three lines are the ones that will
   actually be read. */

.cl-wa__place {
    display: flex;
    align-items: flex-start;
    gap: var(--space-xs);
    padding: var(--space-sm);
    border-radius: var(--radius-sm);
    background: var(--color-paper-sunken);
    border: 1px solid var(--color-rule-soft);
}

.cl-wa__place-pin {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--color-rule);
    background: var(--color-paper-raised);
    color: var(--color-accent);
}

.cl-wa__place-pin svg { width: 15px; height: 15px; }

.cl-wa__place-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.cl-wa__place-name { font-size: 12.5px; font-weight: 600; color: var(--color-ink); overflow-wrap: anywhere; }
.cl-wa__place-address { font-size: 11.5px; color: var(--color-ink-soft); overflow-wrap: anywhere; }

/* The coordinates, in the tabular face, because they are numbers being
   compared against a map somewhere else. */
.cl-wa__place-fix {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-size: 11px;
    color: var(--color-muted);
}

/* The clock a bubble carries on a phone. Enough for it to read as a message
   rather than as a block of text, and the only piece of device furniture drawn
   here: a notch and a speaker slit would say nothing about the template. */
.cl-wa__time {
    align-self: flex-end;
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-size: 10.5px;
    color: var(--color-muted);
}

.cl-wa__buttons { display: flex; flex-direction: column; gap: 4px; margin-top: var(--space-xs); }

/*
 * A button, with the mark for its kind beside the label.
 *
 * Flex rather than the old block with `text-align: center`, because the icon
 * and the label have to sit on one baseline and stay centred together — a
 * floated or inline icon drifts off the label's line as soon as the label
 * wraps, which a long "Get 20% off your next order" does at this width.
 *
 * The icon is 13px against a 12.5px label: an icon drawn at the label's own
 * size reads as part of the word rather than as a mark on it.
 */
.cl-wa__btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 7px;
    border-top: 1px solid var(--color-rule-soft);
    text-align: center;
    font-size: 12.5px;
    color: var(--color-accent);
}

.cl-wa__btn > svg { flex: none; width: 13px; height: 13px; }

.cl-wa__carousel { margin-top: var(--space-xs); }
.cl-wa__carousel-label { font-size: 11px; color: var(--color-muted); }
.cl-wa__carousel-track {
    display: flex;
    gap: var(--space-xs);
    overflow-x: auto;
    padding-top: 6px;

    /*
     * No bar at all, and a fade instead.
     *
     * Measured, not assumed: `::-webkit-scrollbar { height: 4px }` is ignored by
     * current Chrome — setting it alone leaves the default 15px — and
     * `scrollbar-width: thin` bottoms out at 11px. Against a 152px card and a
     * 6px gap, 11px of grey furniture under the strip takes the eye before the
     * cards do, and it cannot be made smaller.
     *
     * So the affordance moves into the strip itself: the last card is clipped
     * by the fade below, which says "there is more" the way a carousel does on
     * the phone this is depicting. Wheel, trackpad, drag and keyboard scrolling
     * are all untouched — only the painted bar is gone.
     */
    scrollbar-width: none;
    -ms-overflow-style: none;

    /* Every card the same height, whatever is in it. Stated rather than left
       to the flex default, because a card is a fixed thing on the phone this
       depicts and a ragged strip reads as a rendering fault. */
    align-items: stretch;
}

.cl-wa__carousel-track::-webkit-scrollbar { display: none; }

/* The fade. On the wrapper, not the track, so it does not scroll away with the
   cards — and pointer-events:none so it never eats a click on the last card. */
.cl-wa__carousel { position: relative; }

.cl-wa__carousel::after {
    content: "";
    position: absolute;
    top: 0;
    inset-inline-end: 0;
    bottom: 0;
    width: 28px;
    pointer-events: none;
    background: linear-gradient(to right, transparent, var(--color-paper-raised));
    border-start-start-radius: 0;
    border-start-end-radius: var(--radius-sm);
    border-end-end-radius: var(--radius-sm);
    border-end-start-radius: 0;
}

/* linear-gradient has no logical form. The strip moves to the other edge with
   the inset above, so the fade has to run the other way — otherwise it is a
   hard edge against the cards rather than a fade away from them. */
[dir="rtl"] .cl-wa__carousel::after {
    background: linear-gradient(to left, transparent, var(--color-paper-raised));
}

.cl-wa__card {
    /* 184, not 152: the preview column is wider than the strip was using, and
       at 152 the body ran to three cramped lines and the picture was too small
       to judge a crop by. Still narrow enough that a second card is partly
       visible, which is what says "this scrolls". The media slot follows on its
       own, being sized by ratio rather than in pixels.

       The old floor stands: "Prise de RDV sur WhatsApp" is a real button label
       on this account and broke across three lines at 140. */
    flex: 0 0 184px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: var(--space-xs);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-sm);
}

/* A carousel card's picture uses the same palette-derived plane as every
   header slot. Format is communicated by the label, never by a drifting wash. */
.cl-wa__card-media {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    /*
     * Wide, the way WhatsApp will crop it.
     *
     * Meta does not publish a carousel ratio — the media-card reference says
     * only that "media assets are automatically cropped to a wide ratio based
     * on the WhatsApp user's device", and the nearest published constraint is
     * the link-preview rule of "4:1 width/height or less" at 300px or wider.
     * So 1.91:1 is Meta's de-facto wide format rather than a quoted spec: close
     * to what a card actually renders as, and honest about being an
     * approximation.
     *
     * Stated as a ratio rather than a pixel height so the slot stays
     * proportional if the card width changes, and so every card in the strip
     * crops identically. With object-fit: cover on the image, a client
     * uploading a tall photo sees here what WhatsApp will cut it down to —
     * which is the point of a preview.
     */
    aspect-ratio: 1.91 / 1;
    /* Held against the card's stretch: without this the media slot is the flex
       item that gives, and a card with a picture ends up shorter in the image
       than its neighbours. */
    flex: 0 0 auto;
    overflow: hidden;
    border-radius: var(--radius-xs);
    background: var(--background-media-preview);
    color: var(--color-muted-soft);
}

/* Sized to the card, not to the slot: 140px wide and 60px tall leaves room
   for the mark and nothing else, which is the point of a thumbnail. */
.cl-wa__card-engine {
    flex: 0 0 auto;
    width: 34px;
    height: auto;
    margin: 0;
}

/* The file glyph, where a card has no picture at all. Excludes the engine:
   `.cl-wa__card-media > svg` is more specific than `.cl-wa__card-engine`, so
   without the :not() it silently sized the mark down to a glyph. */
.cl-wa__card-media > svg:not(.cl-wa__card-engine) { width: 18px; height: 18px; }
/* The chosen picture fills the card's media slot and is cropped to it, which
   is how WhatsApp itself treats a card asset — so a client uploading a tall
   photo sees here what will actually be shown. */
.cl-wa__card-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* The slot as a link: a pointer, and the magnifier that says the cropped view
   is not all there is. The cue is hidden until hover or focus so the card
   still reads as a phone card rather than as a gallery. */
.cl-wa__card-media--link {
    cursor: zoom-in;
    display: block;
    /* It is a <button> now: strip the agent's own chrome so it still reads as
       the card's picture and not as a control dropped on top of one. */
    width: 100%;
    padding: 0;
    border: 0;
    font: inherit;
    text-align: inherit;
    appearance: none;
}




/* Keyboard users get the same ring the rest of the portal uses. */
.cl-wa__card-media--link:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
}

/* The picture, full size, on nothing.

   Fixed to the viewport rather than to the preview, because the preview column
   is the thing being escaped. The scrim is opaque enough that the page behind
   stops competing, and the image is capped at the viewport so a large upload
   is shown whole rather than cropped a second time. */
body.is-lightboxed { overflow: hidden; }

/*
 * A picture that opens the viewer.
 *
 * One reset for all of them — the inbox, the media grid, the message preview —
 * because each is a <button> wrapped round an <img> and would otherwise carry
 * the agent's own border, padding and background over the picture. They stay
 * looking like pictures; only the cursor and the focus ring say otherwise.
 */
.cl-wa__media-open,
.cl-inbox__image-open,
.cl-med__thumb-open,
.cl-com__product-open {
    position: relative;
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: inherit;
    appearance: none;
    cursor: zoom-in;
    border-radius: inherit;
}

/*
 * Where the container sets its own aspect ratio and the picture fills it with
 * `height: 100%`, this button is now the thing that percentage resolves
 * against — so it has to be full height or the image falls back to its
 * intrinsic size and stops being cropped. That is not theoretical: it is what
 * made one product card render at a different size to the two beside it.
 *
 * Not applied to the inbox, whose picture is bounded by `max-height` and would
 * be stretched by this.
 */
.cl-med__thumb-open,
.cl-com__product-open { height: 100%; }

/*
 * The magnifier that says a picture opens.
 *
 * Drawn in CSS rather than added to each template: there are four of these and
 * the cue is the same everywhere, so an inline <span> per site would be four
 * copies of one idea. Hidden until hover or focus, so a grid of pictures still
 * reads as pictures.
 */
.cl-wa__media-open::after,
.cl-wa__card-media--link::after,
.cl-inbox__image-open::after,
.cl-med__thumb-open::after,
.cl-com__product-open::after {
    content: "";
    position: absolute;
    inset-block-end: 6px;
    inset-inline-end: 6px;
    width: 22px;
    height: 22px;
    border-radius: 999px;
    --cl-media-open-icon: var(--color-paper-raised);
    background-color: color-mix(in oklab, var(--color-ink) 72%, transparent);
    background-repeat: no-repeat;
    /* A token-coloured magnifier with a plus. Layered CSS keeps the cue tied
       to the active palette instead of baking a white stroke into a data URI. */
    background-image:
        linear-gradient(var(--cl-media-open-icon), var(--cl-media-open-icon)),
        linear-gradient(var(--cl-media-open-icon), var(--cl-media-open-icon)),
        linear-gradient(45deg,
            transparent 40%,
            var(--cl-media-open-icon) 40% 60%,
            transparent 60%),
        radial-gradient(circle at 43% 42%,
            transparent 0 3.4px,
            var(--cl-media-open-icon) 3.6px 4.8px,
            transparent 5px);
    background-position:
        43% 42%,
        43% 42%,
        calc(100% - 4px) calc(100% - 4px),
        center;
    background-size:
        1.5px 6px,
        6px 1.5px,
        6px 6px,
        100% 100%;
    opacity: 0;
    transition: opacity var(--dur-short) var(--ease-out);
    pointer-events: none;
}

.cl-wa__media-open:hover::after,
.cl-wa__media-open:focus-visible::after,
.cl-wa__card-media--link:hover::after,
.cl-wa__card-media--link:focus-visible::after,
.cl-inbox__image-open:hover::after,
.cl-inbox__image-open:focus-visible::after,
.cl-med__thumb-open:hover::after,
.cl-med__thumb-open:focus-visible::after,
.cl-com__product-open:hover::after,
.cl-com__product-open:focus-visible::after { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
    .cl-wa__media-open::after,
    .cl-wa__card-media--link::after,
    .cl-inbox__image-open::after,
    .cl-med__thumb-open::after,
    .cl-com__product-open::after { transition: none; }
}

.cl-wa__media-open:focus-visible,
.cl-inbox__image-open:focus-visible,
.cl-med__thumb-open:focus-visible,
.cl-com__product-open:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

.cl-lightbox {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-lg);
    background: color-mix(in oklab, var(--color-ink) 92%, transparent);
    cursor: zoom-out;
}

.cl-lightbox__img {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
    display: block;
    cursor: default;
    border-radius: var(--radius-xs);
}

.cl-lightbox__close {
    position: absolute;
    inset-block-start: var(--space-md);
    inset-inline-end: var(--space-md);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: color-mix(in oklab, var(--color-paper-raised) 12%, transparent);
    color: var(--color-accent-ink);
    cursor: pointer;
}

.cl-lightbox__close:hover { background: color-mix(in oklab, var(--color-paper-raised) 22%, transparent); }

.cl-lightbox__close:focus-visible {
    outline: 2px solid var(--color-accent-ink);
    outline-offset: 2px;
}

.cl-lightbox__close > svg { width: 20px; height: 20px; }

.cl-wa__card-body {
    font-size: 12px;
    line-height: 1.4;
    color: var(--color-ink-soft);
    /*
     * Exactly three lines, always.
     *
     * The body used to set the card's height, so a one-line card sat beside a
     * four-line one and the strip came out ragged — cards on a phone are a
     * fixed size, and a preview that varies with the copy misrepresents what
     * will be sent. Clamped to three and floored at three: long copy is cut
     * with an ellipsis, short copy still reserves the space, and the buttons
     * underneath line up across every card.
     *
     * The floor is computed from this element's own font-size and line-height,
     * so changing either above does not silently break the alignment.
     */
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: anywhere;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    overflow: hidden;
    min-height: calc(3 * 1.4 * 12px);
}

/*
 * A card's buttons, drawn the way WhatsApp draws them.
 *
 * They were a stack of green text with a 2px gap, which reads as three loose
 * links rather than as the tappable rows they become on a phone. The bubble's
 * own buttons (.cl-wa__btn) are already divided rows, centred, in the accent —
 * these are the same thing at card scale, so the preview is consistent with
 * itself and with the device it depicts.
 *
 * The negative margin pulls the group out to the card's edges: a divider that
 * stops short of the sides looks like an underline on the text above it, not
 * like the edge of a control.
 */
.cl-wa__card-btns {
    display: flex;
    flex-direction: column;
    margin: 4px calc(-1 * var(--space-xs)) calc(-1 * var(--space-xs));
}

.cl-wa__card-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 6px 8px;
    border-top: 1px solid var(--color-rule-soft);
    font-size: 11.5px;
    line-height: 1.35;
    text-align: center;
    color: var(--color-accent);
    overflow-wrap: anywhere;
}

/* Smaller than the bubble's own, in proportion to a 10.5px label on a 152px
   card. `flex: none` because the label is the part that may wrap. */
.cl-wa__card-btn > svg { flex: none; width: 11px; height: 11px; }

.cl-tagset { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: var(--space-sm); }
.cl-tagset__label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--color-muted); }

/* ── Errors + notes ───────────────────────────────────────────────────── */

.cl-errors {
    padding: var(--space-md);
    background: var(--color-halt-wash);
    border: 1px solid var(--color-halt-line);
    border-radius: var(--radius-md);
}

.cl-errors__title { display: block; font-size: 13px; color: var(--color-halt); }
.cl-errors__list { margin: var(--space-xs) 0 0; padding-inline-start: 1.1rem; font-size: 12.5px; color: var(--color-ink-soft); }
.cl-errors__list li { margin-top: 2px; }

.cl-note { margin: 0; font-size: 12.5px; color: var(--color-muted); }
.cl-note--bad { color: var(--color-halt); }
.cl-note--ok {
    padding: var(--space-sm) var(--space-md);
    background: var(--color-live-wash);
    border: 1px solid var(--color-live-line);
    border-radius: var(--radius-md);
    color: var(--color-accent);
}

/* ── Wizard navigation ────────────────────────────────────────────────── */

/*
 * No colophon under a wizard.
 *
 * The action bar is the floor of a step, and the colophon was sitting in the
 * 54px between it and the actual bottom — so the bar could never reach the
 * edge no matter how it was positioned. Two things cannot both be last.
 *
 * The bar wins, because a wizard is a task and the copyright line is page
 * furniture: nobody working through five steps needs Terms of service under
 * the Next button, and it is on every other page in the portal.
 *
 * :has() rather than a modifier class on the layout, so a page gets this by
 * containing a wizard rather than by remembering to declare that it does.
 */
.cl-main:has(.cl-wizard) .cl-colophon {
    display: none;
}

/*
 * The pane keeps its bottom padding under a wizard.
 *
 * It used to be removed here, back when the bar was sticky and had to sit on
 * the floor of the pane. The bar is in flow now (see below), and with no
 * padding under it the bar's own bottom edge was the last pixel of the
 * scroller — on a phone that put Back and Send hard against the bottom of the
 * screen, clipped by the browser's toolbar. The pane's ordinary padding gives
 * it the same clear space every other page ends with.
 */

/*
 * Back and Next, anchored to the bottom of the scroller.
 *
 * It was a floating pill 12px off the bottom, which on top of .cl-main's 24px
 * of padding left it hovering 30px clear of the edge with content sliding
 * underneath on both sides. Sticky was working; it just did not look anchored,
 * because a rounded island with a gap under it reads as something loose on the
 * page rather than as the floor of the step.
 *
 * Now it is the floor: flush to the bottom, edge to edge, one rule along the
 * top. The negative margins cancel .cl-main's own padding — inline so the bar
 * reaches both edges of the pane, and bottom so it sits on the boundary rather
 * than above it — and the padding is put back inside so the buttons stay on
 * the content's own measure.
 *
 * The inline margins are the same expression as .cl-main's horizontal padding
 * by necessity rather than coincidence — this bar cancels that padding, so if
 * one moves the other has to. Written identically so a search for either finds
 * both. The bottom padding is removed on the pane instead of cancelled here;
 * see the rule above for why.
 */
.cl-wizard__nav-end { display: flex; align-items: center; gap: var(--space-sm); }
.cl-wizard__hint { font-size: 12px; color: var(--color-muted); }

/* Back and Next share the same contained action surface on every step. It
   stays in document flow after the step content, so it appears only when the
   user reaches the bottom instead of covering the page while they work. */
.cl-wizard__nav {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    /* No gap here: on a phone the space between Back, Next and the line
       above them belongs to the items (see the end of this section). */
    width: 100%;
    /*
     * No max-width of its own. It had 72rem while every step is a .cl-wstep
     * filling the wizard's 76rem, so Back and Next sat in a bar four rem
     * narrower than the card it followed — visibly inset on every step.
     * The wizard already caps the column; a second, smaller cap here only
     * made the two disagree.
     */
    margin: var(--space-lg) auto 0;
    padding: var(--space-sm) var(--space-md);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-soft);
}

.cl-wizard__nav > .cl-btn--ghost {
    grid-column: 1;
    grid-row: 2;
}

.cl-wizard__nav > span { display: none !important; }

.cl-wizard__nav .cl-wizard__nav-end {
    grid-column: 2;
    grid-row: 2;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-sm);
    width: 100%;
    margin-inline-start: auto;
}

.cl-wizard__nav .cl-btn--primary {
    width: 100%;
    white-space: nowrap;
}

.cl-review-commit {
    grid-column: 1 / -1;
    grid-row: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2xs);
    min-width: 0;
}

.cl-review-commit__facts {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-xs);
    min-width: 0;
    font-size: 12.5px;
    color: var(--color-ink-soft);
    text-align: center;
    white-space: normal;
}

.cl-review-commit__facts strong {
    color: var(--color-ink);
    font-variant-numeric: tabular-nums;
}

.cl-review-commit__dot { color: var(--color-muted); }

.cl-review-commit__risk {
    display: none;
    min-width: 0;
    font-size: 11px;
    line-height: 1.4;
    color: var(--color-warm-ink);
}

@media (min-width: 40rem) {
    .cl-wizard__nav {
        display: flex;
        /*
         * Centred, not stretched.
         *
         * The default is stretch, so Back grew to the height of the row — and
         * on the last step the row is two lines of warning tall, which turned
         * a pill into an oval a third taller than the button it sits opposite.
         */
        align-items: center;
        /*
         * And spaced. The commit block carries the divider on its end edge,
         * which sat flush against Send message with nothing between them.
         * A gap here rather than padding on each side: one rule sets every
         * space in the row, so they cannot drift apart.
         */
        gap: var(--space-md);
        padding: var(--space-md);
        border-radius: var(--radius-lg);
    }

    .cl-wizard__nav > .cl-btn--ghost { grid-area: auto; }

    .cl-wizard__nav .cl-wizard__nav-end {
        grid-area: auto;
        display: flex;
        justify-content: flex-end;
        width: auto;
    }

    .cl-wizard__nav .cl-btn--primary { width: auto; }

    .cl-review-commit {
        grid-area: auto;
        flex: 1;
        align-items: flex-start;
        /* The row's gap now provides the space on both sides; keeping the
           padding as well would double it against Back. */
        border-inline-end: 1px solid var(--color-warm-line);
        padding-inline-end: var(--space-md);
    }

    .cl-review-commit__facts { text-align: start; }

    .cl-review-commit__risk {
        display: block;
        max-inline-size: 72ch;
        text-align: start;
        text-wrap: pretty;
    }
}

/*
 * The bar on a phone: a line of text across the top — the commit line on
 * review, or why Next is greyed out — and Back beside Next under it.
 *
 * The space between them travels with the items rather than being a grid
 * gap. Without one, Back and Next/Send touched and the commit line sat on the
 * buttons; but a gap also opens beside a track with nothing in it, so on a
 * step with no line of text the buttons sat 8px lower in the bar than its
 * padding, and on the first step, with Back hidden, Next started a gap's
 * width in from the edge. A hidden item takes its margin with it.
 *
 * The hint was hidden on a phone, so Next was greyed out on nearly every
 * step and tapping it said nothing. It takes the top row now. Dissolving the
 * end group puts it and the buttons on the bar's own grid: inside the group
 * it squeezed beside Next, and the row's stretch pulled Back to the height of
 * the sentence.
 */
@media (max-width: 39.99rem) {
    .cl-wizard__nav > .cl-btn--ghost { margin-inline-end: var(--space-sm); }
    .cl-review-commit { margin-block-end: var(--space-xs); }

    .cl-wizard__nav .cl-wizard__nav-end { display: contents; }

    .cl-wizard__nav .cl-wizard__hint {
        grid-column: 1 / -1;
        grid-row: 1;
        margin-block-end: var(--space-xs);
        text-align: start;
    }

    .cl-wizard__nav .cl-wizard__nav-end > .cl-btn {
        grid-column: 2;
        grid-row: 2;
    }
}

/* intl-tel-input sits inside our field; its own wrapper must fill it. */
.iti { width: 100%; }
.iti input.cl-input { padding-inline-start: 4.2rem; }

/* ── Template filters ─────────────────────────────────────────────────────
   What is being sent and in which language, then search, category and the
   type toggles — one row that wraps. The type filters narrow the query on the
   server, so they refetch rather than hide rows already downloaded. */

/* Aligned to the top, not the bottom.
   
   Bottom-aligned reads well only while every field is one control tall. The
   moment one is not — the type toggles wrapping to a second row is the way it
   happens — flex-end hangs the shorter fields off that group's baseline and
   lifts its label clear of the others, and a row whose four labels sit at
   three different heights reads as broken rather than as one bar. Every field
   is a label over a control of the same height, so aligning the tops puts all
   the labels on one line whatever any one group does below it. */
.cl-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--space-md);
}

/* Two groups, each wrapping inside itself: what is being sent stays together
   at the head of the row however narrow it gets, and the filters wrap behind
   it rather than around it. */
.cl-filters__setup,
.cl-filters__set {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--space-md);
}

.cl-filters__setup { flex: 0 1 auto; }

/*
 * A basis small enough that the two halves stay on one line.
 *
 * This was 420px, which with a wider setup half in front of it exceeded the
 * bar on a narrow desktop and sent the whole filter group under the language
 * box — leaving a hole beside it the width of the page. The half grows to
 * whatever is left either way, so the basis only ever decided when it stopped
 * fitting, and it was deciding too early.
 */
.cl-filters__set { flex: 1 1 16rem; }

.cl-fld--language { flex: 0 1 200px; }

/*
 * The language box when it is the only thing in the setup group.
 *
 * `.cl-filters__setup` is `flex: 0 1 auto`, so it sizes to what its children
 * *contain*, not to the flex-basis they ask for — and a flex-basis has no
 * effect once its parent has already collapsed around it. Beside the New
 * message wizard's send switch that goes unnoticed, because the switch makes
 * the group wide enough for the 200px above to land. Alone, as on the Auto
 * Broadcast template step, the group shrank to the width of the word inside
 * the button and the box came out at 95px — a control visibly squeezed next
 * to the full-size fields it leads.
 */
.cl-filters__setup:has(> .cl-fld--language:only-child) { flex: 0 1 16rem; }

/*
 * `flex-basis`, not `inline-size`.
 *
 * A flex item takes its main size from its basis, so the 200px above won
 * against any width set here and the box stayed 200px inside a group that had
 * grown around it. Handing it the whole group is what makes the two move
 * together.
 */
.cl-filters__setup > .cl-fld--language:only-child {
    flex-basis: 100%;
    max-inline-size: 100%;
}
.cl-fld--send { flex: 0 0 auto; }
/* Grows, within reason. A select reading "All categories" at half the width
   of the page is not better for being wider, hence the cap. */
.cl-fld--category { flex: 1 1 11rem; max-inline-size: 19rem; }

/* Sized to the toggles it holds, so it wraps as a group instead of inside
   itself.

   A basis in pixels is a guess at how wide four translated labels are, and it
   was wrong: 320px let the group sit beside the other two filters in a column
   too narrow for its own contents, so the toggles broke two-and-two into a
   ragged block. `max-content` is that measurement rather than a guess about
   it — the row breaks before this group when the toggles will not fit beside
   the filters, and the group then has the whole row and lays them out in one
   line. It shrinks below that only when the viewport itself is narrower,
   which is the one place wrapping them is right. */
.cl-fld--types { flex: 1 1 max-content; }

/* The language and the type are not filters: they decide which list is
   fetched at all, where the three behind this line only narrow it. A rule the
   height of one control says so without a second heading. */
.cl-filters__fence {
    align-self: stretch;
    width: 1px;
    min-height: 40px;
    background: var(--color-rule);
}

@media (max-width: 48rem) {
    .cl-filters__fence { display: none; }
}

/* On a phone the fields stack, one to a row, and share one edge. Sized for a
   shared row they ended at three different widths down the stack: the
   language box at its 200px or 16rem, search full width, the category
   capped at 19rem. */
@media (max-width: 40rem) {
    .cl-filters__setup,
    .cl-filters__setup:has(> .cl-fld--language:only-child),
    .cl-filters__set { flex: 1 1 100%; }

    .cl-filters__setup > .cl-fld--language,
    .cl-filters__set > .cl-fld--search,
    .cl-filters__set > .cl-fld--category {
        flex: 1 1 100%;
        max-inline-size: none;
    }
}

/* ── One message or a broadcast ───────────────────────────────────────────
   A switch, not a listbox. There are two of them, and which one is on changes
   every step after this — so both stay on screen, and the one in force is the
   one wearing the accent. Sized to the fields beside it, not to the tiles it
   replaced: this used to be two large cards on a step of their own. */

.cl-seg {
    display: inline-flex;
    padding: 2px;
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    background: var(--color-paper-sunken);
}

.cl-seg__opt {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 34px;
    padding: 0 var(--space-sm);
    border: 0;
    border-radius: calc(var(--radius-sm) - 2px);
    background: transparent;
    font-family: var(--font-sans);
    font-size: 12.5px;
    color: var(--color-ink-soft);
    white-space: nowrap;
    cursor: pointer;
    transition: background-color var(--dur-short) var(--ease-out),
                color var(--dur-short) var(--ease-out);
}

.cl-seg__opt svg { width: 15px; height: 15px; }
.cl-seg__opt:hover:not(.is-on) { color: var(--color-ink); }
.cl-seg__opt:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.cl-seg__opt.is-on {
    background: var(--color-accent);
    color: var(--color-accent-ink);
    font-weight: 600;
}

/* Raised only while the list is being fetched again, which is the moment a
   second click would start a second fetch. */
.cl-seg__opt:disabled { cursor: default; }

/* On a phone the two halves are 40px, the height of the fields around them.
   At 34 they were the smallest targets on a step whose every other control is
   44, and the one choice on it that changes every step after. */
@media (max-width: 40rem) {
    .cl-seg__opt { min-height: 40px; }
}

.cl-toggles { display: flex; flex-wrap: wrap; gap: 6px; }

.cl-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 40px;
    padding: 0 var(--space-sm);
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    font-family: var(--font-sans);
    font-size: 12.5px;
    color: var(--color-ink-soft);
    cursor: pointer;
    transition: border-color var(--dur-short) var(--ease-out),
                background-color var(--dur-short) var(--ease-out),
                color var(--dur-short) var(--ease-out);
}

.cl-toggle svg { width: 15px; height: 15px; }
.cl-toggle:hover { border-color: var(--color-rule-track); }
.cl-toggle:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

/* An engaged filter is unmistakably on — a faint tint reads as a hover
   state, and a filter someone forgot is on is a list they think is empty. */
.cl-toggle.is-on {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-accent-ink);
    font-weight: 600;
}

.cl-filters__summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    flex-wrap: wrap;
}

/* ── Media slot ───────────────────────────────────────────────────────────
   A template whose header carries an image, video or document sends real
   media the recipient will see. Naming it in a line of text undersold that,
   so it is drawn as a media block.

   The mark inside runs the same product story as the rail — including real
   estate alerts and commerce — before resting on confirmation. */

.cl-slot {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 16 / 9;
    width: 100%;
    margin-bottom: var(--space-xs);
    overflow: hidden;
    border-radius: var(--radius-sm);
    background: var(--background-media-preview);
    border: 1px solid var(--color-rule-soft);
}

/* The engine is fixed at 48px for the rail; a media block wants it large.
   Overriding the flex basis too, since the rail pins it. */
.cl-slot__engine {
    flex: 0 0 auto;
    width: min(46%, 108px);
    height: auto;
    margin: 0;
}

/* No colour overrides here on purpose. The engine's keyframes set `stroke`
   themselves — resolving from ink to accent as the story lands — and a CSS
   animation outranks a normal declaration, so an override on the shell could
   never apply. On the scenes it *would* have applied, which is worse: the
   slot would have drifted from the rail it is supposed to match. */

/* The format, named on the block itself. */
.cl-slot__label {
    position: absolute;
    inset-inline-start: var(--space-xs);
    bottom: var(--space-xs);
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 9px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--color-rule-soft);
    background: var(--color-paper-raised);
    font-size: 11px;
    font-weight: 600;
    color: var(--color-ink-soft);
}

.cl-slot__label svg { width: 12px; height: 12px; }

/* Every format shares the same plane. Its icon and label carry the distinction,
   which keeps template previews consistent across pages and card sizes. */
.cl-slot--video {
    background: var(--background-media-preview);
}

.cl-slot--document {
    background: var(--background-media-preview);
}


/* ── New message: the context bar ─────────────────────────────────────────
   What was chosen on the first step, kept in view once that step is gone —
   and still changeable while changing it is cheap. Reads as a line of text
   with two of its words swappable, not as a form: it is a reminder first and
   a control second. */

.cl-ctx {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2xs) var(--space-xs);
    margin-bottom: var(--space-md);
    padding: var(--space-2xs) var(--space-sm);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-pill);
    background: var(--color-paper-raised);
    font-size: 12.5px;
    color: var(--color-muted);
}

.cl-ctx__label {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    color: var(--color-muted);
}

.cl-ctx__label svg {
    width: 13px;
    height: 13px;
    fill: none;
    stroke: var(--color-live);
    stroke-width: 2.4;
}

.cl-ctx__sep { color: var(--color-rule-track); }

/* Fixed: the choice is no longer editable at this step, so it is set as
   plain emphasis rather than as a disabled control — a greyed-out select
   invites a click that will never do anything. */
.cl-ctx__fixed { font-weight: 600; color: var(--color-ink); }

.cl-ctx__field { display: inline-flex; min-width: 0; }

.cl-ctx__pick {
    max-width: 100%;
    padding: 2px 4px;
    border: 1px solid transparent;
    border-radius: var(--radius-xs);
    background: transparent;
    font: inherit;
    font-weight: 600;
    color: var(--color-accent);
    cursor: pointer;
    transition: border-color var(--dur-short) var(--ease-out),
                background-color var(--dur-short) var(--ease-out);
}

.cl-ctx__pick:hover {
    border-color: var(--color-accent-line);
    background: var(--color-accent-wash);
}

.cl-ctx__pick:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }

/* The cost of changing the type here, said before it is pressed. */
.cl-ctx__note {
    margin-inline-start: auto;
    font-size: 11.5px;
    color: var(--color-warn);
}

@media (max-width: 34rem) {
    .cl-ctx { border-radius: var(--radius-md); }
    .cl-ctx__note { margin-inline-start: 0; flex-basis: 100%; }
}


/* ── Rail: links that leave the portal ────────────────────────────────────
   Four go to the WABA console and one to the public API reference. They are
   marked rather than styled differently: the same row, the same weight, with
   a small corner arrow at the trailing edge — the mark every interface uses
   for "this opens somewhere else". Quiet at rest so the list still reads as
   one list, and it takes the link's own colour on hover. */

.cl-rail__away {
    width: 12px;
    height: 12px;
    margin-inline-start: auto;
    flex-shrink: 0;
    opacity: .45;
    transition: opacity var(--dur-short) var(--ease-out);
}

.cl-rail__link--away:hover .cl-rail__away,
.cl-rail__link--away:focus-visible .cl-rail__away { opacity: 1; }

/*
 * The corner arrow points out of the page, so it has to point the way the page
 * leaves. `margin-inline-start: auto` already moves it to the trailing edge in
 * Arabic; without this it would sit on the left pointing right, back into the
 * text it just left. An SVG path mirrors for nobody — only the transform does.
 */
[dir="rtl"] .cl-rail__away { transform: scaleX(-1); }

@media (prefers-reduced-motion: reduce) {
    .cl-rail__away { transition: none; }
}


/* ── Rail: a section that needs a subscription ────────────────────────────
   The row stays legible — this is a feature someone might want, not an error
   — but it is quieter than the sections they can actually open, and it ends
   in the word PRO so the state is read before the row is clicked. */

.cl-rail__link.is-locked { color: var(--color-muted); }

.cl-rail__link.is-locked svg:first-child { opacity: .6; }

.cl-rail__link.is-locked:hover { color: var(--color-ink-soft); }

.cl-rail__pro {
    margin-inline-start: auto;
    padding: 1px 6px;
    border-radius: var(--radius-pill);
    background: var(--color-warm-wash);
    font-family: var(--font-display);
    font-size: 9.5px;
    font-weight: 700;
    letter-spacing: .06em;
    color: var(--color-warm-ink);
    flex-shrink: 0;
}


/* ── Phone input: the country picker ──────────────────────────────────────
   intl-tel-input ships its own stylesheet, which is a plain white 1px-border
   list with 12px rows. It is the only control in the portal that did not look
   like the portal, and at that size the flags and dial codes are hard to hit
   and harder to read.

   Styled here rather than in a page sheet because the same picker appears in
   registration and in the new-message wizard, and a control that looks like
   two different controls in two places is worse than one that looks like
   neither. */

.iti { width: 100%; }

/* The button that opens the list. */
.iti__selected-country.iti__selected-country {
    /* Rounded on the leading edge only — the side of the field the country
       button sits on, which intl-tel-input moves itself under [dir=rtl]. */
    border-start-start-radius: var(--radius-sm);
    border-start-end-radius: 0;
    border-end-end-radius: 0;
    border-end-start-radius: var(--radius-sm);
    transition: background-color var(--dur-short) var(--ease-out);
}

.iti__selected-country:hover,
.iti__selected-country:focus-visible { background: var(--color-paper-sunken); }

.iti__selected-dial-code {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    color: var(--color-ink-soft);
}

/*
 * The panel.
 *
 * Selectors are doubled from here down. intl-tel-input's own stylesheet is
 * imported by the JavaScript bundle, so it lands after this file in the
 * document and wins every tie on equal specificity — the rows restyled
 * happily while the panel kept its 3px corners. Doubling the class outranks
 * it without resorting to !important.
 */
.iti__dropdown-content.iti__dropdown-content {
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-md);
    background: var(--color-paper-raised);
    box-shadow: var(--shadow-soft), var(--shadow-sheet);
    overflow: hidden;
}

/* The search box, which the library renders bare. */
.iti__search-input.iti__search-input {
    width: 100%;
    padding: var(--space-xs) var(--space-sm);
    border: 0;
    border-bottom: 1px solid var(--color-rule-soft);
    background: var(--color-paper-raised);
    font-family: var(--font-sans);
    font-size: 13px;
    color: var(--color-ink);
}

.iti__search-input::placeholder { color: var(--color-muted); }
.iti__search-input:focus { outline: none; background: var(--color-paper-sunken); }

.iti__country-list.iti__country-list {
    max-width: 100%;
    max-height: 17rem;
    padding: 4px;
    scrollbar-width: thin;
    scrollbar-color: var(--color-rule) transparent;
}

.iti__country-list::-webkit-scrollbar { width: 4px; }
.iti__country-list::-webkit-scrollbar-track { background: transparent; }
.iti__country-list::-webkit-scrollbar-thumb { background: var(--color-rule); border-radius: var(--radius-pill); }

/* A row you can actually hit: 40px, the portal's own control height. */
.iti__country.iti__country {
    display: flex;
    align-items: center;
    gap: var(--space-2xs);
    min-height: 40px;
    padding: var(--space-2xs) var(--space-xs);
    border-radius: var(--radius-sm);
    font-size: 13px;
    color: var(--color-ink);
}

.iti__country-name { color: var(--color-ink); }

.iti__dial-code {
    margin-inline-start: auto;
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    font-size: 12px;
    color: var(--color-muted);
}

/* Hover and keyboard highlight share one neutral state; selection remains the
   explicit tick, so moving through the list does not look like committing. */
.iti__country.iti__highlight,
.iti__country:hover {
    background: var(--color-paper-sunken);
    color: var(--color-ink);
}

.iti__country.iti__highlight .iti__country-name,
.iti__country:hover .iti__country-name { color: var(--color-ink); }

/* The line under the preferred countries — Morocco first, then the three the
   product actually sends to. */
.iti__divider.iti__divider {
    margin: 4px 0;
    border-bottom: 1px solid var(--color-rule-soft);
}

.iti__flag-box { flex: 0 0 auto; }

/* ── The flags themselves ─────────────────────────────────────────────────
   intl-tel-input draws every flag at 16×12 from one 3904px sprite strip. At
   that size a flag is not readable as a flag: Morocco's is 754 red pixels and
   14 green ones, so the star that distinguishes it disappears and the control
   shows a plain red rectangle. Several of the countries this product sends to
   are in the same position — plain fields, or a device whose detail is a few
   pixels across.

   Scaling it is not a matter of setting a bigger width. The sprite is one
   strip and each country's horizontal offset is a hard-coded pixel value in
   the library's own CSS (`.iti__ma { --iti-flag-offset: -2128px }`, and 240-odd
   more) — widen the window alone and every flag shows a slice of its
   neighbour.

   So the offset is multiplied rather than replaced. The library already keeps
   the per-country value in a custom property, which means one rule here scales
   every country at once, correctly, with no per-country overrides to fall out
   of step when the library's sprite changes.

   1.5 is chosen because it is the largest factor that keeps every derived
   value on a whole pixel — 24×18 from 16×12, a 5856×18 sprite, and Morocco at
   exactly -3192px. A sprite on a half-pixel resamples, and a resampled flag is
   blurry in a way a solid-coloured one hides but a detailed one does not. */
.iti__flag.iti__flag {
    --cl-flag-scale: 1.5;

    width: calc(var(--iti-flag-width) * var(--cl-flag-scale));
    height: calc(var(--iti-flag-height) * var(--cl-flag-scale));
    background-size:
        calc(var(--iti-flag-sprite-width) * var(--cl-flag-scale))
        calc(var(--iti-flag-sprite-height) * var(--cl-flag-scale));
    background-position: calc(var(--iti-flag-offset) * var(--cl-flag-scale)) 0;

    /* 1px of radius on a 24px flag reads as a square corner. */
    border-radius: 2px;
}

/* The globe stands in for "no country chosen". It is a single image rather
   than a window onto the strip, so it takes the size but not the offset. */
.iti__globe.iti__globe {
    background-size: contain;
    background-position: center;
}


/* ── Listbox ──────────────────────────────────────────────────────────────
   The portal's own dropdown, for the places a native <select> will not do.
   A <select> draws its popup with the operating system — grey system menu,
   system font, system row height — and no stylesheet reaches inside it. This
   is a button and a list, so it can look like everything around it. */

.cl-lb { position: relative; }

.cl-lb__button {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-xs);
    width: 100%;
    min-height: 40px;
    padding: var(--space-2xs) var(--space-sm);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-sm);
    background: var(--color-paper-raised);
    font-family: var(--font-sans);
    font-size: 13px;
    color: var(--color-ink);
    text-align: start;
    cursor: pointer;
    transition: border-color var(--dur-short) var(--ease-out);
}

.cl-lb__button:hover { border-color: var(--color-rule-control); }
.cl-lb__button:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.cl-lb__button[aria-expanded="true"] { border-color: var(--color-accent); }

.cl-lb__value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Nothing chosen yet reads as a prompt, not as a value. */
.cl-lb__value.is-placeholder { color: var(--color-muted); }

.cl-lb__caret { width: 15px; height: 15px; flex-shrink: 0; color: var(--color-muted); }

.cl-lb__button[aria-expanded="true"] .cl-lb__caret { transform: rotate(180deg); }

.cl-lb__panel {
    position: absolute;
    z-index: var(--z-dropdown);
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    max-height: 15rem;
    overflow-y: auto;
    padding: 4px;
    margin: 0;
    list-style: none;
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-md);
    background: var(--color-paper-raised);
    box-shadow: var(--shadow-soft), var(--shadow-sheet);
    scrollbar-width: thin;
    scrollbar-color: var(--color-rule) transparent;
}

.cl-lb__panel::-webkit-scrollbar { width: 4px; }
.cl-lb__panel::-webkit-scrollbar-track { background: transparent; }
.cl-lb__panel::-webkit-scrollbar-thumb { background: var(--color-rule); border-radius: var(--radius-pill); }

.cl-lb__option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-xs);
    min-height: 36px;
    padding: var(--space-2xs) var(--space-xs);
    border-radius: var(--radius-sm);
    font-size: 13px;
    color: var(--color-ink);
    cursor: pointer;
}

/*
 * Hover and keyboard are one state deliberately: two different highlights on
 * one list means the reader has to work out which one the Enter key will take.
 */
.cl-lb__option.is-active { background: var(--color-paper-sunken); color: var(--color-ink); }

.cl-lb__option.is-selected { font-weight: 600; }

.cl-lb__tick {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    opacity: 0;
    color: var(--color-accent);
}

.cl-lb__option.is-selected .cl-lb__tick { opacity: 1; }

.cl-lb__empty {
    padding: var(--space-xs);
    font-size: 12.5px;
    color: var(--color-muted);
}


/* The listbox, inline in a line of text — the context bar's two choices read
   as words in a sentence, so the control is the word plus a caret rather than
   a bordered field sitting in a bar. */

/* Sized to its own content, not to the row. Without this the wrapper takes a
   full flex line and the control drifts to the far left of an empty bar. */
.cl-lb--inline { flex: 0 0 auto; width: auto; }

.cl-lb--inline .cl-lb__button {
    min-height: auto;
    width: auto;
    padding: 2px 6px;
    border-color: transparent;
    background: transparent;
    font-weight: 600;
    color: var(--color-accent);
}

.cl-lb--inline .cl-lb__button:hover {
    border-color: var(--color-accent-line);
    background: var(--color-accent-wash);
}

.cl-lb--inline .cl-lb__caret { width: 13px; height: 13px; color: currentColor; }

/* Sized to its own options rather than to the sentence it sits in. */
.cl-lb--inline .cl-lb__panel { inset-inline-end: auto; min-width: 12rem; }


/* ── The listbox as a form field ──────────────────────────────────────────
   The sending-number picker. It stands where a `.cl-input` would, so it
   matches the height and the invalid state of the fields above and below it
   rather than looking like a control borrowed from somewhere else. */

.cl-lb--field .cl-lb__button { min-height: 44px; font-size: 14px; }

.cl-lb--field.is-invalid .cl-lb__button { border-color: var(--color-halt); }

/* The button is space-between for the two-child case (value, caret). With a
   flag in front, the value has to claim the middle or the three children
   spread out across the control. */
.cl-lb--field .cl-lb__value { flex: 1 1 auto; }

.cl-lb__button:disabled {
    background: var(--color-paper-sunken);
    color: var(--color-muted);
    cursor: not-allowed;
}

.cl-lb__button:disabled:hover { border-color: var(--color-rule); }

/* ── Flags ────────────────────────────────────────────────────────────────
   A country flag, at a size where it is actually a flag. These are uploaded
   images of real proportions, not a sprite, so they get a fixed box and
   `cover`: a 3:2 flag and a 1:1 flag in the same column with `contain` would
   sit at two different visual weights and make the list look uneven.

   The hairline matters more than it looks. Several of the flags this product
   carries are white at the edge, and without it they bleed into the paper and
   read as a torn shape rather than a flag. */
.cl-lb__flag {
    flex: 0 0 auto;
    width: 26px;
    height: 18px;
    object-fit: cover;
    border-radius: 3px;
    box-shadow: 0 0 0 1px var(--color-rule-soft);
}

/* ── A number as an option ────────────────────────────────────────────────
   Two lines: the number itself, and the country under it. The number is what
   is being chosen, so it is the one set in the tabular face — a column of
   dial-coded numbers only reads as a column if the digits line up. */

/* Taller than a one-line option: the base 36px leaves a two-line row with no
   air above the number and none below the country. */
.cl-lb__option--number { min-height: 44px; align-items: center; gap: var(--space-xs); }

.cl-lb__stack {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
    flex: 1 1 auto;
}

.cl-lb__line {
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* On a phone a sending number wraps rather than losing its last digits —
   "Morocco - +21265406…" — in the list and in the closed control alike. The
   number is the thing being chosen. */
@media (max-width: 40rem) {
    .cl-lb__option--number .cl-lb__line,
    .cl-lb:has(.cl-lb__option--number) .cl-lb__value {
        white-space: normal;
        overflow-wrap: anywhere;
    }
}

.cl-lb__sub {
    font-size: 11.5px;
    color: var(--color-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The highlight already carries the accent; the sub-line following it too
   would leave the row with no quiet part. */
.cl-lb__option.is-active .cl-lb__sub { color: var(--color-ink-soft); }

/* ── Announcements ────────────────────────────────────────────────────────
 *
 * The dashboard's marketing modals, shown in the portal. Shell chrome: the
 * markup exists only on the page views where there is one to show, so nothing
 * here is paid for by the pages that have none.
 *
 * The body is arbitrary HTML written in CKEditor by whoever wrote the
 * announcement, so the rules below have to make *anything* legible — headings
 * nobody sized, images at their upload dimensions, a YouTube embed with a
 * hard-coded width. That is why this block styles descendants rather than
 * classes: there are no classes to hook, and there never will be.
 */

.cl-ann__scrim {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    background: var(--color-scrim);
}

.cl-ann__modal {
    position: fixed;
    z-index: calc(var(--z-modal) + 1);
    top: 50%;
    inset-inline-start: 50%;
    transform: translate(-50%, -50%);
    width: min(44rem, calc(100vw - 2 * var(--space-md)));
    max-height: min(90vh, 54rem);
    /* The visible height on a phone, where 90vh is measured with the browser's
       toolbar hidden and could push the foot — the only buttons out — under
       it. The same everywhere else. */
    max-height: min(90dvh, 54rem);
    display: flex;
    flex-direction: column;
    border-radius: var(--radius-lg);
    background: var(--color-paper-raised);
    box-shadow: var(--shadow-soft), var(--shadow-sheet);
    overflow: hidden;
    animation: cl-ann-rise var(--dur-mid) var(--ease-out) both;
}

.cl-ann__modal:focus { outline: none; }

/* It arrives on page load rather than on a click, so it rises into place
   instead of appearing: a dialog that is simply *there* on the first paint
   reads as something that went wrong. */
@keyframes cl-ann-rise {
    from { opacity: 0; transform: translate(-50%, calc(-50% + 12px)) scale(0.985); }
    to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

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

/* Floats over the content: an announcement usually opens with a picture, and
   a close button in a bar above it would push that picture down the screen. */
.cl-ann__close {
    position: absolute;
    top: var(--space-sm);
    inset-inline-end: var(--space-sm);
    z-index: 1;
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    border: 1px solid var(--color-rule-soft);
    border-radius: 50%;
    background: var(--color-paper-raised);
    color: var(--color-muted);
    cursor: pointer;
    transition: color var(--dur-short) var(--ease-out),
                background var(--dur-short) var(--ease-out);
}

.cl-ann__close svg { width: 14px; height: 14px; }
.cl-ann__close:hover { background: var(--color-paper-sunken); color: var(--color-ink); }
.cl-ann__close:disabled { opacity: 0.5; cursor: not-allowed; }

.cl-ann__body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--space-lg);
    color: var(--color-ink-soft);
    font-size: 14.5px;
    line-height: 1.6;
}

/* ── Whatever was written in the editor ──────────────────────────────── */

.cl-ann__body > :first-child { margin-top: 0; }
.cl-ann__body > :last-child { margin-bottom: 0; }

.cl-ann__body p { margin: 0 0 var(--space-sm); }

.cl-ann__body h1,
.cl-ann__body h2,
.cl-ann__body h3,
.cl-ann__body h4 {
    margin: var(--space-md) 0 var(--space-xs);
    font-family: var(--font-display);
    font-weight: 600;
    line-height: 1.25;
    color: var(--color-ink);
    /* A pasted heading can be one long unbroken word — a URL, a product code —
       and the dialog must not scroll sideways because of it. */
    overflow-wrap: anywhere;
}

/* The close button floats over this corner, so an opening heading keeps out
   of its way. Only the first, and only a heading: a picture may sit under it
   quite happily. */
.cl-ann__body > h1:first-child,
.cl-ann__body > h2:first-child,
.cl-ann__body > h3:first-child,
.cl-ann__body > h4:first-child { padding-inline-end: 2.5rem; }

.cl-ann__body h1 { font-size: 24px; }
.cl-ann__body h2 { font-size: 20px; }
.cl-ann__body h3 { font-size: 17px; }
.cl-ann__body h4 { font-size: 15px; }

.cl-ann__body a {
    color: var(--color-accent);
    text-decoration: underline;
    text-underline-offset: 2px;
    overflow-wrap: anywhere;
}

.cl-ann__body strong { color: var(--color-ink); font-weight: 600; }

/* The markers come back. The sheet's reset strips them from every list,
   which is right for a navigation and wrong for a written list — three
   sentences with no bullets read as three loose paragraphs. */
.cl-ann__body ul,
.cl-ann__body ol { margin: 0 0 var(--space-sm); padding-inline-start: 1.25rem; }
.cl-ann__body ul { list-style: disc; }
.cl-ann__body ol { list-style: decimal; }
.cl-ann__body li::marker { color: var(--color-muted); }

.cl-ann__body li { margin-bottom: 4px; }

.cl-ann__body img {
    max-width: 100%;
    height: auto;
    border-radius: var(--radius-md);
}

.cl-ann__body figure { margin: var(--space-sm) 0; }
.cl-ann__body figcaption { margin-top: 6px; font-size: 12.5px; color: var(--color-muted); }

/* A video keeps its shape at any width. The editor writes no dimensions and
   the frame the service builds carries none either, so the ratio is stated
   here rather than left to the iframe's 300×150 default. */
.cl-ann__body iframe {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    height: auto;
    border: 0;
    border-radius: var(--radius-md);
}

/* Pasted from a spreadsheet more often than not: it scrolls inside its own
   frame rather than widening the dialog. */
.cl-ann__body table {
    display: block;
    width: 100%;
    overflow-x: auto;
    border-collapse: collapse;
    font-size: 13px;
}

.cl-ann__body th,
.cl-ann__body td {
    padding: 6px 10px;
    border: 1px solid var(--color-rule-soft);
    text-align: start;
}

.cl-ann__body blockquote {
    margin: var(--space-sm) 0;
    padding-inline-start: var(--space-sm);
    border-inline-start: 2px solid var(--color-rule-track);
    color: var(--color-muted);
}

.cl-ann__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    flex-wrap: wrap;
    padding: var(--space-sm) var(--space-lg);
    border-top: 1px solid var(--color-rule-soft);
    background: var(--color-paper);
}

/* The permanent one is the quieter of the two, and quieter still than a
   normal quiet button: it is the decision nobody should make by accident. */
.cl-ann__never { color: var(--color-muted); }
.cl-ann__never:hover { color: var(--color-ink); }

/* On a phone the close button is a 44px target like the top-up dialog's: it
   is the way out that is not a decision about the announcement. The target
   grows around the 30px disc rather than the disc itself, so the body's
   opening heading keeps the clearance it was given and nothing moves. */
@media (max-width: 40rem) {
    .cl-ann__close::after {
        content: "";
        position: absolute;
        inset: -7px;
    }
}

@media (max-width: 30rem) {
    .cl-ann__modal { width: calc(100vw - 2 * var(--space-sm)); }
    .cl-ann__body { padding: var(--space-md); }
    .cl-ann__foot { padding: var(--space-sm) var(--space-md); }
    /* Stacked, and the way out on top: at this width the two buttons side by
       side leave neither of them a full tap target. */
    .cl-ann__foot { flex-direction: column-reverse; align-items: stretch; }
    .cl-ann__foot .cl-btn { justify-content: center; }
}

/* ── The announcement banner ──────────────────────────────────────────────
 *
 * The other half of Admin → Marketing Modals: a strip across the top of the
 * portal, in the colours whoever wrote it chose.
 *
 * It sits above .cl-shell in normal flow, so it spans the rail too and scrolls
 * away with the page. Not fixed and not sticky: the rail and the topbar are
 * both already `position: sticky; top: 0`, and a third thing competing for
 * that line either covers one of them or permanently shortens the viewport.
 *
 * The two colours arrive as custom properties on the element itself, so an
 * announcement with none set falls through to the palette in force — which is
 * the right default, and one that follows the theme when it is switched.
 */
.cl-banner {
    --cl-banner-bg: var(--color-accent);
    --cl-banner-ink: var(--color-accent-ink);

    position: relative;
    z-index: calc(var(--z-sticky) + 1);
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 10px clamp(0.75rem, 3vw, 1.5rem);
    background: var(--cl-banner-bg);
    color: var(--cl-banner-ink);
    font-size: 13.5px;
    line-height: 1.45;
}

.cl-banner__inner {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: var(--space-xs) var(--space-sm);
    /* Clear of the close button, and of the dots when there are several: both
       are absolute at the trailing edge, and without this the text centres
       against the full width and drifts under them. */
    padding-inline-end: var(--space-md);
}

/* Room for the dots, only on a strip that has them. */
.cl-banner:has(.cl-banner__dots) .cl-banner__inner {
    padding-inline-end: calc(var(--space-md) + 60px);
}

/*
 * The editor's HTML, flattened to a line.
 *
 * The same CKEditor box writes both kinds, so a banner can arrive carrying an
 * <h2>, three paragraphs and a picture. In a 44px strip none of that is a
 * layout — it is a broken one. Everything block-level goes inline and the
 * type stops varying, which turns a pasted essay into a long sentence rather
 * than into a wall.
 */
.cl-banner__text { min-width: 0; text-align: center; }

.cl-banner__text > :first-child { margin-top: 0; }
.cl-banner__text > :last-child { margin-bottom: 0; }

.cl-banner__text p,
.cl-banner__text h1,
.cl-banner__text h2,
.cl-banner__text h3,
.cl-banner__text h4,
.cl-banner__text ul,
.cl-banner__text ol,
.cl-banner__text li {
    display: inline;
    margin: 0;
    padding: 0;
    font-size: inherit;
    font-weight: inherit;
    line-height: inherit;
    color: inherit;
}

/*
 * Everything is inline now, so two blocks that were paragraphs run together
 * into one word — "…in a bannerFirst line with…". A middot puts them back
 * apart without pretending the strip has paragraphs in it.
 *
 * Not after something that was dropped: a banner opening with a video and then
 * a line of text would otherwise start with a dot and no sentence in front
 * of it.
 */
.cl-banner__text > * + *::before,
.cl-banner__text li + li::before { content: ' · '; opacity: 0.6; font-weight: 400; }

.cl-banner__text > iframe + *::before,
.cl-banner__text > figure.media + *::before,
.cl-banner__text > oembed + *::before,
.cl-banner__text > video + *::before,
.cl-banner__text > table + *::before,
.cl-banner__text > blockquote + *::before { content: none; }

.cl-banner__text strong,
.cl-banner__text b { font-weight: 700; }

.cl-banner__text a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* An image in a banner is a mark beside the words, not a picture. Capped to
   the line's own height so a 1200px hero pasted into the editor becomes a
   logo-sized mark rather than a banner three screens tall. */
.cl-banner__text img {
    display: inline-block;
    vertical-align: text-bottom;
    max-height: 1.6em;
    width: auto;
    border-radius: 4px;
}

/* CKEditor wraps an image in <figure class="image">, so the figure has to flow
   inline or the image inside it is a block in a strip — and dropping figures
   wholesale would take the image with it. */
.cl-banner__text figure { display: inline; margin: 0; }
.cl-banner__text figcaption { display: none; }

/* A video cannot be in a strip. It has a page of its own to live on — put it
   in a modal announcement instead. `figure.media` is what the editor wraps an
   embed in; the iframe rule catches one pasted as raw HTML. */
.cl-banner__text iframe,
.cl-banner__text figure.media,
.cl-banner__text oembed,
.cl-banner__text video,
.cl-banner__text table,
.cl-banner__text blockquote { display: none; }

/*
 * The action. Drawn in the banner's own ink on a wash of it, so it reads as a
 * control against any background the admin picks — a fixed colour would be
 * invisible on the half of them that happen to match it.
 */
.cl-banner__action {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 12px;
    border: 1px solid color-mix(in oklab, var(--cl-banner-ink) 45%, transparent);
    border-radius: 999px;
    background: color-mix(in oklab, var(--cl-banner-ink) 12%, transparent);
    color: inherit;
    font-weight: 600;
    font-size: 12.5px;
    white-space: nowrap;
    transition: background var(--dur-short) var(--ease-out),
                border-color var(--dur-short) var(--ease-out);
}

.cl-banner__action:hover {
    background: color-mix(in oklab, var(--cl-banner-ink) 22%, transparent);
    border-color: color-mix(in oklab, var(--cl-banner-ink) 70%, transparent);
}

.cl-banner__action svg { width: 12px; height: 12px; }

/*
 * Which of several announcements is showing, and a way to the others.
 *
 * Only rendered when there is more than one — one dot is not a control. They
 * sit beside the close button rather than in the middle of the strip: the
 * middle is where the words are, and a row of dots under a single line of text
 * reads as a carousel of pictures.
 *
 * Drawn in the banner's own ink at two opacities, because the strip's colours
 * are the admin's and a fixed grey would be invisible on half of them.
 */
.cl-banner__dots {
    position: absolute;
    top: 50%;
    inset-inline-end: calc(clamp(0.5rem, 2vw, 1rem) + 30px);
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    gap: 5px;
}

.cl-banner__dot {
    width: 7px;
    height: 7px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: currentColor;
    opacity: 0.35;
    cursor: pointer;
    transition: opacity var(--dur-short) var(--ease-out),
                transform var(--dur-short) var(--ease-out);
}

.cl-banner__dot:hover { opacity: 0.7; }
.cl-banner__dot.is-current { opacity: 1; transform: scale(1.25); }

/* The dot is 7px, which is far below any reasonable tap target, so the target
   is grown around it without growing the mark itself. */
.cl-banner__dot::after {
    content: '';
    position: absolute;
    inset: -8px;
}

.cl-banner__dot { position: relative; }

.cl-banner__close {
    position: absolute;
    top: 50%;
    inset-inline-end: clamp(0.5rem, 2vw, 1rem);
    transform: translateY(-50%);
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: inherit;
    opacity: 0.7;
    cursor: pointer;
    transition: opacity var(--dur-short) var(--ease-out),
                background var(--dur-short) var(--ease-out);
}

.cl-banner__close:hover {
    opacity: 1;
    background: color-mix(in oklab, var(--cl-banner-ink) 15%, transparent);
}

.cl-banner__close svg { width: 13px; height: 13px; }

/* The rail is `height: 100vh` and sticks to the top of the viewport. With a
   banner above it, that is a viewport plus a banner of content in a viewport
   of space — so the last nav item is under the fold until the banner has
   scrolled away. Taking the banner's height off it costs nothing when there is
   no banner, because then this rule does not match. */
/* The rail's height used to be reduced by the banner here. It is not needed
   now: body is a flex column, so the shell — and the rail inside it — is
   already whatever is left after the banner, at any banner height. */

@media (max-width: 40rem) {
    /* The extra room is at the trailing edge, where the close button is. */
    .cl-banner {
        padding-block: 9px;
        padding-inline: 0.75rem 2.25rem;
        font-size: 12.5px;
    }
    .cl-banner__inner { justify-content: flex-start; padding-inline-end: 0; }
    .cl-banner__text { text-align: start; }

    /* Pinned to the top rather than centred: at this width the text runs to
       three or four lines, and a dismiss control floating beside the middle
       one reads as though it belongs to that line. */
    .cl-banner__close { top: 7px; transform: none; }

    /* A 26px dismiss is far below a thumb, so the target grows around it the
       way the dots' does — 40px, without moving the mark or the text. */
    .cl-banner__close::after {
        content: "";
        position: absolute;
        inset: -7px;
    }

    /* And the dots move under the text rather than squeezing beside it: at
       375px there is no room for both, and the text is what matters. */
    .cl-banner__dots {
        position: static;
        transform: none;
        justify-content: flex-start;
        margin-top: 6px;
    }

    .cl-banner:has(.cl-banner__dots) .cl-banner__inner { padding-inline-end: 1.5rem; }

    /* Which makes the strip a column: words, then action, then dots. */
    .cl-banner { flex-wrap: wrap; }
    .cl-banner__dots { flex-basis: 100%; }
}

/* ── The Overview's number picker ─────────────────────────────────────────
 *
 * Which WhatsApp number the dashboard's figures are for. It sits in the page
 * header beside "New message", and it exists only for accounts that send
 * through more than one number — see DashboardScopeData::hasChoice.
 */
/* The picker and the page's primary action share the header's end, so this
   sits inside a row rather than starting one — `display: contents` lets the
   select be a sibling of the button instead of a block above it. */
.cl-head__end:has(.cl-scope) { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; }

.cl-scope { display: flex; align-items: center; gap: var(--space-xs); }

/* Wide enough for "Mercedes-Benz Maroc — +212 80-1020020" without the name
   being cut to an initial, and capped so a long one cannot push the primary
   action off the header.

   Both selectors, because portal/select.js upgrades this select into a .cl-lb
   listbox and hides the original — so a rule written only for `select` stopped
   applying to anything visible the moment that ran, and the picker collapsed
   to whatever its own text needed. The listbox is the one on screen; the
   native element is kept for the form, so it is sized too. */
.cl-scope select,
.cl-scope .cl-lb { min-width: 15rem; max-width: min(24rem, 46vw); }

/* The listbox positions its own button, which has to fill the wrapper rather
   than shrink to the label inside it. */
.cl-scope .cl-lb__button { width: 100%; }

/* The panel is at least as wide as the control that opened it: an option like
   a full account name and number should not be narrower than the trigger. */
.cl-scope .cl-lb__panel { min-width: 100%; }

/* The reconciliation line, under the header rather than inside a panel: it is
   about every figure on the page, not about one of them. */
.cl-scope__note {
    margin-top: calc(-1 * var(--space-xs));
    font-size: 12.5px;
    color: var(--color-muted);
}

@media (max-width: 40rem) {
    /* The header's end takes the whole row too: it is a shrink-to-fit flex
       item, so the form's 100% was 100% of a box only as wide as the picker's
       minimum. */
    .cl-head__end:has(.cl-scope) { flex-basis: 100%; }

    .cl-scope { width: 100%; }

    /* The listbox too, for the reason given above the base rule: written for
       `select` alone, this sized the hidden native field and left the picker
       on screen at its 15rem minimum — two thirds of the row, ragged against
       the full-width cards under it, with "Mercedes-Benz Maroc — +212…"
       wrapping onto two lines in its panel. */
    .cl-scope select,
    .cl-scope .cl-lb { flex: 1; min-width: 0; max-width: none; }
}

/* ═══════════════════════════════════════════════════════════
   REVIEW PROMPT — a card in the bottom corner
   ═══════════════════════════════════════════════════════════

   Shaped like a chat panel because that is what it is asking for: a short
   exchange, in the corner, that can be closed. It borrows the panel's own
   surface, hairline and radius rather than inventing a second card style, and
   the accent appears exactly twice — the lit stars and the send button — which
   is the budget the rest of the portal keeps to.

   Everything here is a logical property, so under Arabic the card moves to the
   bottom-left and the star row reverses with the text. No second stylesheet.
*/

.cl-review {
    position: fixed;
    inset-block-end: var(--space-lg);
    inset-inline-end: var(--space-lg);
    z-index: var(--z-toast);
    max-width: calc(100vw - (var(--space-lg) * 2));
}

.cl-review__card {
    /*
     * Wide enough to write in. At 360px both textareas wrapped every few
     * words and the star row sat in the left third of the card, so rating and
     * writing both felt cramped — and the body scrolled on an ordinary laptop,
     * which put the send button out of sight behind a scrollbar.
     */
    width: 420px;
    max-width: 100%;
    background: var(--color-paper-raised);
    border: 1px solid var(--color-rule-soft);
    border-radius: var(--radius-lg);
    /* The one card on screen floating this high, like the schematic on the
       dashboard: it has interrupted, so it should look like it is in front. */
    box-shadow: var(--shadow-soft);
    overflow: hidden;
}

/* Arrives from below, the direction it lives in. Announced by opacity as well
   as movement so it still reads when motion is reduced — see the query at the
   end of this block. */
.cl-review--entering {
    animation: cl-review-rise var(--dur-mid) var(--ease-out);
}

@keyframes cl-review-rise {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: translateY(0); }
}

.cl-review__head {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-md) var(--space-sm);
    border-bottom: 1px solid var(--color-rule-soft);
}

.cl-review__title {
    font-family: var(--font-display);
    font-size: 15px;
    font-weight: 600;
    letter-spacing: -0.015em;
    margin: 0;
}

.cl-review__lede {
    margin: 2px 0 0;
    font-size: 12px;
    color: var(--color-muted);
}

.cl-review__close {
    margin-inline-start: auto;
    flex-shrink: 0;
    width: 26px;
    height: 26px;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: var(--radius-pill);
    background: transparent;
    color: var(--color-muted);
    cursor: pointer;
    transition: background var(--dur-short) var(--ease-out), color var(--dur-short) var(--ease-out);
}

.cl-review__close:hover {
    background: var(--color-paper-sunken);
    color: var(--color-ink);
}

.cl-review__close svg {
    width: 15px;
    height: 15px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
}

/*
 * Two questions and a star row make a tall card. Rather than let it run off
 * the top of a laptop screen, the body scrolls inside a height measured from
 * the viewport — the header with the close button stays put, which is the part
 * a reader needs to reach when they have decided they are done.
 */
.cl-review__body {
    padding: var(--space-md);
    /*
     * Tall enough that this does not normally scroll at all. The cap is still
     * here for a short window or a long translation — the header with the
     * close button stays put when it does, which is the part a reader needs to
     * reach when they have decided they are done.
     */
    max-height: min(74vh, 620px);
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* ── Stars ─────────────────────────────────────────────── */

.cl-review__stars {
    border: 0;
    margin: 0;
    padding: 0;
}

.cl-review__stars legend {
    padding: 0;
    font-size: 12px;
    font-weight: 500;
    color: var(--color-ink-soft);
}

.cl-review__star-row {
    display: flex;
    gap: var(--space-xs);
    margin-top: var(--space-xs);
}

/*
 * Colour is the only thing that moves.
 *
 * The stars used to lift a pixel on hover. Crossing the row then fired five
 * separate transforms in the time it takes to drag a pointer across 130px, and
 * the whole row appeared to twitch — the transition made it worse, because each
 * star was still settling as the next one started. Filling is the feedback the
 * control needs; the movement was decoration that made it feel broken.
 */
.cl-review__star {
    cursor: pointer;
    line-height: 0;
    color: var(--color-rule-track);
    transition: color var(--dur-short) var(--ease-out);
}

/* The input is the control and the star is its label: hidden from sight,
   never from the accessibility tree or the keyboard. */
.cl-review__star input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

.cl-review__star-label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

/* Bigger than a glyph in a sentence: this is the primary control on the card
   and it is aimed at with a pointer, so it gets a target worth aiming at. */
.cl-review__star svg {
    width: 32px;
    height: 32px;
    fill: currentColor;
}

.cl-review__star.is-on { color: var(--color-warm); }

/* Keyboard focus has to land somewhere visible, and the input it belongs to
   is clipped — so the star wears the ring. */
.cl-review__star:focus-within svg {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
    border-radius: var(--radius-xs);
}

.cl-review__star-caption {
    margin: var(--space-2xs) 0 0;
    font-size: 11.5px;
    font-weight: 500;
    color: var(--color-warm-ink);
    min-height: 1.2em;
}

/* ── Comment ───────────────────────────────────────────── */

.cl-review__field { margin-top: var(--space-md); }

.cl-review__label {
    display: flex;
    align-items: baseline;
    gap: var(--space-2xs);
    font-size: 12px;
    font-weight: 500;
    color: var(--color-ink-soft);
}

.cl-review__label span {
    font-weight: 400;
    font-size: 11px;
    color: var(--color-muted);
}

.cl-review__textarea {
    width: 100%;
    margin-top: var(--space-2xs);
    padding: var(--space-xs) var(--space-sm);
    font: inherit;
    font-size: 13px;
    color: var(--color-ink);
    background: var(--color-paper);
    border: 1px solid var(--color-rule-control);
    border-radius: var(--radius-sm);
    resize: vertical;
    min-height: 88px;
    line-height: 1.5;
}

.cl-review__textarea::placeholder { color: var(--color-muted); }

.cl-review__textarea:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 1px;
    border-color: var(--color-accent);
}

.cl-review__counter {
    margin: var(--space-2xs) 0 0;
    font-size: 11px;
    color: var(--color-muted);
    font-variant-numeric: tabular-nums;
    text-align: end;
}

/* Only reachable by pasting past the cap, and it has to be visible when it is:
   a disabled button with no stated reason is the worst version of this. */
.cl-review__counter.is-over {
    color: var(--color-halt);
    font-weight: 500;
}

.cl-review__error {
    margin: var(--space-xs) 0 0;
    font-size: 12px;
    color: var(--color-halt);
}

.cl-review__foot {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-xs);
    margin-top: var(--space-md);
}

/* ── After it is sent ──────────────────────────────────── */

.cl-review__thanks {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-lg) var(--space-md);
}

.cl-review__thanks svg {
    width: 26px;
    height: 26px;
    flex-shrink: 0;
    fill: none;
    stroke: var(--color-live);
    stroke-width: 2.2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.cl-review__thanks strong {
    font-family: var(--font-display);
    font-size: 14px;
    font-weight: 600;
}

.cl-review__thanks p {
    margin: 1px 0 0;
    font-size: 12px;
    color: var(--color-muted);
}

/* On a phone the card is the width of the screen, less the gutter it sits in. */
@media (max-width: 30rem) {
    /* Both insets set the width here. The desktop cap left the card 24px
       narrower than that, anchored to the start: a 12px gutter on one side
       and 36px on the other. */
    .cl-review {
        inset-inline: var(--space-sm);
        inset-block-end: var(--space-sm);
        max-width: none;
    }

    .cl-review__card { width: 100%; }

    /* Less room to give away on a phone, so the body takes the scroll here
       rather than the card growing past the top of the screen. Of the
       visible height where there is one: 62vh is measured with the toolbar
       hidden, which put the head — and its close button — above the screen
       on a short phone with the toolbar showing. */
    .cl-review__body { max-height: 62vh; max-height: 62dvh; }

    /* 26px to close a card that interrupted: the target grows to 42 around
       the same mark, so the header keeps its height. */
    .cl-review__close { position: relative; }

    .cl-review__close::after {
        content: "";
        position: absolute;
        inset: -8px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .cl-review--entering { animation: none; }
    .cl-review__star { transition: none; }
}

/* The invitation keeps a neutral header; the action itself owns the accent. */
.cl-review__head {
    background: var(--color-paper-sunken);
    border-bottom-color: var(--color-rule-soft);
}

.cl-review__note {
    margin: var(--space-xs) 0 0;
    font-size: 11.5px;
    line-height: 1.45;
    color: var(--color-accent);
}

/* The nudge under the suggestion box: why this field is worth filling in. */
.cl-review__hint {
    margin: var(--space-2xs) 0 0;
    font-size: 11.5px;
    color: var(--color-muted);
}

/* The counter follows the hint, so it stops carrying the gap on its own. */
.cl-review__hint + .cl-review__counter { margin-top: 2px; }


/* ── Notes: a warning ─────────────────────────────────────────────────────
 *
 * The third state beside .cl-note--ok and .cl-note--bad, and it earns its
 * place: something that still works but is about to stop is neither. A
 * revoked connector is bad; a connector that cannot renew itself is this.
 */
.cl-note--warn {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    background: var(--color-warn-wash);
    border: 1px solid var(--color-warn-line);
    border-radius: var(--radius-md);
    color: var(--color-warn);
}

.cl-note--warn svg { width: 15px; height: 15px; flex: none; margin-top: 1px; }


/* ── Buttons: leaving something ───────────────────────────────────────────
 *
 * Outlined rather than filled. A filled red button competes with the primary
 * action for the eye, and disconnecting is never the thing a page is for —
 * it is the thing a page must not hide.
 */
.cl-btn--danger {
    color: var(--color-halt);
    border-color: var(--color-halt-line);
    background: var(--color-paper);
}

/* Hover and disabled: see .cl-btn:hover. */
@media (hover: hover) and (pointer: fine) {
    .cl-btn--danger:hover:not(:disabled):where(:not([aria-disabled='true'])) {
        background: var(--color-halt-wash);
        border-color: var(--color-halt);
    }
}


/* ── Wizard: the results spreadsheet ──────────────────────────────────────
 *
 * The mirror of the mapping panel above it. That one takes columns out of a
 * file and puts them into the message; this one takes what comes back — the
 * delivery statuses and the template's catch-data fields — and puts them
 * into a file. So it reads as the same kind of object, and everything below
 * is layout: the panel, fields, segmented control and checkboxes are all the
 * wizard's own and are not restated.
 */

.cl-sheet__columns {
    display: grid;
    gap: var(--space-sm);
    padding-top: var(--space-sm);
    border-top: 1px solid var(--color-rule-soft);
}

/*
 * One group per place a value comes from — what you sent, how it was
 * delivered, what came back. A fieldset because that is what this is: the
 * legend is the group's name to a screen reader as well as to the eye.
 */
.cl-sheet__group {
    margin: 0;
    padding: 0;
    border: 0;
}

.cl-sheet__group-title {
    padding: 0;
    margin-bottom: var(--space-2xs);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-muted);
}

/*
 * Auto-fill, not a fixed count: these are labels of wildly different lengths
 * — "Read at" beside "What did you think of your visit?" — and a fixed grid
 * either wraps the long one to three lines or leaves the short one in a
 * column of white space.
 */
.cl-sheet__checks {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
    gap: var(--space-2xs) var(--space-md);
}

/* The phone column: shown ticked and disabled, with the reason beside it,
   rather than left out of the list — a reader counting the columns they
   will get should find it there. */
.cl-check.is-locked { opacity: 0.85; }
.cl-check.is-locked .cl-check__label { color: var(--color-ink-soft); }

@media (max-width: 40rem) {
    .cl-sheet__checks { grid-template-columns: 1fr; }
}


/* ── The data-cell tooltip ────────────────────────────────────────────────
 *
 * One panel for the whole page, appended to <body> and positioned in viewport
 * coordinates — `.cl-dt__container` scrolls horizontally, so anything placed
 * inside a cell is clipped at the edge of the table, which is exactly where
 * the last column's tooltip needs to be.
 *
 * A definition list rather than a paragraph: fourteen merge fields read as
 * fourteen rows, with the keys in one column and the values in another. That
 * is the whole difference between this and the `title` attribute it replaces.
 */

.cl-tip {
    position: fixed;
    z-index: 90;
    max-width: min(34rem, calc(100vw - 24px));
    max-height: min(26rem, calc(100vh - 24px));
    /*
     * Scrollable, and therefore interactive.
     *
     * This was `pointer-events: none` at first, so a panel following the
     * pointer could never become the thing under it. That reasoning was right
     * and the consequence was fatal: it also made the panel impossible to
     * scroll, and the case this exists for is eight survey questions that fit
     * in no tooltip at any size. The scrollbar is the feature.
     */
    overflow-y: auto;
    overscroll-behavior: contain;
    border-radius: 16px;
    background: var(--color-paper-raised);

    /*
     * Three layers, because one flat shadow is what makes a panel read as a
     * 2010 tooltip: a hairline ring that survives on any background, a short
     * contact shadow that sits it on the surface, and a long soft one that
     * lifts it off the page.
     */
    box-shadow:
        0 0 0 1px var(--color-rule),
        var(--shadow-soft);

    /* Its own scrollbar, quiet. The default one is a grey slab in a panel
       this small. */
    scrollbar-width: thin;
    scrollbar-color: var(--color-rule-track) transparent;
}

.cl-tip::-webkit-scrollbar { width: 10px; }
.cl-tip::-webkit-scrollbar-track { background: transparent; }
.cl-tip::-webkit-scrollbar-thumb {
    border: 3px solid transparent;
    border-radius: 99px;
    background-clip: content-box;
    background-color: var(--color-rule-track);
}

.cl-tip[hidden] { display: none; }

/*
 * The caret.
 *
 * The single thing that makes this read as attached to a cell rather than as
 * a box that happens to be near one. Positioned by the script at the anchor's
 * centre, and flipped with the panel.
 */
.cl-tip::before {
    content: "";
    position: absolute;
    left: var(--cl-tip-caret, 24px);
    width: 10px;
    height: 10px;
    margin-left: -5px;
    background: inherit;
    /* Rotated square rather than a border triangle: a triangle cannot carry
       the ring, so it reads as a notch cut out of the panel's edge. */
    transform: rotate(45deg);
}

.cl-tip[data-side="below"]::before {
    top: -5px;
    box-shadow: -1px -1px 0 0 var(--color-rule);
}

.cl-tip[data-side="above"]::before {
    bottom: -5px;
    box-shadow: 1px 1px 0 0 var(--color-rule);
}

/* Clamped against a viewport edge, it is beside nothing in particular. */
.cl-tip[data-side="beside"]::before { display: none; }

/*
 * The header.
 *
 * Sticky, so the count stays legible while the list scrolls under it — and so
 * it is obvious that the list *is* scrolling.
 */
.cl-tip__head {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    margin: 0;
    padding: var(--space-sm) var(--space-md);
    border-bottom: 1px solid var(--color-rule-soft);
    border-radius: 16px 16px 0 0;
    background: var(--color-paper-raised);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-muted);
}

/* Only offered on a pinned panel: on hover the panel goes when the pointer
   does, so a close button would be a control for something already happening
   — and one nobody could reach without closing it on the way. */
.cl-tip__close {
    display: inline-flex;
    flex: none;
    padding: 3px;
    margin: -3px -4px -3px 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--color-muted);
    cursor: pointer;
}

.cl-tip__close:hover { background: var(--color-paper-sunken); color: var(--color-ink); }
.cl-tip__close svg { width: 14px; height: 14px; }

.cl-tip__list {
    display: grid;
    grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
    gap: var(--space-2xs) var(--space-md);
    margin: 0;
    padding: var(--space-sm) var(--space-md) var(--space-md);
    align-items: baseline;
}

/*
 * NOT uppercased.
 *
 * These keys are whatever the template called its fields, and for a survey
 * that is a whole question — "Comment évaluez-vous la qualité des réparations
 * et interventions effectuées ?". Eight of those in small caps is a wall
 * nobody reads, and caps also flattens the accents French relies on to be
 * scannable. Short machine keys were the only case that ever looked right in
 * caps, and they read fine without it.
 */
.cl-tip__key {
    min-width: 0;
    max-width: 16rem;
    font-size: 12px;
    font-weight: 500;
    color: var(--color-muted);
    overflow-wrap: anywhere;
}

.cl-tip__value {
    min-width: 0;
    margin: 0;
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-ink);
    /* A reply can be a paragraph with its own line breaks, and it should keep
       them rather than becoming one run-on line. */
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/*
 * A quiet stripe per pair.
 *
 * Eight pairs in one weight of grey is a wall: nothing tells the eye where one
 * field ends and the next begins, so reading the fourth value means counting
 * down from the first. The mapping grid answers the same problem the same way
 * — see .cl-map__row:nth-child(odd) — and this is that answer, in the same
 * token.
 *
 * The gap goes to zero and the cells take padding instead, because a stripe
 * drawn across a grid gap is two stripes with a hole between them.
 *
 * Stacked panels are left alone: their keys are questions, they already carry
 * a rule between fields, and striping a questionnaire would fight the numbers.
 */
.cl-tip:not(.cl-tip--stacked) .cl-tip__list {
    gap: 0;
    padding-inline: 0;
}

.cl-tip:not(.cl-tip--stacked) .cl-tip__key,
.cl-tip:not(.cl-tip--stacked) .cl-tip__value {
    padding-block: var(--space-2xs);
    padding-inline-end: var(--space-md);
}

.cl-tip:not(.cl-tip--stacked) .cl-tip__key { padding-inline-start: var(--space-md); }

.cl-tip:not(.cl-tip--stacked) .cl-tip__key:nth-of-type(odd),
.cl-tip:not(.cl-tip--stacked) .cl-tip__value:nth-of-type(odd) {
    background: var(--color-paper-sunken);
}

/*
 * The field's name, in the brand's warm hue.
 *
 * It is the thing being looked up — you arrive knowing the name and wanting
 * the value — so the two sides of the panel read as two different kinds of
 * thing rather than as one grey block: the name is the label, the value is the
 * content.
 *
 * `--color-warm-ink`, not `--color-warm`. Same hue (49.2, the mark's own), but
 * the brand value measures 2.98:1 and these keys are 12px — the token's own
 * note says badge-sized text takes the darker step, and this is that.
 */
.cl-tip__key { color: var(--color-warm-ink); }

/*
 * The questionnaire, striped like the key column beside it.
 *
 * Same reasoning, harder case: eleven survey questions in one weight of grey
 * is a wall, and a reader looking for the fourth answer counts down from the
 * first. The stripe does the separating the hairline used to do — better, at
 * that length — so the rule comes off rather than the two fighting.
 *
 * Numbers stay. A questionnaire has an order that is part of reading it, which
 * is the whole reason this shape exists.
 */
.cl-tip--stacked .cl-tip__list {
    padding-inline: 0;
}

.cl-tip--stacked .cl-tip__key,
.cl-tip--stacked .cl-tip__value {
    padding-inline: var(--space-md);
}

.cl-tip--stacked .cl-tip__key {
    padding-block-start: var(--space-sm);
    border-top: 0;
}

.cl-tip--stacked .cl-tip__value { padding-block-end: var(--space-sm); }

.cl-tip--stacked .cl-tip__key:nth-of-type(odd),
.cl-tip--stacked .cl-tip__value:nth-of-type(odd) {
    background: var(--color-paper-sunken);
}

/*
 * A long value is trimmed, not laid out in full.
 *
 * An image URL is a hundred and sixty characters with no spaces in it. Left to
 * wrap it took five lines and half the panel, and it pushed the seven fields a
 * reader actually came for below the fold — so one field nobody reads decided
 * the shape of the whole thing.
 *
 * Three lines, then an ellipsis. The full string stays reachable: the script
 * puts it on the element's `title`, and the panel can be pinned open.
 */
.cl-tip:not(.cl-tip--stacked) .cl-tip__value {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
}

/*
 * A value that is a destination.
 *
 * One line, ellipsed, and clickable — the panel can be pinned open with a
 * click, which is what makes a link inside it reachable. Underlined rather
 * than coloured alone: colour is never the only signal here either.
 */
.cl-tip__link {
    display: block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    color: var(--color-accent);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.cl-tip__link:hover { color: var(--color-accent-hover); }

/* An empty value is a fact about the row, not a blank line. */
.cl-tip__value--empty {
    font-style: italic;
    color: var(--color-muted-soft);
}

/*
 * One column, when the keys are sentences rather than field names.
 *
 * A survey question in a narrow left-hand column wraps to four cramped lines
 * with its answer stranded beside it. Stacked, the question reads as a
 * question and the answer sits under it.
 */
.cl-tip--stacked .cl-tip__list {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
}

.cl-tip--stacked .cl-tip__key {
    max-width: none;
    padding-top: var(--space-sm);
    border-top: 1px solid var(--color-rule-soft);
    line-height: 1.45;
}

.cl-tip--stacked .cl-tip__key:first-of-type { padding-top: 0; border-top: 0; }

/* The answer is the thing being read; it gets the weight. */
.cl-tip--stacked .cl-tip__value {
    padding-top: 3px;
    padding-bottom: var(--space-sm);
    font-size: 14px;
    font-weight: 500;
}

/* A pinned panel is a thing somebody opened, and says so. */
.cl-tip--pinned {
    box-shadow:
        0 0 0 1px var(--color-accent-line),
        var(--shadow-soft);
}

.cl-tip--pinned .cl-tip__head {
    background: var(--color-accent-wash);
    color: var(--color-accent);
    border-bottom-color: var(--color-accent-line);
}

.cl-tip--pinned[data-side="below"]::before { box-shadow: -1px -1px 0 0 var(--color-accent-line); }
.cl-tip--pinned[data-side="above"]::before { box-shadow: 1px 1px 0 0 var(--color-accent-line); }
.cl-tip--pinned[data-side="below"]::before { background: var(--color-accent-wash); }

/* Arrives rather than appears. Short enough not to be in the way of somebody
   scanning a column of rows. */
@media (prefers-reduced-motion: no-preference) {
    .cl-tip { animation: cl-tip-in .12s ease-out; }

    @keyframes cl-tip-in {
        from { opacity: 0; transform: translateY(-2px); }
        to { opacity: 1; transform: none; }
    }
}

/* On a phone the panel is the width of the screen and sits where it is put;
   there is no room for it to be beside anything. */
@media (max-width: 30rem) {
    .cl-tip { max-width: calc(100vw - 24px); }
    .cl-tip__list { grid-template-columns: minmax(0, 1fr); gap: 0; }
    .cl-tip__key { max-width: none; padding-top: var(--space-sm); border-top: 1px solid var(--color-rule-soft); }
    .cl-tip__key:first-of-type { padding-top: 0; border-top: 0; }
    .cl-tip__value { padding-bottom: var(--space-sm); font-weight: 500; }

    /* One column, so the value sits under its key and takes the key's start
       padding too. The striping gives only the key one: beside each other
       the value had the column gap, stacked it was flush with the panel's
       edge, out of line with its key. */
    .cl-tip:not(.cl-tip--stacked) .cl-tip__value { padding-inline-start: var(--space-md); }
}


/* ── The tooltip's smarter parts ──────────────────────────────────────────
 *
 * Two things the plain version could not do.
 *
 * A questionnaire has an order, and reading one means knowing where you are
 * in it — so the stacked shape numbers its entries. A merge field has no
 * order and is not numbered.
 *
 * And a run of star emoji is not a value anybody can read at a glance: eight
 * of them stacked down a panel are eight near-identical smudges, and telling a
 * three from a four means counting. The script reads the stars as a number and
 * this draws it as a meter, so the whole survey is comparable down one edge.
 */

.cl-tip__key {
    display: flex;
    align-items: baseline;
    gap: var(--space-sm);
}

.cl-tip__label { min-width: 0; overflow-wrap: anywhere; }

/* The number. Tabular, so 1 and 10 occupy the same width and the questions
   start on one line down the panel. */
.cl-tip__index {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.25rem;
    height: 1.25rem;
    border-radius: 999px;
    background: var(--color-paper-sunken);
    font-size: 10.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--color-muted);
}

.cl-tip--stacked .cl-tip__key { padding-top: var(--space-sm); }

/* The rating meter. */
.cl-tip__value--rating { display: flex; }

.cl-tip__rating {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
}

.cl-tip__stars { display: inline-flex; gap: 2px; }

/*
 * Drawn, not passed through.
 *
 * The emoji the form sends renders as a different glyph on every platform, at
 * a size nothing can control and in a colour nothing can change. One shape at
 * one size, filled to the score with the remainder still visible, is what
 * turns "how many" into something read rather than counted.
 */
.cl-tip__star {
    width: 15px;
    height: 15px;
    flex: none;
    /* The unlit half is the same star, not a different symbol: outlined in the
       track colour so the row still shows how many there could have been. */
    fill: none;
    stroke: var(--color-rule-track);
    stroke-width: 1.6;
    stroke-linejoin: round;
}

.cl-tip__star.is-on {
    fill: var(--color-warm);
    stroke: var(--color-warm);
}

.cl-tip__score {
    font-size: 11.5px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--color-muted);
}

/*
 * The bottom fade.
 *
 * A hard cut at the foot of a scrolling panel reads as the end of the list.
 * Sticky rather than fixed to the panel, so it sits at the bottom of the
 * viewport of the scroll area and disappears at the end of the content.
 */
.cl-tip__fade {
    position: sticky;
    bottom: 0;
    height: 20px;
    margin-top: -20px;
    pointer-events: none;
    background: linear-gradient(to bottom, transparent, var(--color-paper-raised));
}

.cl-tip--pinned .cl-tip__index { background: var(--color-accent-wash); color: var(--color-accent); }


/* ── A template preview, on the broadcasts table ──────────────────────────
 *
 * The table has seventeen columns and cannot spend one on a paragraph, but a
 * template's name rarely says what it actually said — "MB SAV Wheel Retention
 * AW03 J+3mois" says less than most. So the name is the cell and the words are
 * behind a hover, in the same panel the data column uses.
 *
 * Drawn as a message rather than as a block of text: it is what a customer
 * received, and reading it as anything else means translating it in your head.
 */

.cl-table__tpl {
    display: inline-block;
    max-width: 100%;
    font-weight: 600;
    color: var(--color-ink);
    cursor: pointer;
    border-radius: 5px;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-decoration-color: var(--color-rule-control);
    text-underline-offset: 3px;
    overflow-wrap: anywhere;
}

.cl-table__tpl:hover { color: var(--color-accent); text-decoration-color: var(--color-accent); }

.cl-table__tpl:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

/*
 * A preview panel is not a panel.
 *
 * `.cl-wa` already is a card — its own border, background and radius — so the
 * tooltip's chrome around it made a card inside a card, and the outer one said
 * nothing the inner one did not. Here the tooltip is only a position: no
 * background, no border, no shadow of its own, and no scroll. The template
 * shows at its own height or not at all.
 */
.cl-tip--preview {
    max-width: min(26rem, calc(100vw - 24px));
    max-height: none;
    overflow: visible;
    padding: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
}

/* Nothing to point at when there is no panel edge to point from. */
.cl-tip--preview::before { display: none; }

/*
 * The preview inside the tooltip is the wizard's preview.
 *
 * `.cl-wa` and its parts already carry the bubble, the body type, the footer
 * and the buttons — this only gives it room inside the panel. Restating any of
 * that here would be a second component pretending to be the first.
 */
/* The preview carries its own card, so it needs no margin inside a panel that
   no longer has any padding. */
.cl-tip__wa {
    margin: 0;
    /* It floats free of a panel now, so it brings its own lift. */
    box-shadow: var(--shadow-soft);
}


/* ── Shared numeric badge ─────────────────────────────────────────────────
   Used by navigation and any compact control that needs a bounded count.
   The component hides zero by default and caps the visible string, so this
   class can keep one stable footprint without each caller inventing a pill. */

.cl-count-badge {
    display: inline-grid;
    place-items: center;
    min-width: 21px;
    height: 19px;
    padding-inline: 5px;
    flex: none;
    border: 1px solid var(--color-accent-line);
    border-radius: var(--radius-pill);
    background: var(--color-accent-wash);
    color: var(--color-accent);
    font-family: var(--font-display);
    font-size: 10.5px;
    font-weight: 700;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* In a flex row the badge owns the trailing edge; elsewhere it stays inline. */
.cl-rail__count.cl-count-badge {
    margin-inline-start: auto;
    color: var(--color-accent);
}

.cl-rail__link.is-active .cl-count-badge {
    border-color: var(--color-accent);
    background: var(--color-accent);
    color: var(--color-accent-ink);
}


/* ═══════════════════════════════════════════════════════════
   Phone column problems
   ═══════════════════════════════════════════════════════════

   Cells in the mapped phone column that will not reach anybody, listed in the
   corner the review card already occupies. It reuses .cl-review and
   .cl-review__card wholesale — the surface, hairline, radius, shadow and rise
   are that component's; only the list inside is new.

   Narrower than the review card: this is read, not written in, and three
   columns of short values want less width than two textareas.

   Logical properties, so it moves to the bottom-left under Arabic with the
   rest of the portal. */

.cl-phix .cl-review__card { width: 380px; }

/* The shared review head is neutral. This variant reports rows that will not
   reach anybody, so it takes the halt tone the reasons already carry. */
.cl-phix .cl-review__head {
    background: var(--color-halt-wash);
    border-block-end-color: var(--color-halt-line);
}

.cl-phix .cl-review__title {
    display: flex;
    align-items: center;
    gap: var(--space-2xs);
    color: var(--color-halt);
}

/* Sized against the title's cap height rather than a fixed pixel, so it holds
   its relationship if the type scale moves. */
.cl-phix__mark {
    flex: none;
    inline-size: 1em;
    block-size: 1em;
    color: var(--color-halt);
}

.cl-phix__list {
    margin: 0;
    padding: 0 var(--space-md);
    list-style: none;
    /* Eight rows fit; beyond that the count line says how many are missing,
       so the panel never grows past the corner it lives in. */
    max-height: 260px;
    overflow-y: auto;
}

.cl-phix__row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: baseline;
    gap: var(--space-2xs) var(--space-xs);
    padding: var(--space-2xs) 0;
    border-block-end: 1px solid var(--color-rule-soft);
    font-size: 12.5px;
}
.cl-phix__row:last-child { border-block-end: 0; }

.cl-phix__cell {
    color: var(--color-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* The value as it sits in the file. Tabular figures so a transposed digit is
   visible against the row above — the portal has no monospace face and this is
   not the place to introduce one — and truncated rather than wrapped, so one
   long cell cannot push the reason out of view. */
.cl-phix__val {
    font-variant-numeric: tabular-nums;
    color: var(--color-ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cl-phix__why {
    color: var(--color-halt);
    font-size: 11px;
    white-space: nowrap;
}

.cl-phix__more,
.cl-phix__foot {
    margin: 0;
    padding: var(--space-2xs) var(--space-md);
    font-size: 11.5px;
    color: var(--color-muted);
}
.cl-phix__foot { padding-block-end: var(--space-md); }
.cl-phix__more { font-variant-numeric: tabular-nums; }

/*
    The question Next raises, in the foot of the same panel.

    Tinted rather than bordered: it sits under a list that is already ruled, and
    a third hairline in a 380px card reads as clutter. The tone is --color-halt
    at low alpha so it belongs to the warning above it.
*/
.cl-phix__ask {
    margin: 0;
    padding: var(--space-sm) var(--space-md) var(--space-md);
    border-block-start: 1px solid var(--color-rule-soft);
    background: color-mix(in srgb, var(--color-halt) 5%, transparent);
}

.cl-phix__askline {
    margin: 0;
    font-size: 12px;
    line-height: 1.5;
    color: var(--color-ink-soft);
}
.cl-phix__askline strong {
    color: var(--color-halt);
    font-variant-numeric: tabular-nums;
}

.cl-phix__acts {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2xs);
    margin-block-start: var(--space-sm);
}

/* Going on anyway is the destructive choice, so it is coloured like one. */
.cl-phix__go {
    background: var(--color-halt);
    border-color: var(--color-halt);
    color: var(--color-accent-ink);
}
.cl-phix__go:hover:not(:disabled) {
    background: color-mix(in srgb, var(--color-halt) 86%, var(--color-ink));
    border-color: color-mix(in srgb, var(--color-halt) 86%, var(--color-ink));
}


/* ═══════════════════════════════════════════════════════════
   SHARED SMS FALLBACK EDITOR
   Hallmark · component: attached composer + delivery pipeline
   genre: modern-minimal · tone: operational · palette: portal tokens

   New Message and Auto Broadcast render the same partial. The editor is one
   compound control, not a pile of nested cards: its tools, textarea and status
   share a boundary; timing and delivery are separated by quiet rules.
   ═══════════════════════════════════════════════════════════ */

.cl-sms-editor-panel,
.cl-sms-editor,
.cl-sms-composer,
.cl-sms-composer__toolbar,
.cl-sms-composer__personalise,
.cl-sms-composer__variables,
.cl-sms-editor__controls,
.cl-sms-flow,
.cl-sms-flow__track,
.cl-sms-flow__step,
.cl-sms-flow__copy {
    min-inline-size: 0;
    max-inline-size: 100%;
}

.cl-sms-editor {
    display: grid;
    gap: var(--space-lg);
}

/* The toolbar is attached to the field below, so inserting a token reads as
   an editing action rather than a separate configuration panel. */
.cl-sms-composer {
    position: relative;
    border: var(--rule-hair) solid var(--color-rule-control);
    border-radius: var(--radius-sm);
    background: var(--color-paper-raised);
    color: var(--color-ink);
    outline: var(--rule-fine) solid transparent;
    outline-offset: var(--space-3xs);
    transition: border-color var(--dur-short) var(--ease-out),
                background-color var(--dur-short) var(--ease-out);
}

.cl-sms-composer__toolbar {
    display: grid;
    gap: var(--space-xs);
    padding: var(--space-xs) var(--space-sm);
    border-block-end: var(--rule-hair) solid var(--color-rule-soft);
    border-start-start-radius: var(--radius-sm);
    border-start-end-radius: var(--radius-sm);
    background: var(--color-paper-sunken);
    color: var(--color-ink);
}

.cl-sms-composer__title,
.cl-sms-composer__toolbar-label {
    font-size: var(--text-xs);
    line-height: 1.4;
}

.cl-sms-composer__title {
    align-self: center;
    color: var(--color-ink-soft);
    font-weight: 600;
}

.cl-sms-composer__personalise {
    display: grid;
    gap: var(--space-2xs);
}

.cl-sms-composer__toolbar-label {
    color: var(--color-muted);
    font-weight: 500;
    white-space: nowrap;
}

/* The rail deliberately owns the only horizontal scrolling in this component.
   At 320px and 375px, long template tokens stay intact without widening the
   page or reducing their touch targets. */
.cl-sms-composer__variables {
    display: flex;
    gap: var(--space-2xs);
    inline-size: 100%;
    overflow-x: auto;
    overflow-y: clip;
    overscroll-behavior-inline: contain;
    padding: var(--space-3xs);
    scrollbar-color: var(--color-rule-control) transparent;
    scrollbar-width: thin;
    /* Both ways. With pan-x alone a vertical swipe that began on the rail
       did nothing, so the page would not scroll from this 48px strip. */
    touch-action: pan-x pan-y;
}

/* A phone draws no scrollbar until the rail is already moving, so it ended in
   a token cut mid-word ("+ {{revi") with nothing to say there were more. The
   edge with more beyond it fades, driven by the rail's own scroll position —
   the fade a table that scrolls sideways has (see .gridjs-wrapper), on the
   same keyframes. A rail that fits draws no fade. */
@supports (animation-timeline: scroll()) {
    @media (max-width: 40rem) {
        .cl-sms-composer__variables {
            --cl-dt-fade-to: right;
            -webkit-mask-image: linear-gradient(to var(--cl-dt-fade-to), transparent, #000 var(--cl-dt-fade-start), #000 calc(100% - var(--cl-dt-fade-end)), transparent);
            mask-image: linear-gradient(to var(--cl-dt-fade-to), transparent, #000 var(--cl-dt-fade-start), #000 calc(100% - var(--cl-dt-fade-end)), transparent);
            animation: cl-dt-edge-fade linear both;
            animation-timeline: scroll(self inline);
        }

        [dir="rtl"] .cl-sms-composer__variables { --cl-dt-fade-to: left; }
    }
}

.cl-sms-composer__variable {
    position: relative;
    display: inline-flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    gap: var(--space-2xs);
    max-inline-size: 100%;
    min-block-size: var(--size-hit);
    padding: var(--space-xs) var(--space-sm);
    border: var(--rule-hair) solid var(--color-rule-control);
    border-radius: var(--radius-pill);
    background: var(--color-paper-raised);
    color: var(--color-ink-soft);
    font-family: var(--font-display);
    font-size: var(--text-xs);
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    transition: background-color var(--dur-short) var(--ease-out),
                border-color var(--dur-short) var(--ease-out),
                color var(--dur-short) var(--ease-out),
                transform var(--dur-micro) var(--ease-out);
}

.cl-sms-composer__variable > svg {
    flex: 0 0 auto;
    inline-size: var(--space-md);
    block-size: var(--space-md);
}

.cl-sms-composer__variable > bdi {
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cl-sms-composer__textarea,
.cl-sms-composer__textarea:hover,
.cl-sms-composer__textarea:focus-visible,
.cl-sms-composer__textarea[aria-invalid='true'] {
    min-block-size: calc(var(--space-2xl) + var(--space-xl));
    border: 0;
    border-radius: 0;
    background: var(--color-paper-raised);
    color: var(--color-ink);
    outline: 0;
    box-shadow: none;
    resize: vertical;
    transition: none;
}

.cl-sms-composer__status {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2xs) var(--space-sm);
    padding: var(--space-xs) var(--space-sm);
    border-block-start: var(--rule-hair) solid var(--color-rule-soft);
    border-end-start-radius: var(--radius-sm);
    border-end-end-radius: var(--radius-sm);
    background: var(--color-paper-raised);
    color: var(--color-muted);
    font-size: var(--text-xs);
    line-height: 1.4;
}

.cl-sms-composer__status output {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2xs);
    margin-inline-start: auto;
    color: inherit;
    font-family: var(--font-display);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Eight states — classes make each state inspectable in the preview while the
   native pseudo/ARIA states remain the production contract. */
.cl-sms-composer.is-hover {
    border-color: var(--color-ink-soft);
}

.cl-sms-composer.is-focus,
.cl-sms-composer:has(.cl-sms-composer__textarea:focus-visible) {
    border-color: var(--color-accent);
    outline-color: var(--color-focus);
}

.cl-sms-composer.is-active {
    border-color: var(--color-ink-soft);
    background: var(--color-paper-sunken);
    color: var(--color-ink);
}

.cl-sms-composer.is-disabled,
.cl-sms-composer[aria-disabled='true'],
.cl-sms-composer:has(.cl-sms-composer__textarea:disabled) {
    background: var(--color-paper-sunken);
    color: var(--color-muted);
    cursor: not-allowed;
    opacity: 0.55;
}

.cl-sms-composer.is-loading,
.cl-sms-composer[aria-busy='true'] {
    cursor: progress;
}

.cl-sms-composer.is-error,
.cl-sms-composer[data-state='error'],
.cl-sms-composer:has(.cl-sms-composer__textarea.is-invalid),
.cl-sms-composer:has(.cl-sms-composer__textarea[aria-invalid='true']) {
    border-color: var(--color-halt);
    background: var(--color-paper-raised);
    color: var(--color-ink);
}

.cl-sms-composer.is-error .cl-sms-composer__status,
.cl-sms-composer[data-state='error'] .cl-sms-composer__status,
.cl-sms-composer:has(.cl-sms-composer__textarea.is-invalid) .cl-sms-composer__status,
.cl-sms-composer:has(.cl-sms-composer__textarea[aria-invalid='true']) .cl-sms-composer__status {
    color: var(--color-halt);
}

.cl-sms-composer.is-success,
.cl-sms-composer[data-state='success'] {
    border-color: var(--color-live);
    background: var(--color-paper-raised);
    color: var(--color-ink);
}

.cl-sms-composer.is-success .cl-sms-composer__status output,
.cl-sms-composer[data-state='success'] .cl-sms-composer__status output {
    color: var(--color-live);
}

.cl-sms-composer.is-success .cl-sms-composer__status output::before,
.cl-sms-composer[data-state='success'] .cl-sms-composer__status output::before {
    content: '\2713';
    font-weight: 700;
}

.cl-sms-composer.is-loading .cl-sms-composer__status::before,
.cl-sms-composer[aria-busy='true'] .cl-sms-composer__status::before,
.cl-sms-composer__variable.is-loading::before,
.cl-sms-composer__variable[aria-busy='true']::before {
    content: '';
    flex: 0 0 auto;
    inline-size: var(--space-sm);
    block-size: var(--space-sm);
    border: var(--rule-fine) solid var(--color-rule-control);
    border-block-start-color: var(--color-accent);
    border-radius: var(--radius-pill);
    animation: cl-sms-spin calc(var(--dur-long) * 3) linear infinite;
}

.cl-sms-composer__variable.is-loading > svg,
.cl-sms-composer__variable[aria-busy='true'] > svg,
.cl-sms-composer__variable.is-error > svg,
.cl-sms-composer__variable[data-state='error'] > svg,
.cl-sms-composer__variable.is-success > svg,
.cl-sms-composer__variable[data-state='success'] > svg {
    display: none;
}

.cl-sms-composer__variable.is-error::before,
.cl-sms-composer__variable[data-state='error']::before {
    content: '!';
    color: var(--color-halt);
    font-weight: 700;
}

.cl-sms-composer__variable.is-success::before,
.cl-sms-composer__variable[data-state='success']::before {
    content: '\2713';
    color: var(--color-live);
    font-weight: 700;
}

.cl-sms-composer__variable.is-hover {
    border-color: var(--color-ink-soft);
    background: var(--color-paper-sunken);
    color: var(--color-ink);
}

.cl-sms-composer__variable.is-focus,
.cl-sms-composer__variable:focus-visible {
    border-color: var(--color-accent);
    outline: var(--rule-fine) solid var(--color-focus);
    outline-offset: var(--space-3xs);
}

.cl-sms-composer__variable.is-active,
.cl-sms-composer__variable:active {
    border-color: var(--color-ink-soft);
    background: var(--color-paper-sunken);
    color: var(--color-ink);
    transform: translateY(var(--rule-hair));
}

.cl-sms-composer__variable.is-disabled,
.cl-sms-composer__variable:disabled,
.cl-sms-composer__variable[aria-disabled='true'] {
    background: var(--color-paper-sunken);
    color: var(--color-muted);
    cursor: not-allowed;
    opacity: 0.55;
}

.cl-sms-composer__variable.is-loading,
.cl-sms-composer__variable[aria-busy='true'] {
    cursor: progress;
}

.cl-sms-composer__variable.is-error,
.cl-sms-composer__variable[data-state='error'] {
    border-color: var(--color-halt);
    background: var(--color-halt-wash);
    color: var(--color-halt);
}

.cl-sms-composer__variable.is-success,
.cl-sms-composer__variable[data-state='success'] {
    border-color: var(--color-live);
    background: var(--color-live-wash);
    color: var(--color-live);
}

@media (hover: hover) and (pointer: fine) {
    .cl-sms-composer:hover:not(.is-disabled):not([aria-disabled='true']):not(.is-error):not([data-state='error']):not(.is-success):not([data-state='success']):not(:has(.cl-sms-composer__textarea.is-invalid)):not(:has(.cl-sms-composer__textarea[aria-invalid='true'])) {
        border-color: var(--color-ink-soft);
    }

    .cl-sms-composer__variable:hover:not(:disabled):not([aria-disabled='true']):not(.is-error):not([data-state='error']):not(.is-success):not([data-state='success']) {
        border-color: var(--color-ink-soft);
        background: var(--color-paper-sunken);
        color: var(--color-ink);
    }
}

/* The later toast rules also use .cl-alert. This scoped reset keeps the inline
   variable warning a calm form message instead of giving it toast elevation. */
.cl-sms-editor > .cl-alert--warn {
    padding: var(--space-sm) var(--space-md);
    border: var(--rule-hair) solid var(--color-warn-line);
    background: var(--color-warn-wash);
    color: var(--color-warn);
    box-shadow: none;
    pointer-events: auto;
}

.cl-sms-editor__timing {
    min-inline-size: 0;
    margin: 0;
    padding: var(--space-lg) 0 0;
    border: 0;
    border-block-start: var(--rule-hair) solid var(--color-rule-soft);
}

.cl-sms-editor__section-title {
    display: block;
    inline-size: 100%;
    padding: 0;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: 1.35;
    letter-spacing: -0.02em;
}

.cl-sms-editor__section-note {
    max-inline-size: 65ch;
    margin-block-start: var(--space-2xs);
    color: var(--color-muted);
    font-size: var(--text-xs);
    line-height: 1.5;
}

.cl-sms-editor__controls {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-sm);
    margin-block-start: var(--space-sm);
}

.cl-sms-editor__controls :is(.cl-input, .cl-select) {
    min-block-size: var(--size-hit);
}

.cl-sms-editor__sender {
    grid-column: 1 / -1;
}

.cl-sms-editor__timing > .cl-fld__error {
    margin-block-start: var(--space-xs);
}

.cl-sms-flow {
    display: grid;
    gap: var(--space-sm);
    padding-block-start: var(--space-lg);
    border-block-start: var(--rule-hair) solid var(--color-rule-soft);
}

.cl-sms-flow__head {
    min-inline-size: 0;
}

.cl-sms-flow__track {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    margin: 0;
    padding: 0;
    list-style: none;
}

.cl-sms-flow__step {
    display: grid;
    grid-template-columns: var(--space-xl) minmax(0, 1fr);
    align-items: center;
    gap: var(--space-sm);
}

.cl-sms-flow__mark {
    display: grid;
    place-items: center;
    inline-size: var(--space-xl);
    block-size: var(--space-xl);
    border: var(--rule-hair) solid var(--color-rule-control);
    border-radius: var(--radius-pill);
    background: var(--color-paper-raised);
    color: var(--color-ink-soft);
}

.cl-sms-flow__mark--sms {
    border-color: var(--color-accent);
    color: var(--color-accent);
}

.cl-sms-flow__mark svg {
    inline-size: var(--space-md);
    block-size: var(--space-md);
}

.cl-sms-flow__copy {
    display: grid;
    gap: var(--space-3xs);
    overflow-wrap: anywhere;
}

.cl-sms-flow__copy strong {
    color: var(--color-ink);
    font-family: var(--font-display);
    font-size: var(--text-sm);
    font-weight: 600;
    line-height: 1.3;
}

.cl-sms-flow__copy small {
    color: var(--color-muted);
    font-size: var(--text-xs);
    line-height: 1.45;
}

.cl-sms-flow__connector {
    display: grid;
    grid-template-columns: var(--space-xl) minmax(0, 1fr);
    min-block-size: var(--space-lg);
}

.cl-sms-flow__connector > span {
    justify-self: center;
    inline-size: var(--rule-hair);
    block-size: 100%;
    background: var(--color-rule-control);
}

.cl-sms-flow__connector > svg {
    display: none;
}

.cl-sms-flow__summary,
.cl-sms-editor__cost {
    display: flex;
    align-items: flex-start;
    gap: var(--space-xs);
    margin: 0;
    padding-block-start: var(--space-sm);
    border-block-start: var(--rule-hair) solid var(--color-rule-soft);
    color: var(--color-muted);
    font-size: var(--text-xs);
    line-height: 1.5;
}

.cl-sms-flow__summary svg,
.cl-sms-editor__cost svg {
    flex: 0 0 auto;
    inline-size: var(--space-md);
    block-size: var(--space-md);
    margin-block-start: var(--space-3xs);
    color: var(--color-accent);
}

.cl-sms-editor__cost {
    font-variant-numeric: tabular-nums;
}

/* The content, rather than a device name, chooses the point where the timing
   row and delivery path have enough room to run horizontally. */
@container feature-step (min-width: 34rem) {
    .cl-sms-composer__toolbar {
        grid-template-columns: max-content minmax(0, 1fr);
        align-items: center;
    }

    .cl-sms-composer__personalise {
        grid-template-columns: max-content minmax(0, 1fr);
        align-items: center;
    }

    .cl-sms-editor__controls:has(.cl-sms-editor__sender) {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }

    .cl-sms-editor__sender {
        grid-column: auto;
    }

    .cl-sms-flow__track {
        grid-template-columns:
            minmax(0, 1fr) minmax(var(--space-lg), .24fr)
            minmax(0, 1fr) minmax(var(--space-lg), .24fr)
            minmax(0, 1fr);
        align-items: center;
    }

    .cl-sms-flow__connector {
        display: flex;
        align-items: center;
        min-block-size: 0;
    }

    .cl-sms-flow__connector > span {
        flex: 1 1 auto;
        inline-size: auto;
        block-size: var(--rule-hair);
    }

    .cl-sms-flow__connector > svg {
        display: block;
        flex: 0 0 auto;
        inline-size: var(--space-md);
        block-size: var(--space-md);
        color: var(--color-rule-control);
    }

    [dir='rtl'] .cl-sms-flow__connector svg {
        transform: rotate(180deg);
    }
}

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

@media (prefers-reduced-motion: reduce) {
    .cl-sms-composer,
    .cl-sms-composer__variable {
        transition: none;
    }

    .cl-sms-composer.is-loading .cl-sms-composer__status::before,
    .cl-sms-composer[aria-busy='true'] .cl-sms-composer__status::before,
    .cl-sms-composer__variable.is-loading::before,
    .cl-sms-composer__variable[aria-busy='true']::before {
        animation-duration: calc(var(--dur-long) * 6);
    }
}

/* Optional feature choices stay card-sized on wide screens. Two choices no
   longer stretch across the panel; three fit naturally on a laptop row. */
@media (min-width: 48rem) {
    .cl-feature-grid:not(.cl-feature-grid--compact) {
        display: flex;
        flex-wrap: wrap;
        align-items: stretch;
        gap: var(--space-sm);
        border: 0;
        border-radius: 0;
        background: transparent;
        overflow: visible;
    }

    .cl-feature-grid:not(.cl-feature-grid--compact) .cl-feature-option {
        flex: 1 1 calc(var(--space-3xl) * 3);
        max-inline-size: calc(var(--space-3xl) * 4);
        border: var(--rule-hair) solid var(--color-rule);
        border-radius: var(--radius-md);
    }

    .cl-feature-grid:not(.cl-feature-grid--compact) .cl-feature-option:not([style*="display: none"]) ~ .cl-feature-option:not([style*="display: none"]) {
        border-block-start: var(--rule-hair) solid var(--color-rule);
        border-inline-start: var(--rule-hair) solid var(--color-rule);
    }
}

/* Zoho uses the same option-card scene language as spreadsheet results. */
.cl-zoho-packet { animation: cl-zoho-transfer 2.8s ease-in-out infinite; }
@keyframes cl-zoho-transfer {
    0%, 12% { transform: translateX(0); opacity: 0; }
    24% { opacity: 1; }
    76% { opacity: 1; }
    90%, 100% { transform: translateX(83px); opacity: 0; }
}
.cl-zoho-target {
    margin-block: var(--space-lg);
    padding: var(--space-lg);
    border: 1px solid var(--color-rule);
    border-radius: var(--radius-md);
    background: var(--color-paper-raised);
    min-width: 0;
}
.cl-zoho-target h2, .cl-zoho-target h3 { margin: 0 0 var(--space-sm); font-family: var(--font-sans); font-size: 17px; }
.cl-zoho-target p { color: var(--color-ink-soft); font-size: 13px; line-height: 1.65; }
.cl-zoho-target .cl-label { margin-block: var(--space-md) var(--space-xs); }
.cl-zoho-target .cl-input { width: 100%; max-width: 42rem; }
.cl-zoho-target summary { color: var(--color-accent); cursor: pointer; padding-block: var(--space-sm); }
.cl-zoho-target form { display: grid; justify-items: start; }
.cl-zoho-destination-row { display: flex; flex-wrap: wrap; gap: var(--space-md); align-items: center; justify-content: space-between; padding-block: var(--space-md); border-bottom: 1px solid var(--color-rule); font-size: 13px; }
.cl-zoho-destination-row > div { min-width: 0; overflow-wrap: anywhere; }
.cl-ab__source-demo--zoho .cl-feature-visual { width: 100%; max-width: 320px; height: auto; }
@media (prefers-reduced-motion: reduce) { .cl-zoho-packet { animation: none; opacity: 1; } }

.cl-zoho-fields-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:24px;margin-top:24px;align-items:start}
.cl-zoho-fields{min-width:0;border:0;padding:0;margin:0}
.cl-zoho-field{display:flex;align-items:center;gap:10px;padding:10px 0;cursor:pointer;border-bottom:1px solid var(--cl-border,#e1e7ee)}
.cl-zoho-field input{accent-color:var(--cl-primary,#008569);width:17px;height:17px;flex-shrink:0}
.cl-zoho-field:has(input:disabled){opacity:.55}
.cl-zoho-field-actions{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}
.cl-zoho-preview{min-width:0}.cl-zoho-preview pre{max-height:480px;overflow:auto;padding:18px;border:1px solid var(--cl-border,#e1e7ee);border-radius:12px;background:#f8fafb;font-size:13px;line-height:1.65;color:#202731}
@media(max-width:760px){.cl-zoho-fields-layout{grid-template-columns:1fr}}
.cl-zoho-connection-test{border-top:1px solid var(--color-rule);margin-top:24px;padding-top:24px;display:grid;justify-items:start;gap:12px}
.cl-zoho-connection-test .cl-hint{margin:0}.cl-zoho-test-success{color:var(--color-accent);font-weight:600}

/* Results destinations and immutable delivery attempt history. */
.cl-webhook-auth { border: 0; padding: 1.25rem 0; margin: 1rem 0; border-block: 1px solid var(--cl-border, #dce4ed); }
.cl-result-record { padding: 1rem 0; border-bottom: 1px solid var(--cl-border, #dce4ed); overflow-wrap: anywhere; }
.cl-result-record summary { cursor: pointer; font-weight: 600; }
.cl-result-response { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 24rem; overflow: auto; padding: 1rem; background: #f7f9fb; border-radius: .75rem; font-size: .85rem; }

/* Shared frosted backdrop for portal modals. Filter only the backdrop so
   dialog content stays sharp. :modal excludes non-modal popovers/previews. */
html body dialog:modal::backdrop,
html body :is(.cl-ann__scrim, .cl-cr__scrim, .cl-tm__scrim,
    .cl-developer__scrim, .cl-cal__scrim, .cl-lightbox, .cl-fbk__lightbox) {
    background: var(--color-scrim);
    -webkit-backdrop-filter: blur(8px) saturate(80%);
    backdrop-filter: blur(8px) saturate(80%);
}

@media (prefers-reduced-transparency: reduce) {
    html body dialog:modal::backdrop,
    html body :is(.cl-ann__scrim, .cl-cr__scrim, .cl-tm__scrim,
        .cl-developer__scrim, .cl-cal__scrim, .cl-lightbox, .cl-fbk__lightbox) {
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
    }
}

/* ═══════════════════════════════════════════════════════════
   TEXT FIELDS ON A PHONE
   ═══════════════════════════════════════════════════════════

   Anything a phone types into is 16px.

   iOS Safari zooms the whole page in on a field whose text is under 16px the
   moment it takes focus, and leaves it zoomed afterwards. Every field here was
   13–14px, so tapping a table's search, a form field or a picker's search
   threw the page sideways and left the reader to pinch it back — on the
   portal's most-used controls. 16px is also simply the size a thumb-typed
   line reads at.

   Phone-sized touch screens only, either way up: the second query is a phone
   held sideways, which is wider than 40rem and zooms just the same. A tablet
   matches neither and keeps the denser desktop size. Page sheets that set a
   compact field's size on purpose do so with more specific selectors and are
   untouched.

   Last in the sheet, because each of these has its base rule above and the
   same specificity: whichever comes later wins. The picker's search field and
   the top-up amount live in their own sheets and say the same thing there. */
@media (pointer: coarse) and (max-width: 40rem),
       (pointer: coarse) and (max-height: 30rem) {
    .cl-input,
    .cl-select,
    .cl-textarea,
    .cl-field input,
    .cl-dt__input,
    .cl-dt__select,
    .gridjs-search-input,
    .iti__search-input.iti__search-input,
    .cl-review__textarea { font-size: 16px; }
}
