/* ============================================================================
   portal.css — LI.FI Portal app-shell layout glue (page-local, tier-3 surface)
   ----------------------------------------------------------------------------
   Owns ONLY the shell scaffolding: the rail / top-bar / content / right-rail
   grid, view-switching, and a few Portal-specific compositions (org trigger,
   account chip, KPI sparkline, route row, status dot). ALL content primitives
   come from the design system — .side-nav, .brand-lockup, .dropdown,
   .search-trigger, .stat-card, .panel, .data-table, .chip, .btn-*. No brand
   colour / size literals: everything via --surface-* / --text-* / --accent-* /
   --space-* / --text-* / --fw-* tokens. The single-page shell + JS view-switch
   mirrors the playground's pattern (build the shell once, swap the content).
   ============================================================================ */

body.portal-body {
  margin: 0;
  min-height: 100vh;
  color: var(--text-primary);
  font-family: var(--font-sans);
  /* Portal rail default — 2 ladder steps below the shared .app-shell default
     (--w-6 384 → --w-4 320). Scoped here so the shared --rail-width default
     (which the Playground shell convergence will inherit) stays untouched. */
  --rail-width: var(--w-4);
  /* Canvas chrome inset — a uniform 24 (matches --rail-gap + --canvas-bottom, so
     the canvas pads 24 on every chrome side; the rail shares the inset and stays
     top-aligned). Header breathing lives on .portal-view__inner now, so this no
     longer needs inflating. Pinned explicitly so the shared .app-shell default
     the Playground convergence inherits can't drift it. */
  --canvas-top: var(--space-24);
}
/* App surfaces are flat — suppress the marketing ambient halo. Body background
   comes from .app-shell (--surface-sunk) so the glass rail reads as a sibling
   of the Playground rail. */
body.portal-body::before,
body.portal-body::after { display: none !important; content: none !important; }

/* Root overscroll match (Jun 2026) — the global `html { background:
   var(--surface-page) }` paints the ROOT white; the macOS rubber-band /
   overscroll area beyond the body scroll container reveals it (body bg does
   NOT propagate to the viewport once html carries its own). Lift the root to
   --surface-sunk so overscroll matches the canvas; scoped via :has() so every
   other page keeps its page-white root. This resolves to the NEUTRAL :root
   --surface-sunk (html is outside the per-org `.portal-canvas` theme scope) —
   correct for the LI.FI-1.0 cold-load default and the pre-JS frame. Under an
   org theme, portal-render.js mirrors the canvas's themed sunk onto the root's
   inline background-color, which overrides this fallback. */
html:has(body.portal-body) { background: var(--surface-sunk); }

/* ── Rail — composes the shared .app-rail (fixed glass panel + collapse).
   Portal adds only head/search/org/nav/footer layering. ──────────────────── */
/* Full radius on all four corners. The shared .app-rail squares its top-right
   so a docked .drawer-handle meets it flush (the playground's minimize notch);
   the Portal rail has no handle, so it reads as a fully-rounded panel. Routes
   through --panel-radius so it tracks the corners-composer like any .panel. */
.portal-rail { border-radius: var(--panel-radius); }
/* gap: --space-2 (not --space-4) so the 3-icon cluster fits the head without
   overflowing — the rightmost toggle then clears the rail's right edge, so its
   focus/hover ring isn't cut by the rail's overflow-x: hidden. */
.portal-rail__actions { display: inline-flex; align-items: center; gap: var(--space-2); flex-shrink: 0; }
.portal-rail__search { padding: 0 var(--space-4); }
.portal-rail__search .search-trigger { width: 100%; }
/* Extra breathing room above (search) + below (nav) on top of the rail's
   12px flex gap — sets the switcher apart as its own zone. */
.portal-rail__org { padding: 0; margin: var(--space-8) 0; }
/* The .side-nav stretches to the rail width (vs its default fixed 240px), and
   runs flush — the rail .panel already supplies the 24px gutter, so the nav's
   own 16px padding would double-inset the rows past the search bar + brand. */
.portal-rail__nav .side-nav { width: auto; min-width: 0; padding: 0; }
/* Nav gating (demo) — a disconnected wallet leaves only Dashboard reachable;
   connecting unlocks the rest. The shared connect signal is
   body[data-playground-wallet-connected="true"] (set by the wallet-connect
   core); absent = disconnected. The Analytics parent (no data-view) + its sub
   list are hidden too. portal.js falls the active view back to Dashboard on
   disconnect so a gated view is never left showing. */
body:not([data-playground-wallet-connected="true"]) .portal-rail__nav .side-nav__item:not([data-view="dashboard"]),
body:not([data-playground-wallet-connected="true"]) .portal-rail__nav .side-nav__sub {
  display: none;
}
/* Footer — account identity + Support/Docs (composes .app-rail__footer). */
.portal-rail__foot {
  margin-top: var(--space-8);
  padding-top: var(--space-12);
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}
/* The Support/Docs row is the standard .action-bar action-bar--footer (equal-
   width neutral buttons). The rail footer drops the footer variant's divider +
   top-pin chrome — the panel gap already separates it from the nav, and a
   hairline reads as noise in the narrow rail. (The shared component keeps its
   divider for modals; this is a per-consumer opt-out.) */
.portal-rail__foot-links.action-bar--footer {
  border-top: 0;
  padding-top: 0;
  margin-top: 0;
}
.portal-rail__account { display: flex; align-items: center; gap: var(--space-8); padding: var(--space-8); border-radius: var(--space-8); }
.portal-rail__account-id { min-width: 0; flex: 1 1 auto; }
.portal-rail__account-name { font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--text-primary); line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.portal-rail__account-wallet { display: inline-flex; align-items: center; gap: var(--space-4); font-size: var(--text-caption); color: var(--text-muted); }
.portal-rail__copy { display: inline-flex; width: var(--space-16); height: var(--space-16); color: var(--text-faint); }

/* ── Collapsed (mini) rail — Portal persistent click-toggle, PUSH model ───────
   The Portal rail toggles between expanded (full --rail-width) and a 72px icon
   rail via the head's .portal-rail__toggle button (NOT hover). Deliberately
   distinct from the shared hover-expand .app-rail--mini OVERLAY variant (which
   stays the catalog's pristine demo, untouched): the lever is a SINGLE var —
   body.is-rail-mini shrinks --rail-width to --rail-mini, and the base .app-rail
   width AND .app-canvas padding-left both read --rail-width, so they resize in
   lockstep → the rail PUSHES the canvas, never overlays. Because collapse is a
   deliberate STATE, the icon-only treatment is gated on the class (not :hover).
   Desktop-only (≥901px); below that the responsive .is-rail-collapsed full-hide
   takes over. JS: portal.js → initPortalRailToggle. */

/* The base .app-rail transitions transform + padding only — add `width` so the
   rail glides in lockstep with .app-canvas's 0.3s padding push (else it snaps). */
body.portal-body .app-rail {
  transition: width 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),
              transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1),
              padding 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* Toggle icon (arrow-left-to-line glyph, ⇤). Expanded (default): 0° (⇤ dock-left
   = collapse). Collapsed: rotated 180° (⇥ = expand) — the rotation IS the toggle's
   animation, and the one glyph doubles for both directions. */
.portal-rail__toggle-icon {
  transform: rotate(0deg);
  transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}

@media (min-width: 901px) {
  /* THE LEVER — shrink --rail-width; rail width + canvas padding both follow. */
  body.portal-body.is-rail-mini { --rail-width: var(--rail-mini); }

  /* Tighter side padding so 40px buttons / 32px avatars / 16px icons centre in
     the 72 column (vertical rhythm unchanged). */
  body.portal-body.is-rail-mini .app-rail { padding-inline: var(--space-12); }

  /* Toggle rotates → ⇥ (expand). */
  body.portal-body.is-rail-mini .portal-rail__toggle-icon { transform: rotate(180deg); }

  /* Head — brand + theme + notifications drop out; the toggle becomes the sole
     control, centred where the brand mark sat (the "logo → expand icon" swap). */
  body.portal-body.is-rail-mini .app-rail__head { justify-content: center; }
  body.portal-body.is-rail-mini .brand-lockup { display: none; }
  body.portal-body.is-rail-mini .portal-rail__actions { gap: 0; }
  body.portal-body.is-rail-mini .portal-rail__actions > button:not(.portal-rail__toggle) {
    display: none;
  }

  /* Trailing text in rows that stay → clipped (icon-only column). */
  body.portal-body.is-rail-mini :is(
    .search-trigger__label, .dropdown__label, .dropdown__chevron,
    .side-nav__label, .side-nav__trail
  ) {
    max-width: 0; min-width: 0; margin-inline: 0; padding-inline: 0;
    overflow: hidden; white-space: nowrap; opacity: 0;
  }
  /* Whole blocks with no icon-only form → removed (Support/Docs, the text-only
     Analytics sub-list, the disconnected Connect button, the wallet card's
     identity/trail columns — see the account rule below for why these are
     display:none, not clipped). */
  body.portal-body.is-rail-mini :is(
    .portal-rail__foot-links, .side-nav__sub, .ui-wallet-mount__empty,
    .ui-wallet-mount .list-item__body, .ui-wallet-mount .list-item__trail
  ) { display: none; }
  body.portal-body.is-rail-mini .side-nav__item--parent::after { display: none; }

  /* Centre every icon-bearing row in the 72 column. */
  body.portal-body.is-rail-mini .portal-rail__search { padding: 0; }
  body.portal-body.is-rail-mini :is(.search-trigger, .side-nav__item) {
    justify-content: center; gap: 0; padding-inline: 0;
  }

  /* Org switcher → avatar only. ── AVATAR HOLD-POSITION (load-bearing, DON'T DROP) ──
     Two axes, both so the org mark doesn't jump during the collapse:
     • VERTICAL — the ZONE is pinned to its expanded height + vertically centres
       the card, so the mark holds its Y. A shrinking card would drift the centred
       avatar up ~4px and snap (instant, while the width is eased).
     • HORIZONTAL — the card keeps the avatar LEFT-anchored (NOT justify-centred).
       Centring would re-centre the mark in the STILL-WIDE rail at t0 (a ~70px
       rightward dart, then a slide back). Left-anchored, the mark just glides the
       last few px to the rail centreline as the width shrinks (the avatar nearly
       fills the 48 collapsed column, so left ≈ centre).
     navigation.md → expand/collapse motion rules 2 + 4. */
  body.portal-body.is-rail-mini .portal-rail__org {
    min-height: calc(var(--space-32) + var(--space-24) + 2px);   /* = the 58px expanded org-card height */
    display: flex; align-items: center; justify-content: center;
  }
  body.portal-body.is-rail-mini .portal-rail__org .dropdown { width: 100%; }
  body.portal-body.is-rail-mini .dropdown__trigger--card {
    gap: 0; padding: var(--space-8);   /* justify-content stays flex-start (base) — see HORIZONTAL above */
  }
  /* Keep the org dropdown usable when opened from the 48px collapsed trigger
     (--match-trigger would otherwise pin the panel to 48px). */
  body.portal-body.is-rail-mini .portal-rail__org .dropdown__panel { min-width: var(--w-3); }

  /* Account (wallet mount) → bare avatar. THREE load-bearing pieces, DON'T DROP:
     (1) VERTICAL HOLD — the MOUNT is pinned to its expanded height + vertically
         centres its card, so the account avatar holds its Y through the collapse.
         Without the pin the card shrinks (body removed) and the bottom-anchored
         avatar drops ~11px and snaps (instant, while the width is eased).
     (2) HORIZONTAL GLIDE — mount + list-item keep the avatar LEFT-anchored (NOT
         justify-centred), same reason as the org card above: centring darts the
         mark ~70px right in the still-wide rail at t0. Left-anchored it glides.
     (3) GRID→FLEX — the base .list-item is a GRID (lead·body·trail); collapsing
         only the body/trail tracks leaves the avatar floating in its lead column,
         detached from the row. Override to a flex box (width:auto so it hugs the
         lone avatar-tandem and anchors at the rail's left content edge).
     navigation.md → expand/collapse motion rules 2 + 4. */
  body.portal-body.is-rail-mini .ui-wallet-mount {
    min-height: 86px;   /* = the expanded connected-card height (avatar + 2-line identity) */
    display: flex; align-items: center; justify-content: flex-start;
  }
  body.portal-body.is-rail-mini .ui-wallet-mount .list-item {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    width: auto;
    height: var(--space-48);
    padding: 0;
    background: transparent; border-color: transparent; box-shadow: none;
  }
}

/* Monogram avatars (org switcher + account chip) — the page-local
   .portal-initial primitive was retired June 2026 → the canonical
   .avatar--initial monogram (styles.css). Only the 28px rail tier is
   page-local here; colour + glyph come from the shared primitive + shared.js. */
/* Org switcher marks (trigger + panel rows) ride the 32px (sm) avatar
   tier so the menu rows match the card trigger's avatar; the footer
   account chip stays a tighter 28px. */
.portal-org-mark { --avatar-size: var(--space-32); }
.portal-account-mark { --avatar-size: var(--space-28); }

/* Org switcher trigger — the card-style dropdown trigger
   (.dropdown__trigger--card, styles.css) owns the full-width fill,
   left-alignment, label truncation, and chevron; the panel matches the
   trigger width via .dropdown__panel--match-trigger. No portal-specific
   chrome needed — only the 28px monogram tier above (.portal-org-mark). */

/* ── View switching ─────────────────────────────────────────────────────── */
.portal-view { display: none; }
.portal-view.is-active { display: block; }
/* Extra top inset for the page content ONLY — pushes the header down past the
   uniform --canvas-top (24) so the H1 gets breathing room (24 + 32 = 56) without
   inflating the canvas chrome inset or the rail. Lives on the content wrapper,
   not --canvas-top, to decouple header breathing from the canvas/rail inset. */
.portal-view__inner { max-width: 1280px; margin: 0 auto; padding-top: var(--space-32); }

/* ── Page header (within a view) ────────────────────────────────────────── */
/* margin-bottom is the canvas vertical rhythm → --grid-gap-y (Spacing composer's
   vertical axis). The flex `gap` below is the internal title↔actions cluster gap
   (stays --space-24). Same split applies to every section rule below: the
   between-section margin tokenizes to --grid-gap-y; intra-component gaps stay
   on --space-*. */
.portal-page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-24);
  /* Header gets one ladder step MORE separation from the first content section
     than the inter-section rhythm — calc keeps it tracking the Spacing composer's
     --grid-gap-y while adding +8 of header breathing room. */
  margin-bottom: calc(var(--grid-gap-y) + var(--space-8));
}
/* Parent-section eyebrow — appears ONLY on secondary pages (Analytics
   children + the Integration Detail entity head), naming the parent section
   the page lives under, in the brand accent. The current page is the H1 below,
   so the eyebrow shows the parent ONLY — no repeated current-page leaf. Top-level
   pages omit it entirely. On the Portal canvas --accent-primary resolves to the
   active org's themed accent. Shared by both heads via the group. */
.portal-page-head__eyebrow,
.portal-entity-head__eyebrow {
  font-size: var(--text-caption);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--accent-primary);
  margin-bottom: var(--space-12);
}
.portal-page-head__title {
  font-size: var(--text-h1);
  font-weight: var(--fw-bold);
  letter-spacing: var(--tracking-tight);
  line-height: 1.1;
  margin: 0;
}
.portal-page-head__sub {
  margin: var(--space-12) 0 0;
  font-size: var(--text-body);
  color: var(--text-muted);
}
.portal-page-head__actions { display: flex; align-items: center; gap: var(--space-12); flex-shrink: 0; }

/* Leading org brand avatar — the active org's mark (real logo or brand-toned
   monogram, via portal-render → orgMarkHTML) sits large to the LEFT of the
   title on every top-level page head. Same identity-lead idea as the universal
   .screen-header__avatar composition, applied to this distinct big-h1 primitive.
   Switches the row to a grid only when the avatar is present, so it reads
   [avatar] [title-block] [actions]; the scoped selector beats .portal-org-mark's
   32px size (orgMarkHTML adds that class too) to land the large 64 tier. */
.portal-page-head:has(.portal-page-head__avatar) {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
}
.portal-page-head .portal-page-head__avatar { --avatar-size: var(--space-64); }

/* ── KPI grid ───────────────────────────────────────────────────────────── */
.portal-kpi-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  column-gap: var(--grid-gap-x);
  row-gap: var(--grid-gap-y);
  margin-bottom: var(--grid-gap-y);
}
/* Inline sparkline that fills the bottom of a .stat-card. The stroke colour
   is driven by a per-card --spark custom property (defaults to accent).
   Height doubled 40 → 80 (pass 2, June 2026) — the viewBox is fixed +
   preserveAspectRatio="none", so the same data path stretches taller; the
   stroke stays 2px via vector-effect: non-scaling-stroke. */
.portal-kpi-spark {
  display: block;
  width: 100%;
  height: var(--space-80);
  margin-top: var(--space-12);
  overflow: visible;
}
.portal-kpi-spark__line { fill: none; stroke: var(--spark, var(--accent-primary)); stroke-width: 2; vector-effect: non-scaling-stroke; }
.portal-kpi-spark__area { fill: var(--spark, var(--accent-primary)); opacity: 0.12; stroke: none; }

/* ── Two-column row: chart (wide) + side panel (narrow) ─────────────────── */
.portal-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  column-gap: var(--grid-gap-x);
  row-gap: var(--grid-gap-y);
  margin-bottom: var(--grid-gap-y);
}

/* ── Panel density ──────────────────────────────────────────────────────────
   .portal-panel composes the universal .panel primitive and tunes ONLY its
   density via the token (4px tighter than the 24px default). Header/body/footer
   come from the shared .panel slot model + .screen-header.
   The bespoke .portal-panel__head / __title / __sub / __link were retired
   June 2026 → .panel__header.screen-header.screen-header--main + .link-arrow
   (atomic, no aliases — see CLAUDE.md → "Panel primitive"). */
.portal-panel { --panel-padding: var(--space-20); }

/* ── Canvas panels sit FLAT (elevated = chrome + overlays only) ───────────────
   The Portal canvas stacks ~44 content panels; an --elev-3 cast shadow on each
   reads as noise that double-encodes the panel↔canvas tint delta. Flip the two
   surface tokens on the canvas WRAPPER so every descendant .panel follows (the
   base .panel reads --panel-shadow / --panel-border) — while the app rail,
   dropdowns, modals + FAB (all OUTSIDE .portal-canvas) keep the :root elevated
   default. This scoped override is the static per-canvas default; the Theme
   Composer's Panels section paints the same two tokens here to flip per-theme,
   and inline vars win over this rule. See CLAUDE.md → "Panel primitive". */
.portal-canvas {
  --panel-surface: var(--surface-card);   /* content tier — white-capable, tracks the surface seed (frame tier --surface-raised is locked below page) */
  --panel-shadow: none;
  --panel-border: var(--border-subtle);
}

/* The canvas KPI tiles are content surfaces too — unify them with the panels
   by reading the SAME three panel-surface tokens instead of their own Paper
   material (whose --mat-bg is a hardcoded neutral white that never tracked the
   org-tinted --surface-card). Result: panels + cards are ONE pixel-identical
   content surface in every state — same fill, same (no) shadow, same hairline —
   and they flip together when the composer toggles the canvas elevated/flat.
   Scoped to the KPI grid so stat cards nested INSIDE panels keep their own
   Paper lift. See CLAUDE.md → "Panel primitive". */
.portal-kpi-grid .stat-card {
  background: var(--panel-surface);
  box-shadow: var(--panel-shadow);
  border: 1px solid var(--panel-border);
}

/* ── Portal KPI stat-card variant (June 2026) — the value / delta / gap
   improvements now live on the BASE .stat-card (source of truth; the catalog,
   marketing + other dashboards inherit them). What stays variant-specific:
   this KPI variant drops the footer description (the .stat-card-label slot
   stays on the base for future variants that want it — per the variant's
   brief). The doubled sparkline lives above on .portal-kpi-spark. */
.portal-kpi-grid .stat-card-footer { display: none; }

/* ── Area chart — composes the catalog .area-chart primitive ───────────────
   .portal-chart is the .area-chart WRAPPER; the <svg> inside is rendered by
   the shared catalog renderer (LifiLineAreaCharts.render, driven per-org from
   portal-render.js → applySparks). The chart's line + fill follow --series-1,
   bound here to the per-page accent slot so a chart can re-tint per page (the
   Fee-revenue chart overrides it to tertiary) without stomping the brand
   --accent-primary token. aspect-ratio matches the 760×240 viewBox so the
   chart scales without distortion or letterboxing. */
.portal-chart {
  aspect-ratio: 760 / 240;
  --portal-chart-accent: var(--accent-primary);
  --series-1: var(--portal-chart-accent);
  /* Secondary series (volume chart's transactions overlay) — sibling of the
     chart accent in the spectral palette so the two lines pair without either
     dominating. Per-chart consumers can override (e.g. Fees chart). */
  --series-2: var(--text-secondary);
}
/* Premium chart line — thin, crisp, no drop-shadow. vector-effect keeps the
   stroke a true 1px at any render width (the base .area-line stroke is in
   viewBox units, so it'd thicken as the chart scales up). The gradient fill is
   injected per-chart in portal-render.js → applyGradientFill. */
.portal-chart .area-line {
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  filter: none;
}
/* Dual-axis volume chart — transactions is a LINE-only overlay (no area fill)
   so it sits over the volume area without two stacked fills muddying the read.
   The line uses a dashed stroke so the two metrics differ in texture too, not
   just color — robust against the case where --series-2 reads too close to
   --series-1 in a partner-tinted theme. The secondary dot stays solid for a
   clean hover pointer. */
.portal-chart .area-fill.series-2 { display: none; }
.portal-chart .area-line.series-2 {
  stroke-dasharray: 4 3;
  opacity: 0.85;
}
/* Hover focus dot follows the chart's own accent (the base .area-dot--focus is
   hardwired to --accent-primary; the Fee chart is tertiary). */
.portal-chart .area-dot--focus.series-1 { fill: var(--series-1); }
.portal-chart .area-dot--focus.series-2 { fill: var(--series-2); }

/* ── Route arrow — the → in a from/to chain pair ──────────────────────────
   Shared by the data-table route cells (portal-render.js → routeCell) AND the
   Top-routes bar-list labels. June 2026: the Top-routes list was rebuilt on
   the .portal-barlist primitive (same as Top chains by volume — a route pair
   label + value + magnitude track), so the old .portal-routes / .portal-route /
   __pair / __val row classes were retired; only this shared arrow remains. */
.portal-route__arrow { color: var(--text-faint); }

/* ── Route bar-list rows (Top routes) — the richer .portal-barlist variant ──
   An .avatar-stack (the two chain marks overlapping) leads, then chain1 + an
   arrow ICON (icons/arrow-right.svg) + chain2 as the label, at a bumped type
   size. The base .portal-barlist row (single chain — Top chains by volume) is
   unchanged. */
.portal-barlist__row--route .portal-barlist__label { font-size: var(--text-body); gap: var(--space-4); }
.portal-barlist__row--route .portal-barlist__val { font-size: var(--text-body); }
/* Extra right margin on the stack (on top of the 4px label gap) so the chain
   labels sit a touch further from the avatars. Scoped to the route stack — the
   universal .avatar-stack is untouched. */
.portal-barlist__row--route .avatar-stack { margin-right: var(--space-4); }
.portal-route__name { white-space: nowrap; }
.portal-route__arrow-icon { width: var(--space-16); height: var(--space-16); color: var(--text-faint); flex: 0 0 auto; }
/* Size variant — a thinner 4px track (vs the default 8px). Apply to the
   .portal-barlist container; documented at design-system/portal.html#portal-barlist. */
.portal-barlist--sm .portal-barlist__track { height: var(--space-4); }
/* Route lists get more row breathing room (16 → 24) than the default barlist
   gap — the route rows are richer (stack + dual label + bar). Scoped via :has()
   to barlists containing route rows, so the single-chain breakdowns (Top chains
   by volume, fee splits) keep the 16px default. */
.portal-barlist:has(.portal-barlist__row--route) { gap: var(--space-24); }
/* Chain dot — page-local stand-in; swap for /avatars/ chain marks later. */
.portal-chain-dot { width: var(--space-16); height: var(--space-16); border-radius: 50%; flex-shrink: 0; }

/* ── Status dot (in chips / cells) ──────────────────────────────────────── */
.portal-dot { width: var(--space-8); height: var(--space-8); border-radius: 50%; display: inline-block; }

/* ── Recent transactions table tweaks ──────────────────────────────────── */
.portal-tx-table .data-table { width: 100%; }
.portal-tx-explorer tbody tr { cursor: default; }
/* .portal-pager retired June 2026 → composes the DS .pagination primitive
   (compact variant: prev / counter / next). All three Portal consumers (the
   dashboard txn pager, the Transactions explorer, the Audit log) migrated to
   `.pagination .pagination--sm .pagination--compact` + an inline-style flex
   row for layout (matches catalog #pagination-with-info). See CLAUDE.md →
   "Primitive retirement is atomic" + catalog `#pagination-compact`. */

/* ── Placeholder view (un-built sections, Phase 2+) ─────────────────────── */
.portal-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-12);
  min-height: 320px;
  text-align: center;
  color: var(--text-muted);
}
.portal-placeholder__title { font-size: var(--text-h3); font-weight: var(--fw-semibold); color: var(--text-primary); }

/* ── Responsive ─────────────────────────────────────────────────────────── */
@media (max-width: 1100px) {
  .portal-kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .portal-cols { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 760px) {
  /* The .app-rail + JS auto-collapse own the rail/canvas responsive; only the
     in-canvas grids need a narrow override here. */
  .portal-kpi-grid { grid-template-columns: 1fr; }
}

/* ════ Integrations list (Phase 2) ═════════════════════════════════════════ */

/* Meta row under the page title — usage meter + plan note. */
.portal-meta-row {
  display: flex;
  align-items: center;
  gap: var(--space-16);
  flex-wrap: wrap;
  margin-top: var(--space-12);
}
.portal-meta-row__note { font-size: var(--text-sm); color: var(--text-muted); }

/* Usage meter pill — N / M with a mini progress bar. Page-local for now;
   candidate for promotion to a DS primitive once a 2nd consumer appears. */
.portal-usage {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  padding: var(--space-4) var(--space-12);
  border-radius: var(--r-full);
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  font-size: var(--text-sm);
}
.portal-usage__bar {
  width: var(--space-40);
  height: var(--space-4);
  border-radius: var(--r-full);
  background: color-mix(in oklch, var(--text-primary) 12%, transparent);
  overflow: hidden;
}
.portal-usage__fill { display: block; height: 100%; border-radius: var(--r-full); background: var(--accent-primary); }
.portal-usage__text { color: var(--text-secondary); }
.portal-usage__text strong { color: var(--text-primary); font-weight: var(--fw-semibold); }
/* At-limit tone. */
.portal-usage--limit { border-color: color-mix(in oklch, var(--danger) 40%, transparent); background: color-mix(in oklch, var(--danger) 6%, transparent); }
.portal-usage--limit .portal-usage__fill { background: var(--danger); }
.portal-usage--limit .portal-usage__text strong { color: var(--danger); }

/* Stat trend — the List KPIs use plain coloured text (not chips). */
.portal-trend { font-size: var(--text-caption); font-weight: var(--fw-medium); }
.portal-trend--pos { color: var(--success); }
.portal-trend--flat { color: var(--text-muted); }

/* Toolbar — search (left) + filter controls (right). */
.portal-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-16);
  margin-bottom: var(--space-16);
  flex-wrap: wrap;
}
.portal-toolbar__right { margin-left: auto; display: flex; align-items: center; gap: var(--space-12); }

/* Search field — .input with a leading icon. */
.portal-search { position: relative; flex: 1 1 280px; max-width: 360px; }
.portal-search__icon {
  position: absolute;
  left: var(--space-12);
  top: 50%;
  transform: translateY(-50%);
  width: var(--space-16);
  height: var(--space-16);
  color: var(--text-faint);
  pointer-events: none;
}
.portal-search .input { width: 100%; padding-left: var(--space-40); }
.portal-seg-count { margin-left: var(--space-4); opacity: 0.55; }

/* Integration cell — monogram avatar (.avatar--initial avatar--sm avatar--rounded) + name. */
.portal-integ { display: inline-flex; align-items: center; gap: var(--space-12); font-weight: var(--fw-medium); color: var(--text-primary); }

/* Portal data tables — kebab action cell. */
.portal-data .data-table { width: 100%; }
.portal-cell-action { text-align: right; width: var(--space-40); }

/* Plan-limit alert actions (the at-limit banner pairs a CTA + dismiss). */
.portal-alert-actions { display: flex; align-items: center; gap: var(--space-8); flex-shrink: 0; }
.portal-limit-alert { margin-bottom: var(--space-24); }

/* State blocks within the Integrations view — the demo toggle swaps them. */
.portal-integ-state { display: none; }
.portal-integ-state.is-shown { display: block; }

/* Demo-only "Preview state" toggle — dashed border signals it's not product chrome. */
.portal-demo-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-12);
  margin-bottom: var(--space-24);
  padding: var(--space-8) var(--space-12);
  border: 1px dashed var(--border-subtle);
  border-radius: var(--space-12);
}
.portal-demo-toggle__label {
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
  font-weight: var(--fw-semibold);
}

/* Empty state. */
.portal-empty { text-align: center; padding: var(--space-40) var(--space-32); }
.portal-empty__title { font-size: var(--text-h2); font-weight: var(--fw-bold); letter-spacing: var(--tracking-tight); margin: 0 0 var(--space-8); }
.portal-empty__desc { max-width: 520px; margin: 0 auto var(--space-24); color: var(--text-muted); }
.portal-empty__actions { display: flex; justify-content: center; gap: var(--space-12); margin-bottom: var(--space-40); }
.portal-steps { display: grid; grid-template-columns: repeat(3, 1fr); column-gap: var(--grid-gap-x); row-gap: var(--grid-gap-y); text-align: left; max-width: 820px; margin: 0 auto; }
.portal-step { padding: var(--space-20); border: 1px solid var(--border-subtle); border-radius: var(--card-radius); background: var(--surface-card); }
.portal-step__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-28);
  height: var(--space-28);
  border-radius: 50%;
  background: color-mix(in oklch, var(--accent-primary) 12%, transparent);
  color: var(--accent-primary-mid);
  font-weight: var(--fw-semibold);
  font-size: var(--text-sm);
  margin-bottom: var(--space-12);
}
.portal-step h3 { font-size: var(--text-body); font-weight: var(--fw-semibold); margin: 0 0 var(--space-4); }
.portal-step p { font-size: var(--text-sm); color: var(--text-muted); margin: 0; }

/* Empty-state illustration — overlapping cards + checkmark + add-FAB on a dotted grid. */
.portal-empty__art {
  position: relative;
  width: 100%;
  max-width: 440px;
  height: 180px;
  margin: 0 auto var(--space-28);
  border-radius: var(--card-radius);
  background-color: var(--surface-raised);
  background-image: radial-gradient(color-mix(in oklch, var(--text-primary) 8%, transparent) 1px, transparent 1px);
  background-size: 16px 16px;
  border: 1px solid var(--border-subtle);
  overflow: hidden;
}
.portal-empty__card { position: absolute; border-radius: var(--space-12); box-shadow: var(--elev-1); }
.portal-empty__card--back { width: 176px; height: 92px; top: 30px; left: 50%; transform: translateX(-22%); background: var(--surface-card); border: 1px solid var(--border-subtle); }
.portal-empty__card--front {
  width: 204px;
  height: 104px;
  top: 44px;
  left: 50%;
  transform: translateX(-72%);
  background: color-mix(in oklch, var(--accent-primary) 6%, var(--surface-card));
  border: 1px solid color-mix(in oklch, var(--accent-primary) 30%, transparent);
  padding: var(--space-16);
  display: flex;
  align-items: flex-start;
  gap: var(--space-12);
}
.portal-empty__check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--space-24);
  height: var(--space-24);
  border-radius: 50%;
  background: color-mix(in oklch, var(--accent-primary) 18%, transparent);
  color: var(--accent-primary-mid);
}
.portal-empty__check svg { width: var(--space-12); height: var(--space-12); }
.portal-empty__lines { display: flex; flex-direction: column; gap: var(--space-8); flex: 1; padding-top: var(--space-4); }
.portal-empty__lines i { display: block; height: var(--space-8); border-radius: var(--r-full); background: color-mix(in oklch, var(--accent-primary) 22%, transparent); }
.portal-empty__lines i:first-child { width: 80%; }
.portal-empty__lines i:last-child { width: 50%; }
.portal-empty__fab {
  position: absolute;
  top: 26px;
  right: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-40);
  height: var(--space-40);
  border-radius: 50%;
  background: var(--accent-primary);
  color: var(--on-accent-primary);
  box-shadow: var(--elev-2);
}
.portal-empty__fab svg { width: var(--space-20); height: var(--space-20); }

/* ════ Integration Detail (Phase 3) ════════════════════════════════════════ */

/* Integration rows become clickable (row → Detail). */
.portal-data tbody tr { cursor: pointer; }
.portal-data tbody tr:hover { background: color-mix(in oklch, var(--text-primary) 4%, transparent); }

/* Entity header — avatar + name + status + integrator-string + actions. */
.portal-entity-head { display: flex; align-items: flex-start; gap: var(--space-16); margin-bottom: calc(var(--grid-gap-y) + var(--space-8)); }
.portal-entity-head__avatar { --avatar-size: var(--space-56); --avatar-radius: var(--space-12); }
.portal-entity-head__id { flex: 1 1 auto; min-width: 0; }
.portal-entity-head__title-row { display: flex; align-items: center; gap: var(--space-12); }
.portal-entity-head__title { font-size: var(--text-h2); font-weight: var(--fw-bold); letter-spacing: var(--tracking-tight); line-height: 1.1; margin: 0; }
.portal-entity-head__sub { display: inline-flex; align-items: center; gap: var(--space-8); margin-top: var(--space-8); font-size: var(--text-sm); color: var(--text-muted); }
.portal-entity-head__actions { display: flex; gap: var(--space-8); flex-shrink: 0; }

/* Tab rail — .tab-nav-list used standalone, with a hairline under it. */
.portal-detail-tabs { margin-bottom: var(--grid-gap-y); border-bottom: 1px solid var(--border-subtle); }

/* Tab panels. */
.portal-tab { display: none; }
.portal-tab.is-shown { display: block; }

/* Title + inline chip (e.g. "LI.FI fees" + DYNAMIC). */
.portal-titled { display: flex; align-items: center; gap: var(--space-8); }

/* Metric+action cards (Overview top row). */
.portal-metric-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: var(--grid-gap-x); row-gap: var(--grid-gap-y); margin-bottom: var(--grid-gap-y); }
.portal-metric { display: flex; flex-direction: column; gap: var(--space-12); }
.portal-metric__head { display: flex; align-items: center; gap: var(--space-8); font-size: var(--text-sm); color: var(--text-muted); }
.portal-metric__head svg { color: var(--text-faint); flex-shrink: 0; }
.portal-metric__value { font-size: var(--text-h2); font-weight: var(--fw-bold); letter-spacing: var(--tracking-tight); line-height: 1.1; }
.portal-metric__value--ok { color: var(--success); }
.portal-metric__note { font-size: var(--text-caption); color: var(--text-muted); }
.portal-metric .btn-block { margin-top: auto; }


/* Fee-rule leading icon tile. */
.portal-fee-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-40);
  height: var(--space-40);
  border-radius: var(--space-12);
  background: color-mix(in oklch, var(--accent-primary) 10%, transparent);
  color: var(--accent-primary-mid);
}
.portal-fee-icon--b { background: color-mix(in oklch, var(--accent-tertiary) 12%, transparent); color: var(--accent-tertiary); }
.portal-fee-icon--c { background: color-mix(in oklch, var(--success) 12%, transparent); color: var(--success); }
.portal-fee-rules .list-item__trail { display: inline-flex; align-items: center; gap: var(--space-12); }

/* Fee receivers + Onboarding 2-col. */
.portal-detail-cols { display: grid; grid-template-columns: minmax(0, 1fr) 360px; column-gap: var(--grid-gap-x); row-gap: var(--grid-gap-y); }
.portal-collected { display: inline-flex; flex-direction: column; align-items: flex-end; }
.portal-collected strong { font-variant-numeric: tabular-nums; }

/* Onboarding checklist. */
.portal-onboard-bar { display: block; width: 100%; height: var(--space-4); border-radius: var(--r-full); background: color-mix(in oklch, var(--text-primary) 12%, transparent); overflow: hidden; margin: var(--space-16) 0; }
.portal-checklist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-12); }
.portal-checklist li { display: flex; align-items: center; gap: var(--space-12); font-size: var(--text-sm); color: var(--text-primary); }
.portal-checklist li.is-done { color: var(--text-muted); text-decoration: line-through; }
.portal-checklist .link-arrow { margin-left: auto; }
.portal-check {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--space-20);
  height: var(--space-20);
  border-radius: 50%;
  background: var(--accent-primary);
  color: var(--on-accent-primary);
}
.portal-check svg { width: var(--space-12); height: var(--space-12); }
.portal-check--todo { background: transparent; border: 2px solid color-mix(in oklch, var(--text-primary) 25%, transparent); }

@media (max-width: 1100px) {
  .portal-metric-grid { grid-template-columns: 1fr; }
  .portal-detail-cols { grid-template-columns: 1fr; }
}

/* ── Detail · Wallets tab ── */
.portal-wallet-callout { margin-bottom: var(--space-16); }
.portal-wallet-note { margin-bottom: var(--space-24); }
.portal-wallet-group + .portal-wallet-group { margin-top: var(--space-32); }
.portal-wallet-group__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-16); margin-bottom: var(--space-12); }
.portal-wallet-group__title { display: flex; align-items: center; gap: var(--space-8); font-size: var(--text-body-lg); font-weight: var(--fw-semibold); }
.portal-wallet-group__desc { margin: var(--space-2) 0 0; font-size: var(--text-sm); color: var(--text-muted); }
/* Per-chain overrides nest under the default with a connector line. */
.portal-wallet-overrides { margin: var(--space-8) 0 0 var(--space-24); padding-left: var(--space-16); border-left: 1.5px solid var(--border-subtle); }
.portal-wallet-group .list-item__title { display: flex; align-items: center; gap: var(--space-8); flex-wrap: wrap; }
.portal-wallet-chain { color: var(--text-muted); font-weight: var(--fw-regular); }
.portal-wallet-trail { display: inline-flex; align-items: center; gap: var(--space-16); }
.portal-wallet-actions { display: inline-flex; align-items: center; gap: var(--space-2); }
.portal-wallet-empty { display: flex; align-items: center; gap: var(--space-16); padding: var(--space-20); border: 1px dashed var(--border-subtle); border-radius: var(--card-radius); }
.portal-wallet-empty__icon { display: inline-flex; align-items: center; justify-content: center; width: var(--space-40); height: var(--space-40); border-radius: var(--space-12); background: color-mix(in oklch, var(--text-primary) 6%, transparent); color: var(--text-muted); flex-shrink: 0; }
.portal-wallet-empty__text { flex: 1 1 auto; display: flex; flex-direction: column; gap: var(--space-2); }
.portal-wallet-empty__text strong { font-size: var(--text-sm); color: var(--text-primary); }
.portal-wallet-empty__text span { font-size: var(--text-caption); color: var(--text-muted); }

/* ── Detail · Fees tab ── */
.portal-fees-notice { margin-bottom: var(--space-24); }
.portal-fees-summary { display: grid; grid-template-columns: 1fr 1fr 1.5fr; gap: var(--space-24); align-items: start; }
.portal-fees-summary > * + * { border-left: 1px solid var(--border-subtle); padding-left: var(--space-24); }
.portal-fees-summary__label { font-size: var(--text-caption); text-transform: uppercase; letter-spacing: var(--tracking-wide); color: var(--text-muted); font-weight: var(--fw-semibold); margin-bottom: var(--space-8); }
.portal-fees-summary__value { font-size: var(--text-h1); font-weight: var(--fw-bold); letter-spacing: var(--tracking-tight); line-height: 1.1; }
.portal-fees-summary__value-row { display: flex; align-items: center; gap: var(--space-8); }
.portal-fees-summary__sub { margin-top: var(--space-8); font-size: var(--text-caption); color: var(--text-muted); }
.portal-fees-rates { display: flex; flex-direction: column; gap: var(--space-8); }
.portal-fees-rate { display: flex; align-items: center; justify-content: space-between; gap: var(--space-12); font-size: var(--text-sm); color: var(--text-secondary); }
.portal-fees-withdraw { margin-top: var(--space-16); }
.portal-fees-gas { margin-top: var(--space-8); font-size: var(--text-caption); color: var(--text-muted); text-align: center; }
.portal-fees-toolbar { display: flex; align-items: center; gap: var(--space-12); margin: var(--space-24) 0 var(--space-16); flex-wrap: wrap; }
.portal-fees-toolbar__right { margin-left: auto; display: flex; gap: var(--space-8); }
.portal-fees-table tbody tr { cursor: default; }
.portal-fee-chain { display: inline-flex; align-items: center; gap: var(--space-12); }
.portal-fee-chain > span { display: flex; flex-direction: column; }
.portal-fee-chain__name { font-weight: var(--fw-medium); color: var(--text-primary); }
.portal-fee-chain__amt { font-size: var(--text-caption); color: var(--text-muted); }
.portal-fee-bar { display: block; width: 96px; height: var(--space-4); border-radius: var(--r-full); background: color-mix(in oklch, var(--text-primary) 10%, transparent); overflow: hidden; margin-top: var(--space-4); }
.portal-fee-bar__fill { display: block; height: 100%; background: var(--accent-primary); border-radius: var(--r-full); }
.portal-fee-age { display: inline-flex; align-items: center; gap: var(--space-8); }
@media (max-width: 1100px) {
  .portal-fees-summary { grid-template-columns: 1fr; }
  .portal-fees-summary > * + * { border-left: 0; padding-left: 0; padding-top: var(--space-16); border-top: 1px solid var(--border-subtle); }
}

/* ════ Settings (Phase 4) ══════════════════════════════════════════════════ */
/* Stacked settings sections, constrained reading width. */
.portal-settings { display: flex; flex-direction: column; gap: var(--space-24); max-width: 860px; }

/* ── Theme panel — the canonical Portal theming surface ─────────────────────
   Two independent scopes (Chrome → :root · Brand → .portal-canvas). Each is a
   .dropdown whose trigger names the active theme; the panel lists every theme
   via lifiRenderThemeCardRow (select applies, edit pencil opens LifiThemeEditor
   in a modal). Wiring: portal.js → initPortalSettingsTheme. */
.portal-theme-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--grid-gap-x); }
.portal-theme-scope__title { margin: 0 0 var(--space-4); }
.portal-theme-scope__note { margin: 0 0 var(--space-8); }
.portal-theme-trigger {
  display: flex; align-items: center; gap: var(--space-12);
  width: 100%;
  padding: var(--space-8) var(--space-12);
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--card-radius);
  cursor: pointer; text-align: left;
  font-family: var(--font-sans);
  -webkit-appearance: none; appearance: none;
  color: var(--text-primary);
}
.portal-theme-trigger__name { flex: 1 1 auto; min-width: 0; }
.portal-theme-trigger__chev { flex: 0 0 auto; opacity: 0.5; }
.portal-theme-list {
  min-width: var(--w-3); max-height: var(--w-4);
  overflow-y: auto;
  display: flex; flex-direction: column; gap: var(--gap-card-sm);
}
/* The card rows must keep their natural height and let the panel scroll —
   without this they inherit flex-shrink:1 and compress to fit max-height
   (20 themes × ~50px ≫ the panel cap), shrinking every card. */
.portal-theme-list > .theme-card--row { flex-shrink: 0; }
@media (max-width: 720px) { .portal-theme-grid { grid-template-columns: 1fr; } }

.portal-form { max-width: 560px; }
.portal-form .form-group:last-of-type { margin-bottom: 0; }
.portal-form__actions { display: flex; gap: var(--space-12); margin-top: var(--space-24); }
/* Danger zone — destructive-tinted panel. */
.portal-danger-zone { border: 1px solid color-mix(in oklch, var(--danger) 35%, transparent); }
.portal-danger-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-24); padding: var(--space-16) 0; }
.portal-danger-row + .portal-danger-row { border-top: 1px solid var(--border-subtle); }
.portal-danger-row__title { font-size: var(--text-body); font-weight: var(--fw-semibold); margin-bottom: var(--space-2); }
.portal-danger-row p { margin: 0; max-width: 52ch; }

/* ════ Members (Phase 4) ═══════════════════════════════════════════════════ */
.portal-member { display: inline-flex; align-items: center; gap: var(--space-12); }
.portal-member__id { display: flex; flex-direction: column; min-width: 0; }
.portal-member__name { display: inline-flex; align-items: center; gap: var(--space-8); font-weight: var(--fw-medium); color: var(--text-primary); }
.portal-member__email { font-size: var(--text-caption); color: var(--text-muted); }
.portal-you { font-size: var(--text-micro); font-weight: var(--fw-semibold); text-transform: uppercase; letter-spacing: var(--tracking-wide); color: var(--accent-primary-mid); background: color-mix(in oklch, var(--accent-primary) 12%, transparent); padding: var(--space-2) var(--space-8); border-radius: var(--r-full); }
.portal-members-table tbody tr { cursor: default; }
/* Roles reference grid. */
.portal-roles-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); column-gap: var(--grid-gap-x); row-gap: var(--grid-gap-y); }
.portal-role__name { font-size: var(--text-body); font-weight: var(--fw-semibold); margin-bottom: var(--space-4); }
.portal-role p { margin: 0; font-size: var(--text-sm); color: var(--text-muted); line-height: 1.5; }
@media (max-width: 1100px) { .portal-roles-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ════ Audit Log (Phase 4) ═════════════════════════════════════════════════ */
.portal-actor { display: inline-flex; align-items: center; gap: var(--space-8); font-weight: var(--fw-medium); color: var(--text-primary); }
.portal-audit-detail { font-size: var(--text-sm); color: var(--text-secondary); }
.portal-audit-table tbody tr { cursor: default; }

/* ════ Status (Phase 4) ════════════════════════════════════════════════════ */
/* Overall banner — large LED + headline + uptime chip. */
.portal-status-banner { display: flex; flex-direction: row; align-items: center; gap: var(--space-16); margin-bottom: var(--grid-gap-y); }
.portal-status-banner__text { flex: 1 1 auto; min-width: 0; }
.portal-status-banner__title { font-size: var(--text-h3); font-weight: var(--fw-bold); letter-spacing: var(--tracking-tight); }
.portal-status-banner__sub { margin-top: var(--space-2); font-size: var(--text-sm); color: var(--text-muted); }
/* Status LED — dot with a soft halo ring; tone via modifier. */
.portal-status-led { display: inline-block; width: var(--space-12); height: var(--space-12); border-radius: 50%; flex-shrink: 0; }
.portal-status-led--lg { width: var(--space-16); height: var(--space-16); }
.portal-status-led--ok { background: var(--success); box-shadow: 0 0 0 var(--space-4) color-mix(in oklch, var(--success) 18%, transparent); }
.portal-status-led--warn { background: var(--warn); box-shadow: 0 0 0 var(--space-4) color-mix(in oklch, var(--warn) 18%, transparent); }
/* Component uptime strip — 90-day tick bar via repeating gradient (green
   ticks; the --warn variant overlays an amber degradation window). */
.portal-status-trail { display: inline-flex; align-items: center; gap: var(--space-12); }
.portal-uptime-pct { font-size: var(--text-sm); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; min-width: var(--space-56); text-align: right; }
.portal-uptime {
  display: block;
  margin-top: var(--space-8);
  height: var(--space-16);
  border-radius: var(--space-2);
  background: repeating-linear-gradient(90deg, color-mix(in oklch, var(--success) 70%, transparent) 0 4px, transparent 4px 7px);
}
.portal-uptime--warn {
  background:
    linear-gradient(90deg, transparent 0 74%, color-mix(in oklch, var(--warn) 80%, transparent) 74% 88%, transparent 88%),
    repeating-linear-gradient(90deg, color-mix(in oklch, var(--success) 70%, transparent) 0 4px, transparent 4px 7px);
}
.portal-status-list .list-item__body { min-width: 0; }
/* Past-incidents list. */
.portal-incidents { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.portal-incident { display: flex; align-items: flex-start; gap: var(--space-12); padding: var(--space-12) 0; }
.portal-incident + .portal-incident { border-top: 1px solid var(--border-subtle); }
.portal-incident__dot { width: var(--space-8); height: var(--space-8); border-radius: 50%; flex-shrink: 0; margin-top: var(--space-4); }
.portal-incident__dot--ok { background: var(--success); }
.portal-incident__dot--warn { background: var(--warn); }
.portal-incident__body { flex: 1 1 auto; min-width: 0; }
.portal-incident__title { font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--text-primary); }
.portal-incident__meta { margin-top: var(--space-2); font-size: var(--text-caption); color: var(--text-muted); }

/* ════ API Keys (Phase 4) ══════════════════════════════════════════════════ */
.portal-kpi-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.portal-keys-callout { margin-bottom: var(--space-24); }
.portal-scopes { display: inline-flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
/* Revoked / inactive rows read quieter. */
.portal-row-muted td { opacity: 0.6; }
.portal-keys-table tbody tr { cursor: default; }
/* Detail · Webhooks — event-type chip cluster under an endpoint URL. */
.portal-wh-events { margin-top: var(--space-8); }
.portal-wh-deliveries tbody tr { cursor: default; }

/* ════ Analytics (Phase 4) ═════════════════════════════════════════════════ */

/* Horizontal bar-list — label + value on one line, track below. Reused for
   "by chain" / "by fee type" breakdowns. Page-local; promotion candidate. */
.portal-barlist { display: flex; flex-direction: column; gap: var(--space-16); }
.portal-barlist__row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-8) var(--space-12); align-items: center; }
.portal-barlist__label { display: inline-flex; align-items: center; gap: var(--space-8); min-width: 0; font-size: var(--text-sm); color: var(--text-secondary); }
.portal-barlist__val { font-size: var(--text-sm); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
.portal-barlist__track { grid-column: 1 / -1; height: var(--space-8); border-radius: var(--r-full); background: color-mix(in oklch, var(--text-primary) 8%, transparent); overflow: hidden; }
.portal-barlist__fill { display: block; height: 100%; border-radius: var(--r-full); background: var(--accent-primary); }

/* ════════════════════════════════════════════════════════════════════════
   NEW-INTEGRATION WIZARD — .iwiz (4-step modal) + .network-row
   ------------------------------------------------------------------------
   Portal composition (integration-wizard.js). The modal/panel shell, stepper
   (.steps--circles-inline), inputs, alerts, option/list rows, secret field,
   chips and buttons are all DS primitives; this block adds only the flow-
   specific layout glue + the .network-row expandable wallet-family row.
   Catalog: design-system/portal.html#integration-wizard. See CLAUDE.md →
   "LI.FI Portal — partner console".
   ════════════════════════════════════════════════════════════════════════ */
.iwiz { gap: var(--space-16); }

/* Top bar — full-width stepper (no close button; Cancel in the footer is the
   dismiss affordance, alongside ESC + overlay-click). Asymmetric breathing
   room: a little above, a lot more below, so the stepper doesn't crowd the
   title (the title + form drop down). Applies to every step. */
.iwiz__top {
  display: flex; align-items: center;
  margin-top: var(--space-8);
  margin-bottom: var(--space-16);   /* + the panel's --space-16 flex gap = 32 stepper→title */
}
.iwiz__top .steps { flex: 1 1 auto; min-width: 0; }

/* Focus-ring gutter — .modal-body scrolls (overflow-y:auto forces overflow-x),
   which clips an input's 3px focus ring on the sides. A 4px inline gutter +
   matching negative margin gives the ring room while keeping inputs visually
   at the same inset (CLAUDE.md → "overflow: auto clips child shadows"). Safe
   because .panel / .modal don't clip overflow. */
/* Horizontal gutter for focus rings + card shadows despite the modal-body's
   overflow:auto clip (net-zero on content position). 8px so the full-bleed
   string cards (.iwiz__string-list, padding:0) keep their shadow room. */
.iwiz .modal-body { padding-inline: var(--space-8); margin-inline: calc(-1 * var(--space-8)); }

/* String-step cards span the full modal-body width — drop the .list--cards
   8px inset (scoped override, not the shared primitive) so the cards touch
   both sides, flush with the title + alert above/below them. */
.iwiz__string-list { padding: 0; }
/* Bespoke string-card design (scoped — NOT the shared .list--cards tier):
   roomier all-around padding than the default 12 so the row breathes. */
.iwiz__string-list .list-item { padding: var(--space-16); }
/* Keep the radio-dot on the TITLE's line as the custom card grows. The
   .list-item grid centers the row by default, so when the body expands
   (input + caption) the dot drops to the card's vertical middle. Top-align the
   row, pin the lead to the title's line-height (16 × 1.35) so the 20px dot
   centers on the title line, and keep the trail chip centered. */
.iwiz__string-list .list-item { align-items: start; }
.iwiz__string-list .list-item__lead { min-height: calc(var(--text-body) * 1.35); }
.iwiz__string-list .list-item__trail { align-self: center; }

/* Title + description as one unit — internal gap is the tight title→desc
   spacing (fine as-is); margin-bottom is the independent control for space
   below the unit (pushes the step content down without touching title→desc). */
.iwiz__head { display: flex; flex-direction: column; gap: var(--space-4); margin-bottom: var(--space-12); }
.iwiz__title {
  margin: 0;
  font-size: var(--text-h2); font-weight: var(--fw-bold);
  line-height: var(--lh-h2); letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
}
.iwiz__desc { margin: 0; }
.iwiz__optional { font-size: var(--text-caption); color: var(--text-muted); }
/* "Optional" shown inline in a field label — matches the label size, muted +
   regular weight (the required/optional signal; the red * was retired). */
.iwiz__label-optional { font-weight: var(--fw-regular); color: var(--text-muted); }

/* Footer — Cancel (neutral) far left; primary far right; Back stacks left of
   the primary inside the right cluster. NOT the equal-width .action-bar. */
.iwiz__footer {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-12);
  padding-top: var(--space-8);   /* tighter now that the divider's gone (panel flex-gap already separates) */
}
.iwiz__footer-left, .iwiz__footer-right { display: inline-flex; align-items: center; gap: var(--space-12); }
.iwiz__footer button { white-space: nowrap; }   /* labels stay one line ("Finish & open integration") */
.iwiz__status { font-size: var(--text-sm); color: var(--text-muted); }

/* Section label (NETWORK FAMILIES / EVM L2 · SPECIFIC CHAINS). */
.iwiz__section-label {
  display: flex; align-items: center; gap: var(--space-8);
  margin: var(--space-8) 0 var(--space-4);
  font-family: var(--font-mono); font-size: var(--text-caption); font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-loose); text-transform: uppercase;
  color: var(--text-muted);
}
.iwiz__section-label .iwiz__optional { text-transform: none; letter-spacing: var(--tracking-normal); }

/* Wizard alerts — icon top-aligned for multi-line messages. */
/* Extra breathing between the string cards (form) and their permanence alert —
   adds to the modal-body's 8px flex gap. Adjacent-sibling so ONLY the string
   step's alert is pushed down; the wallets-step alert (follows the head) is
   untouched. */
.iwiz__string-list + .iwiz__alert { margin-top: var(--space-12); }

/* Required-wallets progress — relocated out of the alert to a line under the
   L2 card, right above the footer actions. */
.iwiz__required-progress {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-12); margin-top: var(--space-4);
}
.iwiz__required-label { font-size: var(--text-sm); color: var(--text-muted); }
.iwiz__required-count {
  font-family: var(--font-mono); font-size: var(--text-sm); font-weight: var(--fw-semibold);
  color: var(--text-secondary); font-variant-numeric: tabular-nums;
}

/* String step. */
.iwiz__custom-string { margin-top: var(--space-8); display: flex; flex-direction: column; gap: var(--space-4); }

/* API-key step — monospace readonly key input (it's a key) + the prominent
   Copy + Download row under it. */
.iwiz__key-input { font-family: var(--font-mono); }

/* ── .input-counter — in-field character counter ──────────────────────────
   An input wrapped so a live "N / max" count sits inside its right edge; the
   input reserves right padding so typed text never runs under the count.
   Count is an input-size semibold muted caption; .input-counter__count--over
   turns it danger at the cap. The count is JS-bound (the consumer rewrites the
   text + toggles --over); the input's maxlength is the real constraint.
   Distinct from the label-row counter (.field-label-row + .field-counter in
   styles.css) — that rides the label row, this overlays the field. Page-local
   (renamed from .iwiz__counter-field / .iwiz__field-counter June 2026 — the
   wizard name field is the only consumer today); promote to styles.css when a
   2nd consumer appears. Catalog: design-system/portal.html#input-counter. */
.input-counter { position: relative; display: block; }
.input-counter > .input { padding-right: var(--space-64); }
.input-counter__count {
  position: absolute; top: 50%; right: var(--space-16);
  transform: translateY(-50%);
  font-size: var(--text-sm); font-weight: var(--fw-semibold);
  color: var(--text-faint); font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.input-counter__count--over { color: var(--danger); }
.iwiz__key-actions { display: flex; gap: var(--space-8); margin-top: var(--space-8); }
.iwiz__key-actions > * { flex: 1 1 0; }
.iwiz__saved-card { cursor: pointer; align-items: center; }
.iwiz__saved-check { flex: 0 0 auto; }
.iwiz__fee-note { display: flex; align-items: flex-start; gap: var(--space-8); }
.iwiz__fee-note svg { width: var(--space-16); height: var(--space-16); flex: 0 0 auto; margin-top: 1px; color: var(--text-faint); }

.iwiz__adding { font-size: var(--text-sm); font-weight: var(--fw-semibold); color: var(--accent-primary); }

/* L2 generic mark — ink-toned rounded square with white "L2". */
.iwiz__l2-mark {
  width: var(--space-32); height: var(--space-32); border-radius: var(--space-8);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--text-primary); color: var(--surface-raised);
  font-family: var(--font-mono); font-size: var(--text-caption); font-weight: var(--fw-bold);
}

/* ── .network-row — expandable wallet-family row ──────────────────────────
   Card-tier row naming a network family with a trailing state action
   (+ Add · Added · Adding) and an inline expandable drawer holding the add
   form. Built on the .list--cards card recipe + the universal .reveal
   drawer (styles.css). Form padding lives on .network-row__form (a
   nested child), NOT .reveal__inner, so the 0fr collapse hits true zero
   (CLAUDE.md → grid-rows accordion bug). Portal-scoped; one consumer
   today (the wizard) — promotion candidate if a 2nd "add accounts" flow
   appears. */
.network-row {
  background: var(--surface-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--card-radius);
  box-shadow: var(--elev-1);
}
.network-row + .network-row { margin-top: var(--space-8); }
.network-row__head { display: flex; align-items: center; gap: var(--space-12); padding: var(--space-12); }
.network-row__icon { flex: 0 0 auto; }
.network-row__body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.network-row__name {
  display: inline-flex; align-items: center; gap: var(--space-8);
  font-size: var(--text-sm); font-weight: var(--fw-semibold); color: var(--text-primary);
}
.network-row__sub {
  font-size: var(--text-caption); color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.network-row__sub.mono { font-family: var(--font-mono); }
.network-row__trail { flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--space-8); }

/* Adding state — accent rim (canonical card .is-active recipe). */
.network-row[data-state="adding"] {
  border-color: color-mix(in oklch, var(--accent-primary) 64%, transparent);
  box-shadow: var(--elev-2);
}

/* Drawer — the universal .reveal primitive (styles.css); the row keeps
   only its ancestor-state opener + the marker class. Mechanics retired
   June 2026 (standardization sweep). */
.network-row[data-state="adding"] .network-row__drawer { grid-template-rows: 1fr; }
.network-row__form {
  display: flex; flex-direction: column; gap: var(--space-12);
  padding: var(--space-12); border-top: 1px solid var(--border-subtle);
}
.network-row__form .form-group { margin: 0; }   /* flex gap owns spacing here */
.network-row__addr { display: flex; gap: var(--space-8); align-items: stretch; }
.network-row__addr .input { flex: 1 1 auto; min-width: 0; }
.network-row__addr .btn-neutral { flex: 0 0 auto; }
.network-row__form-actions { display: flex; justify-content: flex-end; gap: var(--space-8); }

/* ═══════════════════════════════════════════════════════════════════════════
   WITHDRAW-FEES MODAL — .wfee (withdraw-fees.js)
   The 4-state withdraw flow: select → signing → success | error. Page-local
   layout glue only — the chrome comes from DS primitives (.modal/.panel,
   .screen-header, .chip-avatar, .checkbox, .list--cards, .alert, .spot-icon,
   .detail-list, .btn-*). Same conventions as .iwiz above.
   ═══════════════════════════════════════════════════════════════════════════ */
.wfee { gap: var(--space-12); }

/* Result steps (success / error) drop the title bar and keep only the close
   affordance, right-aligned. */
.wfee__bar { display: flex; justify-content: flex-end; }

/* Chain context pill — centered under the title. */
.wfee__chain-row { display: flex; justify-content: center; }

/* Shadow gutter (same trick as .iwiz) — .modal-body clips paint on both axes,
   so the elev-1 cards (hero, token rows, receipt) get breathing room via a
   padding/negative-margin pair; the token list then drops its own padding so
   rows align flush with the hero card. */
.wfee .modal-body { padding-inline: var(--space-8); margin-inline: calc(-1 * var(--space-8)); }
.wfee__tokens { padding: 0; }

/* ── Amount hero — "Amount to withdraw" ──────────────────────────────────
   Display card on the canonical card surface; the value dims to faint ink
   when nothing is selected (state shifts ink, never weight). */
.wfee__amount {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--space-4);
  padding: var(--space-24) var(--space-20);
  background: var(--surface-card);
  border: 1px solid color-mix(in oklch, currentColor 4%, transparent);
  border-radius: var(--card-radius);
  box-shadow: var(--elev-1);
}
.wfee__amount-label { font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--text-secondary); }
.wfee__amount-value {
  font-size: var(--text-h1); line-height: var(--lh-h1);
  font-weight: var(--fw-extrabold); letter-spacing: var(--tracking-tight);
  color: var(--text-primary); font-variant-numeric: tabular-nums;
}
.wfee__amount-value--zero { color: var(--text-faint); }
.wfee__amount-sub { font-size: var(--text-caption); color: var(--text-muted); }

/* ── Select-all bar ──────────────────────────────────────────────────────── */
.wfee__selectbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 var(--space-4);
}
.wfee__selectbar-count {
  font-size: var(--text-sm); font-weight: var(--fw-semibold);
  color: var(--text-primary); font-variant-numeric: tabular-nums;
}
.wfee__selectbar-total { color: var(--text-faint); font-weight: var(--fw-regular); }

/* ── Token rows — lead pairs checkbox + token avatar; trail stacks amount
      over usd. Locked (signing) rows stay selected + legible; only the
      checkbox dims via its own :disabled treatment. ─────────────────────── */
.wfee__token-lead { display: inline-flex; align-items: center; gap: var(--space-12); }
.wfee__token-trail { flex-direction: column; align-items: flex-end; gap: var(--space-2); }
.wfee__token-usd { font-size: var(--text-caption); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.wfee__tokens--locked .list-item { pointer-events: none; }

/* ── Receiver note + gas estimate ────────────────────────────────────────── */
.wfee__meta {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-12); padding: 0 var(--space-4);
}
.wfee__meta-note {
  display: inline-flex; align-items: center; gap: var(--space-8);
  font-size: var(--text-caption); color: var(--text-muted); min-width: 0;
}
.wfee__meta-note svg { width: var(--space-16); height: var(--space-16); flex: 0 0 auto; color: var(--text-faint); }
.wfee__meta-gas { font-size: var(--text-caption); color: var(--text-muted); white-space: nowrap; }
.wfee__meta-gas strong { color: var(--text-primary); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }

/* ── Signing ─────────────────────────────────────────────────────────────── */
.wfee__signing-btn .spinner { margin-right: var(--space-8); }

/* ── Footer actions — primary grows, secondary hugs (2-up result rows;
      single block CTA on select). ─────────────────────────────────────── */
.wfee__actions { display: flex; gap: var(--space-8); }
.wfee__actions > .btn-primary { flex: 1 1 auto; }
.wfee__actions > * { white-space: nowrap; }

/* ── Result (success / error) ────────────────────────────────────────────── */
.wfee__result {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: var(--space-8);
  padding: var(--space-8) var(--space-20) var(--space-4);
}
.wfee__result .spot-icon { margin-bottom: var(--space-8); }
.wfee__result-title {
  margin: 0;
  font-size: var(--text-h3); line-height: var(--lh-h3);
  font-weight: var(--fw-bold); letter-spacing: var(--tracking-snug);
  color: var(--text-primary);
}
.wfee__result-desc {
  margin: 0; max-width: var(--w-6);
  font-size: var(--text-sm); line-height: var(--lh-sm); color: var(--text-secondary);
}
.wfee__result-desc strong { color: var(--text-primary); font-weight: var(--fw-semibold); }

/* Receipt — .detail-list--divided in a card frame. */
.wfee__receipt {
  background: var(--surface-card);
  border: 1px solid color-mix(in oklch, currentColor 4%, transparent);
  border-radius: var(--card-radius);
  box-shadow: var(--elev-1);
  padding: var(--space-8) var(--space-16);
}
.wfee__receipt .detail-list__row { padding: var(--space-8) 0; }
.wfee__receipt .detail-list__desc {
  display: inline-flex; align-items: center; gap: var(--space-8);
  color: var(--text-primary); font-weight: var(--fw-medium);
  font-variant-numeric: tabular-nums;
}
.wfee__receipt-chain .avatar { --avatar-size: var(--space-20); }

/* Error detail — the tier-2 .error-detail primitive (styles.css, alert
   family). The page-local .wfee__error it started as was promoted June
   2026 — atomic retirement, no alias. */

/* ── Batch legs — the per-chain checklist (bselect → brun → bsummary) ─────
   The select state reuses the token-row composition (checkbox lead); the
   run + summary states swap the checkbox slot for a status MARK at the
   same 20-px geometry, so the picks visibly become progress markers.
   Marks: queued (hollow ring) → signing (spinner) → done ✓ | failed ✕ |
   skipped – (filled semantic discs, --on-* ink). Promotion candidate if a
   second checklist-flow consumer appears (a "steps--checklist" sibling). */
.wfee__leg-mark {
  width: var(--space-20); height: var(--space-20);
  border-radius: var(--r-full);
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
}
.wfee__leg-mark svg { width: var(--space-12); height: var(--space-12); }
.wfee__leg-mark--queued { border: var(--space-2) solid color-mix(in oklch, var(--text-primary) 16%, transparent); }
.wfee__leg-mark--done    { background: var(--success); color: var(--on-success); }
.wfee__leg-mark--failed  { background: var(--danger);  color: var(--on-danger); }
.wfee__leg-mark--skipped {
  background: color-mix(in oklch, var(--text-primary) 8%, transparent);
  color: var(--text-faint);
}

/* Run + summary rows are read-only (no hover affordance); the in-flight leg
   carries the row-recipe selected tint so the eye tracks the signature down
   the list. Status ink shifts per state — ink, never weight. */
.wfee__legs .list-item { pointer-events: none; }
.wfee__leg--signing { background: color-mix(in oklch, var(--accent-primary) 6%, transparent); }
.wfee__leg--skipped .list-item__title { color: var(--text-muted); }
.wfee__leg--failed .wfee__leg-status { color: var(--danger); }
.wfee__leg--done .wfee__leg-status { font-variant-numeric: tabular-nums; }

/* Summary rows host a trailing explorer icon-link — re-enable pointer events
   on just the link inside the read-only row. */
.wfee__legs .list-item a { pointer-events: auto; }
.wfee__leg-trail { display: inline-flex; align-items: center; gap: var(--space-8); }

/* Org-wide batch — quiet integration group labels between leg clusters
   (xs monogram + name; emitted by withdraw-fees.js → legGroupHTML whenever
   the slug changes). The list's own card gap separates label from rows; the
   extra margin-top widens the seam ABOVE a new group so clusters read as
   distinct (first label skips it — the hero/selectbar already breathe). */
.wfee__leg-group {
  display: flex; align-items: center; gap: var(--space-8);
  margin-top: var(--space-8);
  font-size: var(--text-caption);
  font-weight: var(--fw-semibold);
  color: var(--text-secondary);
}
.wfee__leg-group:first-child { margin-top: 0; }
