/*
 * coson/portal shell chrome: layout, navigation, header, dashboard grid,
 * settings list. Structural only — every colour/radius/shadow value is a
 * --portal-* token (theme.css), never a literal, so a host theme applies
 * with zero changes here. Class names are prefixed `cp-` to avoid clashing
 * with coson-ui's `cc-` component classes or host/page-specific styles.
 */

.cp-shell {
    display: flex;
    height: 100vh;
    overflow: hidden;
    font-family: var(--portal-font-sans);
    background: rgb(var(--portal-color-surface-muted));
    color: rgb(var(--portal-color-text));
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

/* ---------- desktop rail nav ---------- */
.cp-nav-desktop {
    width: 15.75rem;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    background: rgb(var(--portal-color-surface));
    border-right: 1px solid rgb(var(--portal-color-border));
    padding: 1.125rem 0.875rem;
    overflow: hidden;
}
.cp-nav-header { flex-shrink: 0; }
/* Block container, so the user card inside it can't space itself off the nav
   list with an auto margin — the gap belongs here. */
.cp-nav-footer { flex-shrink: 0; margin-top: 1rem; }
.cp-nav-scroll {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
}
.cp-nav-scroll::-webkit-scrollbar { display: none; }

/* brand block — host logo (or initial square) + name + tagline */
.cp-brand {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.5rem 0.625rem 1.375rem;
}
.cp-brand-logo {
    width: 2rem;
    height: 2rem;
    border-radius: 0.5625rem;
    flex-shrink: 0;
    background: rgb(var(--portal-color-primary));
    color: rgb(var(--portal-color-primary-contrast));
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 800;
    font-size: 0.9375rem;
    overflow: hidden;
}
/* Host wordmark lockup shown in the sidebar header, in its natural colours. */
.cp-brand-wordmark {
    max-width: 11.5rem;
    max-height: 2.5rem;
    width: auto;
    height: auto;
    object-fit: contain;
}
.cp-brand-name {
    font-weight: 800;
    font-size: 0.9375rem;
    color: rgb(var(--portal-color-text));
    letter-spacing: -0.01em;
    line-height: 1.2;
}
.cp-brand-tag {
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgb(var(--portal-color-slate-400));
}

.cp-nav-group { margin-top: 0.875rem; }
.cp-nav-group-pinned { margin-top: auto; }
.cp-nav-group-label {
    display: flex;
    align-items: center;
    font-size: 0.65625rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgb(var(--portal-color-slate-400));
    /* Left padding + gap centre the 5px dot on the nav items' 1rem icon column
       and start the label on their text baseline column (0.75rem + 1rem icon
       + 0.75rem gap = 2.5rem). */
    padding: 0.25rem 0.75rem 0.25rem 1.09375rem;
    gap: 1.09375rem;
    margin-bottom: 0.375rem;
}
.cp-nav-group-label::before {
    content: "";
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: rgb(var(--portal-color-slate-300));
    flex-shrink: 0;
}
/* Shown instead of the module list when a signed-in user holds no permission
   that puts anything in the sidebar — a permission-less account is a real,
   reachable state now that personal 2FA left the rail for the user card
   (694f1ef), which used to be the one item everybody could always see. */
.cp-nav-empty {
    padding: 0.625rem 1.09375rem;
    font-size: 0.8125rem;
    line-height: 1.4;
    color: rgb(var(--portal-color-slate-400));
}

.cp-nav-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5625rem 0.75rem;
    border-radius: 0.625rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: rgb(var(--portal-color-text-muted));
    text-decoration: none;
    transition: background 0.12s ease, color 0.12s ease;
}
.cp-nav-item:hover {
    background: rgb(var(--portal-color-surface-muted));
    color: rgb(var(--portal-color-text));
}
.cp-nav-item.cp-active {
    background: rgb(var(--portal-color-primary) / 0.08);
    color: rgb(var(--portal-color-primary));
    font-weight: 700;
}
.cp-nav-item-icon {
    display: inline-flex;
    width: 1rem;
    height: 1rem;
    flex-shrink: 0;
    color: rgb(var(--portal-color-slate-400));
}
.cp-nav-item.cp-active .cp-nav-item-icon { color: rgb(var(--portal-color-primary)); }
.cp-nav-label { flex: 1; }
.cp-nav-badge {
    margin-left: auto;
    font-size: 0.59375rem;
    font-weight: 800;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: rgb(var(--portal-color-slate-400));
    padding: 0.125rem 0.375rem;
    border-radius: var(--portal-radius-sm, 5px);
    /* A TINT, not a chip (Matt, 2026-07-28: the digits' background should blend in).
       Solid slate-100 on the white rail read as a hard pill competing with the item's
       own label — at 55% it sits back and still separates the count from the text.
       The tokens are space-separated RGB channels, which is what makes the `/ alpha`
       syntax work at all; a hex value here could not do this. */
    background: rgb(var(--portal-color-slate-100) / 0.55);
}
/* On hover and on the active row the badge follows its item rather than staying grey
   against a tinted background — a slate pill on the primary-tinted active row was the
   one place it looked pasted on. */
.cp-nav-item:hover .cp-nav-badge { background: rgb(var(--portal-color-slate-100) / 0.9); }
.cp-nav-item.cp-active .cp-nav-badge {
    background: rgb(var(--portal-color-primary) / 0.1);
    color: rgb(var(--portal-color-primary));
}

/* ---------- sidebar user card (pinned bottom) ---------- */
.cp-user-card {
    display: flex;
    align-items: center;
    gap: 0.6875rem;
    padding: 0.6875rem 0.75rem;
    border-radius: var(--portal-radius-md);
    background: rgb(var(--portal-color-surface-muted));
    border: 1px solid rgb(var(--portal-color-border));
}
.cp-avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: rgb(var(--portal-color-primary));
    color: rgb(var(--portal-color-primary-contrast));
    font-weight: 800;
    flex-shrink: 0;
}
.cp-avatar-sm { width: 2.25rem; height: 2.25rem; font-size: 0.875rem; }
.cp-avatar-lg { width: 3.25rem; height: 3.25rem; font-size: 1.125rem; }
.cp-user-link {
    display: flex;
    align-items: center;
    gap: 0.6875rem;
    flex: 1;
    min-width: 0;
    text-decoration: none;
    color: inherit;
    border-radius: var(--portal-radius-sm);
}
.cp-user-link:hover .cp-user-name { color: rgb(var(--portal-color-primary)); }
.cp-user-meta { flex: 1; min-width: 0; }
.cp-nav-credit {
    margin-top: 0.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    font-size: 0.6875rem;
    letter-spacing: 0.02em;
    color: rgb(var(--portal-color-text-muted));
}
.cp-nav-credit strong { font-weight: 700; color: rgb(var(--portal-color-text)); }
/* Sized in em so it tracks the credit line's own type rather than needing a
   second magic number if that font-size changes.
 *
 * Desaturated deliberately: the sidebar HEADER already carries the wordmark in
 * full brand colour, and a second coloured mark in the footer competes with it
 * for the same attention. Greyed, it reads as a byline — which is what it is.
 * Note this is the opposite call to the header, where a grayscale filter was
 * removed for exactly the same reason in reverse (it made the primary logo look
 * washed out). */
.cp-nav-credit-mark {
    height: 1.375em;
    width: auto;
    filter: grayscale(1);
    opacity: 0.55;
}
.cp-nav-credit-mark:hover { opacity: 0.8; }
.cp-user-name {
    font-size: 0.84375rem;
    font-weight: 700;
    color: rgb(var(--portal-color-text));
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.cp-user-sub {
    font-size: 0.71875rem;
    color: rgb(var(--portal-color-text-muted));
    display: flex;
    align-items: center;
    gap: 0.3125rem;
    min-width: 0;
}
.cp-user-role { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cp-user-2fa { display: inline-flex; align-items: center; gap: 3px; flex-shrink: 0; font-weight: 700; }
.cp-user-2fa::before {
    content: "";
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: currentColor;
}
.cp-user-2fa-on { color: rgb(var(--portal-color-success)); }
.cp-user-2fa-off { color: rgb(var(--portal-color-danger)); }

/* The user card's action rail: profile, security, log out. */
.cp-user-actions {
    display: flex;
    align-items: center;
    gap: 0.125rem;
    flex-shrink: 0;
}
/* One rule for all three, so the anchors and the logout button (inside its own
   form, for CSRF) cannot drift apart visually. */
.cp-user-action {
    border: none;
    background: none;
    cursor: pointer;
    color: rgb(var(--portal-color-slate-400));
    padding: 0.25rem;
    border-radius: var(--portal-radius-sm);
    display: inline-flex;
    flex-shrink: 0;
    text-decoration: none;
}
.cp-user-action:hover {
    color: rgb(var(--portal-color-text));
    /* A tint of the border colour, not `surface`. The user card's own background
       IS surface-muted, so hovering with plain surface put a WHITE square behind
       the icon — it read as a rendering fault rather than a hover state. */
    background: rgb(var(--portal-color-border) / 0.55);
}
/* The security action keeps its 2FA state colour on hover — the state is the
   point, so it must not be overridden by the generic hover above. */
.cp-user-action.cp-user-2fa-on:hover { color: rgb(var(--portal-color-success)); }
.cp-user-action.cp-user-2fa-off:hover { color: rgb(var(--portal-color-danger)); }
.cp-user-action:focus-visible {
    outline: 2px solid rgb(var(--portal-color-primary));
    outline-offset: 1px;
}

/* Retained: the mobile drawer still renders the older single-button markup. */
.cp-user-logout {
    border: none;
    background: none;
    cursor: pointer;
    color: rgb(var(--portal-color-slate-400));
    padding: 0.25rem;
    border-radius: var(--portal-radius-sm);
    display: inline-flex;
    flex-shrink: 0;
}
.cp-user-logout:hover {
    color: rgb(var(--portal-color-text));
    background: rgb(var(--portal-color-surface));
}

/* ---------- mobile nav: burger + drawer (hidden above the breakpoint) ----------

   Replaces a fixed bottom bar that rendered every visible item in one flat
   horizontally-scrolling strip. For a super-admin that was thirteen cells with
   no scroll affordance, so most were unreachable in practice, and it got worse
   with every module added. The drawer has no such ceiling, keeps the desktop
   group headers, and finally gives the user card (with its "NO 2FA" warning) a
   mobile home.

   [x-cloak] is declared here because the shell loads no bundled JS/CSS — the
   feature modules that use Alpine each ship their own copy of this rule inline,
   and the drawer would flash open on first paint without it. */
[x-cloak] { display: none !important; }

.cp-nav-mobile { display: none; flex-shrink: 0; }

/* flex-shrink:0 above and here: .cp-breadcrumbs is min-width:0 so the crumb
   ellipses rather than shoving the topbar past its fixed height — the burger
   must not be the thing that gives way. */
.cp-burger {
    display: flex;
    align-items: center;
    justify-content: center;
    /* 44px minimum touch target. */
    width: 2.75rem;
    height: 2.75rem;
    margin-left: -0.5rem;
    padding: 0;
    border: 0;
    border-radius: var(--portal-radius-md);
    background: transparent;
    color: rgb(var(--portal-color-text));
    cursor: pointer;
}
.cp-burger:hover { background: rgb(var(--portal-color-surface-muted)); }

/* Sits above .cp-nav-mobile's retired z-index 40 and below coson-ui's
   .cc-modal-backdrop at 100, so a modal opened from a page still wins. */
.cp-drawer-layer { position: fixed; inset: 0; z-index: 60; }

.cp-drawer-scrim {
    position: absolute;
    inset: 0;
    background: rgb(var(--portal-color-ink) / 0.45);
}

.cp-drawer {
    position: absolute;
    /* Left edge, matching the desktop rail, so navigation lives on the same
       side at every screen size. */
    inset: 0 auto 0 0;
    display: flex;
    flex-direction: column;
    width: min(19rem, 86vw);
    background: rgb(var(--portal-color-surface));
    border-right: 1px solid rgb(var(--portal-color-border));
    box-shadow: var(--portal-shadow-md);
    /* Notched phones: without this the sign-out row can sit under the home
       indicator. */
    padding-bottom: env(safe-area-inset-bottom, 0);
    padding-left: env(safe-area-inset-left, 0);
}

.cp-drawer-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex-shrink: 0;
    height: 4.125rem;
    padding: 0 0.75rem 0 1.25rem;
    border-bottom: 1px solid rgb(var(--portal-color-border));
}
.cp-drawer-title {
    font-size: 0.9375rem;
    font-weight: 800;
    color: rgb(var(--portal-color-text));
}
.cp-drawer-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    border: 0;
    border-radius: var(--portal-radius-md);
    background: transparent;
    color: rgb(var(--portal-color-text-muted));
    cursor: pointer;
}
.cp-drawer-close:hover { background: rgb(var(--portal-color-surface-muted)); }

/* The drawer scrolls its own list; the page behind is locked (see
   body.cp-drawer-open below) so the two cannot fight. */
.cp-drawer-scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 0.875rem 0.75rem;
}

.cp-drawer-foot {
    flex-shrink: 0;
    padding: 0.75rem;
    border-top: 1px solid rgb(var(--portal-color-border));
}

/* Drawer rows reuse .cp-nav-item from the desktop rail, so there is one visual
   language and one set of rules — but they need a bigger touch target here. */
.cp-drawer .cp-nav-item { min-height: 2.75rem; }

.cp-drawer-enter { transition: transform 0.15s ease; }
.cp-drawer-from { transform: translateX(-100%); }

body.cp-drawer-open .cp-scroll { overflow: hidden; }

/* First reduced-motion block in this sheet: the drawer is the first thing here
   that moves far enough to matter. */
@media (prefers-reduced-motion: reduce) {
    .cp-drawer-enter { transition: none; }
    .cp-drawer-from { transform: none; }
}

/* ---------- skeleton placeholders ----------

   One primitive for the whole app. Pair with wire:loading.delay so a skeleton
   only appears when there is genuinely a wait — flashing one on a 40ms response
   is worse than showing nothing.

   The shimmer is this sheet's first @keyframes; everything else here is a
   transition. */
.cp-skel {
    display: block;
    border-radius: var(--portal-radius-sm);
    /* Two slate stops with a lighter band travelling across them. Built from
       tokens, not literals, so a host theme carries it. */
    background-image: linear-gradient(
        90deg,
        rgb(var(--portal-color-slate-100)) 0%,
        rgb(var(--portal-color-slate-100) / 0.45) 40%,
        rgb(var(--portal-color-slate-100)) 80%
    );
    background-size: 300% 100%;
    animation: cp-skel-shimmer 1.4s ease-in-out infinite;
}
@keyframes cp-skel-shimmer {
    0% { background-position: 100% 0; }
    100% { background-position: 0 0; }
}

.cp-skel-lines { display: flex; flex-direction: column; gap: 0.5rem; }
.cp-skel-line { height: 0.75rem; }
.cp-skel-title { height: 1.375rem; width: 40%; }
.cp-skel-block { height: 100%; min-height: 6rem; width: 100%; }
.cp-skel-circle { width: 2.375rem; height: 2.375rem; border-radius: 50%; }

/* A whole tile standing in for a stat while it loads. */
.cp-skel-stat {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 1.25rem;
    background: rgb(var(--portal-color-surface));
    border: 1px solid rgb(var(--portal-color-border));
    border-radius: var(--portal-radius-lg);
    box-shadow: var(--portal-shadow-card);
}

/* Buttons: a spinner plus the disabled state, so a slow save cannot be
   double-submitted. REVIEW-FINDINGS recorded login as double-submittable for
   exactly this reason. */
.cp-spinner {
    display: inline-block;
    width: 0.875rem;
    height: 0.875rem;
    border: 2px solid rgb(255 255 255 / 0.45);
    border-top-color: #fff;
    border-radius: 50%;
    animation: cp-spin 0.6s linear infinite;
}
@keyframes cp-spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    /* Static tint rather than a travelling band; still clearly a placeholder. */
    .cp-skel {
        animation: none;
        background-image: none;
        background-color: rgb(var(--portal-color-slate-100));
    }
    .cp-spinner { animation-duration: 1.8s; }
}

@media (max-width: 768px) {
    .cp-nav-desktop { display: none; }
    /* No bottom bar to clear any more, so .cp-main keeps its normal padding. */
    .cp-nav-mobile { display: block; }
}

/* ---------- header / breadcrumbs / user menu ---------- */
.cp-main {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}
.cp-topbar {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 1rem;
    height: 4.125rem;
    padding: 0 1.75rem;
    border-bottom: 1px solid rgb(var(--portal-color-border));
    background: rgb(var(--portal-color-surface) / 0.9);
    backdrop-filter: blur(8px);
    /* Needed so the bell panel (a descendant) can paint over .cp-scroll:
       backdrop-filter already makes this a stacking context, and with no
       z-index it renders in DOM order — beneath the scroll area that follows
       it. See .cp-bell-panel. */
    position: relative;
    z-index: 20;
}

/* the single padded scroll region (header + content share this padding) */
.cp-scroll {
    flex: 1;
    overflow-y: auto;
    padding: 2rem 2.5rem;
}
/* cap the measure so pages don't stretch full-bleed on wide monitors; header
   and content live inside this so they stay aligned. */
.cp-scroll-inner { max-width: 78rem; }

.cp-page-header { margin-bottom: 1.5rem; }
.cp-page-header-row { display: flex; align-items: flex-start; }
.cp-page-subtitle {
    font-size: 0.9375rem;
    color: rgb(var(--portal-color-text-muted));
    margin: 0.5rem 0 0;
    max-width: 40rem;
}
/* 30px/800 — DESIGN-CONTRACT §5. One size for every page in the app: feature
   packages render their titles through this component, so a package must never
   style its own <h1>. */
.cp-page-title {
    font-size: 1.875rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.1;
    color: rgb(var(--portal-color-text));
    margin: 0;
}
.cp-page-actions { margin-left: auto; display: flex; gap: 0.5rem; }

/* Eyebrow — the brand's spaced uppercase overline, for section headers.
   Muted, not --portal-color-primary: primary is the colour of *actions*, and an
   overline reading as dark as a button made every page header look like it had
   two titles. DESIGN-CONTRACT §5 says slate-400, which is 2.9:1 on white and
   fails AA at this size; slate-500 (text-muted) is the nearest passing tone. */
.cp-eyebrow {
    font-size: 0.71875rem;
    font-weight: 700;
    letter-spacing: var(--portal-letterspacing-eyebrow, 0.14em);
    text-transform: uppercase;
    color: rgb(var(--portal-color-text-muted));
    margin-bottom: 0.375rem;
}

/* ---------- main content / sidebar split ---------- */
.cp-body { display: flex; }
.cp-content { flex: 1; min-width: 0; }
.cp-sidebar { width: 16rem; flex-shrink: 0; }
.cp-breadcrumbs {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    font-size: 0.8125rem;
    font-weight: 600;
    color: rgb(var(--portal-color-text-muted));
    /* The crumb's page name is page-supplied data (a company, a quote title), so
       it has no length bound. min-width:0 lets it shrink and ellipsis below
       rather than wrap and push the topbar past its fixed height. */
    min-width: 0;
}
.cp-crumb-pill {
    display: inline-flex;
    align-items: center;
    gap: 0.4375rem;
    font-size: 0.8125rem;
    font-weight: 700;
    /* slate-600 on the slate-100 fill (DESIGN-CONTRACT §5). text-muted here is
       only 3.9:1 against that fill. */
    color: rgb(var(--portal-color-slate-600));
    background: rgb(var(--portal-color-slate-100));
    padding: 0.375rem 0.8125rem;
    border-radius: var(--portal-radius-pill, 999px);
    text-decoration: none;
    white-space: nowrap;
}
/* Leading dot marks the pill as the section the page sits in, echoing the
   sidebar's group labels. */
.cp-crumb-pill::before {
    content: "";
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: rgb(var(--portal-color-primary));
    flex-shrink: 0;
}
.cp-crumb-sep { display: inline-flex; color: rgb(var(--portal-color-slate-300)); }
/* The current page is the crumb that matters, so it reads darker than the
   section pill it hangs off (DESIGN-CONTRACT §5: fg2). */
.cp-crumb-page {
    font-size: 0.875rem;
    font-weight: 600;
    color: rgb(var(--portal-color-slate-700));
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cp-topbar-actions { margin-left: auto; display: flex; align-items: center; gap: 0.75rem; }
.cp-bell {
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 0.6875rem;
    border: 1px solid rgb(var(--portal-color-border));
    background: rgb(var(--portal-color-surface));
    color: rgb(var(--portal-color-text-muted));
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}
.cp-bell:hover { color: rgb(var(--portal-color-text)); }

/* ---------- notification bell: badge + panel ----------

   The panel is anchored to the bell rather than teleported to <body> (the
   drawer's approach), so .cp-topbar needs a stacking position of its own:
   backdrop-filter already makes it a stacking context, and without a z-index it
   paints in DOM order — i.e. underneath .cp-scroll, which follows it. 20 sits
   below .cp-drawer-layer's 60, so the mobile drawer still covers the topbar. */
.cp-bell-wrap { position: relative; display: flex; }
.cp-bell-badge {
    position: absolute;
    top: -0.1875rem;
    right: -0.1875rem;
    min-width: 1.0625rem;
    height: 1.0625rem;
    padding: 0 0.25rem;
    border-radius: var(--portal-radius-pill);
    /* Danger, not brand: the badge's job is to be noticed against a topbar
       already carrying brand colour elsewhere. */
    background: rgb(var(--portal-color-danger));
    color: #fff;
    font-size: 0.625rem;
    font-weight: 800;
    line-height: 1.0625rem;
    text-align: center;
    /* Reads as a chip sitting on the button, not part of its border. */
    box-shadow: 0 0 0 2px rgb(var(--portal-color-surface));
}

.cp-bell-panel {
    position: absolute;
    /* Below the bell, right-aligned to it; near the viewport edge on narrow
       screens the max-width keeps it on screen. */
    top: calc(100% + 0.625rem);
    right: 0;
    z-index: 50;
    width: min(23rem, calc(100vw - 2rem));
    display: flex;
    flex-direction: column;
    max-height: min(28rem, 70vh);
    background: rgb(var(--portal-color-surface));
    border: 1px solid rgb(var(--portal-color-border));
    border-radius: var(--portal-radius-lg);
    box-shadow: var(--portal-shadow-md);
    overflow: hidden;
}

.cp-bell-head {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 0.875rem;
    border-bottom: 1px solid rgb(var(--portal-color-border));
}
.cp-bell-title {
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: var(--portal-letterspacing-eyebrow);
    text-transform: uppercase;
    color: rgb(var(--portal-color-slate-400));
}
.cp-bell-close {
    margin-left: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    border: 0;
    border-radius: var(--portal-radius-sm);
    background: transparent;
    color: rgb(var(--portal-color-text-muted));
    cursor: pointer;
}
.cp-bell-close:hover { background: rgb(var(--portal-color-surface-muted)); color: rgb(var(--portal-color-text)); }

.cp-bell-body { overflow-y: auto; }

.cp-bell-item {
    display: flex;
    gap: 0.625rem;
    padding: 0.75rem 0.875rem;
    border-bottom: 1px solid rgb(var(--portal-color-border));
    text-decoration: none;
    color: inherit;
}
.cp-bell-item:last-child { border-bottom: 0; }
.cp-bell-item[href]:hover { background: rgb(var(--portal-color-surface-muted)); }
/* Unread carries a left rail rather than a bold row: the panel marks everything
   it shows as read on open, so the distinction is only alive for one glance and
   should not restyle half the list. */
.cp-bell-item-unread { box-shadow: inset 0.1875rem 0 0 rgb(var(--portal-color-primary)); }

.cp-bell-item-icon {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: var(--portal-radius-sm);
    background: rgb(var(--portal-color-surface-muted));
    color: rgb(var(--portal-color-slate-600));
}
.cp-bell-item-text { display: flex; flex-direction: column; gap: 0.0625rem; min-width: 0; }
.cp-bell-item-title { font-size: 0.8125rem; font-weight: 700; color: rgb(var(--portal-color-text)); }
.cp-bell-item-body {
    font-size: 0.75rem;
    color: rgb(var(--portal-color-text-muted));
    /* Two lines, then ellipsis: a long quote title must not push the timestamp
       out of the panel. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.cp-bell-item-time { margin-top: 0.125rem; font-size: 0.6875rem; color: rgb(var(--portal-color-slate-400)); }

.cp-bell-empty { padding: 2rem 1.25rem; text-align: center; }
.cp-bell-empty-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: var(--portal-radius-pill);
    background: rgb(var(--portal-color-surface-muted));
    color: rgb(var(--portal-color-slate-400));
}
.cp-bell-empty-title { margin: 0.75rem 0 0.25rem; font-size: 0.875rem; font-weight: 700; color: rgb(var(--portal-color-text)); }
.cp-bell-empty-body { margin: 0; font-size: 0.75rem; line-height: 1.5; color: rgb(var(--portal-color-text-muted)); }

/* Skeleton rows mirror .cp-bell-item's geometry so nothing shifts when the real
   rows land. */
.cp-bell-skeleton-row {
    display: flex;
    gap: 0.625rem;
    padding: 0.75rem 0.875rem;
    border-bottom: 1px solid rgb(var(--portal-color-border));
}
.cp-bell-skeleton-lines { flex: 1; padding-top: 0.25rem; }
/* Sized here, not with the primitive's width prop: that sets width only, and the
   circle's height is fixed, so passing a width alone gives an oval. */
.cp-bell-skeleton-row .cp-skel-circle { width: 1.75rem; height: 1.75rem; }

/* ---------- toasts ----------

   Above .cc-modal-backdrop's 100: a toast confirming something done inside a
   modal has to be visible over it, or the confirmation is invisible exactly when
   it is most needed. */
.cp-toasts {
    position: fixed;
    z-index: 110;
    bottom: 1.25rem;
    right: 1.25rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    /* The container is always present (an aria-live region added at the same
       moment as its content is often not announced), so it must not swallow
       clicks on the page behind it. */
    pointer-events: none;
}
.cp-toast {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: 0.625rem;
    width: min(21rem, calc(100vw - 2.5rem));
    padding: 0.75rem 0.875rem;
    background: rgb(var(--portal-color-surface));
    border: 1px solid rgb(var(--portal-color-border));
    border-radius: var(--portal-radius-lg);
    box-shadow: var(--portal-shadow-md);
}
.cp-toast-icon {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.375rem;
    height: 1.375rem;
    border-radius: var(--portal-radius-pill);
    background: rgb(var(--portal-color-success) / 0.14);
    color: rgb(var(--portal-color-success));
}
.cp-toast-text { flex: 1; min-width: 0; }
.cp-toast-title { margin: 0; font-size: 0.8125rem; font-weight: 700; color: rgb(var(--portal-color-text)); }
.cp-toast-body { margin: 0.125rem 0 0; font-size: 0.75rem; color: rgb(var(--portal-color-text-muted)); }
.cp-toast-close {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.25rem;
    height: 1.25rem;
    border: 0;
    border-radius: var(--portal-radius-sm);
    background: transparent;
    color: rgb(var(--portal-color-slate-400));
    cursor: pointer;
}
.cp-toast-close:hover { color: rgb(var(--portal-color-text)); }

/* Legacy user-menu classes retained for hosts/pages that still render the
   inline variant; the shell now renders the sidebar .cp-user-card instead. */
.cp-user-menu { margin-left: auto; display: flex; align-items: center; gap: 0.625rem; }
.cp-user-menu-name { font-size: 0.8125rem; font-weight: 700; }
.cp-user-menu-email { font-size: 0.75rem; color: rgb(var(--portal-color-text-muted)); }

/* ---------- dashboard: section labels + jump grid ---------- */
.cp-section-label {
    font-size: 0.71875rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgb(var(--portal-color-slate-400));
    margin: 0 0 1rem;
}
.cp-jump-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: 0.875rem;
    margin-bottom: 1.75rem;
}
.cp-jump-card {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    background: rgb(var(--portal-color-surface));
    border: 1px solid rgb(var(--portal-color-border));
    border-radius: 0.875rem;
    padding: 1.125rem;
    text-decoration: none;
    transition: box-shadow 0.12s ease, border-color 0.12s ease;
}
.cp-jump-card:hover {
    box-shadow: var(--portal-shadow-md);
    border-color: rgb(var(--portal-color-border-strong));
}
.cp-jump-icon {
    width: 2.375rem;
    height: 2.375rem;
    border-radius: 0.625rem;
    background: rgb(var(--portal-color-surface-muted));
    color: rgb(var(--portal-color-text-muted));
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.cp-jump-body { flex: 1; min-width: 0; }
.cp-jump-title {
    display: block;
    font-weight: 700;
    font-size: 0.9375rem;
    color: rgb(var(--portal-color-text));
}
.cp-jump-sub {
    display: block;
    font-size: 0.71875rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: rgb(var(--portal-color-text-muted));
    margin-top: 2px;
}
.cp-jump-arrow {
    color: rgb(var(--portal-color-slate-300));
    flex-shrink: 0;
    margin-top: 0.25rem;
    display: inline-flex;
}

/* ---------- dashboard widgets ---------- */
.cp-widget-title {
    font-size: 0.875rem;
    font-weight: 700;
    color: rgb(var(--portal-color-text));
    margin-bottom: 0.625rem;
}

/* ---------- form primitives (reusable across pages) ---------- */
.cp-field { margin-bottom: 1.125rem; }
.cp-label {
    display: block;
    font-size: 0.84375rem;
    font-weight: 700;
    color: rgb(var(--portal-color-text));
    margin-bottom: 0.4375rem;
}
.cp-input {
    width: 100%;
    font-family: inherit;
    font-size: 0.875rem;
    padding: 0.6875rem 0.8125rem;
    border: 1px solid rgb(var(--portal-color-border-strong));
    border-radius: 0.625rem;
    outline: none;
    background: rgb(var(--portal-color-surface));
    color: rgb(var(--portal-color-text));
    box-sizing: border-box;
}
.cp-input:focus { border-color: rgb(var(--portal-color-primary)); }
.cp-check {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.84375rem;
    color: rgb(var(--portal-color-text-muted));
    margin: 1rem 0 1.375rem;
    cursor: pointer;
}
.cp-btn-block { width: 100%; justify-content: center; }
.cp-label-row { display: flex; align-items: baseline; justify-content: space-between; }
.cp-auth-link {
    font-size: 0.8125rem;
    font-weight: 600;
    color: rgb(var(--portal-color-text-muted));
    text-decoration: none;
}
.cp-auth-link:hover { color: rgb(var(--portal-color-text)); text-decoration: underline; }

/* ---------- full-screen auth split (sign-in) ---------- */
.cp-auth {
    display: flex;
    min-height: 100vh;
    font-family: var(--portal-font-sans);
    background: rgb(var(--portal-color-surface-muted));
    -webkit-font-smoothing: antialiased;
}
.cp-auth-brand {
    flex: 1.3;
    background: rgb(var(--portal-color-ink));
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 2.75rem;
}
.cp-auth-logo-row { display: flex; align-items: center; gap: 0.75rem; }
.cp-auth-logo {
    width: 2.375rem;
    height: 2.375rem;
    border-radius: 0.625rem;
    background: rgba(255, 255, 255, 0.12);
    border: 1px solid rgba(255, 255, 255, 0.18);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    color: #fff;
    font-weight: 800;
}
.cp-auth-logo img { width: 1.5rem; height: 1.5rem; object-fit: contain; filter: brightness(0) invert(1); }
/* White host wordmark on the dark brand panel (already light — no filter). */
.cp-auth-wordmark { max-width: 13rem; max-height: 3rem; width: auto; height: auto; object-fit: contain; }
.cp-auth-brand-name { font-weight: 800; font-size: 1rem; color: #fff; letter-spacing: -0.01em; }
.cp-auth-brand-tag {
    font-size: 0.625rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.5);
}
.cp-auth-quote { border-left: 2px solid rgba(255, 255, 255, 0.3); padding-left: 1rem; }
.cp-auth-quote-text { font-size: 1.1875rem; font-style: italic; color: #fff; line-height: 1.4; }
.cp-auth-quote-cite { font-size: 0.8125rem; color: rgba(255, 255, 255, 0.5); margin-top: 0.375rem; }
.cp-auth-tags { display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; }
.cp-auth-tag { display: flex; align-items: center; gap: 0.375rem; font-size: 0.78rem; font-weight: 600; color: rgba(255, 255, 255, 0.65); }
.cp-auth-tag::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: rgba(255, 255, 255, 0.4); }
.cp-auth-form { flex: 1; display: flex; align-items: center; justify-content: center; padding: 2rem; }
.cp-auth-form-inner { width: 100%; max-width: 22.5rem; }
.cp-auth-title { font-weight: 800; font-size: 1.875rem; color: rgb(var(--portal-color-text)); margin: 0 0 0.5rem; }
.cp-auth-sub { font-size: 0.875rem; color: rgb(var(--portal-color-text-muted)); margin: 0 0 1.75rem; line-height: 1.5; }
@media (max-width: 820px) { .cp-auth-brand { display: none; } }

/* ---------- dashboard stat tiles ---------- */
/* Six per row (Matt, 2026-07-30) — was four. Two intermediate breakpoints
   rather than one, so six narrow tiles don't jump straight to two wide ones
   on a mid-size screen. */
.cp-stat-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 1rem; margin-bottom: 1.75rem; }
@media (max-width: 1400px) { .cp-stat-grid { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 900px) { .cp-stat-grid { grid-template-columns: repeat(2, 1fr); } }
.cp-stat {
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    padding: 1.25rem;
    background: rgb(var(--portal-color-surface));
    border: 1px solid rgb(var(--portal-color-border));
    border-radius: var(--portal-radius-lg);
    box-shadow: var(--portal-shadow-card);
    text-decoration: none;
}
/* Monochrome isometric line motif, top-right (replaces the old coloured blur):
   two opposing 60deg/-60deg hairline bands + a fine 45deg weave, so it reads as
   a faceted isometric texture rather than flat stripes. Masked so it dissolves
   into the card and never fights the value/label. */
.cp-stat-halo {
    position: absolute; top: 0; right: 0;
    width: 12rem; height: 8.5rem;
    pointer-events: none;
    /* True isometric cube lattice: the three axes of an isometric projection —
       vertical, +30deg and -30deg — so the corner reads as stacked 3D cubes
       rather than a flat diamond weave. */
    background-image:
        repeating-linear-gradient(90deg,
            rgb(var(--portal-color-slate-400) / 0.8) 0, rgb(var(--portal-color-slate-400) / 0.8) 1.5px,
            transparent 1.5px, transparent 22px),
        repeating-linear-gradient(30deg,
            rgb(var(--portal-color-slate-400) / 0.8) 0, rgb(var(--portal-color-slate-400) / 0.8) 1.5px,
            transparent 1.5px, transparent 22px),
        repeating-linear-gradient(-30deg,
            rgb(var(--portal-color-slate-300) / 0.7) 0, rgb(var(--portal-color-slate-300) / 0.7) 1.5px,
            transparent 1.5px, transparent 22px);
    -webkit-mask-image: radial-gradient(85% 95% at 100% 0%, #000 0%, rgba(0,0,0,.8) 30%, rgba(0,0,0,.3) 58%, transparent 78%);
    mask-image: radial-gradient(85% 95% at 100% 0%, #000 0%, rgba(0,0,0,.8) 30%, rgba(0,0,0,.3) 58%, transparent 78%);
}
/* Lift the motif slightly on hover — the tile feels alive without colour. */
.cp-stat:hover .cp-stat-halo { opacity: 1; }
.cp-stat .cp-stat-halo { opacity: 0.85; transition: opacity 0.2s ease; }
.cp-stat-icon {
    position: relative;
    display: flex; align-items: center; justify-content: center;
    width: 2.375rem; height: 2.375rem;
    border-radius: 0.625rem;
    margin-bottom: 1rem;
}
/* tabular-nums so the digits do not shimmy while the count-up runs, and so a
   tile going 8 -> 12 -> 137 does not shift its own width mid-animation. */
.cp-stat-value {
    position: relative;
    font-size: 1.75rem;
    font-weight: 800;
    letter-spacing: -0.01em;
    line-height: 1.1;
    color: rgb(var(--portal-color-text));
    font-variant-numeric: tabular-nums;
}
.cp-stat-label { position: relative; margin-top: 0.125rem; font-size: 0.8125rem; color: rgb(var(--portal-color-text-muted)); }

/* Neutral Theme: stat chips are monochrome slate. The `tone` prop is kept on
   the component (hosts/modules still pass it) but resolves to the same slate
   treatment, so the dashboard reads as one system rather than a colour wheel. */
/* Brand-coloured chips (Matt, 2026-07-28), matching the quote panel below and the
   quote document's cover. --portal-color-brand rather than --portal-color-primary:
   primary resolves to the same dark slate as the chrome, so asking for it here would
   have changed nothing visible. */
.cp-stat-icon {
    background: rgb(var(--portal-color-brand) / 0.1);
    color: rgb(var(--portal-color-brand));
    border: 1px solid rgb(var(--portal-color-brand) / 0.18);
}

/* ---------- dashboard overview row (panels + quote) ---------- */
.cp-overview { display: grid; grid-template-columns: 1.5fr 1fr; gap: 1rem; align-items: stretch; min-height: 20rem; }

/* Both columns end level (Matt, 2026-07-28).
   align-items:stretch was already stretching the COLUMNS, but the cards inside them
   kept their own content height, so the inspiration quote sat about 70px shorter than
   Recent activity beside it. Each column is a flex stack whose LAST card absorbs the
   slack — which works whether a column holds one panel or several, unlike putting
   height:100% on every card.
   box-sizing explicitly: this application has no global border-box reset, so padding
   on a stretched card would otherwise overflow the height it was given. */
.cp-overview > div { display: flex; flex-direction: column; }
.cp-overview > div > * { box-sizing: border-box; }
.cp-overview > div > *:last-child { flex: 1 1 auto; }

/* min-height rather than a fixed height: Recent activity is capped at 6 entries, so it
   has a natural ceiling, and the floor stops the pair collapsing when there is little
   or no activity to show. */
@media (max-width: 900px) { .cp-overview { grid-template-columns: 1fr; min-height: 0; } }
.cp-quote-panel {
    position: relative;
    overflow: hidden;
    display: flex; flex-direction: column; justify-content: flex-end;
    /* Fills the height its column was given, so it ends level with the panel beside
       it. The min-height is the floor for a narrow viewport, where the columns stack
       and there is no neighbour to match. */
    height: 100%;
    min-height: 11rem; padding: 1.75rem;
    border-radius: var(--portal-radius-lg);
    /* The brand colour, same as the stat chips above and the quote document cover, so
       the three agree about what the brand looks like. Was --portal-color-ink. */
    background: rgb(var(--portal-color-brand));
}
/* Same isometric cube lattice as the stat tiles (vertical, +30deg, -30deg), in
   white for the ink panel, and masked into the top-right corner so it fades out
   before it reaches the quote. Drawn as a pseudo-element: no markup needed. */
.cp-quote-panel::before {
    content: "";
    position: absolute; top: 0; right: 0;
    width: 14rem; height: 10rem;
    pointer-events: none;
    background-image:
        repeating-linear-gradient(90deg,
            rgba(255, 255, 255, 0.18) 0, rgba(255, 255, 255, 0.18) 1.5px,
            transparent 1.5px, transparent 22px),
        repeating-linear-gradient(30deg,
            rgba(255, 255, 255, 0.18) 0, rgba(255, 255, 255, 0.18) 1.5px,
            transparent 1.5px, transparent 22px),
        repeating-linear-gradient(-30deg,
            rgba(255, 255, 255, 0.12) 0, rgba(255, 255, 255, 0.12) 1.5px,
            transparent 1.5px, transparent 22px);
    -webkit-mask-image: radial-gradient(85% 95% at 100% 0%, #000 0%, rgba(0,0,0,.8) 30%, rgba(0,0,0,.3) 58%, transparent 78%);
    mask-image: radial-gradient(85% 95% at 100% 0%, #000 0%, rgba(0,0,0,.8) 30%, rgba(0,0,0,.3) 58%, transparent 78%);
}
/* Positioned, so the quote paints over the lattice rather than under it. */
.cp-quote-text { position: relative; font-size: 1.1875rem; font-style: italic; color: #fff; line-height: 1.4; }
.cp-quote-cite { position: relative; margin-top: 0.75rem; font-size: 0.8125rem; color: rgba(255, 255, 255, 0.55); }
.cp-activity { display: flex; flex-direction: column; }
.cp-activity-row { display: flex; align-items: center; gap: 0.625rem; padding: 0.6875rem 0; border-top: 1px solid rgb(var(--portal-color-border)); font-size: 0.84375rem; color: rgb(var(--portal-color-text-muted)); }
.cp-activity-row:first-of-type { border-top: 0; }
.cp-activity-dot { width: 6px; height: 6px; border-radius: 50%; background: rgb(var(--portal-color-slate-300)); flex-shrink: 0; }
.cp-activity-row b { color: rgb(var(--portal-color-text)); font-weight: 700; }
.cp-activity-when { margin-left: auto; font-size: 0.75rem; color: rgb(var(--portal-color-text-muted)); flex-shrink: 0; }

.cp-mb { margin-bottom: 1rem; }

/* ---------- keyboard focus ----------
   The shell had NO focus-visible style anywhere and .cp-input actively removed
   the outline, so keyboard users had no idea where they were. One rule covers
   every interactive element in the shell and in feature packages. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: 2px solid rgb(var(--portal-color-primary));
    outline-offset: 2px;
    border-radius: inherit;
}
.cp-input:focus-visible {
    outline: none;
    border-color: rgb(var(--portal-color-primary));
    box-shadow: 0 0 0 3px rgb(var(--portal-color-primary) / 0.15);
}

/* ---------- onboarding checklist (first sign-in) ---------- */
.cp-onboard-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
}
.cp-onboard-sub {
    margin: 0.2rem 0 0;
    font-size: 0.8125rem;
    color: rgb(var(--portal-color-text-muted));
}
.cp-onboard-dismiss {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: var(--portal-radius-md);
    background: transparent;
    border: none;
    color: rgb(var(--portal-color-slate-400));
    cursor: pointer;
}
.cp-onboard-dismiss:hover {
    background: rgb(var(--portal-color-slate-100));
    color: rgb(var(--portal-color-slate-600));
}
.cp-onboard-steps {
    display: flex;
    flex-direction: column;
    gap: 0.875rem;
    margin-top: 1rem;
}
.cp-onboard-step {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
}
.cp-onboard-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 1.875rem;
    height: 1.875rem;
    margin-top: 0.0625rem;
    border-radius: 999px;
    background: rgb(var(--portal-color-brand) / 0.1);
    color: rgb(var(--portal-color-brand));
}
.cp-onboard-step.is-done .cp-onboard-icon {
    background: rgb(var(--portal-color-success) / 0.12);
    color: rgb(var(--portal-color-success));
}
.cp-onboard-label {
    font-weight: 600;
    font-size: 0.875rem;
    color: rgb(var(--portal-color-slate-800));
}
.cp-onboard-text {
    margin: 0.15rem 0 0;
    font-size: 0.8125rem;
    color: rgb(var(--portal-color-text-muted));
}
.cp-onboard-text a {
    color: rgb(var(--portal-color-brand));
}
.cp-onboard-link {
    display: inline-block;
    margin-top: 0.4rem;
    padding: 0;
    background: none;
    border: none;
    font-size: 0.8125rem;
    font-weight: 600;
    color: rgb(var(--portal-color-brand));
    cursor: pointer;
}
.cp-onboard-link:hover { text-decoration: underline; }

/* ---------- generic card primitive ---------- */
.cp-card {
    background: rgb(var(--portal-color-surface));
    border: 1px solid rgb(var(--portal-color-border));
    border-radius: var(--portal-radius-lg);
    box-shadow: var(--portal-shadow-card);
    padding: 1.25rem 1.5rem;
}

/* ---------- modules admin (super-admin switchboard) ---------- */
.cp-modules-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: 1rem;
}
.cp-module-card {
    display: flex;
    align-items: center;
    gap: 0.875rem;
    padding: 0.875rem 1rem;
    background: rgb(var(--portal-color-surface));
    border: 1px solid rgb(var(--portal-color-border));
    border-radius: var(--portal-radius-lg);
    box-shadow: var(--portal-shadow-card);
}
.cp-module-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 3rem;
    height: 3rem;
    border-radius: var(--portal-radius-md);
    background: rgb(var(--portal-color-slate-100));
    color: rgb(var(--portal-color-slate-700));
}
.cp-module-text {
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    flex: 1;
    min-width: 0;
}
.cp-module-name {
    font-weight: 700;
    font-size: 0.9375rem;
    color: rgb(var(--portal-color-text));
}
.cp-module-status { font-size: 0.8125rem; color: rgb(var(--portal-color-text-muted)); }
.cp-module-card.is-off { opacity: 0.72; }
.cp-module-card.is-off .cp-module-icon { color: rgb(var(--portal-color-slate-400)); }
.cp-toggle {
    position: relative;
    flex-shrink: 0;
    width: 2.75rem;
    height: 1.5rem;
    padding: 0;
    border: 1px solid rgb(var(--portal-color-border-strong));
    border-radius: var(--portal-radius-pill);
    background: rgb(var(--portal-color-slate-100));
    cursor: pointer;
    transition: background 0.15s ease, border-color 0.15s ease;
}
.cp-toggle-knob {
    position: absolute;
    top: 50%;
    left: 0.1875rem;
    transform: translateY(-50%);
    width: 1.125rem;
    height: 1.125rem;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 2px rgb(21 38 59 / 0.25);
    transition: left 0.15s ease;
}
.cp-toggle.is-on { background: rgb(var(--portal-color-primary)); border-color: rgb(var(--portal-color-primary)); }
.cp-toggle.is-on .cp-toggle-knob { left: calc(100% - 1.3125rem); }
.cp-toggle:disabled { opacity: 0.6; cursor: default; }

/* ---------- tabs (settings sections, underline strip) ---------- */
/* Rectangular tabs on a shared baseline rule, the active one picked out by a
   solid underline in the brand accent — not a segmented pill (Matt,
   2026-07-30): a pill reads as a toggle between two states, where this is a
   set of distinct pages, and it stopped scaling once a module could
   contribute more than two or three sections (Quotes now has Quotes +
   Templates). Left-aligned with the cards below, same reasoning as before.
   Scrolls horizontally rather than wrapping on a narrow screen, so a long
   tab list stays one line instead of stacking into a second row of pills. */
.cp-tabs-wrap {
    display: flex;
    justify-content: flex-start;
    /* Owned here rather than by every caller: six settings screens wrapped this strip in
       an inline margin-bottom:22px and the two account screens did not, so the identical
       control sat flush against its content on /profile and spaced on /settings. */
    margin-bottom: 1.375rem;
    border-bottom: 1px solid rgb(var(--portal-color-border));
    overflow-x: auto;
    overflow-y: hidden; /* a lone overflow-x computes overflow-y to auto too,
        and .cp-tab's -1px margin-bottom + 3px border-bottom is enough
        content-height overflow to trip a phantom vertical scrollbar */
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    -ms-overflow-style: none;
}
.cp-tabs-wrap::-webkit-scrollbar { display: none; }
.cp-tabs {
    display: inline-flex;
    gap: 0.25rem;
}
/* A real tab, not a thin link with a rule under it: generous click target,
   a hover fill with rounded top corners so it reads as liftable, and a 3px
   active bar heavy enough to read as the primary navigation on the page —
   this strip IS the page's content on the Settings landing screen, so it
   has to carry more visual weight than a caption-sized label (Matt,
   2026-07-30, "the tabs need improving greatly"). */
.cp-tab {
    position: relative;
    padding: 0.75rem 1rem;
    margin-bottom: -1px; /* sits flush on the wrap's own baseline rule */
    border-radius: 0.5rem 0.5rem 0 0;
    border-bottom: 3px solid transparent;
    font-size: 0.9375rem;
    font-weight: 600;
    color: rgb(var(--portal-color-text-muted));
    text-decoration: none;
    white-space: nowrap;
    transition: color .14s ease, border-color .14s ease, background-color .14s ease;
}
.cp-tab:hover { color: rgb(var(--portal-color-text)); background: rgb(var(--portal-color-surface-muted)); }
.cp-tab-disabled { opacity: 0.6; cursor: default; }
.cp-tab-disabled:hover { color: rgb(var(--portal-color-text-muted)); background: none; opacity: 0.6; }
.cp-tab-active,
.cp-tab-active:hover {
    font-weight: 700;
    color: rgb(var(--portal-color-text));
    background: rgb(var(--portal-color-surface-muted));
    border-bottom-color: rgb(var(--portal-color-primary));
}

/* ---------- notification preferences ---------- */
.cp-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
/* ---------- reserved .test domain hint ---------- */

/*
 * Sits under an email field whose value is a .test address, which this application never
 * sends to. Amber like the demo banner: nothing is broken, but an expectation needs
 * correcting before somebody waits for an email that will not arrive.
 */
.cp-test-domain-hint {
    display: flex;
    align-items: flex-start;
    gap: 0.4rem;
    margin: 0.4rem 0 0;
    font-size: 0.78125rem;
    line-height: 1.5;
    color: rgb(var(--portal-color-text-muted));
}
.cp-test-domain-hint svg {
    flex-shrink: 0;
    margin-top: 0.15rem;
    color: rgb(var(--portal-color-warning));
}
.cp-test-domain-hint strong {
    color: rgb(var(--portal-color-text));
    font-weight: 700;
}
.cp-test-domain-hint code {
    font-family: var(--portal-font-mono);
    font-size: 0.75rem;
}

/* ---------- demo environment banner ---------- */

/*
 * Amber, not the danger red: nothing is wrong, but everything here is temporary. Sits
 * above the topbar and spans the full content column, because a warning the eye can skip
 * is a warning that did not happen.
 */
.cp-demo-banner {
    display: flex;
    align-items: flex-start;
    /* Stated, not inherited. This banner renders above .cp-auth on the signed-out
       screens, and .cp-auth is the only thing setting a family there — so outside it the
       text fell back to the browser default. */
    font-family: var(--portal-font-sans);
    gap: 0.625rem;
    padding: 0.6rem 1.25rem;
    background: rgb(var(--portal-color-warning) / 0.1);
    border-bottom: 1px solid rgb(var(--portal-color-warning) / 0.35);
    color: rgb(var(--portal-color-text));
    font-size: 0.8125rem;
    line-height: 1.5;
}
.cp-demo-banner strong {
    font-weight: 800;
}
.cp-demo-banner-tag {
    flex-shrink: 0;
    padding: 0.05rem 0.5rem;
    border-radius: var(--portal-radius-pill);
    background: rgb(var(--portal-color-warning));
    color: #fff;
    font-size: 0.6875rem;
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* ---------- settings screens share one content width ---------- */

/*
 * Every settings screen's content column, so they stop being 760px, 640px, 56rem and
 * full width depending on which one you opened. The token lives in theme.css; a screen
 * that needs the width reads it rather than picking its own.
 */
.cp-settings-col {
    width: min(100%, var(--portal-settings-width));
}
.cp-notification-page {
    width: min(100%, var(--portal-settings-width));
}
.cp-notification-view-switch {
    display: inline-flex;
    gap: 0.25rem;
    margin-bottom: 1.25rem;
    padding: 0.25rem;
    border: 1px solid rgb(var(--portal-color-border));
    border-radius: var(--portal-radius-md);
    background: rgb(var(--portal-color-surface-muted));
}
.cp-notification-view-switch a {
    padding: 0.55rem 0.875rem;
    border-radius: 0.55rem;
    color: rgb(var(--portal-color-text-muted));
    font-size: 0.8125rem;
    font-weight: 700;
    text-decoration: none;
}
.cp-notification-view-switch a:hover {
    color: rgb(var(--portal-color-text));
}
.cp-notification-view-switch a.is-active {
    color: rgb(var(--portal-color-text));
    background: rgb(var(--portal-color-surface));
    box-shadow: 0 1px 2px rgb(21 38 59 / 0.08);
}
.cp-notification-stack {
    display: grid;
    gap: 1rem;
}
.cp-notification-card .cc-card-title {
    padding: 1.125rem 1.25rem 0.3rem;
    border-bottom: 0;
    font-size: 1rem;
    font-weight: 800;
}
.cp-notification-card .cc-card-body {
    padding: 0.25rem 1.25rem 1.25rem;
}
.cp-notification-copy {
    max-width: 46rem;
    margin: 0 0 1.1rem;
    color: rgb(var(--portal-color-text-muted));
    font-size: 0.875rem;
    line-height: 1.55;
}
.cp-notification-grid-wrap {
    overflow-x: auto;
    margin-inline: -1.25rem;
}
.cp-notification-grid {
    width: 100%;
    min-width: 34rem;
    border-collapse: collapse;
    font-size: 0.875rem;
}
.cp-notification-grid th {
    padding: 0.7rem 1.25rem;
    border-bottom: 1px solid rgb(var(--portal-color-border));
    color: rgb(var(--portal-color-slate-600));
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 0.08em;
    text-align: center;
    text-transform: uppercase;
}
.cp-notification-grid th:first-child {
    width: 100%;
    text-align: left;
}
.cp-notification-grid td {
    padding: 0.9rem 1.25rem;
    border-bottom: 1px solid rgb(var(--portal-color-border));
    color: rgb(var(--portal-color-text));
    text-align: center;
}
.cp-notification-grid td:first-child {
    min-width: 17rem;
    font-weight: 650;
    text-align: left;
}
.cp-notification-grid tbody tr:last-child td {
    border-bottom: 0;
}
.cp-notification-grid tbody tr:hover {
    background: rgb(var(--portal-color-surface-muted) / 0.7);
}
.cp-notification-check {
    display: inline-grid;
    place-items: center;
    min-width: 2.75rem;
    min-height: 2.25rem;
    border-radius: var(--portal-radius-sm);
}
.cp-notification-check input {
    width: 1.05rem;
    height: 1.05rem;
    margin: 0;
    accent-color: rgb(var(--portal-color-primary));
    cursor: pointer;
}
.cp-notification-check:has(input:disabled) {
    background: rgb(var(--portal-color-slate-100) / 0.75);
}
.cp-notification-check input:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
.cp-notification-actions {
    display: flex;
    justify-content: flex-end;
    padding-top: 1.1rem;
    border-top: 1px solid rgb(var(--portal-color-border));
}
.cp-device-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}
.cp-device-state {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    color: rgb(var(--portal-color-slate-700));
    font-size: 0.8125rem;
    font-weight: 700;
}
.cp-device-state::before {
    width: 0.55rem;
    height: 0.55rem;
    border-radius: 50%;
    background: rgb(var(--portal-color-slate-300));
    content: "";
}
.cp-device-state.is-on::before {
    background: rgb(var(--portal-color-success));
    box-shadow: 0 0 0 0.2rem rgb(var(--portal-color-success) / 0.12);
}
.cp-device-message {
    margin: 0.75rem 0 0;
    color: rgb(var(--portal-color-text-muted));
    font-size: 0.8125rem;
}
.cp-device-message.is-error {
    color: rgb(var(--portal-color-danger));
}

/* One-time push opt-in: uses the shared modal surface, with its own accessible
   scrim and a deliberately warm product explanation rather than a permission
   prompt appearing without context. */
.cp-push-promo-backdrop {
    padding: 1rem;
}
.cp-push-promo-scrim {
    position: absolute;
    inset: 0;
    width: 100%;
    border: 0;
    background: transparent;
    cursor: default;
}
.cp-push-promo {
    position: relative;
    z-index: 1;
    width: min(100%, 30rem);
    padding: 1.75rem;
    outline: none;
}
.cp-push-promo-close {
    position: absolute;
    top: 1rem;
    right: 1rem;
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: rgb(var(--portal-color-text-muted));
    cursor: pointer;
}
.cp-push-promo-close:hover {
    background: rgb(var(--portal-color-surface-muted));
    color: rgb(var(--portal-color-text));
}
.cp-push-promo-icon {
    display: grid;
    place-items: center;
    width: 3rem;
    height: 3rem;
    margin-bottom: 1.1rem;
    border-radius: 0.9rem;
    /* Flat brand, not the old indigo-to-azure wordmark gradient: the wordmark it was
       sampled from is gone, and this was the last blue left in the shell. */
    background: rgb(var(--portal-color-brand));
    color: #fff;
    box-shadow: 0 8px 18px rgb(var(--portal-color-brand) / 0.22);
}
.cp-push-promo h2 {
    margin: 0 2.5rem 0.5rem 0;
    color: rgb(var(--portal-color-text));
    font-size: 1.25rem;
    letter-spacing: -0.02em;
}
.cp-push-promo > p {
    margin: 0;
    color: rgb(var(--portal-color-text-muted));
    font-size: 0.875rem;
    line-height: 1.6;
}
.cp-push-promo-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.625rem;
    margin-top: 1.25rem;
}
.cp-push-promo .cp-push-promo-error {
    margin-top: 0.8rem;
    color: rgb(var(--portal-color-danger));
}
.cp-push-promo .cp-push-promo-footnote {
    margin-top: 1rem;
    font-size: 0.75rem;
}
.cp-push-promo-footnote a {
    color: rgb(var(--portal-color-slate-700));
    font-weight: 700;
}

@media (max-width: 38rem) {
    .cp-device-row {
        align-items: flex-start;
        flex-direction: column;
    }
    .cp-push-promo-actions .cc-btn {
        justify-content: center;
        width: 100%;
    }
}
