/* Core dashboard stylesheet. Cascade layers are ordered from foundational
   tokens through deliberate page-level overrides. */
/* ============================================================
   CASCADE LAYERS

   The order is declared once, on the line below this comment, and that
   ONE line settles every tie in this file: a later layer wins over an
   earlier one REGARDLESS of selector specificity. That is the point.
   This stylesheet had rules quietly cancelling each other (a `background`
   shorthand that ate the select chevron; page-scoped rules wrestling with
   the base component rules they were only meant to nudge). A layer
   boundary is a decision you can read; a specificity count is one you
   have to work out, in your head, every time.

     tokens      the :root custom-property block. Nothing else.
     base        unqualified element rules: *, body, the shared
                 input/select/textarea chrome, ::placeholder, scrollbars.
                 Specificity (0,0,n) ONLY. Anything carrying a state or
                 attribute qualifier -- select:hover, input:focus,
                 input[type="range"] -- is class-weight and stays in
                 `components`, because it already outranks plain class
                 rules today and must keep doing so.
     components  every class/ID-based component rule. The bulk of the file.
     overrides   rules that deliberately re-style a component IN A CONTEXT:
                 `.app-main .tab-panel .panel`, the hub's
                 [data-page] / .sc-activity-slot / .dash-half block, and
                 the CLEAN PASS at the bottom of the file.

   *** UNLAYERED CSS BEATS EVERY LAYER, AT ANY SPECIFICITY. ***
   Three modules inject a <style> block into <head> at runtime:
     ui/static/charts.js              .bchart-*
     ui/static/shortcuts-activity.js  .act-* (model / readout / dual range)
     ui/static/chain-logos.js         .chain-logo
   Left unlayered they would silently outrank everything below. Each of
   those three blocks therefore opens with `@layer components {`. They are
   appended to <head> AFTER this file's <link>, so inside `components` they
   still come last and still win ties at equal specificity -- precisely the
   cascade they had before layers existed. Add a fourth injector, wrap it too.

   ============================================================ */
@layer tokens, base, components, overrides;

@layer tokens {
:root {
  /* Gotham's near-flat dark surfaces rely on crisp hairline borders for
     separation. Keep enough border contrast to distinguish adjacent planes. */
  --background: #111111;                 /* the page */
  /* Content boxes sit one step ABOVE the page. The reference draws its containers
     at the page tone and lets the border alone define them; tried that here and the
     owner preferred the lift — with this much instrumentation on screen the fill is
     what separates a tool from the canvas it sits on. The WALLET RAIL is the
     exception and stays page-toned (see .wallet-rail), so the chrome column reads
     as the floor and the content boxes read as things standing on it. */
  --card: #191919;                       /* one step above the page */
  /* --surface is the panel tone. Controls distinguish themselves through their
     alpha fill and border rather than introducing another fixed surface. */
  --surface: #191919;                    /* same plane as --card */
  --surface-2: #232323;                  /* hover / active, one step above a card */
  /* Surfaces that sit OVER the page — menus, modals, drawers, popovers. These
     are the ONLY things that get a fill, because they must read as detached
     from whatever they cover. Everything on the page itself is --card, which is
     now the page tone: a panel is drawn by its EDGE, not by a fill. */
  --float: #202020;                      /* ABOVE a card — menus/modals must detach from it */
  /* Controls lift off whatever is BEHIND them with a white wash rather than a
     fixed grey — the preset's own trick (`bg-input/30` over --input white/15%).
     A fixed grey can't work here: the same input renders on the page (0.145),
     on a card (0.205) and inside the drawer, and one value cannot read on all
     three. An alpha composites correctly over every one of them. */
  --control-bg: oklch(1 0 0 / 6%);
  --control-bg-hover: oklch(1 0 0 / 10%);

  /* ONE elevation system for everything that floats. Menus, popovers, pickers,
     drawers and gates were each carrying their own shadow (.45 / .55 / .6, three
     different blurs) and picking --border or --border-strong at random. Two
     tiers now: `pop` for things anchored to a control, `modal` for things that
     take over the screen. Floating surfaces always use the STRONGER border —
     they sit over arbitrary content and need the extra definition. */
  --shadow-pop: 0 10px 30px rgba(0, 0, 0, .5);
  --shadow-modal: 0 24px 70px rgba(0, 0, 0, .6);
  /* A drawer is flush to the RIGHT screen edge, so an omnidirectional modal
     shadow casts most of itself off-screen. It needs the light thrown LEFTWARD,
     onto the content it covers — which is a real third tier, not drift. It lives
     here rather than inline in .drawer, where it was silently overwriting
     --shadow-modal two declarations after reading it. */
  --shadow-drawer: -12px 0 32px rgba(0, 0, 0, .45);
  /* ONE inset scale. --pad-card is the inset EVERY card uses, so a
     popover and a card have the same internal margin. */
  --pad-card: 16px;
  --pad-modal: 16px;   /* every dialog's inset — was 24, which read as a different app to the boxes behind it */

  /* ONE spacing scale. Gaps were declared at fourteen distinct values — 1, 2, 3,
     4, 5, 6, 7, 8, 9, 10, 11, 12, 14 and 16px — so no two blocks shared a
     rhythm and the eye reads the difference as sloppiness rather than as
     hierarchy. Five rungs, and every gap/inset picks one. The rungs were chosen
     so that no existing value moves by more than 2px: this regularises the
     rhythm without relaying out a single component. */
  --sp-0: 2px;    /* hairline: icon clusters, inline glyph gaps */
  --sp-1: 6px;    /* tight: label→control, chip internals */
  --sp-2: 8px;    /* default: most flex/grid gaps */
  --sp-3: 12px;   /* block: between related groups */
  --sp-4: 16px;   /* section: between top-level boxes */
  /* These carry the separation now that the tones are nearly flat (see the note
     on --background). 10%/15% was tuned against a 0.060 tonal step; at 0.043 an
     edge has to be visible on its own. */
  /* Below the reference's .13 on purpose. Their boxes carry more internal
     contrast, so a .13 edge reads as definition there and as a hard outline
     here — the boxes ended up looking drawn-on rather than delineated. */
  /* 13% — the reference's exact value. At our old 8% the hairline was barely
     there, which was survivable while a lighter fill also marked the box out and
     is not survivable now that the border is the only thing drawing it. */
  /* SOLID, not white-at-alpha. These were rgba(255,255,255,.13/.14), which rendered
     as three different greys depending on the plane behind them (#303030 on the page,
     #373737 on a panel, #3F3F3F inside a hover fill). Fixed hexes give up that depth
     adaptation and buy one stroke that is the same everywhere — which is the point.
     The two are now genuinely distinct (#262626 vs #3B3B3B) rather than one alpha step
     apart, so "strong" finally means something. */
  /* WHAT IS IN HAND IS ON TOP. OF EVERYTHING. Not of its own table — that was the
     first version of this token, at 20, chosen to clear a sticky column header at 3 —
     and 20 is under the wallet rail (55), so a wallet dragged out of a run's list
     slid beneath the left menu on its way past. Every value picked to clear one
     neighbour is a value that loses to the next one.
     So it is stated once, above every plane the app draws: the rail, the feed, the
     tab strip, a quick menu, a picker, the help overlay. It is used by all three
     things a hand can be holding — a wallet lifted out of any list, a group chip
     being reordered, a sequence row being moved — and only ever while the pointer is
     down. Below the marquee and the drag ghost (10020/10021), which are the two marks
     drawn ON the gesture rather than by it. */
  --z-wallet-lift: 10015;
  /* The overlay scrollbar's width, and the strip every list reserves on its right for
     it (see scOverlayScroll). One number: the bar is drawn in exactly the space the
     rows and their column heads keep clear, so nothing sits under the thumb and the
     columns line up whether the list scrolls or not. */
  --sc-bar-w: 8px;
  /* The lane is what you aim at; the thumb is what you read. Drawn narrower than the
     strip it sits in, and a step below --border-mid at rest — it marks where you are
     in a list, and at full strength it read as a rule down the edge of the table. */
  --sc-bar-thumb-w: 6px;
  --sc-bar-ink: color-mix(in srgb, var(--border-mid) 72%, transparent);
  --border: #262626;                         /* cards, tables, dividers */
  /* Between the two, and it earns a name because two unrelated things need exactly it:
     the confirm sheets' wallet tables, which are drawn on --float (#202020) where
     --border disappears and --border-strong shouts, and the scrollbar thumb, which is
     the one piece of chrome that has to be findable on every surface at once. */
  --border-mid: #303030;
  --border-strong: #3B3B3B;                  /* inputs, floating surfaces, hover */
  --foreground: oklch(0.985 0 0);        /* #fafafa */
  --muted: oklch(0.708 0 0);             /* #a1a1a1 — preset `muted-foreground` */
  --muted-2: oklch(0.556 0 0);           /* #737373 — dim / placeholder (preset `ring` step) */
  --primary: oklch(0.922 0 0);           /* #e5e5e5 — inverted primary button */
  --primary-fg: oklch(0.205 0 0);        /* #171717 — dark text on it */
  --ring: oklch(0.556 0 0);              /* #737373 — focus ring */
  --radius: 0px;
    /* The body face was written out literally at the one place that set it, while
       --mono and --num were tokenised — the sans stack was the only part of the
       type system you could not reach from a variable. */
    --font-sans: "Geist", -apple-system, "SF Pro Text", system-ui, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
    /* Native UA chrome the stylesheet cannot reach — date pickers, <option> popups,
       spellcheck underlines, default scrollbars — renders LIGHT against a dark app
       without this one line. */
    color-scheme: dark;
    /* --primary one step lighter, for its hover. Was a literal #e6e6e6 sitting one
       step off --primary's #e5e5e5: invisible drift, and exactly the kind that makes
       a palette impossible to move later. */
    --primary-hover: oklch(0.945 0 0);
    /* THE LABEL. One treatment everywhere a control is named: sentence case, body
       size, medium weight. It replaces an 11px UPPERCASE letter-spaced eyebrow —
       which shouted the least important text on a card. A label NAMES a control;
       it is not a section marker, and it should not out-shout the value beside it.
       The --eyebrow-* tokens survive for the things that really are markers
       (state pills, table headers). */
    --label-size: var(--body-size);
    --label-weight: 400;                         /* the preset's own value */
  /* (The preset's --chart-1…5 grayscale ramp was declared here with ZERO
     consumers in any CSS or JS — charts.js paints from --green/--sell. Deleted:
     a token nothing reads is a decision nobody can act on.) */
  /* TWO control rungs, and only two. There were four (34/30/28/26), which meant
     buttons rendered at 28/30/34 while fields rendered at 26/28/34 — the two
     families did not share a ladder, so a button beside an input lined up only
     by luck. Now every control, of either family, is one of these:
       --control-h     the DEFAULT: anything in a form/field, and primary actions
       --control-h-sm  COMPACT: header + toolbar clusters, small/tiny buttons,
                       inline step controls inside a sentence, single-line chips
     Nothing hardcodes its own height, so any button set beside any input
     matches. Two rungs is the invariant; a third is a regression. */
  --control-h: 32px;
  --control-h-sm: 28px;

  /* ONE wallet identity anatomy — ONE component, density variants.
     A wallet is drawn in six places (rail row, .dz-chip, .sc-wallet-card wide +
     narrow, .wallet-chip, .loop-startw-chip / .loop-sub). They used to disagree
     on height (28/30/31/53), name size (12/12.5/13) and inset (four recipes) —
     five renderings of one concept. Everything below is shared by ALL of them.

     A wallet's IDENTITY is its title + its address. Nothing else. Every variant
     used to lead with a colour dot as well, but that colour was hue =
     index * 360 / count — derived from the wallet's POSITION in the list and the
     list's LENGTH, so minting one relay re-hued every existing wallet and two
     wallets could swap colours because a third appeared. It looked like an
     identity and was not one, so it is gone from every wallet rendering; the row
     now leads with the thing that IS stable.

     The ONLY licensed difference between variants is HOW MANY LINES are shown:
       1 line  → height: var(--control-h-sm)   name + figures. A chip sits on the
                 compact CONTROL rung — a chip IS a control-sized object.
       2 lines → height: var(--chip-h-2)       + the address, on line 2 under the
                 name (narrow slots, where a one-liner starves both).
       3 lines → the rail row only: + the [group] tag and the last-tx stamp. */
  --chip-h-2: 44px;         /* two-line wallet card (narrow .sc-dropzone slots) */
  --chip-x: 18px;           /* square ✕ / copy hit area, every variant */
  --chip-pad-x: 9px;        /* the ONLY horizontal inset a wallet row/chip uses */
  --chip-gap: 8px;          /* name → figures gap, every variant */
  --chip-name-size: var(--body-size);  /* the wallet NAME — it is body copy */
  --chip-addr-size: var(--cap-size);   /* the truncated 0x address, every variant */
  --chip-fig-size: var(--fig-inline);  /* a balance figure, every variant */

  /* Numeric-input width ladder. A field holding 3 digits must NOT stretch to the
     width of an address field — every numeric control picks a rung. ch-based, so
     the box tracks the numeric face instead of a guessed pixel count. */
  --num-w-sm: 7ch;          /* 2–4 digits: counts, seconds, percentages */
  --num-w-md: 9ch;          /* 5–7 digits: bps, fee tiers, decimals */
  --num-w-lg: 13ch;         /* decimal amounts: 0.0001 … 1,234,567.8 */

  /* ============================ TYPE: THREE ROLES ============================
     The app had 22 distinct font-sizes across ~198 declarations. Sizes 11.5,
     12, 12.5 and 13 cannot be told apart at a glance, so six "levels" of
     hierarchy read as one level plus noise. Gotham uses three treatments and
     lets weight and position carry the rest.

       --title-size   section/card/modal headings. SENTENCE CASE, not uppercase.
       --body-size    everything you read or type: labels, table cells, inputs,
                      buttons, help text, wallet names. One size.
       --cap-size     the ONLY surviving uppercase: a micro caption sitting
                      directly above a figure ("TOTAL ETH"). Nothing else shouts.

     Figures get three rungs and no more, by the job they do. */
  --title-size: 20px;
  --title-weight: 400;
  --body-size: 14px;
  --body-line: 20px;      /* their 14/20 — a 1.5 ratio sets 21 and rides looser */
  --title-line: 28px;     /* their 20/28 */
  --cap-size: 12px;
  --fig-inline: 14px;       /* a figure inside a row of body text */
  --fig-card: 20px;         /* the headline number on a card */
  --fig-hero: 30px;         /* the balance triptych */

  /* Eyebrow tokens now both resolve to the single caption tier — the 9px rung
     is gone (it was unreadable and it made "uppercase" mean two things). */
  --eyebrow-size: var(--cap-size);
  --eyebrow-cap-size: var(--cap-size);
  --eyebrow-track: .08em;   /* letter-spacing */
  --eyebrow-weight: 400;

  /* Semantic colours encode product data: buy/sell direction, confirmation,
     failure, warnings, and recency. They are not decorative brand accents. */
  /* ONE RED, and every red in the app is now this one. It was the preset's
     oklch(0.704 0.191 22.216) = #FF6467, with a family of Tailwind reds scattered
     beside it in literals: #f87171 on ten error strings, #fca5a5 on three washes,
     #7f1d1d on the danger button's border, #b91c1c parked in --danger-2. Nothing
     distinguished them — they were whatever red was to hand when each rule was
     written — and #FF6467 sat one column away from a #E5484D "Sell" in the Logs
     table, which read as a mistake rather than a distinction. All of them now point
     here, so failures, destructive controls, error text and a sell are the same red
     and this line is the only place to change it. */
  --red: #E5484D;
  --blue: #3b82f6; --blue-fg: #bfdbfe;
  --green: #22c55e; --amber: #f59e0b;
  /* THE BRAND ACCENT. One colour, named for its job rather than its hue — it was
     --orange, and a token called --orange holding a blue is a trap for whoever reads
     the next call site. Everything that means "act on this" reads from here: the
     active tab's ink, + Create, the selected group's bar, a range's fill, Disperse in
     the logs, the running banner. */
  --accent: #2A78D6;
  /* The token side of the allocation — the bar segment, its legend dot, and the big
     chart when it is showing that series. One declaration, because those three are the
     same fact drawn three ways and charts.js reads this via cssVar(). */
  --alloc-tok: #2EBFE7;
  --violet: #a78bfa;
  --mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  /* FIGURES are set in the SANS, not a monospace. Measured on the reference: every
     number on its page — balances, prices, percentages, counts — renders in Geist
     sans at weight 400 with font-variant-numeric NORMAL. The only monospace it
     uses anywhere is the step badge on an onboarding card. Ours put every figure in
     Geist Mono, which is what made the numbers read as heavier and wider than the
     labels beside them.
     --mono is UNCHANGED and still monospace: it sets addresses, hashes, raw JSON
     and log lines, where character alignment is the actual point. Digits in a
     stat tile are not that. Where our figures sit in an aligned COLUMN (the rail's
     balance columns, the derived rows) the call sites keep
     `font-variant-numeric: tabular-nums`, which Geist supports — so columns still
     line up without the whole app being monospaced to get it. */
  --num: var(--font-sans);

  /* app-shell elevation + geometry */
  --rail: #111111;            /* the rail is page-level; its border divides it */
  --shell-header-h: 52px;     /* full-width top header row */
  /* Left rail. 448px is the reference's OWN side-panel width, so at a 1440
     canvas the split lands on its 31% / 67% proportion instead of our 21% / 79%.
     The rail carries a name, an address and TWO balance columns — at 300px it was
     the cramped half, not the generous one. */
  --shell-left-w: 448px;      /* left rail column */
  /* Hub Activity/Sweep/Transfer boxes share one height. Sized so their bodies
     do NOT scroll: at 540px Sweep overflowed by 164px and Activity by 107px, so
     the controls you were about to press sat below a scroll line inside a card
     that already had a fixed height. Sweep is the tallest of the three and sets
     the floor. Raise this if a card grows a row; do not re-introduce the inner
     scrollbar — a fixed-height box that also scrolls hides its own controls. */
  --dash-box-h: 712px;
  /* --shell-right-w removed with the right Activity feed rail (now a 2-col shell). */

  /* Compatibility aliases for existing component rules. */
  --bg: var(--background);
  --panel: var(--card);
  --panel-2: var(--surface);
  --text: var(--foreground);
  /* (The legacy `--accent: var(--ring)` alias that stood here is gone: --accent is the
     BRAND accent now, declared above, and this line — later in the same block — was
     silently overwriting it with the focus-ring grey. Neither alias had a single use.) */
  --accent-2: var(--primary);
  --ok: var(--green); --bad: var(--red);
  --danger: var(--red); --danger-2: var(--red); --warn: var(--amber);
  /* SELL IS RED, not orange. It was --orange, which put the one colour this UI uses
     for "act on this" — primary buttons, the active tab's ink, the selected group's
     bar, a range's fill — on a market SIDE as well, so a sell figure and a call to
     action were the same colour. Sell is red now and orange stays an accent.
     An alias, not a second hex: --red is the app's one red (see it above) and a sell
     is one of the things it means, so the two cannot drift apart. */
  --sell: var(--red); --buy: var(--green); --approve: var(--violet);
  /* THE SELECTION WASH. The platform's own highlight is a saturated blue that
     belongs to no part of this palette, and selected text is not an ALERT — it is a
     range you are about to type over. Enough contrast to see the range, quiet enough
     that a field which selects its contents on open does not shout. */
  --select-wash: color-mix(in srgb, var(--foreground) 22%, transparent);
}
}  /* end @layer tokens */

@layer base {
* { box-sizing: border-box; }

/* One body size for the whole document, so anything that does NOT set its own
   font-size inherits the body role instead of the browser's 16px default. */
:where(body) { font-size: var(--body-size); }

/* Controls inherit the page face and size. Without this, a <button>/<input>/
   <select> falls back to the UA's own font stack at 13.333px and quietly
   escapes the type scale. */
:where(button, input, select, textarea) { font: inherit; }

/* ---- Interaction states ----
   Written with :where() so every rule below has ZERO specificity: any component
   rule wins over it, which is what you want for a base layer. These are the two
   states the app had no styling for at all — keyboard focus was invisible on
   .btn, and a disabled control was pixel-identical to an enabled one, so the
   only way to discover a control was dead was to click it. */
:where(button, a, input, select, textarea, [tabindex], [role="button"]):focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}
:where(button, input, select, textarea):disabled,
:where([aria-disabled="true"]) {
  opacity: .45;
  cursor: not-allowed;
}

/* The `hidden` ATTRIBUTE must actually hide. The UA stylesheet implements it as
   `[hidden] { display: none }`, which ANY author `display` beats — so an element
   with `.foo { display: flex }` stays visible while `el.hidden = true`, and the
   JS reads as correct. This bit exactly once already (.act-derived rendering
   inside the action bar). !important because the whole point is to outrank a
   component's own display. */
[hidden] { display: none !important; }
}  /* end @layer base */

/* Self-hosted numeric face (Geist Mono, MIT) — consistent figures on any machine */
@font-face {
  font-family: "Geist Mono";
  src: url("/static/fonts/GeistMono-Regular.woff2") format("woff2");
  font-weight: 400 700;
  font-display: swap;
}

/* Geist Sans (Vercel, SIL OFL) — the reference's text face, self-hosted from
   the upstream @fontsource package.

   TWO weights, deliberately. The reference serves 400 and 500 and NOTHING else;
   its headings still declare 600, so the browser synthesises that from 500.
   Shipping a real 600 here would render our headings genuinely heavier than
   theirs — matching the declaration but not the result. font-display: swap
   matches theirs too, so first paint uses the fallback rather than blocking. */
@font-face {
  font-family: "Geist";
  src: url("/static/fonts/Geist-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Geist";
  src: url("/static/fonts/Geist-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@layer components {
/* Ethereum mark, painted in the current text color, prefixed to ETH amounts.
   A SOLID silhouette, deliberately: the canonical 4-face mark carries two faces
   at opacity .55, and at the ~7px this renders at in a chip or table cell those
   faces vanish and the rest reads as a smudge. One flat octahedron stays legible
   at every size it is used (chips, drop-zone cards, loop rows, session totals). */
.eth-lead::before {
  content: "";
  display: inline-block;
  width: .68em; height: 1em;
  vertical-align: -.12em; margin-right: 4px;
  background-color: currentColor;
  opacity: .85;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 417'%3E%3Cpath d='M128 0 0 212.3 128 270.4 256 212.3Z'/%3E%3Cpath d='M128 312.2 0 236.6 128 416.9 256 236.6Z'/%3E%3C/svg%3E") no-repeat center / contain;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 417'%3E%3Cpath d='M128 0 0 212.3 128 270.4 256 212.3Z'/%3E%3Cpath d='M128 312.2 0 236.6 128 416.9 256 236.6Z'/%3E%3C/svg%3E") no-repeat center / contain;
}
}  /* end @layer components */

@layer base {
body {
  margin: 0;
  background: var(--background);
  color: var(--foreground);
  /* Split out of the `font:` shorthand deliberately. As a shorthand this set the
     size to a hardcoded 13.5px, which no font-size sweep could see and which
     every em-based child inherited — an <h2> at 1.5em landed on 20.25px.

     Face + metrics are the reference's, read off it with getComputedStyle:
     Geist, 14px on a 20px line, letter-spacing NORMAL, smoothing AUTO. The app
     had -webkit-font-smoothing: antialiased and negative tracking on every
     heading; both are ours, neither is theirs, and together they are most of
     why our type read heavier. */
  font-family: var(--font-sans);
  font-size: var(--body-size);
  line-height: var(--body-line);
  letter-spacing: normal;
  -webkit-font-smoothing: auto;
}
}  /* end @layer base */

@layer components {
/* ============================================================
   App shell — a full-height grid of [wallet rail | canvas], both floating on the
   page with one 8px frame. There is NO header row: the chrome it carried (coin
   picker, totals, ↻ / profile / ⚙) lives in the rail's head, so the rail runs the
   full height and the canvas starts at the top of the window.
   ============================================================ */
.app {
  display: grid;
  /* min() so the rail can never eat the canvas on a small screen. --shell-left-w
     is 448px — the reference's own panel width, which is ~31% at 1440 but would be
     55% at 820. The vw ceiling holds that PROPORTION instead of the pixel count,
     and it clamps the width sidebar.js writes inline for a user-dragged rail too,
     which a token-only cap would not. */
  grid-template-columns: clamp(440px, var(--shell-left-w), 34vw) 1fr;
  /* TWO rows: a full-width header, then the body. */
  grid-template-rows: auto 1fr;
  height: 100vh; overflow: hidden;
  /* ONE frame, owned here. Every region floats on the page with the same 8px the
     reference keeps between any two boxes — so the rail, the canvas and the boxes
     inside it all sit on one rhythm instead of each adding its own margin and
     doubling up at the seam. All four sides, in one shorthand: a `padding-top`
     longhand above a `padding` shorthand is silently reset by it. */
  /* NO shell gap or padding. The rail is HARD-FIXED to the window's left edge
     now — flush, no gutter — with its right border as the divider running from
     the header down to the floor. The canvas carries its own side gutter instead
     (see .app-main), so only the centre floats. */
  gap: 0;
  padding: 0;
}
/* ---- the wallet header ----
   Occupies only the wallet-rail column. The stats/action canvas spans both grid
   rows on the right, so its first box begins at the top of the window. */
.app-header {
  /* The context-bar control tokens, declared here as well as on .rail-head and
     .shell-top — this cluster lives in the header now, and a `var(--ctx-ctl-h)` with
     no declaration in scope is invalid-at-computed-value-time, so `width` fell back to
     auto and the icon buttons shrank to their glyph (18px). The comment above the
     .rail-head block warns about exactly this; the header was the case it missed. */
  --ctx-gap: var(--sp-2);
  --ctx-ctl-h: var(--control-h-sm);
  --ctx-ctl-pad: 10px;
  grid-column: 1; grid-row: 1;
  display: flex; align-items: center; gap: var(--ctx-gap);
  height: 48px; box-sizing: border-box;
  padding: 0 16px;
  background: var(--background);
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
/* The compact context row carries the coin picker, refresh, and Gotham Wallet. */
/* Brand mark anchors the left edge. One compact-control token sizes it, the
   picker, refresh, and the combined Gotham Wallet button. */
.app-brand {
  height: var(--ctx-ctl-h); display: inline-flex; align-items: center;
  flex: 0 0 auto; overflow: hidden;
}
.app-brand-vector {
  display: block; width: auto; height: var(--ctx-ctl-h); max-width: 96px; object-fit: contain;
}
/* PAGE NAV — Trade | Overview, immediately after the brand.
   Uses .rail-btn (the reference's 32px button: no border, neutral-alpha/3 fill,
   14/20 Medium) so the app has ONE button vocabulary rather than a second one
   invented for the header. The active page is the same button at a higher fill —
   the accent is reserved for actions like + Create, and colouring "where I am"
   the same as "do a thing" would make the nav read as a call to action. */
.app-nav { display: flex; align-items: center; gap: var(--sp-2); flex: 0 0 auto; }
/* Page navigation uses shared Gotham tokens; it must not introduce a second
   accent or a header-only palette. */
.app-nav-btn { background: transparent; color: var(--muted); }
.app-nav-btn:hover { background: var(--surface); color: var(--foreground); }
.app-nav-btn.active { background: var(--surface); color: var(--foreground); }
/* The chrome row no longer fills the bar — it takes the remaining space beside
   the nav and keeps its own right-alignment. */
.app-header .rail-head-top {
  display: flex; align-items: center; gap: var(--ctx-gap);
  flex: 1 1 auto; min-width: 0; margin: 0; padding: 0; border: 0;
}
.app-header #ctx-token {
  display: inline-flex; align-items: center; gap: var(--sp-2); flex: 0 0 108px; min-width: 0;
}
.app-header #ctx-token .cs { width: 100%; }
.app-header #ctx-token .cs-btn { height: var(--ctx-ctl-h); }
.app-header .ctx-actions { margin-left: 0; }
/* With portfolio totals removed, the complete control cluster stays right-aligned
   as one compact group instead of leaving the coin picker beside the wordmark. */
.app-header .rail-head-top { justify-content: flex-end; }
.app-header .ctx-item { flex: 0 0 auto; }
/* ---- the rail, hard-fixed left ----
   Flush to the window on its left, top and bottom: no radius, and its only
   border is the RIGHT edge, which now runs unbroken from the header to the
   floor. It used to float on an 8px gutter with a border on all four sides,
   which made it read as another card rather than the app's frame. */
.app .wallet-rail {
  grid-column: 1; grid-row: 2;
  border-width: 0 1px 0 0; border-radius: 0;
  min-height: 0;
}
/* the center canvas is the only independently-scrolling region */
.app-main {
  grid-column: 2; grid-row: 1 / -1;
  /* the shell no longer pads, so the canvas owns its own side gutter */
  padding-inline: var(--sp-2);
  min-width: 0; min-height: 0; overflow-y: auto;
  /* RESERVE THE SCROLLBAR ALWAYS. Without this the canvas is 10px wider on a tab
     that does not scroll than on one that does, so every tab switch shifted the
     whole page sideways — and a list crossing the fold made it jump mid-drag and
     jump back when the wallet came out again. */
  scrollbar-gutter: stable;
  /* The FRAME must equal the RHYTHM. This was 26px top / 32px side / 64px
     bottom while the gaps between boxes are --sp-4 (16px), so the margin around
     the grid read as twice the space inside it: the header sat further from the
     first card than the cards sat from each other, and the wallet rail sat
     further from the first box than that box sat from its neighbour. One value
     on all four sides is what makes the page look evenly gridded. */
  /* The 8px lives INSIDE the scroll region, so it is part of the scrolled
     content: visible when you are at the top or the bottom (the box's real edge
     is in sight and wants its gutter) and gone the moment you scroll past, when
     the box should run to the window edge rather than stop short of it. */
  padding-block: var(--sp-2);
  background: var(--background);
  /* No inset top shadow. It painted an 18px dark gradient across the top of the
     canvas to suggest content scrolling under a lip — but the canvas has no lip:
     the boxes float on the page with an 8px gutter, so the gradient just sat
     BEHIND the first card as a smudge with nothing casting it. */

  /* Tokens stay global in :root. Page-scoped palette overrides create drift
     between the canvas and rail and are not allowed. */
}
/* NO measure cap. The canvas fills the whole centre column, inset only by
   .app-main's own 8px padding — so the content sits 8px off the rail's edge and
   8px off the window on the right, and nothing else. The 1280px cap that used to
   live here (with auto gutters either side) is gone: on a wide monitor it left
   two dead bars rather than giving the tables the width they wanted. */
/* The tx-notifications panel lives in the DOM under .app-main but belongs to
   the right feed family — shield it so the restyle above does not touch it. */
#toast-stack {
  /* The token re-shield is gone: it existed only to undo .app-main's divergent
     values for a panel that sits under .app-main but belongs to the feed family.
     With a single ramp there is nothing to undo. */
}

/* ---- Top header (full-width shell chrome) ----
   ONE rhythm for the whole bar:
     • every item is separated by --ctx-gap (no per-element margins);
     • a .ctx-vr divider marks a LOGICAL GROUP boundary and only that, and gets
       exactly --ctx-gap either side (coin | network … totals | actions);
     • every control — coin select, "+", account pill, ↻ and ⚙ — is exactly
       --ctx-ctl-h tall with the same horizontal padding; icon buttons are
       square at that height. */
/* The .shell-top ELEMENT is gone (its contents moved into .rail-head), but the
   --ctx-* variables it scoped are still read by every control that moved with
   them — .hdr-iconbtn, .ctx-addbtn, .ctx-acct-pill, .ctx-totals. Scoped to the
   rail head now, which is where those controls live. Orphaned here they would
   have silently fallen back to each consumer's own default, and the three that
   have NO fallback (lines with a bare var(--ctx-ctl-h)) would have collapsed. */
.rail-head {
  --ctx-gap: var(--sp-3);
  --ctx-ctl-h: var(--control-h-sm);
  --ctx-ctl-pad: 10px;
}
.shell-top {
  --ctx-gap: var(--sp-3);
  --ctx-ctl-h: var(--control-h-sm);
  --ctx-ctl-pad: 10px;
  display: flex; align-items: center; gap: var(--ctx-gap);
  padding: 0 24px;
  /* No fill of its own and NO rule underneath. The reference's header sits
     directly on the page — measured: background = page tone, border-bottom = 0.
     Ours was a distinct bar (--rail) with a hairline under it, which framed the
     chrome as a fifth box competing with the four real ones below. */
  background: transparent;
  font-size: var(--body-size);
}
.brand {
  display: flex; align-items: center; gap: var(--sp-2); flex: 0 0 auto;
  font-weight: 400; font-size: var(--body-size);
}
/* The mark was a plain filled square — a placeholder that read as a rendering
   glitch rather than a logo. The wordmark carries the brand on its own. */
.brand .brand-mark { display: none; }
/* header groups are borderless text; only the coin select + buttons keep chrome */
.shell-top .ctx-meta, .shell-top .ctx-item { display: inline-flex; align-items: center; gap: var(--sp-2); }
.shell-top .ctx-vr { width: 1px; height: 22px; background: var(--border); flex: 0 0 auto; }
.rail-head .ctx-right, .shell-top .ctx-right { margin-left: auto; display: inline-flex; align-items: center; gap: var(--ctx-gap); }
.rail-head .ctx-totals, .shell-top .ctx-totals { display: inline-flex; align-items: center; gap: var(--ctx-gap); }
/* ↻ and ⚙ are ONE action group, so they sit tighter than the bar rhythm. */
.shell-top .ctx-actions { display: inline-flex; align-items: center; gap: var(--sp-1); flex: 0 0 auto; }
/* Right-cluster square icon buttons (↻ refresh, ⚙ settings): bordered square,
   icon only, hover lift — the corner-button look. .btn base supplies the
   centering + transition; this overrides its size/padding. */
.rail-head .hdr-iconbtn, .shell-top .hdr-iconbtn {
  width: var(--ctx-ctl-h); height: var(--ctx-ctl-h); padding: 0; flex: 0 0 auto;
  background: transparent; color: var(--muted);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-size: var(--body-size); line-height: 1;
}
.rail-head .hdr-iconbtn:hover, .shell-top .hdr-iconbtn:hover {
  background: var(--surface-2); color: var(--foreground); border-color: var(--border-strong);
}
/* While loading, btnLoading swaps the glyph for a text label ("Refreshing…")
   that can't fit a 34px square — hide the text so only the ::before spinner shows. */
.rail-head .hdr-iconbtn.loading, .shell-top .hdr-iconbtn.loading { font-size: 0; }

/* ============================================================
   Wallet rail — left rail: persistent wallet list, sort/search,
   app-wide selection + drag source. Mirrors the feed's treatment
   (sticky column, collapses to a thin strip, overlays when narrow).
   ============================================================ */
.wallet-rail {
  grid-column: 1; grid-row: 1;
  min-height: 0; overflow: hidden;
  display: flex; flex-direction: column;
  /* A BOX floating on the page, not a column welded to the window edge: border on
     all four sides, page tone, and the same 8px it keeps from every other box.
     It was a full-bleed slab with a single right-hand rule, which made it read as
     part of the window chrome rather than as one of the app's regions. */
  background: var(--background);
  border: 1px solid var(--border);
  /* The rail does NOT scroll as a whole and its edges are always in view, so it
     keeps a real 8px inset top and bottom — the shell no longer supplies one. */
  /* No vertical margin. The rail is flush now — it starts at the header's
     bottom edge and runs to the floor; the 8px was from when it floated. */
  margin: 0;
  z-index: 55;
}

/* ---- Left-rail nav: single-page shell, so the nav renders NOTHING. The #tabs
   element (and its hidden state-tracking buttons) stays in the DOM — sidebar.js
   requires a .tabs child and activateTab() still toggles .active on the
   buttons — but it takes zero height so the wallet list fills the full rail. */
.wallet-rail .tabs { display: none; }

/* The rail column has ONE left edge: head, sort/search, group folders, wallet
   rows and the footer all inset by --chip-pad-x. They used to run 14 / 12 / 14 /
   14 / 12, so the title, the search box and the row beneath it each started on a
   different pixel. */
/* The rail head carries the whole app's chrome now, in three rows:
     1  coin picker .................. ↻ profile ⚙
     2  network dot · ETH total · TOKEN total
     3  ✚ New   ⤓ Import   ⇤
   It is ONE element on purpose — sidebar.js subtracts its measured height to size
   the wallet list, so a row added as a SIBLING would overflow the list by its
   height instead of shortening it. */
.rail-head {
  display: flex; flex-direction: column; gap: var(--sp-2); flex: 0 0 auto;
  /* NO block padding, so this bar collapses to nothing. Everything it used to carry
     has moved out or been dropped — the network name, the ETH/token totals, Import
     (hidden), and the collapse glyph, which sidebar.js relocates — so its only child
     measures 0 and any block padding is a band of empty rail above the search field.
     The search row itself is balanced (11.5px over the field, 12.5 under); what read
     as "top heavy" was this strip sitting on top of it. Horizontal inset stays, so a
     button returning here still lines up with the rail's other controls. */
  padding: 0 var(--chip-pad-x);
}
.rail-head-top {
  display: flex; align-items: center; gap: var(--sp-2); min-width: 0;
}
/* the actions sit hard right of the coin picker */
.rail-head-top > .ctx-actions {
  margin-left: auto; flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: var(--sp-1);
}
/* .ctx-prof is a WRAPPER (it anchors the profile popover), so unlike its two bare
   siblings its box is not the button. Left as an inline span it baseline-aligned
   and sat 3px high. */
.rail-head-top .ctx-prof { display: inline-flex; align-items: center; }
.rail-head-top > #ctx-token { min-width: 0; flex: 0 1 auto; }
.rail-head-top #coin-select { max-width: 100%; }
.rail-head-top .ctx-actions .ctx-prof + * { border-left: 1px solid var(--border); }
/* .ctx-prof is a WRAPPER (it anchors the profile popover), so unlike its two bare
   siblings its box is not the button. Left as an inline span it baseline-aligned
   and sat 3px high — the misalignment you could see in the row. */

/* Wallet-origin surfaces still use the shared total figure anatomy. */
.rail-head-top > .ctx-totals {
  display: flex; align-items: baseline; gap: var(--sp-1) var(--sp-3);
  flex: 1 1 auto; min-width: 0; flex-wrap: wrap;
  justify-content: flex-end; margin-left: auto;
}
/* In the full-width header every group uses gap; no legacy wrapping inserts an
   accidental gulf between the coin picker and actions. */
.app-header .rail-head-top > .ctx-actions {
  margin-left: 0; gap: var(--sp-1);
}
.rail-head-top .ctx-total { display: flex; align-items: baseline; gap: var(--sp-1); white-space: nowrap; }
/* the figure may shrink and ellipse; the ticker eyebrow never does */
.rail-head-top .ctx-total-fig { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.rail-head-top .ctx-total-cap { flex: 0 0 auto; }

/* (.rail-head-totals is gone — its network label left the app and its two figures
   moved to .rail-head-top. The give-way ladder those rules implemented now lives
   on the top row, above.) */
.rail-controls {
  flex: 0 0 auto; display: flex; flex-direction: column; gap: var(--sp-1);
  padding: var(--sp-3) var(--chip-pad-x); border-bottom: 1px solid var(--border);
}
/* Sort + search share one rung and one inset. The select's chevron gutter is
   owned by --sel-gut (set in the compact-select list), so only padding-LEFT is
   restated here — a `padding` shorthand would delete the gutter. */
.rail-controls select, .rail-controls input {
  height: var(--control-h-sm); font-size: var(--body-size); padding-left: 8px;
}
.rail-controls input { padding-right: 8px; }
/* Draggable group folders above the wallet list. Drag a chip into any wallet
   input to add ALL its wallets; drop wallets onto a chip to assign them. */
.rail-groups { display: flex; flex-wrap: wrap; gap: var(--sp-1); padding: var(--sp-1) var(--chip-pad-x) var(--sp-2); }
.rail-groups:empty { display: none; }
.rail-group-chip {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: 3px 5px 3px 9px; background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius, 0px); cursor: grab; user-select: none; max-width: 100%;
}
.rail-group-chip:hover { border-color: var(--border-strong); }
/* Active = the rail is currently filtered to this chip. Click it again to clear. */
.rail-group-chip.active { border-color: var(--ring); background: var(--surface-2); color: var(--foreground); }
.rail-group-chip.active .rail-group-name { color: var(--foreground); }
.rail-dust-chip.active { border-color: var(--amber); }
.rail-dust-chip.active .rail-group-name { color: var(--amber); }
.rail-group-chip.dragging { opacity: .5; }
/* ---- the character count on a bounded field ----
   On the label's own line, hard right, in the quiet tier: it is a limit, not a
   reading, until you reach it — and then it is the reason the next key did nothing,
   so it changes colour rather than appearing. */
.rail-count-line { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.rail-count-hint {
  font-family: var(--num); font-variant-numeric: tabular-nums;
  font-size: var(--cap-size); color: var(--muted-2); transition: color .12s ease;
}
.rail-count-hint.is-full { color: var(--foreground); }
/* ---- where a dragged chip will land ----
   The gap OPENS on the side the pointer is on, so the mark is the space the chip is
   about to occupy rather than a line drawn near it. An inset shadow, so a chip about
   to be pushed does not itself move. */
/* ---- A CHIP IS PICKED UP, not dragged as a file ----
   The browser's own drag draws a translucent copy and an arrow with a green plus —
   the cursor for "this will be copied somewhere", which is not what reordering a
   list is. So the chip is moved by POINTER, like the sequence rows and the wallets:
   it lifts, it follows the hand, the others slide out from under it, and the cursor
   is the one you would expect to be holding something with.
   pointer-events:none while lifted is what lets elementFromPoint read the chip
   UNDERNEATH the one in hand — without it the gesture only ever finds itself. */
.rail-group-chip { transition: transform .16s ease, box-shadow .12s ease, border-color .12s ease; }
.rail-group-chip.is-reordering {
  cursor: grabbing; position: relative; z-index: var(--z-wallet-lift, 60);
  pointer-events: none; transition: none;
  background: var(--surface-2); border-color: var(--border-strong);
  box-shadow: var(--shadow-pop);
}
/* The whole page holds the cursor while a chip is in hand: the pointer spends the
   drag over other elements, and each of those would otherwise assert its own.
   A wallet in hand is the same fact — and it is a HAND, not the copy arrow with the
   green plus that the browser's own drag insists on. */
body.is-chip-dragging, body.is-chip-dragging *,
body.is-wallet-lifting, body.is-wallet-lifting * { cursor: grabbing !important; }
/* A group chip is a drop target too — same gesture, same accent as the zones. */
.rail-group-chip.drop-hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.rail-group-name {
  font-size: var(--label-size); font-weight: var(--label-weight); line-height: var(--body-line);
  color: var(--foreground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rail-group-count { font-size: var(--cap-size); font-family: var(--num); color: var(--muted-2); font-variant-numeric: tabular-nums; }
.rail-group-del { background: none; border: 0; color: var(--muted-2); cursor: pointer; font-size: var(--body-size); line-height: 1; padding: 0 1px; }
.rail-group-del:hover { color: var(--sell); }
.rail-group-add {
  display: inline-flex; align-items: center; padding: 3px 9px; background: none;
  border: 1px dashed var(--border); border-radius: var(--radius, 0px); color: var(--muted);
  cursor: pointer; font-size: var(--label-size); font-weight: var(--label-weight);
}
.rail-group-add:hover { color: var(--foreground); border-color: var(--border-strong); }
/* + Group and + Wallet are pushed to the RIGHT of the bar, away from the Dust
   chip on the left: chips on that row FILTER the list, these two ADD to it. */
.rail-group-add:first-of-type { margin-left: auto; }
.rail-group-new { border-style: solid; }
.rail-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0; }
/* EVERY OTHER BAND IS FIXED. The rail is a flex column, and a flex child with a height
   still shrinks when the column runs out of room — so opening the Quick swap drawer
   took its 204px out of the search row and the toolbar, squashing them. Only .rail-body
   gives: it is the part with a scrollbar, so it is the part that can afford to. */
.wallet-rail > .rail-head,
.wallet-rail > .rail-search-row,
.wallet-rail > #rail-groups,
.wallet-rail > .rail-quickswap,
.wallet-rail > .rail-foot { flex: 0 0 auto; }
.rail-list { display: flex; flex-direction: column; gap: 0; }
/* THE SAME EMPTY STATE EVERY DROP SLOT SHOWS: a mark over a line of text, centred in
   the space the rows would have filled. It was a sentence in the top-left corner of a
   rail-height void, which reads as a label that lost its list rather than as the list
   being empty. .sc-drop-empty's shape, stated here because the rail's message is not a
   drop hint and must not inherit that slot's other rules. */
.rail-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  flex: 1 1 auto; gap: var(--sp-2); padding: var(--sp-5) var(--chip-pad-x);
  color: var(--muted); font-size: var(--body-size); line-height: 1.4; text-align: center;
  user-select: none;
}
/* The list fills its scroller when the message is all it holds, so "centred" means the
   middle of the rail rather than the middle of one paragraph's worth of box. */
.rail-list:has(> .rail-empty) { min-height: 100%; }
.rail-empty-text { max-width: 26ch; }
.rail-empty-mark { display: inline-flex; color: var(--muted-2); }
.rail-empty-mark svg {
  width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.25;
  stroke-linecap: round; stroke-linejoin: round;
}
/* Footer note under a capped/streaming rail list (top-N of many, fetch progress) */
.rail-more {
  color: var(--muted-2); font-size: var(--cap-size); font-weight: var(--eyebrow-weight); text-transform: uppercase;
  letter-spacing: var(--eyebrow-track); padding: 9px 14px; border-top: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}

/* One wallet row: selectable + draggable, hairline-divided, no per-row card.
   This is a LIST ROW, not a stat card — it used to stack an uppercase 17px name,
   an address, then a 2-up grid of caption-over-24px-figure tiles, ~115px of
   height each, which made the rail unusable past a handful of wallets. Now it
   scans WHAT IT IS over WHAT IT HOLDS:

       NAME ......................................... [group]
       0x1234…5678 ⧉ ................. USDC 1,234.5   ETH 0.0421
       ▎LAST TX  got 0.012 Ξ ......................... 4m

   ~52px tall (~68px with the optional LAST TX line), balances right-aligned in
   a fixed-width tabular block so every row's figures share one column.

   The row IS the grid — there is no leading slot in front of the identity, so
   the name and the address start at the row's own inset instead of being pushed
   18px in by a colour dot whose hue came from a list position. .rail-wallet-id is
   `display: contents` (the .sc-wallet-card idiom), so name / address / balances /
   last-tx are direct children of this grid rather than a flex box inside it. */
.rail-wallet {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  /* Every cell centred on the ROW's middle. In the reference both the identity
     block (y 12..56 of 68) and the values block (y 24..44) share the row's centre
     line at 34; ours had the two columns settling at different heights, so a name
     and the figure beside it did not sit on the same axis. */
  align-items: center; align-content: center;
  column-gap: var(--chip-gap); row-gap: var(--sp-0);
  background: transparent; border: none; border-bottom: 1px solid var(--border); border-radius: 0;
  padding: var(--sp-3) var(--chip-pad-x); min-height: 56px; cursor: pointer; user-select: none;
  transition: background .12s;
}
/* NAME LINE — the wallet's title and, when it belongs to one, its group tag, side
   by side in the identity column. The tag used to be a grid item of its own in
   column 2, which put it above the Balance figure once the rail became the
   four-column table; membership belongs beside the name, not over a number. The
   name is the only thing here that shrinks, so a long title ellipsises and the tag
   keeps its shape (bounded, so a long GROUP name can't eat the row either). */
.rail-wallet-nameline {
  grid-column: 1; grid-row: 1;
  display: flex; align-items: center; gap: var(--sp-1); min-width: 0;
}
/* The group chip's vocabulary at row scale — same --surface-2 fill and hairline the
   chips above the list wear, so a group looks like the same object whether you are
   filtering by it or reading which one a wallet is in. Written AS TYPED: it was
   uppercased inside brackets, and the rail deliberately renders no other name in
   caps (see .rail-wallet-name). 18px tall, which clears the 12px type inside it
   without out-growing the 20px name line it sits on. */
.rail-wallet-group,
.sc-wallet-group {
  /* Bounded twice, because either bound alone fails: a percentage lets a long name
     take a fixed share of a narrow column, and a ch cap alone would let it out-run
     the column entirely. ~12 characters, and never more than a third of the line —
     the wallet's own name is what this row is for. Full name on the tag's title. */
  /* 15ch, which is exactly what GROUP_NAME_MAX allows a name to be, so a name
     created under that limit is never cut off. The NAME beside it is what gives way
     — it is the only thing on the line allowed to shrink — and the badge still
     ellipsises for the longer names that predate the cap. Shrinkable rather than
     rigid: on a narrow rail a 15-character badge would otherwise squeeze the title
     out entirely, and the wallet's own name is what the row is for. */
  flex: 0 1 auto; max-width: min(52%, 15ch);
  height: 18px; box-sizing: border-box;
  display: inline-flex; align-items: center; padding: 0 5px;
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius, 0px);
  font-family: inherit; font-size: var(--cap-size); font-weight: 400; line-height: 1;
  color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rail-wallet:hover { background: var(--surface); }
/* Accent bar, accent wash. (The pair of rules that actually resolve this live together
   further down, beside .sc-wallet-card's; see "SELECTION IS BLUE, AND ITS FILL IS AN
   ALPHA" for why the fill is an alpha rather than a grey.) */
.rail-wallet.selected {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  box-shadow: inset 2px 0 0 var(--accent);
}
/* (A row in hand is styled with the grip, at the end of this file.) */
/* ---- the list ARRIVES ----
   A rail that swaps its contents in one frame gives you no way to see that it
   changed; dealt one row at a time you can watch the new order land. The same drop
   the walkthrough's previews use, at the same duration — this is the product doing
   what the preview shows it doing.
   Opt-in per paint (see RAIL_DEAL): a balance poll repaints this list too, and a
   rail that shimmered every few seconds would be a rail nobody could read. */
@keyframes rail-deal { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: none; } }
/* Every list that can be REORDERED deals: the rail, the panes' wallet tables and
   the log. They are the same event — "this list is not the list you were looking at"
   — so they get the same 200ms drop and the same 18ms step, and a sort reads as the
   rows landing in their new order rather than as a frame that changed. */
.is-dealing > .rail-wallet,
.is-dealing > .sc-wallet-card,
.is-dealing > .dlog-item {
  animation: rail-deal .2s cubic-bezier(.25, .46, .45, .94) both;
  animation-delay: calc(var(--i, 0) * 18ms);
}
@media (prefers-reduced-motion: reduce) {
  .is-dealing > .rail-wallet,
  .is-dealing > .sc-wallet-card,
  .is-dealing > .dlog-item { animation: none; }
}
/* Hierarchy by COLOUR, not by dimming. Name, address AND last-tx all used to sit
   at opacity .5 until the row was selected — which put the two things that tell
   one wallet from another (the title and the address) behind a veil, on the one
   surface whose whole job is being scanned. Now the identity is legible at rest
   and the type ramp carries the hierarchy (name → --foreground, address →
   --muted, mono); only the SECONDARY metadata recedes: the group tag, which is
   context, not identity. (The last-tx line it used to share these rules with is
   gone — the row states its recency with a coloured edge and a stamp in the Actions
   column instead of a second line of prose.)
   The tag recedes by TEXT COLOUR, not by opacity: it carries a fill and a hairline
   now, and an opacity would have faded those along with the letters — leaving a
   translucent box over whatever the row's background happens to be. */
.rail-wallet-group,
.sc-wallet-group { transition: color .12s, border-color .12s; }
.rail-wallet:hover .rail-wallet-group,
.rail-wallet.selected .rail-wallet-group,
.sc-wallet-card:hover .sc-wallet-group,
.sc-wallet-card.selected .sc-wallet-group {
  color: var(--foreground); border-color: var(--border-mid);
}

/* Identity grid — the wrapper dissolves into .rail-wallet's grid:
     col 1 (flexible)  row 1 name + group tag   row 2 address + copy
     col 2/3 (fixed)   rows 1–2 balances, centred against them
   The balance block is a fixed width, so its two figures hold the same column
   on every row no matter how long a name or group tag runs. */
.rail-wallet-id { display: contents; }
/* Wallet name — size/weight come from the shared chip-anatomy rule. The
   The rail used to UPPERCASE the name while every drop slot rendered it as typed,
   so dragging a wallet out of the rail visibly changed its name. A wallet reads the
   same everywhere now; the rail earns its scannability from weight and layout. */
/* 14px on a 20px line at BODY weight — measured off the reference's wallet row
   (Wallets Dark 12:1416): every text node in that row is 20px tall, i.e. 14px
   type, and the name is not bolded. Ours inherited a heavier weight and a 1.3
   line, which is what made the column read as chunky beside it. The name is
   already distinguished from the address by colour and by the mono/sans split —
   it does not need weight as well. */
/* (Placement lives on .rail-wallet-nameline now — the name is a flex child of that
   row, not a grid item of the row itself. It is also the only thing on the line
   allowed to shrink, hence min-width: 0 for the ellipsis to engage.) */
.rail-wallet-name {
  min-width: 0;
  font-size: var(--body-size); font-weight: 400; line-height: 20px;
  color: var(--foreground);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Truncated 0x address (mono, muted — matches .wallet-chip-addr) + a subtle,
   borderless copy icon button beside it. app.js wraps the two in .rail-wallet-addr-row. */
.rail-wallet-addr-row {
  grid-column: 1; grid-row: 2;
  display: flex; align-items: center; gap: var(--sp-1); min-width: 0;
}
.rail-wallet-addr {
  font-family: var(--mono); color: var(--muted); letter-spacing: .01em;
  font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rail-wallet-copy {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--chip-x); height: var(--chip-x); padding: 0;
  background: none; border: 0; border-radius: var(--radius, 0px);
  color: var(--muted-2); cursor: pointer; line-height: 1; font-size: var(--cap-size);
  transition: color .12s, background .12s;
}
.rail-wallet-copy:hover { color: var(--foreground); background: var(--surface-2); }
.rail-wallet-copy:focus-visible { outline: 1px solid var(--ring); outline-offset: 1px; }
/* (A `.rail-wallet-copy svg` rule sized an icon that does not exist — app.js sets
   the button's textContent to the ⧉ glyph. Dead since it was written.) */

/* Balances: a right-aligned tabular PAIR on the address line — two equal cells,
   each a micro-caption pinned left and its figure pinned right. Fixed overall
   width, so captions form one column and figures another straight down the rail. */
/* Widths sized to the FIGURES, which are all these cells hold now. 84/64 was
   measured against an 11.5px figure beside a 9px caption; at the current 14px
   figure the token cell needed 133px to fit both, which a 300px rail cannot
   give without starving the name. Dropping the repeated caption is what buys
   the room back. */
.rail-wallet-bals {
  /* Spans BOTH rows and centres, so the figures sit on the row's middle line the
     way they do in the reference (values block y 24..44 of a 68px row, centre 34
     — the same centre as the name/address block). Pinned to row 2 they lined up
     with the ADDRESS instead, half a line below the name they belong to. */
  grid-column: 2; grid-row: 1 / span 2; align-self: center; justify-self: end;
  display: grid; grid-template-columns: 78px 58px;
  column-gap: var(--sp-2); margin-top: 0;
}
/* The one-time header. Mirrors the row grid EXACTLY — same widths, same gap,
   same inset — or the labels do not sit over their columns.
   It lives INSIDE the scrolling .rail-body, pinned to its top: it has to share
   the scroller's content box, because a sibling above .rail-body is laid out
   across the scrollbar the rows are inset by and every label lands one
   scrollbar-width right of its column. Sticky also keeps the labels on screen
   once you scroll past the first row — the whole point of naming a column. */
.rail-bal-head {
  position: sticky; top: 0; z-index: 1; background: var(--rail);
  display: grid; grid-template-columns: 78px 58px; column-gap: var(--sp-2);
  justify-content: end; padding: 6px var(--chip-pad-x) 4px;
  border-bottom: 1px solid var(--border);
}
.rail-bal-head span {
  font-size: var(--cap-size); font-weight: var(--eyebrow-weight); line-height: 1;
  text-transform: uppercase; letter-spacing: var(--eyebrow-track); color: var(--muted-2);
  text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rail-wallet-balcell {
  min-width: 0; display: flex; flex-direction: row;
  align-items: baseline; justify-content: flex-end;
}
/* caption — EXACT .act-cap ("ETH AMOUNT") treatment. Shrinks before the figure
   does, so an eight-letter ticker never eats into the number. */
.rail-cap {
  flex: 0 3 auto; min-width: 0; overflow: hidden; white-space: nowrap;
  font-family: inherit; font-size: var(--eyebrow-cap-size); font-weight: var(--eyebrow-weight); line-height: 1;
  text-transform: uppercase; letter-spacing: var(--eyebrow-track); color: var(--muted-2);
}
/* figure — the row's data point: numeric face, tabular, right-aligned. Size is
   the shared --chip-fig-size; the 600 WEIGHT is a kept difference (the rail is
   the surface you scan balances on, so its figures carry more ink). */
.rail-wallet-bal, .rail-wallet-eth {
  font-family: var(--num); font-weight: 400; line-height: 1.2;
  font-variant-numeric: tabular-nums;
  color: var(--foreground); min-width: 0; text-align: right;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rail-wallet-bal.dim, .rail-wallet-eth.dim { color: var(--muted-2); }

/* (ETH figure is styled by the shared .rail-wallet-bal, .rail-wallet-eth rule
   above; the WETH amber flag is handled by the .bal-weth rule further down.) */

/* (The .rail-lasttx block that stood here is gone with the second row it styled —
   the LAST TX line. What it carried now lives on the row itself: the buy/sell accent
   became the row's left edge and the relative time moved into the Actions column, both
   defined near the bottom of this file. The ETH amount is the stamp's tooltip.) */
.rail-foot {
  flex: 0 0 auto; padding: 8px var(--chip-pad-x); border-top: 1px solid var(--border);
  font-size: var(--body-size); color: var(--muted); min-height: 1px;
}
.rail-foot:empty { display: none; }

/* Loop-run groups: a collapsible header (name · count · ETH total) that folds a
   minted subsidiary family so it doesn't fill the flat rail. Rows inside are the
   same .rail-wallet grid, so click/drag/right-click behave identically. */
.rail-group { border-top: 1px solid var(--border); }
.rail-group-head {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%; padding: 8px 12px;
  background: none; border: 0; cursor: pointer; color: var(--muted);
  font-family: inherit; font-size: var(--body-size); text-align: left;
}
.rail-group-head:hover { color: var(--foreground); background: var(--surface); }
.rail-group-caret { flex: 0 0 auto; width: 10px; color: var(--muted-2); }
.rail-group-label {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-weight: 400; letter-spacing: .02em;
}
.rail-group-count {
  flex: 0 0 auto; color: var(--muted-2); font-variant-numeric: tabular-nums;
  border: 1px solid var(--border-strong); padding: 0 5px; font-size: var(--cap-size); line-height: 15px;
}
.rail-group-stat {
  flex: 0 0 auto; margin-left: auto; color: var(--muted);
  font-variant-numeric: tabular-nums; font-size: var(--cap-size);
}
.rail-group.open > .rail-group-head { color: var(--foreground); }
.rail-group .rail-wallet:last-child { border-bottom: none; }

/* Dust section: archived (empty) wallets, collapsed at the bottom of the rail */
.rail-dust { border-top: 1px solid var(--border); margin-top: 4px; }
.rail-dust-head { display: flex; align-items: center; }
.rail-dust-toggle {
  display: flex; align-items: center; gap: var(--sp-2); flex: 1; min-width: 0; padding: 8px 12px;
  background: none; border: 0; cursor: pointer; color: var(--muted);
  font-family: inherit; font-size: var(--body-size); text-align: left;
}
.rail-dust-toggle:hover { color: var(--foreground); }
.rail-dust-caret { width: 10px; color: var(--muted-2); }
.rail-dust-label { text-transform: uppercase; letter-spacing: var(--eyebrow-track); font-weight: var(--eyebrow-weight); }
.rail-dust-stat { margin-left: auto; color: var(--muted); font-variant-numeric: tabular-nums; }
.rail-dust-refresh {
  flex: 0 0 auto; background: none; border: 0; color: var(--muted-2); cursor: pointer;
  font-size: var(--body-size); padding: 8px 12px 8px 4px;
}
.rail-dust-refresh:hover { color: var(--foreground); }
.rail-dust-refresh:disabled { opacity: .5; cursor: default; }
.rail-dust-row {
  display: flex; align-items: center; gap: var(--sp-2); padding: 6px 12px;
  border-top: 1px solid var(--border); opacity: .72;
}
.rail-dust-row .wallet-chip { flex: 1; min-width: 0; }
.rail-dust-un {
  flex: 0 0 auto; background: none; border: 1px solid var(--border-strong);
  color: var(--muted); font-family: inherit; font-size: var(--cap-size); padding: 2px 8px; cursor: pointer;
}
.rail-dust-un:hover { color: var(--foreground); border-color: var(--muted); }

/* Collapsed: a thin vertical strip that expands on click (mirrors the feed).
   The grid column tracks a CSS var so collapsing narrows the whole column. */
.app:has(> .wallet-rail.collapsed) { grid-template-columns: 42px 1fr; }
.wallet-rail.collapsed .tabs,
.wallet-rail.collapsed .rail-head,
.wallet-rail.collapsed .rail-controls,
.wallet-rail.collapsed .rail-body,
.wallet-rail.collapsed .rail-foot { display: none; }
.rail-strip {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-3);
  width: 100%; min-height: 160px; padding: 12px 0;
  background: var(--rail); border: none; cursor: pointer; color: var(--muted);
}
.rail-strip:hover { background: var(--surface); color: var(--foreground); }
.rail-strip[hidden] { display: none; }
.rail-strip-label {
  writing-mode: vertical-rl; text-orientation: mixed;
  font-size: var(--cap-size); font-weight: 400; text-transform: uppercase; letter-spacing: var(--eyebrow-track);
}

/* ---- Wallet drop zone (reusable target fed by rail drags) ---- */
.wallet-dropzone { position: relative; }
/* THE DRAG RING IS DRAWN INSIDE THE BOX. It was `outline: 2px dashed` with a 2px
   offset — i.e. painted entirely OUTSIDE the element, in space that belongs to whatever
   sits next to it. Anything adjacent (a sibling row, a scroll container's edge, a
   panel's own border) covered it, which is why a zone being dragged over showed a ring
   on two sides and nothing on the other two.
   An inset box-shadow cannot leave the element's own box, so it can never be covered,
   and it needs no room reserved for it. Solid rather than dashed because a shadow has
   no dash — and because everything else in this app is a solid rule. */
/* OUTLINE, not an inset box-shadow. A shadow is painted in the element's own
   background layer, so the wallet cards inside — which carry opaque backgrounds —
   covered the ring on every edge they touched, and a full zone showed no ring at
   all. Outlines are painted last, above every descendant, so the cards cannot
   hide it. outline-offset: -2px keeps it INSIDE the box, which is the other half
   of the problem: an outside ring gets clipped by whatever sits against the zone. */
/* ACCENT, not the dimmed white it used to be. "You can let go here" is the same class
   of statement as "this one is selected" — it is the app acting on your gesture — and
   the accent is what every one of those reads from. A grey ring over a grey panel also
   had the least contrast of anything on screen at the one moment you need it most: mid
   drag, with the pointer moving. Solid stroke, wash fill, both from the one token. */
.wallet-dropzone.drop-hover {
  outline: 2px solid var(--accent); outline-offset: -2px;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
/* ---- THE chip anatomy — SINGLE-LINE density ----
   name (truncates) · balance (right, tabular) · ✕ (square hit area).
   Height is the COMPACT CONTROL RUNG, not a chip-only number: a chip is a
   control-sized object, so a chip and a .btn.small in the same row line up.
   One line has no room for the address, so the chip carries the full address in
   its `title` — the identity is a hover away, not absent. */
.dz-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
.dz-chip {
  display: inline-flex; align-items: center; gap: var(--chip-gap);
  height: var(--control-h-sm); min-width: 0; max-width: 100%;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0 var(--chip-pad-x);
}
/* The wallet NAME, at one size, in every variant. */
.dz-chip-name, .sc-wallet-name, .wallet-chip-title, .loop-startw-nm, .loop-sub-nm {
  font-size: var(--chip-name-size); font-weight: 400;
}
/* .rail-wallet-name is OUT of that list. The reference's wallet rows carry the
   name at body weight — it is already separated from the address by colour and by
   the sans/mono split, and 600 on every row is what made the column read heavy. */
.rail-wallet-name { font-size: var(--chip-name-size); }
/* The truncated 0x address, at one size, in every variant that shows one. It is
   always `truncAddr()` (0x1234…5678) — ONE helper in app.js, never re-cut per
   site — so the same wallet reads identically in the rail, a drop slot and the
   sub list. */
.wallet-chip-addr, .rail-wallet-addr, .loop-sub-addr, .sc-wallet-addr {
  font-size: var(--chip-addr-size);
}
/* A balance FIGURE, at one size, in every variant. */
.dz-chip-bal, .sc-wallet-bal, .sc-wallet-eth, .wallet-chip-bal,
.rail-wallet-bal, .rail-wallet-eth, .loop-startw-bal, .loop-sub-bal {
  font-size: var(--chip-fig-size);
}
.dz-chip-name {
  flex: 0 1 auto; min-width: 0; max-width: 168px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dz-chip-x {
  flex: 0 0 auto; width: var(--chip-x); height: var(--chip-x);
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; color: var(--muted-2); cursor: pointer;
  font-size: var(--cap-size); line-height: 1; padding: 0;
}
.dz-chip-x:hover { color: var(--foreground); background: var(--surface-2); }
.dz-empty { color: var(--muted-2); font-size: var(--body-size); }
.dz-pick { flex: 0 0 auto; }

/* Elements accepting a rail drop flash a subtle target state. */
/* Offset NEGATIVE so the ring sits inside the target's own box. At +2px it was
   drawn outside and whatever sat flush against the element clipped it, which is
   why a highlighted target often showed a rule on two sides and nothing on the
   other two. */
.drop-target-hover {
  outline: 2px dashed var(--accent); outline-offset: -2px;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

/* Narrow viewports: default the left rail collapsed to reclaim canvas width. */
@media (max-width: 1200px) {
  .app { grid-template-columns: 42px 1fr; }
  .app:has(> .wallet-rail:not(.collapsed)) { grid-template-columns: clamp(440px, var(--shell-left-w), 34vw) 1fr; }
}

/* ---- Tab panels (the nav chrome is gone — the hidden #tabs buttons only
   track the active section; activateTab() still shows/hides these panels) ---- */
.tab-panel { display: none; }
.tab-panel.active { display: block; }

.hint { color: var(--muted); font-size: var(--body-size); margin: 0 0 14px; }
.hint strong { color: var(--foreground); font-weight: 400; }
/* A hint sitting INSIDE a flex row (status text beside a button): the row's gap
   owns the spacing, so the block margin goes. */
.hint.inline { margin: 0; }

/* ---- "?" hover-help (ONE reusable component) ----
   A title row (.h1row) pairs a heading with a small squared "?" badge; the
   descriptive paragraph that used to sit under the title now lives in .help-tip,
   revealed on hover or keyboard focus. Pure CSS, no click. */
.h1row { display: flex; align-items: center; gap: var(--sp-2); }
.h1row h2, .h1row h3 { margin: 0; }
/* ---- Field groups ----
   Flattened from bordered cards to an eyebrow label over a hairline divider, so a
   group sitting inside a panel (Sequencer quick-trade, Settings sections) no
   longer reads as a box-in-a-box. The legend becomes the section eyebrow. */
.field-group {
  border: none; border-radius: 0;
  padding: 0; margin: 0 0 18px;
}
.field-group legend {
  display: block; width: 100%; padding: 0 0 10px; margin-bottom: 14px;
  border-bottom: 1px solid var(--border);
  font-size: var(--cap-size); text-transform: uppercase;
  letter-spacing: var(--eyebrow-track); color: var(--muted); font-weight: 400;
}
details.adv-group { padding: 0; }
details.adv-group > summary {
  cursor: pointer; font-size: var(--cap-size); text-transform: uppercase;
  letter-spacing: var(--eyebrow-track); color: var(--muted); font-weight: 400;
  user-select: none; padding-bottom: 10px; border-bottom: 1px solid var(--border);
}
details.adv-group[open] > summary { margin-bottom: 14px; }

/* ---- Preset callouts ---- */
.preset-field { margin-bottom: 10px; }
.preset-note {
  background: color-mix(in srgb, var(--blue) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--blue) 40%, transparent);
  color: var(--blue-fg);
  border-radius: var(--radius); padding: 9px 11px; margin-bottom: 8px; font-size: var(--body-size);
}
.preset-warn {
  background: color-mix(in srgb, var(--amber) 12%, transparent);
  border: 1px solid var(--amber); color: #fcd34d;
  border-radius: var(--radius); padding: 9px 11px; margin-bottom: 8px; font-size: var(--body-size); font-weight: 400;
}

.grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sp-4); }
@media (max-width: 960px) { .grid { grid-template-columns: 1fr; } }
/* results panels start hidden; give the form the full width until they show */
.grid:has(> .results-panel[hidden]) { grid-template-columns: 1fr; }
/* Full-width vertical stack: each panel spans the full width, stacked top to
   bottom (Sequencer — form on top, results below, never split-screen). */
.stack { display: flex; flex-direction: column; gap: var(--sp-4); }

/* ---- Panels / cards ---- */
.panel {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--pad-card);
}
}  /* end @layer components */
@layer overrides {
/* Inside the center canvas the tab bodies are de-boxed: the recessed floor IS the
   surface, so panels drop their card chrome and sit flat on it (no border/bg/pad),
   separated by the section eyebrows/hairlines rather than a wrapping box. Results
   panels stay chromeless too. */
.app-main .tab-panel .panel {
  background: transparent; border: none; padding: 0;
}
}  /* end @layer overrides */
@layer components {
.panel h2 { margin: 0 0 4px; font-size: var(--title-size); font-weight: var(--title-weight); line-height: var(--title-line); }
/* One-line canvas subtitle sitting directly under a center title. */
.canvas-sub { color: var(--muted-2); font-size: var(--body-size); font-weight: 400; margin: 5px 0 22px; }
/* h3 is a subheading, not a caption. It used to be uppercase at caption size,
   which made "uppercase" mean two different things — a label over a figure AND
   a section head. Uppercase now has exactly one job; a subheading gets its
   hierarchy from weight, like every other heading. */
.panel h3 { margin: 0; font-size: var(--body-size); color: var(--foreground); font-weight: 400; }

/* ---- Fields ---- */
.fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3); }
.field { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.field.wide { grid-column: 1 / -1; }
.field > span {
  font-size: var(--label-size); color: var(--foreground); font-weight: var(--label-weight);
  line-height: var(--body-line);
}
.field em { color: var(--muted-2); font-style: normal; text-transform: none; letter-spacing: normal; font-weight: 400; }
}  /* end @layer components */

@layer base {
/* Height comes from the --control-h ladder, so the box carries NO vertical
   padding — mixing the two is what made same-row controls sit a pixel or two
   out of line whenever one rule redeclared `padding` and another `height`. */
input, select, textarea {
  background: var(--control-bg); border: 1px solid var(--border-strong); color: var(--foreground);
  border-radius: var(--radius); padding: 0 10px; font-size: var(--body-size);
  width: 100%; min-width: 0; height: var(--control-h); font-family: inherit;
  transition: border-color .12s, box-shadow .12s;
}
/* A textarea is the one control sized by its `rows`, not by the ladder. */
textarea {
  height: auto; padding: 8px 10px; line-height: 1.45; resize: vertical;
}
/* Custom dropdown chrome for EVERY select: reset native appearance, draw our own
   chevron as a data-URI, one consistent look app-wide (squared, hover/focus border).
   The chevron gutter is a VARIABLE, not a hardcoded padding-right: compact selects
   used to restate `padding: 0 6px` and silently delete the gutter, dropping the
   chevron on top of the text. They now set --sel-gut / --sel-chev instead. */
select {
  --sel-gut: 30px; --sel-chev: 11px;
  cursor: pointer; appearance: none; -webkit-appearance: none;
  padding-right: var(--sel-gut);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23a1a1aa' stroke-width='1.6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right var(--sel-chev) center;
}
}  /* end @layer base */
@layer components {
select:hover { border-color: var(--border-strong); }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--ring); box-shadow: 0 0 0 1px var(--ring); }
/* Compact selects: a shorter left inset and a tighter chevron gutter. They only
   ever set padding-LEFT — the gutter stays owned by --sel-gut. */
.dash-sort, .dash-filter, .dash-group-sel, .dash-group-new,
.picker-group, .rail-controls select, #coin-select,
.loop-dest-sel, .loop-stop-dir, .loop-stop-asset {
  --sel-gut: 24px; --sel-chev: 8px; padding-left: 8px;
}
}  /* end @layer components */
@layer base {
input::placeholder { color: var(--muted-2); }
}  /* end @layer base */
@layer components {
input.mono, .mono { font-family: var(--mono); }
input[type="checkbox"], input[type="radio"] { height: auto; width: auto; padding: 0; accent-color: var(--foreground); }
/* A range input is a track, not a box: strip the field chrome the base rule gives
   every input (a 34px bordered surface drawn around a slider). */
input[type="range"] {
  height: 24px; padding: 0; background: transparent; border: none; box-shadow: none;
}
input[type="range"]:focus { border: none; box-shadow: none; }

/* ---- Numeric inputs ----
   A field holding "300" must not be as wide as one holding an 0x address. Every
   numeric control gets a rung off the --num-w ladder, right-aligned tabular
   digits, and the numeric face. Text/address inputs stay flexible (width: 100%).
   Matched three ways so it works without touching the JS builders:
     • input[type=number]      — the drawer's Swap/Advanced fields
     • input[inputmode=...]    — scNum() sets .inputMode = "decimal"
     • .input-num              — opt-in for type=text numeric fields in the markup */
input[type="number"],
input[inputmode="decimal"], input[inputmode="numeric"],
.input-num {
  width: var(--num-w-md); flex: 0 0 auto;
  text-align: right; font-family: var(--num); font-variant-numeric: tabular-nums;
}
/* Native steppers overlap right-aligned digits on hover; the field is still
   keyboard-steppable (↑/↓), it just stops drawing the mouse spinner. */
input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* the two off-ladder rungs, plus the amount fields that hold real money */
.input-num.wide, input.sq-amount, input#qt-amount { width: var(--num-w-lg); }
.input-num.narrow { width: var(--num-w-sm); }

/* ---- Buttons ---- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1);
  background: var(--control-bg); color: var(--foreground);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0 12px; height: var(--control-h); font-size: var(--body-size); font-weight: 400; cursor: pointer;
  white-space: nowrap; transition: background .12s, border-color .12s, color .12s;
}
.btn:hover { background: var(--control-bg-hover); border-color: var(--border-strong); }
/* Disabled is a COLOUR, not a dimming. The one app-wide disabled rule lives late in
   this file (unlayered, search "EVERY disabled button") so it outranks every
   per-surface variant; nothing here may re-introduce an opacity fade. */
/* .small and .tiny are the SAME rung — the compact one. They differ in inset,
   type size and ink, which is what actually separates a secondary action from a
   tertiary one; a 2px height difference only ever read as a misalignment. */
.btn.small { height: var(--control-h-sm); padding: 0 10px; font-size: var(--body-size); }
.btn.tiny { height: var(--control-h-sm); padding: 0 8px; font-size: var(--body-size); color: var(--muted); }
.btn.primary { background: var(--primary); border-color: var(--primary); color: var(--primary-fg); font-weight: 400; }
.btn.primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
/* Secondary action beside a .primary (Cancel, Recover, Generate receivers…).
   It was used all over the app with no rule at all, so it rendered identical to
   the default button and the two read as equal weight. */
.btn.ghost { background: transparent; color: var(--muted); }
.btn.ghost:hover { background: var(--surface); color: var(--foreground); border-color: var(--border-strong); }
.btn.danger { background: transparent; border-color: var(--red); color: var(--red); font-weight: 400; }
.btn.danger:hover { background: var(--red); border-color: var(--red); color: #fff; }
.btn.copy { color: var(--muted); }
.copy-v { display: inline-flex; align-items: center; gap: var(--sp-1); justify-content: flex-end; }

/* ---- Wallets (legacy rotation rows) ---- */
.wallet-block { margin-top: 16px; }
.wallet-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.wallet-list { display: flex; flex-direction: column; gap: var(--sp-3); }
.wallet-row { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; }
.wallet-row-head { display: flex; gap: var(--sp-2); align-items: center; margin-bottom: 8px; }
.wallet-row-head .row-label { flex: 1; font-size: var(--cap-size); font-weight: 400; color: var(--muted); text-transform: uppercase; letter-spacing: var(--eyebrow-track); }
.checkbox-field { flex-direction: column; }
.checkbox-field input[type="checkbox"] { width: auto; align-self: flex-start; }
.wallet-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3); }

/* ---- Run actions ---- */
.run-actions {
  display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap;
  margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--border);
}
.run-actions .btn.danger { margin-left: auto; }
/* An action row that already sits under a divider (a fieldset's own rule) drops
   the rule + the extra lead-in. Replaces per-element inline styles in the markup. */
.run-actions.flush { margin-top: 10px; padding-top: 0; border-top: none; }

/* ---- Status / error ---- */
.status {
  display: flex; align-items: center; gap: var(--sp-3);
  background: color-mix(in srgb, var(--blue) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--blue) 35%, transparent);
  color: var(--blue-fg);
  border-radius: var(--radius); padding: 10px 12px; margin-bottom: 12px; font-size: var(--body-size);
}
/* The hidden attribute must win over `display:flex` above — otherwise
   clearBusy() / showStatusDone()'s auto-hide (which set `.hidden = true`)
   never actually hide the banner, so a status note sits there permanently. */
.status[hidden], .error-box[hidden] { display: none !important; }
.status::before {
  content: ""; width: 13px; height: 13px; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--blue-fg) 35%, transparent); border-top-color: var(--blue-fg);
  animation: spin .8s linear infinite; flex: 0 0 auto;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Completed state: green with a static check instead of the spinner. */
.status.done {
  background: color-mix(in srgb, var(--green) 10%, transparent);
  border-color: color-mix(in srgb, var(--green) 35%, transparent);
  color: #86efac;
}
.status.done::before {
  content: "✓"; width: auto; height: auto; border: none; border-radius: 0;
  animation: none; color: var(--green); font-weight: 400; font-size: var(--body-size);
}

.error-box {
  background: color-mix(in srgb, var(--red) 12%, transparent);
  border: 1px solid var(--red); color: var(--red);
  border-radius: var(--radius); padding: 12px; margin-bottom: 12px;
  white-space: pre-wrap; font-family: var(--mono); font-size: var(--body-size);
}

/* The global status/error strip lives inside .sticky-nav so it stays visible
   from any scroll position; cap a long error so it can't swallow the page. */
.sticky-nav .status, .sticky-nav .error-box { margin-bottom: 10px; }
.sticky-nav .error-box { max-height: 30vh; overflow-y: auto; }

/* Button-local busy: the clicked run button carries its own inline spinner
   (+ a label swap done in JS) for the duration of ITS operation. */
.btn.loading::before {
  content: ""; width: 11px; height: 11px; border-radius: 50%;
  border: 2px solid color-mix(in srgb, currentColor 35%, transparent); border-top-color: currentColor;
  animation: spin .8s linear infinite; flex: 0 0 auto;
}
.btn.loading:disabled { opacity: .8; }
/* An ICON button spins its own glyph instead. It keeps its markup while busy (see
   btnLoading), so the generic ::before spinner would be a SECOND spinner beside the
   arrow it already has — in a 34px square with no room for either.
   After .btn.loading::before deliberately: same specificity, so order decides. */
.hdr-iconbtn.loading::before { display: none; }
.hdr-iconbtn.loading svg { animation: spin .8s linear infinite; transform-origin: 50% 50%; }
@media (prefers-reduced-motion: reduce) {
  .hdr-iconbtn.loading svg { animation: none; opacity: .5; }
}

/* ---- Result ---- */
.result-header { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin-bottom: 12px; }
.mode-badge { padding: 3px 10px; border-radius: var(--radius); font-size: var(--cap-size); font-weight: 400; text-transform: uppercase; letter-spacing: var(--eyebrow-track); }
.mode-badge.dry { background: color-mix(in srgb, var(--blue) 18%, transparent); color: #93c5fd; }
.mode-badge.exec { background: color-mix(in srgb, var(--red) 20%, transparent); color: var(--red); }
.hchip { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 3px 8px; font-size: var(--body-size); color: var(--muted); }

.addr-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: 14px; }
.addr-chip { display: inline-flex; flex-direction: column; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 5px 10px; }
.addr-k { font-size: var(--cap-size); font-weight: 400; color: var(--muted); text-transform: uppercase; letter-spacing: var(--eyebrow-track); }
.addr-v { font-size: var(--body-size); font-family: var(--mono); }

.empty { color: var(--muted); padding: 18px; text-align: center; font-style: italic; }

.step-cards { display: flex; flex-direction: column; gap: var(--sp-3); }
.step-card {
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--border); border-radius: var(--radius); padding: 11px 13px;
}
.step-card.action-approve { border-left-color: var(--approve); }
.step-card.action-sell { border-left-color: var(--sell); }
.step-card.action-buy { border-left-color: var(--buy); }
.step-head { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-bottom: 8px; }
.step-idx { color: var(--muted); font-family: var(--mono); font-size: var(--body-size); }
.action-tag { font-size: var(--eyebrow-size); font-weight: var(--eyebrow-weight); letter-spacing: var(--eyebrow-track); padding: 2px 7px; border-radius: var(--radius); background: var(--surface-2); }
.action-approve .action-tag { background: color-mix(in srgb, var(--violet) 22%, transparent); color: var(--approve); }
/* Mixed from --sell, not from --orange: the wash behind the label has to be the label's
   own colour. It only said "orange" because sell was orange, so once sell went red this
   tag would have been red text on an orange field. Its two neighbours here already mix
   from their own token. */
.action-sell .action-tag { background: color-mix(in srgb, var(--sell) 22%, transparent); color: var(--sell); }
.action-buy .action-tag { background: color-mix(in srgb, var(--green) 22%, transparent); color: var(--buy); }
.step-wallet { font-weight: 400; font-size: var(--body-size); }
.step-addr { color: var(--muted); font-size: var(--body-size); margin-left: auto; font-family: var(--mono); }
.step-body { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: var(--sp-1) 14px; }
.kv { display: flex; justify-content: space-between; gap: var(--sp-2); padding: 3px 0; border-bottom: 1px dotted var(--border); min-width: 0; }
.kv .k { color: var(--muted); font-size: var(--body-size); }
.kv .v { font-size: var(--body-size); text-align: right; word-break: break-all; font-family: var(--mono); }

.raw { margin-top: 14px; }
.raw summary, details > summary { cursor: pointer; color: var(--muted); font-size: var(--body-size); }
.raw-json {
  background: var(--background); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 10px; overflow-x: auto; font-family: var(--mono); font-size: var(--cap-size); margin-top: 8px;
  max-height: 400px; overflow-y: auto;
}

/* ---- Vault panel ---- */
.vault-bar { display: flex; gap: var(--sp-2); margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
.vault-bar-spacer { flex: 1 1 auto; }
.vault-bar .btn.active { background: var(--surface-2); border-color: var(--border-strong); }
.btn-ico { font-size: var(--body-size); line-height: 1; opacity: .85; }
.vault-form-host:empty { display: none; }
.vault-form-host { margin-bottom: 12px; }
.vault-form {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 14px; margin-top: 12px; display: flex; flex-direction: column; gap: var(--sp-3); max-width: 480px;
}
.vault-form.inline { max-width: none; }
.vault-form h3 { margin: 0 0 4px; font-size: var(--cap-size); font-weight: 400; color: var(--muted); text-transform: uppercase; letter-spacing: var(--eyebrow-track); }
.vault-form .field { flex-direction: column; }
.vault-form .btn { align-self: flex-start; }

/* Vault rows sit in a single hairline-divided list (no per-row card): the chip
   (identity + right-aligned balance) takes the flexible lead, actions trail. */
.vault-list { display: flex; flex-direction: column; gap: 0; border-top: 1px solid var(--border); }
.vault-wallet {
  display: flex; align-items: center; gap: var(--sp-3);
  background: transparent; border: none; border-bottom: 1px solid var(--border); border-radius: 0;
  padding: 9px 4px; flex-wrap: wrap;
}
.vault-wallet:hover { background: var(--surface); }
.vw-title { display: flex; gap: var(--sp-2); align-items: center; flex: 0 1 320px; min-width: 200px; }
.vw-title-input { font-weight: 400; }
.btn.tiny.danger { color: var(--red); border-color: var(--red); background: transparent; }
.btn.tiny.danger:hover { background: var(--red); color: #fff; border-color: var(--red); }

/* ---- Wallet balances ---- */
.bal-line { font-family: var(--mono); font-size: var(--cap-size); color: var(--muted); line-height: 1.35; }
.bal-hint { margin-top: 4px; }
.bal-hint[hidden] { display: none; }
.bal-empty { color: var(--muted-2); font-style: italic; }
.bal-err { color: var(--red); }

/* ---- Legs (disperse/collect) ---- */
.leg-card { border-left: 3px solid var(--blue); }
.leg-card.leg-error { border-left-color: var(--red); }
.leg-error .action-tag { background: color-mix(in srgb, var(--red) 22%, transparent); color: var(--red); }

/* ---- Sequencer ---- */
.seq-note {
  background: color-mix(in srgb, var(--amber) 12%, transparent);
  border: 1px solid var(--amber); color: #fcd34d;
  border-radius: var(--radius); padding: 9px 11px; margin-bottom: 12px; font-size: var(--body-size); font-weight: 400;
}
.seq-sets { display: flex; flex-direction: column; gap: var(--sp-3); }
.seq-set { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; }
/* Rows inside a set are the inner level: drop their card border and separate them
   with hairline dividers + spacing instead of nesting a bordered card in a card. */
.seq-rows.wallet-list { gap: 0; }
.seq-set .seq-row.wallet-row {
  background: transparent; border: none; border-bottom: 1px solid var(--border);
  border-radius: 0; padding: 10px 0;
}
.seq-set .seq-rows.wallet-list > .seq-row:last-child { border-bottom: none; padding-bottom: 2px; }
.seq-set-head { display: flex; gap: var(--sp-3); align-items: flex-end; flex-wrap: wrap; margin-bottom: 12px; }
.seq-set-label { font-size: var(--cap-size); font-weight: 400; text-transform: uppercase; letter-spacing: var(--eyebrow-track); color: var(--muted); }
.seq-set-actions { display: flex; gap: var(--sp-1); margin-left: auto; }
.seq-rows-head { margin-top: 4px; margin-bottom: 8px; }
.seq-rows { gap: var(--sp-2); }
.seq-row { background: var(--card); }
.seq-row-remove-wrap { justify-content: flex-end; }
.seq-row-remove-wrap .btn { align-self: flex-start; margin-top: 2px; }
.trade-fields {
  grid-template-columns: minmax(0,.6fr) minmax(0,2fr) minmax(0,.9fr) minmax(0,1.1fr) minmax(0,.9fr) auto;
  align-items: end;
  column-gap: var(--sp-3);
}
@media (max-width: 720px) { .trade-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.seq-trade-row .sq-side { font-weight: 400; }

/* Single-line wallet chip standing in for a FIELD in a sequencer trade row. It
   is the one variant on the DEFAULT rung rather than the compact one, because
   here the chip is a column of the form (Delay · Wallet · Side · Amount · Unit)
   and must share the 34px edge of the inputs either side of it. Still a ladder
   rung and still the shared inset — not a third height. */
.seq-trade-row .row-wallet-chip {
  flex-wrap: nowrap;
  height: var(--control-h);
  padding: 0 var(--chip-pad-x);
  background: var(--surface);
  border: 1px solid var(--border);
}
.seq-trade-row .row-wallet-chip .wallet-chip-id { flex: 0 1 auto; }
.seq-trade-row .row-wallet-chip .wallet-chip-addr { display: none; }
.seq-trade-row .row-wallet-chip .wc-copy { display: none; }
.seq-trade-row .row-wallet-chip .wallet-chip-bal {
  flex: 1 1 auto; flex-basis: auto; margin-left: auto; padding-left: 10px;
  text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Wallets are added by dragging from the rail — an empty set body reads as a
   drop zone; the whole set (or the empty sets area) highlights on drag-over. */
.seq-rows:empty {
  min-height: 46px; border: 1px dashed var(--border-strong); border-radius: var(--radius);
  display: flex; align-items: center; justify-content: center;
}
.seq-rows:empty::before {
  content: "Drag wallets from the rail to add rows"; color: var(--muted-2); font-size: var(--body-size);
}
.seq-rows-hint { margin: 0 0 0 auto; font-size: var(--cap-size); color: var(--muted-2); }
.seq-set.drop-target-hover { outline: 1px dashed var(--accent); outline-offset: -1px; }
.seq-set.drop-target-hover .seq-rows { border-color: var(--accent); }
#seq-sets.drop-target-hover { outline: 1px dashed var(--accent); outline-offset: 3px; border-radius: var(--radius); }
.seq-head-actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.seq-head-actions .btn { height: var(--control-h-sm); }
.seq-progress {
  background: color-mix(in srgb, var(--blue) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--blue) 35%, transparent);
  color: var(--blue-fg);
  border-radius: var(--radius); padding: 9px 12px; margin-bottom: 12px; font-size: var(--body-size); font-weight: 400;
}
.seq-set-result { border: 1px solid var(--border); border-radius: var(--radius); padding: 13px; margin-bottom: 12px; }
.seq-set-result-head { display: flex; gap: var(--sp-2); align-items: center; margin-bottom: 10px; }
.seq-set-tag { font-size: var(--eyebrow-size); font-weight: var(--eyebrow-weight); letter-spacing: var(--eyebrow-track); padding: 3px 8px; border-radius: var(--radius); background: var(--surface-2); color: var(--foreground); }
.seq-step-tag { font-size: var(--cap-size); font-weight: 400; padding: 3px 8px; border-radius: var(--radius); background: transparent; border: 1px solid var(--border); color: var(--muted); }

/* ---- Data table core (the Flow list table reuses .dash-table-wrap/.dash-table;
   the old Gotham dashboard page that also used it is gone) ---- */
.dash-table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); }
.dash-table { width: 100%; border-collapse: collapse; font-size: var(--body-size); }
.dash-table th, .dash-table td { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
.dash-table tbody tr:last-child td { border-bottom: none; }
.dash-table th { color: var(--muted); text-transform: uppercase; letter-spacing: var(--eyebrow-track); font-size: var(--cap-size); font-weight: 400; background: var(--surface); }
.dash-table tbody tr:hover { background: var(--surface); }
.dash-table td { vertical-align: middle; }

/* ---- Header context bits (de-boxed: borderless text, thin dividers) ---- */
.ctx-value {
  color: var(--foreground); font-weight: 400; font-size: var(--body-size);
  max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Vault group = STATE (borderless pill) + ACTION (bordered button). They are two
   different things, so they never run together as one "Locked Unlock" string:
   the pill stays flat text and the button carries control chrome.
   --ctx-ctl-h / --ctx-ctl-pad are declared on .shell-top, and the vault pair now
   lives in the settings drawer instead — outside that scope the var() would be
   invalid at computed-value time and both would collapse to auto height / zero
   padding, so each carries the header's own value as its fallback. */
.ctx-pill {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  height: var(--ctx-ctl-h, var(--control-h-sm)); padding: 0;
  font-size: var(--body-size); font-weight: 400; border: none; background: transparent;
}
.ctx-pill .ctx-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted-2); flex: 0 0 auto; }
.ctx-pill.locked .ctx-dot { background: var(--red); }
.ctx-pill.unlocked .ctx-dot { background: var(--green); }
#ctx-vault-btn {
  height: var(--ctx-ctl-h, var(--control-h-sm)); padding: 0 var(--ctx-ctl-pad, 10px);
  border: 1px solid var(--border); background: transparent;
  color: var(--muted); font-size: var(--body-size); font-weight: 400;
}
#ctx-vault-btn:hover {
  background: var(--surface-2); border-color: var(--border-strong);
  color: var(--foreground); text-decoration: none;
}
#coin-select {
  height: var(--ctx-ctl-h);
  font-size: var(--body-size); font-weight: 400; width: auto; max-width: 180px;
}
/* Compact "+" add-a-coin button next to the coin select (hosted only) — square,
   same height as every other header control. */
.ctx-addbtn {
  width: var(--ctx-ctl-h); height: var(--ctx-ctl-h); padding: 0; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--title-size); font-weight: var(--title-weight); line-height: 1; color: var(--muted);
}
.ctx-addbtn:hover { color: var(--foreground); }
.ctx-addbtn[hidden] { display: none; }
/* Totals: two independent, LABELLED figures with a hairline between them and the
   same --ctx-gap either side as a .ctx-vr — never "◆ — — USDC" run together.
   The ETH diamond glyph is gone: at 12.5px its mask rendered as a smudge, so the
   unit is now a text eyebrow (ETH · <TICKER>) sitting beside its figure, exactly
   like the .act-cap / .rail-cap captions used everywhere else. Figures are
   tabular in a fixed-height box, so "—" occupies the same room as a value. */
.ctx-total {
  display: inline-flex; align-items: baseline; gap: var(--sp-1); height: 22px;
  white-space: nowrap;
}
.ctx-total-cap {
  font-family: inherit; font-size: var(--eyebrow-cap-size); font-weight: var(--eyebrow-weight);
  text-transform: uppercase; letter-spacing: var(--eyebrow-track); color: var(--muted-2);
  max-width: 8ch; overflow: hidden; text-overflow: ellipsis;
}
.ctx-total-fig {
  font-family: var(--num); font-variant-numeric: tabular-nums;
  font-weight: 400; font-size: var(--body-size); color: var(--foreground);
}
/* ONE separator idiom in this bar: a .ctx-vr marks a GROUP boundary, and inside
   a group the flex gap separates. No per-item padding: combining both doubled
   the intended space between ETH and token totals. */

/* ---- Network indicator: chain-colored dot + chain NAME (no "Network" word) ---- */
.ctx-net { gap: var(--sp-2); }
.ctx-net-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto;
  background: var(--muted-2);
}

/* ---- Account / credit pill (hosted only) ---- */
.ctx-acct { position: relative; display: inline-flex; align-items: center; flex: 0 0 auto; }
.ctx-acct[hidden] { display: none; }
.ctx-acct-pill {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  height: var(--ctx-ctl-h); padding: 0 var(--ctx-ctl-pad); cursor: pointer;
  background: transparent; color: var(--foreground);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-size: var(--body-size); font-weight: 400; font-family: var(--num);
}
.ctx-acct-pill:hover { background: var(--surface-2); border-color: var(--border-strong); }
.ctx-acct-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted-2); flex: 0 0 auto; }
.ctx-acct-dot.on { background: var(--green); }
.ctx-acct-credit { color: var(--foreground); }

.ctx-acct-pop {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 70;
  width: 300px; padding: var(--pad-card);
  background: var(--float); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
}
.ctx-acct-pop[hidden] { display: none; }
.ctx-acct-ph {
  font-size: var(--eyebrow-size); text-transform: uppercase; letter-spacing: var(--eyebrow-track);
  color: var(--muted-2); font-weight: var(--eyebrow-weight); margin-bottom: 10px;
}
.ctx-acct-row {
  display: flex; justify-content: space-between; gap: var(--sp-3); align-items: baseline;
  padding: 6px 0; border-bottom: 1px solid var(--border); font-size: var(--body-size);
}
.ctx-acct-row span { color: var(--muted); }
.ctx-acct-row b {
  color: var(--foreground); font-weight: 400; font-family: var(--num);
  word-break: break-all; text-align: right;
}
.ctx-acct-num { font-variant-numeric: tabular-nums; }
.ctx-acct-btns { display: flex; gap: var(--sp-2); margin-top: 12px; }
.ctx-acct-btns .btn { flex: 1 1 0; }
.ctx-acct-msg { font-size: var(--body-size); color: var(--green); margin-top: 8px; min-height: 15px; }
.ctx-acct-msg.err { color: var(--red); }

/* ---- Profile popover (header, left of ⚙) ----
   The button is a .hdr-iconbtn like its neighbours — only the glyph differs, an
   inline SVG stroked in currentColor so it inherits the button's muted→foreground
   hover exactly as the ↻ / ⚙ characters do. The panel is the account popover's
   anatomy (same float, border, shadow, width) carrying the activity loop's tile
   anatomy inside: caption → figure row → sparkline. */
.ctx-prof { position: relative; display: inline-flex; align-items: center; flex: 0 0 auto; }
.rail-head .hdr-iconbtn svg, .shell-top .hdr-iconbtn svg {
  width: 15px; height: 15px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round;
}

.ctx-prof-pop {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 70;
  width: 300px; padding: var(--pad-card);
  background: var(--float); border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-pop);
}
.ctx-prof-pop[hidden] { display: none; }

/* ---- fee deposit + fee wallet, INSIDE the profile dropdown -----------------
   Deliberately placed next to .ctx-prof-pop, the element it modifies. These rules
   first lived beside the rail-head rules and were destroyed when that dead block
   was deleted — a rule that lives nowhere near what it styles is a rule that gets
   swept up by someone else's cleanup.

   The pill and its status dot stay in the DOM but undrawn: app.js resolves both at
   init and bails out of the whole hosted-account wiring if either is missing. The
   nested popover is flattened to an inline block so there is no popover-inside-a-
   popover, and no dot-plus-dash stub in the rail before you have connected. */
#ctx-prof-pop:not([hidden]) { display: flex; flex-direction: column; }
#ctx-prof-pop > .ctx-acct { order: 1; }   /* below everything profile.js appends */
#ctx-prof-pop .ctx-acct { display: block; width: 100%; }
#ctx-prof-pop .ctx-acct-pill,
#ctx-prof-pop .ctx-acct-dot { display: none; }
#ctx-prof-pop #ctx-acct-pop {
  position: static; width: auto; min-width: 0; margin: 0; inset: auto;
  background: none; border: 0; box-shadow: none; padding: 0;
}
#ctx-prof-pop #ctx-acct-pop .ctx-acct-ph { margin-top: var(--sp-3); }
.ctx-prof-ph {
  font-size: var(--eyebrow-size); text-transform: uppercase; letter-spacing: var(--eyebrow-track);
  color: var(--muted-2); font-weight: var(--eyebrow-weight); margin-bottom: 10px;
}
/* Same 54px trend box as .act-spark — this IS that chart, in a popover. */
.ctx-prof-spark { height: 54px; margin-top: 14px; }
.ctx-prof-spark[hidden] { display: none; }
.ctx-prof-spark svg { display: block; width: 100%; height: 100%; overflow: visible; }
.ctx-prof-spark polyline { fill: none; stroke-width: 1.5;
  stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
.ctx-prof-spark polygon { stroke: none; opacity: .85; }
/* The chart's timeframe, on the caption's own type ramp. */
.ctx-prof-axis { display: flex; justify-content: space-between; margin-top: 6px; }
.ctx-prof-axis[hidden] { display: none; }
.ctx-prof-empty { font-size: var(--body-size); color: var(--muted-2); margin-top: 12px; }
.ctx-prof-empty[hidden] { display: none; }

.ctx-prof-row {
  display: flex; justify-content: space-between; gap: var(--sp-3); align-items: baseline;
  padding: 6px 0; border-bottom: 1px solid var(--border); font-size: var(--body-size);
}
.ctx-prof-row:first-of-type { margin-top: 14px; }
.ctx-prof-row:last-child { border-bottom: 0; }
.ctx-prof-row span { color: var(--muted); }
.ctx-prof-row b {
  color: var(--foreground); font-weight: 400; font-family: var(--num);
  text-align: right;
}
.ctx-prof-num { font-variant-numeric: tabular-nums; }

/* ---- Config drawer (right slide-over) ---- */
#drawer-backdrop {
  position: fixed; inset: 0; z-index: 60;
  background: color-mix(in srgb, var(--background) 70%, transparent);
  opacity: 0; transition: opacity .18s ease;
}
#drawer-backdrop.open { opacity: 1; }
#drawer-backdrop[hidden] { display: none; }
.drawer {
  position: fixed; top: 0; right: 0; z-index: 70;
  width: 420px; max-width: 92vw; height: 100vh;
  background: var(--float); border-left: 1px solid var(--border-strong); border-radius: 0;
  /* ONE shadow declaration. This rule read --shadow-modal and then overwrote it
     three lines later with a literal, so the token was dead here. The literal
     was the CORRECT value — a right-flush drawer must throw its shadow leftward
     — so it is now the named --shadow-drawer tier instead of an inline surprise. */
  box-shadow: var(--shadow-drawer);
  display: flex; flex-direction: column;
  transform: translateX(100%); transition: transform .2s ease;
}
.drawer.open { transform: translateX(0); }
.drawer[hidden] { display: none; }
.drawer-header {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: 14px 16px; border-bottom: 1px solid var(--border); flex: 0 0 auto;
}
.drawer-header h2 { margin: 0; font-size: var(--title-size); font-weight: var(--title-weight); line-height: var(--title-line); }
.drawer-header-actions { display: flex; align-items: center; gap: var(--sp-2); }
/* A header action cluster is a TOOLBAR, so it takes the compact rung — including
   any plain .btn dropped into it, which would otherwise arrive at 34px. */
.drawer-header-actions .btn { height: var(--control-h-sm); }
.drawer-body { padding: 16px; overflow-y: auto; flex: 1 1 auto; }

/* ---- Reusable wallet chip: THE wallet identity component ----
   stacked title over mono address · balance right-aligned · copy on the far
   right. Same block in the vault list, dashboard table, pickers, pick-buttons,
   row chips, unwrap and flow. It is the TWO-LINE density: title + address, the
   whole of a wallet's identity and nothing else. */
.wallet-chip { display: inline-flex; align-items: center; gap: var(--chip-gap); min-width: 0; max-width: 100%; flex-wrap: nowrap; }
.wallet-chip-id { display: flex; flex-direction: column; gap: var(--sp-0); min-width: 0; }
.wallet-chip-title { line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wallet-chip-addr { color: var(--muted); line-height: 1.25; font-family: var(--mono); }
/* Same balance treatment as every other chip: right-aligned tabular figures. */
.wallet-chip-bal {
  margin-left: auto; padding-left: 12px; flex: 0 0 auto;
  color: var(--muted); white-space: nowrap;
  font-family: var(--num); font-variant-numeric: tabular-nums;
}

/* vault row + unwrap row now lead with a wallet-chip; keep the chip taking the
   flexible lead so the remaining controls sit to the right. */
.vault-wallet > .wallet-chip { flex: 1; min-width: 200px; }
}  /* end @layer components */

@layer base {
/* ---- Scrollbars (subtle) ----
   The thumb is inset by a 2px border it does NOT paint under: `background-clip:
   padding-box` keeps its fill inside the padding box, so a transparent border lets
   whatever is behind the scrollbar show through and the thumb reads as a 6px bar with
   air around it.
   That border used to be `2px solid var(--background)`, which is the same trick with the
   ground hardcoded — and it only works on one ground. Anywhere else it painted a #111
   frame around the thumb: over the confirm sheet's wallet lists (--float) that came out
   as a dark rectangle outlining the scrollbar, which is what it looked like — an outline
   nobody drew on purpose. Transparent costs nothing and is right on every surface. */
::-webkit-scrollbar { width: 10px; height: 10px; }
/* background-COLOR, not the `background` shorthand: the shorthand includes
   background-clip and would reset it, so the two would depend on declaration order to
   agree. The longhand cannot. */
::-webkit-scrollbar-thumb {
  /* --border-mid, not --border: at #262626 the thumb was 21 points off black and read as
     an absence rather than a control — you could not see where in a list you were. */
  background-color: var(--border-mid);
  border: 2px solid transparent; background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background-color: var(--border-strong); }
::-webkit-scrollbar-track { background: transparent; }
/* A corner sits where the two bars meet and defaults to the UA's own light grey. */
::-webkit-scrollbar-corner { background: transparent; }

/* ---- THE TABLES' OWN BAR, drawn OVER them ----
   A classic scrollbar sits outside the padding box, so every list that scrolls was ten
   pixels narrower than the box around it: row fills, row rules and the hover wash all
   stopped short of the border with a strip beside them that nothing inside the list
   could paint. These lists hide the native bar and draw one over the rows instead (see
   scOverlayScroll) — the rows get the full width, and what tells you where you are in
   them floats on top of the right-hand edge the way an overlay scrollbar does. */
.sc-oscroll { scrollbar-width: none; }
.sc-oscroll::-webkit-scrollbar { width: 0; height: 0; }
/* Zero height and sticky: it takes no space in the list and rides the scroll, so the
   thumb's offset is measured against the VISIBLE box without a positioned wrapper.
   Its own pointer events are off — only the thumb takes them — or the rail would
   swallow clicks along the whole right edge of the list. */
.sc-oscroll-rail {
  position: sticky; top: var(--sc-bar-top, 0px); left: 0; z-index: 6;
  height: 0; overflow: visible; pointer-events: none;
}
.sc-oscroll-rail[hidden] { display: none; }
/* The rail is a flex item in most of these lists; it must cost nothing on either axis.
   (Its negative bottom margin, which takes back the column's gap, is set in JS from
   the parent's own computed value — see scOverlayScroll.) */
.sc-oscroll-rail { flex: 0 0 0; align-self: stretch; min-height: 0; }
/* SQUARE, like every other edge in this app, and one drawn shape on both platforms —
   the point of replacing the native bar was that macOS and Windows drew two different
   controls. It sits in the strip the rows keep clear, so its width never changes: a
   thumb that fattens on hover would be a control that moves while you reach for it. */
.sc-oscroll-thumb {
  /* 6px DRAWN in the 8px lane the rows keep clear: the strip is sized to be reached
     with a pointer, the mark inside it to be read. Centred in that lane, so the gap on
     either side of it is even. */
  position: absolute; top: 0; right: calc((var(--sc-bar-w) - var(--sc-bar-thumb-w)) / 2);
  width: var(--sc-bar-thumb-w); border-radius: 0;
  background-color: var(--sc-bar-ink); pointer-events: auto; cursor: default;
  transition: background-color .12s ease;
}
.sc-oscroll:hover > .sc-oscroll-rail > .sc-oscroll-thumb,
.sc-oscroll-thumb.is-dragging { background-color: var(--border-strong); }
@media (prefers-reduced-motion: reduce) {
  .sc-oscroll-thumb { transition: none; }
}

/* ---- Sticky table headers, continued across the scrollbar gutter ----
   A classic scrollbar sits OUTSIDE the padding box, so nothing a descendant
   paints can reach it: the sticky header band stops one scrollbar-width short
   of the box edge, and the darker body shows through beside it as a notch —
   visible on both tables that scroll internally and put a --surface band over a
   --background list (Logs, and the rail's balance columns).

   Only the ::-webkit-scrollbar pseudos can paint in that strip, so the band is
   continued in the SCROLLBAR's own background — flat colour for the band, one
   pixel of --border for its bottom rule, transparent below so the rest of the
   track keeps showing the list. The track is then inset by the same height,
   which keeps the thumb strictly below the band instead of riding up into it at
   scrollTop 0. Nothing here moves layout, so head and rows still resolve their
   columns from the same content width.

   :vertical, because these boxes can also produce a horizontal bar, and a
   top-to-bottom gradient across a 10px-tall one is entirely band colour. */
.dlog-list,
.rail-body {
  --sticky-head-h: 33px;   /* 32px band + its 1px bottom rule */
  /* Their column head sticks INSIDE the scroller, so the bar starts under it rather
     than running across it — the head is not part of what you are scrolling. */
  --sc-bar-top: var(--sticky-head-h);
}
.dlog-list::-webkit-scrollbar:vertical,
.rail-body::-webkit-scrollbar:vertical {
  background: linear-gradient(
    var(--surface) 0 calc(var(--sticky-head-h) - 1px),
    var(--border) calc(var(--sticky-head-h) - 1px) var(--sticky-head-h),
    transparent var(--sticky-head-h));
}
.dlog-list::-webkit-scrollbar-track:vertical,
.rail-body::-webkit-scrollbar-track:vertical { margin-top: var(--sticky-head-h); }
}  /* end @layer base */

@layer components {
/* ---- Wallet multi-select picker ---- */
.picked-hidden { display: none !important; }
.picker-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 200; }
.picker { background: var(--float); border: 1px solid var(--border-strong); border-radius: var(--radius); width: min(480px, 92vw); max-height: 82vh; display: flex; flex-direction: column; padding: var(--pad-modal); box-shadow: var(--shadow-modal); }
/* ---- a dialog ARRIVES and LEAVES, it does not blink ----
   The ground fades and the sheet comes up a few pixels while it does — enough to say
   "this is on top of what you were looking at" rather than replacing the screen
   between two frames. The open animation is CSS alone, so every .picker in the app
   gets it for nothing. Closing needs a class, because the element has to survive long
   enough to animate: preflightSheet adds .is-closing and detaches on transitionend.
   pointer-events go off while it leaves — the sheet is already settled and a click
   landing on a fading dialog would hit a control that is no longer the answer. */
@keyframes picker-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes picker-rise { from { opacity: 0; transform: translateY(8px) scale(.985); }
                         to   { opacity: 1; transform: none; } }
.picker-overlay {
  animation: picker-in .14s ease-out both;
  transition: opacity .14s ease-in;
}
.picker-overlay > .picker { animation: picker-rise .18s cubic-bezier(.22, 1, .36, 1) both; }
/* picker-in/picker-rise are declared with fill-mode `both`, so they go on pinning
   opacity and transform after they finish — and an animation's value beats a
   transition's on the same property, so the fade-out computed as opacity 1 for its whole
   duration and the dialog just vanished when it was detached. close() clears the
   animation and commits that frame BEFORE adding this class, which is what gives the
   transition a real value to leave from; `animation: none` here keeps the two states
   agreeing for anything that adds the class without going through close(). */
.picker-overlay.is-closing { animation: none; opacity: 0; pointer-events: none; }
.picker-overlay.is-closing > .picker {
  animation: none;
  transition: opacity .14s ease-in, transform .14s ease-in;
  opacity: 0; transform: translateY(6px) scale(.99);
}
@media (prefers-reduced-motion: reduce) {
  .picker-overlay, .picker-overlay > .picker { animation: none; transition: none; }
}
.picker-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.picker-title { font-weight: var(--title-weight); font-size: var(--title-size); }
.picker-note { color: var(--muted); font-size: var(--body-size); margin-bottom: 8px; }
.picker-controls { display: flex; gap: var(--sp-2); margin-bottom: 10px; flex-wrap: wrap; }
.picker-search { flex: 1; min-width: 140px; }
/* A FIXED SLOT, not a box that grows to its longest option. `auto` sized it to the
   widest group name in the vault, so one long name pushed the search field down to its
   min-width and the two controls stopped lining up. 120px is "All groups" and no more;
   a longer name ellipsises inside it (.cs-text), which is what the rail's own chips do
   with the same names.
   The class lands on the WRAPPER — custom-select.js copies it there — so this is the
   slot, and the button inside it is what gets the height below. */
.picker-group { flex: 0 0 120px; width: 120px; }
/* (Its height is set at the end of this file — see the note there.) */
.picker-list { overflow-y: auto; border: 1px solid var(--border); border-radius: var(--radius); padding: 4px; flex: 1; }
.picker-item { display: flex; align-items: center; gap: var(--sp-2); padding: 6px; cursor: pointer; border-radius: var(--radius); }
.picker-item:hover { background: var(--surface-2); }
.picker-item input { width: auto; flex: 0 0 auto; }
.picker-item .wallet-chip { flex: 1; }
.picker-foot { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-4); }
.picker-count {
  color: var(--muted); font-size: var(--body-size); margin-right: auto;
  display: inline-flex; align-items: baseline; gap: var(--sp-1);
}
/* THE MARK CARRIES THE COLOUR, the sentence carries the words. A footer line is the
   dialog's quietest text — it sits under a list you are reading — and a whole line of
   colour there competes with the button beside it for the eye. The ✓ is in the app's
   accent, the same blue the row it is confirming wears, and the sentence stays the
   muted grey every other footer line is set in. */
.picker-count-ok { font-size: var(--body-size); line-height: 1; color: var(--accent); }

.wc-copy { flex: 0 0 auto; }

/* ---- Flow (transaction graph) ---- */
.flow-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; }
.flow-head h2 { margin: 0; }
.flow-head-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* Cytoscape mounts directly into #flow-graph, so it must be a fixed-size,
   padding-free box. The legend sits in a sibling row below (see JS). */
#flow-graph.flow-graph {
  height: 520px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #060606;
  padding: 0;
  margin-bottom: 12px;
  position: relative;
  overflow: hidden;
}
.flow-empty {
  color: var(--muted); font-size: var(--body-size); text-align: center;
  padding: 40px 12px;
  /* Center the empty/error message inside the fixed-height graph box. */
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
}

.flow-node-circle { stroke: var(--background); stroke-width: 2; }
.flow-node-label {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-size: var(--cap-size); fill: var(--foreground); paint-order: stroke;
  stroke: var(--background); stroke-width: 3px; stroke-linejoin: round;
}
.flow-edge { opacity: 0.9; }

.flow-legend {
  display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);
  font-size: var(--body-size); color: var(--muted);
}
.flow-legend-key { display: inline-flex; align-items: center; gap: var(--sp-1); }
.flow-legend-swatch { width: 12px; height: 12px; border-radius: var(--radius); display: inline-block; }
.flow-legend-count { margin-left: auto; font-family: var(--mono); color: var(--muted-2); }

/* ---- Flow list table (mirrors .dash-table) ---- */
.flow-table td { vertical-align: middle; }
/* Rows are hover-targets that highlight the matching edge in the graph. */
.flow-table tbody tr { cursor: pointer; }
.flow-table tbody tr:hover { background: var(--surface-2); }
.flow-table .wallet-chip { background: transparent; padding: 0; border: none; }
.flow-endpoint { display: inline-flex; align-items: center; }
.flow-arrow-cell { text-align: center; color: var(--muted); }
.flow-uniswap { color: var(--muted); font-weight: 400; }
.flow-tx-cell { display: flex; align-items: center; gap: var(--sp-1); }
.flow-time { color: var(--muted); font-size: var(--cap-size); }

.flow-kind {
  display: inline-block; padding: 2px 8px; border-radius: var(--radius);
  font-size: var(--cap-size); font-weight: var(--eyebrow-weight); text-transform: uppercase;
  letter-spacing: var(--eyebrow-track); border: 1px solid transparent;
}
.flow-kind-sell { color: var(--accent); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); }
.flow-kind-buy { color: var(--green); border-color: var(--green); background: color-mix(in srgb, var(--green) 14%, transparent); }
.flow-kind-transfer { color: var(--blue); border-color: var(--blue); background: color-mix(in srgb, var(--blue) 14%, transparent); }
.flow-kind-unwrap { color: var(--violet); border-color: var(--violet); background: color-mix(in srgb, var(--violet) 14%, transparent); }

/* ---- Quick trade panel ---- */
.quick-trade legend { color: var(--foreground); }
.quick-trade-fields { grid-template-columns: minmax(0,1.6fr) minmax(0,.9fr) minmax(0,1.2fr) minmax(0,.9fr); align-items: end; }
@media (max-width: 720px) { .quick-trade-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.quick-trade .run-actions { margin-top: 14px; padding-top: 12px; }

/* Sequencer per-set loop controls */
.seq-loop-toggle {
  display: inline-flex; align-items: center; gap: var(--sp-1);
  font-size: var(--body-size); color: var(--muted); font-weight: 400; cursor: pointer;
  user-select: none;
}
.seq-loop-toggle input { width: auto; height: auto; }
.seq-loop-toggle:hover { color: var(--foreground); }
.seq-loop-field[hidden] { display: none; }
.seq-loop-controls { display: flex; align-items: center; gap: var(--sp-1); flex-wrap: wrap; }
.seq-loop-controls input { width: var(--num-w-sm); }
.seq-loop-controls select { width: auto; }
.seq-loop-x { color: var(--muted); font-size: var(--body-size); }

/* Per-set run bar (Dry run / Execute this one set) */
.seq-set-run { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
.seq-set-run-label { margin: 0; }
.seq-set-run .btn { margin-left: 0; }

/* ONE segmented control for the whole app: a single bordered track holding the options
   flush against each other, the selected one a step darker with its own hairline. It used
   to be a padded track with gaps between the buttons, which read as three small buttons
   in a box rather than one control with a chosen segment.
   Every .seg in here now looks the same — Transfer's Disperse/Collect, Settings' gas
   Speed, Disperse's per-wallet mode — because they are all the same kind of choice. */
.seg {
  display: inline-flex; gap: 0;
  background: var(--surface-2); border: 1px solid var(--border);
  padding: 0; border-radius: var(--radius); overflow: hidden;
}
.seg-btn {
  display: inline-flex; align-items: center; justify-content: center;
  /* 30px inside the track's 1px borders = a 32px control, the app's one control
     height. At 28px an inline .seg sat 2px shy of every .btn and .seg-field beside
     it — two seg species where there should be one. */
  height: 30px; padding: 0 14px;
  background: transparent; color: var(--muted);
  border: none; border-radius: 0;
  font-family: inherit; font-size: var(--body-size); font-weight: 400; line-height: 1;
  cursor: pointer; white-space: nowrap;
  transition: color .12s, background .12s;
}
.seg-btn:hover { color: var(--foreground); }
/* --background, not --card: the track is --surface-2, so the selected segment has to go
   DOWN a step to read as recessed-and-current rather than merely tinted. */
/* ---- the selected segment SLIDES, it does not blink ----
   Same idea as .act-tab-ink one section down: ONE shared element that travels, rather
   than a fill switching off one button and on at the next. A per-button background has
   nothing continuous to interpolate, so the mark can only teleport — which is why the
   tab strip already works this way and these did not.
   The chip is the ink now; the buttons keep only their ink colour. `.seg` is
   overflow: hidden already, so the pill is clipped to the track's corners for free. */
.seg { position: relative; }
.seg-ink {
  position: absolute; left: 0; top: 0; width: 0; height: 100%;
  background: var(--background); box-shadow: inset 0 0 0 1px var(--border);
  border-radius: var(--radius);
  pointer-events: none; z-index: 0;
  transform: translateX(0);
  transition: transform .28s cubic-bezier(.22, 1, .36, 1),
              width     .28s cubic-bezier(.22, 1, .36, 1);
}
/* First placement, and any re-measure (resize, font load), must LAND — a glide in from
   the left is a phantom move nobody asked for. */
.seg-ink.jump { transition: none; }
.seg-btn { position: relative; z-index: 1; }
/* With the ink carrying the fill, the active button only changes its ink colour. The
   fill stays declared for the case where no ink has been placed yet (a .seg built and
   measured in the same frame, or JS not having run) so the selection is never invisible.
   :where() keeps it at zero specificity, so .seg-has-ink wins without a fight. */
.seg-btn.active { background: var(--background); color: var(--foreground); box-shadow: inset 0 0 0 1px var(--border); }
.seg.seg-has-ink > .seg-btn.active { background: none; box-shadow: none; }
@media (prefers-reduced-motion: reduce) {
  .seg-ink { transition: none; }
}

/* Inline toggle used in place of a 2-option dropdown (makeToggle): fills its
   field/row cell, matches input height, buttons split the width evenly. */
.seg-field { display: flex; width: 100%; height: var(--control-h); }
.seg-field .seg-btn { flex: 1 1 0; min-width: 0; padding: 0 8px; font-size: var(--body-size);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.seg-field.disabled { opacity: .5; pointer-events: none; }
/* In a wallet-row head (Disperse mode toggle) it sits next to the Remove button. */
.wallet-row-head .seg-field { width: auto; flex: 1; max-width: 240px; }

/* ---- Unified transfer desk (drag-driven From / To) ---- */
.transfer-desk-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.transfer-desk-head h2 { margin: 0; }
.transfer-desk-head .clear-saved { flex: 0 0 auto; }
.tf-boxes {
  display: grid; grid-template-columns: 1fr auto 1fr; gap: var(--sp-4);
  align-items: stretch; margin-top: 14px;
}
.tf-box {
  display: flex; flex-direction: column; gap: var(--sp-2);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 12px; min-width: 0;
}
.tf-box-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.tf-box-title { font-size: var(--cap-size); color: var(--muted); text-transform: uppercase; letter-spacing: var(--eyebrow-track); font-weight: 400; }
.tf-box-title em { font-style: normal; color: var(--muted-2); font-weight: 400; text-transform: none; letter-spacing: 0; }
.tf-box-actions { display: flex; gap: var(--sp-1); }
/* A POPULATED zone is exactly as tall as its chips — it used to reserve 64px and
   centre 1–2 chips in the middle of it, which is where the "void" came from. The
   comfortable target box is kept for the EMPTY state only. */
.tf-zone {
  flex: 0 0 auto; min-height: 0;
  display: flex; flex-direction: column; gap: var(--sp-2); justify-content: flex-start;
}
.tf-zone:has(.dz-empty) { min-height: 64px; justify-content: center; }
/* Inside a drop zone, chips stack one per row so their (right-aligned) balance
   forms a clean column instead of wrapping inline. */
.tf-zone .dz-chips { flex-direction: column; gap: var(--sp-1); align-items: stretch; }
.tf-zone .dz-chip { width: 100%; }
.tf-arrow { display: flex; align-items: center; justify-content: center; font-size: var(--title-size); color: var(--muted); min-width: 24px; }
.dz-chip-addr { border-style: dashed; }
.dz-chip-addr .dz-chip-name { font-family: var(--mono); font-weight: 400; }
.dz-chip-bal {
  flex: 0 0 auto; margin-left: auto; padding-left: 10px; text-align: right;
  color: var(--muted); font-family: var(--num); font-variant-numeric: tabular-nums;
}

/* Controls sit on one baseline and each field is as wide as its CONTROL, not a
   third of the desk — an "Amount for all" box holding "1.5" was 300px wide. */
.tf-controls {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-3) 16px;
  margin-top: 14px;
}
.tf-controls .field { flex: 0 0 auto; }
.tf-controls select { width: auto; min-width: 150px; }
.tf-overrides { margin-top: 14px; display: flex; flex-direction: column; gap: var(--sp-2); }
.tf-overrides:empty { display: none; }
.tf-overrides-head { font-size: var(--cap-size); font-weight: 400; color: var(--muted); text-transform: uppercase; letter-spacing: var(--eyebrow-track); }
.tf-override-row {
  display: flex; align-items: center; gap: var(--sp-3);
  background: transparent; border: none; border-bottom: 1px solid var(--border);
  border-radius: 0; padding: 8px 2px;
}
.tf-override-row:last-child { border-bottom: none; }
.tf-override-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 400; }
.tf-override-amount {
  width: var(--num-w-lg); flex: 0 0 auto; height: var(--control-h-sm);
  text-align: right; font-family: var(--num); font-variant-numeric: tabular-nums;
}
.tf-override-all { display: flex; align-items: center; gap: var(--sp-1); font-size: var(--body-size); color: var(--muted); flex: 0 0 auto; }
.tf-override-all input { width: auto; }

.tf-total {
  margin-top: 12px; font-size: var(--body-size); font-weight: 400; color: var(--foreground);
  font-variant-numeric: tabular-nums;
}
.tf-total[hidden] { display: none; }

.tf-role-banner {
  margin-top: 14px; padding: 10px 12px; border-radius: var(--radius);
  font-size: var(--body-size); border: 1px solid var(--border); background: var(--surface); color: var(--muted);
}
.tf-role-banner.ok { border-color: color-mix(in srgb, var(--ok) 45%, var(--border)); color: var(--foreground); background: color-mix(in srgb, var(--ok) 10%, transparent); }
.tf-role-banner.bad { border-color: color-mix(in srgb, var(--bad) 45%, var(--border)); color: var(--foreground); background: color-mix(in srgb, var(--bad) 10%, transparent); }

@media (max-width: 720px) {
  .tf-boxes { grid-template-columns: 1fr; }
  .tf-arrow { transform: rotate(90deg); }
}

/* ---- Single-select wallet pick button (popup chip) ---- */
.wallet-pick { display: flex; align-items: center; gap: var(--sp-1); }
.wallet-pick-btn {
  display: flex; flex: 1; align-items: center; gap: var(--sp-2); min-height: var(--control-h);
  padding: 5px 10px; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); cursor: pointer; color: var(--foreground); text-align: left;
}
.wallet-pick-btn .wallet-chip { flex: 1; }
.wallet-pick-btn:hover { border-color: var(--border-strong); }
.wallet-pick-placeholder { color: var(--muted); font-size: var(--body-size); }
.wallet-pick-clear {
  flex: 0 0 auto; min-height: var(--control-h); padding: 0 10px; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer;
  color: var(--muted); font-size: var(--body-size); line-height: 1;
}
.wallet-pick-clear:hover { border-color: var(--border-strong); color: var(--foreground); }

/* Compact wallet chip inside narrow rows (Sequencer/Transfer). The balance drops
   onto its own line under the name so a long balance can never squeeze the name
   and address to zero width. */
.row-wallet-chip { padding: 6px 0; min-width: 0; max-width: 100%; overflow: hidden; flex-wrap: wrap; align-items: center; }
.row-wallet-chip .wallet-chip-id { min-width: 0; overflow: hidden; flex: 1 1 auto; }
.row-wallet-chip .wallet-chip-bal {
  /* The wrapped balance starts at the row's left edge, which is where the name
     and the address now start too. It used to be indented 30px to clear a 28px
     avatar — an element the chip never actually rendered. */
  flex-basis: 100%; margin-left: 0; padding-left: 0;
  overflow: hidden; text-overflow: ellipsis;
}
.row-wallet-chip .wc-copy { flex: 0 0 auto; }

/* ---- in-app assistant terminal (floating, bottom-left) ---- */
.assist-hl {
  outline: 2px solid var(--blue);
  box-shadow: 0 0 0 3px rgba(59,130,246,.35);
  border-radius: var(--radius);
  transition: outline .1s;
}
#assist-toggle {
  position: fixed; left: 16px; bottom: 16px; z-index: 250;
  width: 40px; height: 40px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: #0b0b0b; color: var(--green, #4ade80);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-family: var(--mono); font-size: var(--title-size); font-weight: 400;
  cursor: pointer; box-shadow: var(--shadow-pop);
}
#assist-toggle:hover { border-color: var(--blue); color: var(--foreground); }
#assist-term {
  position: fixed; left: 16px; bottom: 64px; z-index: 250;
  width: 380px; height: 420px; max-width: calc(100vw - 32px); max-height: calc(100vh - 96px);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--float); color: var(--foreground);   /* was a hardcoded #0b0b0b */
  border: 1px solid var(--border-strong); border-radius: var(--radius);  /* was 10px in a squared app */
  box-shadow: var(--shadow-modal);
  font-family: var(--mono); font-size: var(--body-size); line-height: 1.5;
  resize: both;
}
#assist-term[hidden] { display: none; }
#assist-head {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 7px 10px; border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--foreground) 5%, transparent);
}
#assist-head .assist-title {
  font-size: var(--cap-size); text-transform: lowercase; letter-spacing: .05em;
  color: var(--muted); font-weight: 400;
}
#assist-close {
  margin-left: auto; background: none; border: none; cursor: pointer;
  color: var(--muted); font-size: var(--body-size); line-height: 1; padding: 2px 4px;
}
#assist-close:hover { color: var(--foreground); }
#assist-out {
  flex: 1; overflow-y: auto; padding: 10px;
  white-space: pre-wrap; word-break: break-word;
}
#assist-out .a-line { margin: 0 0 2px; }
#assist-out .a-cmd { color: var(--foreground); }
#assist-out .a-sys { color: var(--muted); }
#assist-out .a-err { color: var(--red); }
#assist-out .a-ok { color: var(--green, #4ade80); }
#assist-out .a-summary { color: var(--blue-fg, #bfdbfe); }
#assist-out .a-step { color: var(--foreground); }
#assist-out .a-prompt { color: var(--muted); }
#assist-inputline {
  display: flex; align-items: center; gap: var(--sp-1);
  padding: 8px 10px; border-top: 1px solid var(--border);
}
#assist-inputline .assist-prompt { color: var(--green, #4ade80); font-weight: 400; }
#assist-input {
  flex: 1; height: auto; min-width: 0;
  background: transparent; border: none;
  color: var(--foreground); font-family: var(--mono); font-size: var(--body-size);
  padding: 0; caret-color: var(--green, #4ade80);
}
/* It sits in a bordered input LINE, so the ring goes on the line, not the bare
   field — but it must go somewhere: this had border, outline and box-shadow all
   removed at ID specificity and was invisible to a keyboard in every engine. */
#assist-input:focus-visible { outline: 1px solid var(--ring); outline-offset: 2px; }
#assist-input::placeholder { color: var(--muted-2); }
#assist-input:disabled { opacity: .6; }
#assist-inputline.blink .assist-prompt { animation: assist-blink 1.1s step-start infinite; }
@keyframes assist-blink { 50% { opacity: .25; } }

/* Sequencer: show the wallet NAME clearly in a row (hide the cramped address) */
.row-wallet-chip .wallet-chip-addr { display: none; }
.row-wallet-chip .wallet-chip-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Sequencer row reorder controls */
.seq-row.has-move { display: flex; align-items: center; gap: var(--sp-2); }
.seq-row.has-move > .seq-row-inner { flex: 1; min-width: 0; }
.seq-row-move { display: flex; flex-direction: column; gap: var(--sp-0); flex: 0 0 auto; }
/* A ▲/▼ pair stacked beside a row. The PAIR is what has to match a control, not
   each half — so each half is half a default rung minus the 2px gap between
   them, off the ladder rather than a guessed 18px. */
.seq-row-move .btn.tiny {
  height: calc((var(--control-h) - 2px) / 2);
  padding: 0 6px; line-height: 1; font-size: var(--cap-size);
}

/* ============================================================
   Activity feed — right rail: live job cards on top, history below.
   Collapses to a thin strip (badge = running jobs); below ~1100px
   it overlays the content instead of squeezing it.
   ============================================================ */
.feed {
  grid-column: 3; grid-row: 2;
  min-height: 0; overflow: hidden;
  display: flex; flex-direction: column;
  background: var(--rail); border-left: 1px solid var(--border);
  z-index: 55;
}
.feed-head {
  display: flex; align-items: center; gap: var(--sp-2); flex: 0 0 auto;
  padding: 10px 12px; border-bottom: 1px solid var(--border);
}
.feed-head h2 { margin: 0; flex: 1; font-size: var(--body-size); font-weight: 400; }
.feed-badge {
  background: color-mix(in srgb, var(--blue) 20%, transparent);
  border: 1px solid color-mix(in srgb, var(--blue) 40%, transparent);
  color: #93c5fd; border-radius: var(--radius);
  padding: 1px 7px; font-size: var(--cap-size); font-weight: 400; font-family: var(--mono);
}
.feed-badge[hidden] { display: none; }
.feed-body { flex: 1 1 auto; overflow-y: auto; padding: 10px 12px; }

/* Transaction hashes are also rendered by the current Logs table. */
.feed-tx { flex: 0 0 auto; font-size: var(--cap-size); color: var(--foreground); text-decoration: none; }
a.feed-tx:hover { text-decoration: underline; }

/* One-line note left in the old Results panels after an execute moves to the feed. */
.feed-handoff { color: var(--muted); font-size: var(--body-size); padding: 6px 0; }
.feed-handoff.ok { color: #86efac; }
.feed-handoff.err { color: var(--red); }

/* PK exporter modal */
.pk-export { width: min(560px, 94vw); }
.pk-warn { background: color-mix(in srgb, var(--red) 12%, transparent); border: 1px solid var(--red); color: var(--red); border-radius: var(--radius); padding: 9px 11px; font-size: var(--body-size); margin-bottom: 10px; }
.pk-pw { display: flex; gap: var(--sp-2); margin-bottom: 8px; }
.pk-pw input { flex: 1; }
.modal-error { color: var(--red); font-size: var(--body-size); margin-bottom: 8px; }
.pk-list { display: flex; flex-direction: column; gap: var(--sp-2); max-height: 52vh; overflow-y: auto; }
.pk-row { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 10px; }
.pk-row-top { display: flex; justify-content: space-between; gap: var(--sp-2); margin-bottom: 4px; }
.pk-title { font-weight: 400; }
.pk-addr { color: var(--muted); }
.pk-key-line { display: flex; align-items: center; gap: var(--sp-2); }
.pk-key { font-size: var(--cap-size); word-break: break-all; flex: 1; }

/* ================================================================ PRE-FLIGHT SHEET
   The single execute confirmation: role banner, per-leg table from the arming
   dry run, totals, dry-run age, inline master password, one red Confirm. */
.picker.preflight { width: min(600px, 94vw); }
.preflight-banner { background: color-mix(in srgb, var(--red) 10%, transparent); border: 1px solid var(--red); border-radius: var(--radius); color: var(--foreground); padding: 10px 12px; font-size: var(--body-size); font-weight: 400; margin-bottom: 10px; }
.preflight-body { overflow-y: auto; max-height: 42vh; border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 10px; }
.preflight-table { width: 100%; border-collapse: collapse; font-size: var(--body-size); }
.preflight-table th { text-align: left; color: var(--muted); font-weight: 400; padding: 6px 8px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--card); }
.preflight-table th:nth-child(3), .preflight-table th:nth-child(4) { text-align: right; }
.preflight-table td { padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
.preflight-table tr:last-child td { border-bottom: none; }
.preflight-idx { color: var(--muted-2); width: 26px; }
.preflight-num { text-align: right; font-family: var(--mono); white-space: nowrap; }
.preflight-leg .preflight-note { display: block; color: var(--muted-2); font-size: var(--cap-size); }
.preflight-leg .preflight-note-err { color: var(--red); }
.preflight-err td { background: color-mix(in srgb, var(--red) 6%, transparent); }
.preflight-totals { font-size: var(--body-size); font-weight: 400; margin-bottom: 4px; }
.preflight-stale { font-size: var(--cap-size); color: var(--muted); margin-bottom: 10px; }
.preflight-stale-old { color: var(--amber); }
.preflight-pw-ok { font-size: var(--body-size); color: var(--green); margin-bottom: 8px; }
.preflight-foot { justify-content: flex-end; }
.preflight-confirm { font-weight: 400; }

/* ---- the TILED confirm sheet (Activity) -----------------------------------
   Four settings and a wallet list, not a transaction table. See the note on
   `opts.tiles` in preflightSheet for why this shape exists at all. */
/* The tabs replace the title, so the head keeps them left and the ✕ right. */
.preflight-head-tabs { gap: var(--sp-3); }
.preflight-tabs { flex: 0 0 auto; }
/* 2x2. Two columns rather than four across, because each figure carries a unit and a
   four-up made them narrow enough to wrap — and because two rows of two is the shape
   every other metric block in the app uses. */
.preflight-tiles {
  /* 8px both ways — stated rather than taken from --sp-2 (which is also 8) because the
     even split between the four is part of the block's shape here, not the app's
     default gap reaching in. */
  display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
  margin-bottom: var(--sp-3);
}
.preflight-tile {
  display: flex; flex-direction: column; gap: var(--sp-1);
  padding: 12px 14px;
  border: 1px solid var(--border); border-radius: var(--radius);
  /* A LITERAL, not a token: #282828 sits between --surface-2 (#232323) and
     --border-strong (#3B3B3B) and there is nothing on the scale at it. These four sit on
     the dialog's own --float ground, which is lighter than a panel, so the panel tones
     that lift a tile off a card do not lift one off this. */
  background: #282828;
}
.preflight-tile-cap { font-size: var(--body-size); color: var(--muted); line-height: 20px; }
.preflight-tile-fig {
  display: inline-flex; align-items: baseline; gap: 6px; min-width: 0;
  font-size: var(--title-size); line-height: 28px; color: var(--foreground);
  font-variant-numeric: tabular-nums;
}
/* THE UNIT RECEDES, exactly as it does on the swap stack ("25 % ETH", "0.417 ETH"):
   same size as the figure so the pair reads as one quantity, quieter ink so the number
   is what you land on. At full brightness "6 tr/min" was two equal words and the eye
   had to pick the number out of them. */
.preflight-tile-unit { color: var(--muted-2); }
/* THREE ROWS AND THEN IT SCROLLS, the same stop the Quick swap drawer takes — long
   enough to show that there is a list and how it is shaped, short enough that the
   buttons never leave the sheet. The height is on the ROW CONTAINER, not the box, so
   the header and column heads stay put while the rows move under them. */
/* The list sits on the PAGE tone, not the panel tone: the sheet itself is already
   --surface, so a --surface box on it was a box you could only find by its border. */
/* The rows sit on the DIALOG's own ground (--float, #202020), not the page's. A sheet
   is a surface over the app, so a #111 well inside it read as a hole punched through to
   the page rather than as a list on the sheet — and it was the darkest thing on screen,
   under a header two steps lighter.
   --border-mid for every rule in here — between --border (#262626, which vanished
   against the #202020 ground) and --border-strong (#3B3B3B, which drew the table louder
   than anything in it). It is a token rather than a literal because the scrollbar thumb
   needs the same value for the same reason. */
.preflight-wallets > .sc-io-box,
.preflight-seq > .sc-io-box {
  border: 1px solid var(--border-mid); background: var(--float);
  /* NO GAP. .sc-io-box is a flex column with `gap: var(--sp-1)`, which in the panes is
     invisible — their box is the same tone as what shows through it. Here the box is
     --background and the two header bands are --surface, so those 6px became two lit
     seams cutting the header block in half and offsetting it from the rows. */
  gap: 0;
}
/* ONE HEADER BLOCK, one tone, and it is the tile fill — #282828. The title row and the
   column heads used to be two different greys stacked on each other; then both were
   --surface, which is only 8 points off the --background the rows sit on, so the block
   read as part of the list rather than as its head. At the tiles' tone the header is
   plainly a header and the rows are plainly the page, which is the split the box is
   drawing. A literal for the same reason it is one on .preflight-tile: nothing on the
   scale sits at it, and these panes are on the dialog's lighter --float ground where the
   panel tones do not separate anything. */
.preflight-wallets > .sc-io-box > .sc-io-cap,
.preflight-seq > .sc-io-box > .sc-io-cap { background: #282828; }
/* (The two-sided header's switch is styled in the UNLAYERED TAIL — see "the title row
   as a SWITCH". Both .act-tab and .act-tab-ink are declared in @layer overrides, which
   beats @layer components at any specificity, so a rule here could not reach them.) */
/* (.act-wt-head's fill is declared in the unlayered tail beside the other two rules it
   has to out-rank — see "the confirm sheet's wallet list is READ-ONLY".) */
/* THE ROW GEOMETRY HAS TO BE RESTATED. scWalletCard is a bag of grid items with no
   template of its own — the template lives on the PANES (.act-pane .sc-dropzone
   .sc-wallet-card), because each of them has a different column list. Inside a dialog
   none of those selectors match, so the parts fell into one implicit column and the
   rows collapsed to 28px of unaligned text. Three tracks, not four: with no ✕ and no
   stepper there is no Actions cell to leave room for. Everything else is read off the
   panes so a wallet reads identically here and in the box it came from. */
.preflight-wallets,
.preflight-seq {
  --act-wt-cols: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr);
  /* 20px — the inset EVERY wallet table in the app uses for its rows and column heads
     (.act-pane sets the same). The title row above keeps its own 16px, exactly as it
     does in the panes; that 4px difference is the convention, not a mistake. It was
     16 here, which lined the columns up 4px in from where they sit everywhere else. */
  --act-inset: 20px;
}
.preflight-wallet-rows .sc-wallet-card {
  display: grid; grid-template-columns: var(--act-wt-cols);
  grid-template-rows: auto auto;
  grid-template-areas: "name eth bal" "addr eth bal";
  align-items: center; gap: 0 24px;
  min-height: var(--pf-row-h); padding: 12px var(--act-inset);
  border: 0; border-bottom: 1px solid var(--border); background: none;
}
.preflight-wallet-rows .sc-wallet-card:last-child { border-bottom: 0; }
/* THE STRIP THE BAR IS DRAWN IN. The row's fill and its rule still run to the border —
   this is padding, not width — so nothing is cut; only the last column steps in far
   enough that the thumb never covers a figure. The column head above reserves the same
   through --pf-gutter, which is what keeps the two agreeing. */
.preflight-wallet-rows .sc-wallet-card {
  padding-right: calc(var(--act-inset) + var(--sc-bar-w));
}
/* …and the message an empty side shows sits in the middle of that reserved space
   rather than pinned to the top of it. (.sc-wallet-list is already a flex column.) */
.preflight-wallet-rows > .sc-drop-empty { flex: 1 1 auto; }
/* (Row dividers and the title row's seam are declared in the unlayered tail — the base
   rules they have to out-rank live there, so a copy in this layer loses on layer order
   however specific it is.) */
/* The address is `display: none` on the base class and unhidden by a `.sc-dropzone`
   rule — this list is not a drop zone, so it has to say so itself, or every row is a
   name with an empty line under it. */
.preflight-wallet-rows .sc-wallet-addr { display: block; text-align: left; }
.preflight-wallet-rows .sc-wallet-name { font-size: var(--body-size); color: var(--foreground); }
/* THREE ROWS AND THEN IT SCROLLS — see above.
   scrollbar-gutter: stable, and the head reserves the same strip. The rows scroll and
   the column head does not, so the scrollbar narrows one content box and not the other
   — every figure ended up 10px left of the heading naming it, but only once there were
   four wallets. Reserving it on both sides means the columns line up at three wallets
   and at thirty, and nothing shifts as the list crosses the threshold. */
/* ZERO now: the bar is drawn OVER the rows (see .sc-oscroll), so neither box gives up
   width to it and there is nothing to reserve on the other side. The var stays because
   the head's rule below reads it — one place to put the strip back if a surface ever
   keeps a native bar again. */
.preflight-wallets, .preflight-seq { --pf-gutter: var(--sc-bar-w); }
.preflight-wallet-rows {
  --pf-row-h: 68px;
  /* BOTH SIDES THE SAME HEIGHT. --pf-min-rows is set per pane from the tallest side
     (walletTabsPane), capped at the three rows this list scrolls past, so switching
     from four buyers to no sellers does not collapse the dialog and grow it back —
     which moves the buttons under the pointer for a switch that changed nothing about
     the run. Unset, it is 0 and the list sizes to its rows exactly as before. */
  min-height: calc(var(--pf-min-rows, 0) * var(--pf-row-h));
  max-height: calc(3 * var(--pf-row-h));
  overflow-y: auto;
}
.preflight-wallets .act-wt-head,
.preflight-seq .act-wt-head { padding-right: calc(var(--act-inset) + var(--pf-gutter)); }

/* ---- the same list, in a PICKER, allowed to be a list ----
   openWalletPicker shows this exact box so a wallet reads identically wherever you
   meet one. The single thing that differs is height: the sheet caps its rows at three
   because it is previewing a decision already made, while a picker is the surface you
   make the decision ON — thirty wallets scrolling three at a time is a peephole. So
   the box takes the dialog's slack and the rows scroll inside it. */
.picker-wallets { display: flex; flex: 1 1 auto; min-height: 0; margin-bottom: 0; }
.picker-wallets > .sc-io-box {
  flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
}
.picker-wallets .preflight-wallet-rows { max-height: none; flex: 1 1 auto; min-height: 0; }
/* The picker's own list is SELECTABLE, so its rows take the pointer and the hover the
   drop-zone cards already have. (The selected mark itself is the shared
   .sc-wallet-card.selected — accent bar, alpha wash — declared with the rail's.) */
.picker-wallets .sc-wallet-card { cursor: pointer; }
.picker-wallets .sc-wallet-card:hover { background-color: var(--surface); }
/* ONE PICK, AND THE ROW SAYS WHERE IT GOES. A 2px bar and a wash mark a row as
   selected everywhere else in the app, but this dialog asks a different question —
   "which wallet does this go INTO" — and the answer carries the arrow its menu entry
   already uses ("Gather ETH → one wallet"). The block grows out of the left edge and
   the row's content steps aside for it, so a pick reads as a movement rather than as
   a colour change. The row's own 2px selection bar stays where it is and the block
   covers it as it grows — a pseudo-element paints over an inset shadow — so the bar
   reads as the block's leading edge rather than as a second mark. */
.picker-wallets .sc-wallet-card {
  transition: transform .18s cubic-bezier(.25, .46, .45, .94), background-color .12s;
}
/* THE WHOLE ROW MOVES, not its first column. Padding was pushing the name over while
   the balance and the coin figure stayed where they were — the row rearranged itself
   instead of stepping aside. It TRANSLATES now: every column keeps its place in the
   row and the row moves as one thing, so the last column runs off the right edge and
   is clipped by the list, exactly as a row shoved sideways should be.
   The block is pinned 40px LEFT of the row, so it rides in with the row out of the
   list's own left edge — that is the expansion, and it costs no second animation. */
.picker-wallets .sc-wallet-card::after {
  content: "→";
  position: absolute; left: -40px; top: 0; bottom: 0; width: 40px;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent); color: var(--foreground);
  font-size: var(--body-size); line-height: 1;
  opacity: 0; pointer-events: none;
  transition: opacity .12s ease;
}
.picker-wallets .sc-wallet-card.selected { transform: translateX(40px); }
.picker-wallets .sc-wallet-card.selected::after { opacity: 1; }
/* The row that is shoved sideways overhangs the list; the list cuts it off rather than
   growing a horizontal scrollbar under a dialog nobody scrolls sideways. */
.picker-wallets .preflight-wallet-rows { overflow-x: hidden; }
/* ---- THE PICKER'S LIST IS THE RAIL'S LIST ----
   This is the vault, browsed — the same wallets, the same table, the same job as
   the left rail — so it takes the rail's tones: rows on --background with the
   column head a step up on --surface. It had been inheriting the confirm sheet's
   lighter pair, which is right THERE and wrong here: the sheet's list is a preview
   of a decision already made and reads as locked in, while this one is the surface
   you are still working on. */
/* (The two tones are set in the unlayered tail — the sheet's own #282828 header is
   declared there and no layered rule can reach past it.) */
/* ---- A PICKER DOES NOT RESIZE AS YOU FILTER ----
   The dialog took its height from its rows, so choosing a group of two collapsed it
   to a third of the screen and the next keystroke in the search box grew it back —
   the surface you are reading jumping under you while you type. Fixed height: the
   list scrolls inside it and the dialog stays exactly where it was. */
.picker:has(> .picker-wallets) { height: min(720px, 82vh); }
/* The controls above it are the app's ONE control height, like every other field
   and button in a dialog. "Select all" was .btn.tiny — a 24px chip beside a 32px
   search field, two control sizes in one row. */
.picker-controls > .btn { height: var(--control-h); font-size: var(--body-size); padding: 0 12px; }
/* ---- footer: the stamp and its disclosure in the far corner ---- */
/* Cancel and the confirm are ONE pair and they belong together at the end of the row.
   `space-between` spread all three groups evenly, which put Cancel in the middle of the
   footer — reading as a third thing between the stamp and the button rather than as the
   other half of the choice. The stamp takes the slack instead. */
.preflight-foot-split { justify-content: flex-end; }
.preflight-foot-left { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; margin-right: auto; }
.preflight-foot-left > .preflight-stale { margin-bottom: 0; }
.preflight-notes-btn {
  flex: 0 0 auto; width: 18px; height: 18px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: none; color: var(--muted-2); cursor: pointer;
  transition: color .12s;
}
.preflight-notes-btn:hover, .preflight-notes-btn.open { color: var(--foreground); }
/* Amber once the dry run is old — the same state the stamp beside it takes, and the
   glyph changes to the warning triangle with it (see renderStale). */
.preflight-notes-btn.is-old { color: var(--amber); }
/* The two panes share one box so the sheet can animate between their heights. */
.preflight-panes { overflow: hidden; margin-bottom: var(--sp-3); }
.preflight-panes > .preflight-tiles,
.preflight-panes > .preflight-wallets { margin-bottom: 0; }
@media (prefers-reduced-motion: reduce) {
  .preflight-panes { transition: none !important; }
}
.preflight-notes-icon {
  width: 15px; height: 15px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round;
}
/* Outer clips and carries the animated height; inner holds everything that would
   otherwise be a floor it could not collapse past. */
.preflight-notes { overflow: hidden; margin-bottom: var(--sp-3); }
.preflight-notes[hidden] { display: none; }
/* The leg table as the sheet's second TAB. It keeps its own border (it is the box in
   there), loses the outer margin the untabbed sheet gives it, and caps at the same
   three-rows-then-scroll the wallet list uses so both tabs settle at a similar height. */
.preflight-body-tabbed { margin-bottom: 0; max-height: 204px; }
/* ---- the Sequence tab is the SAME LIST as the Wallets tab ------------------
   It shares every rule above (.preflight-wallets covers the shared ones via the grouped
   selector below); only its columns differ — Wallet | Amount | Side instead of Wallet |
   Balance | Token — because a sequence row states what THIS occurrence moves, not what
   the wallet happens to hold. Side is written in the two colours the Sequence box on the
   card uses. */
.preflight-seq { --act-wt-cols: 22px minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr) 56px; }
/* WALLET | AMOUNT. When the sheet has a figure for what this run does to each wallet
   (preflightSheet's walletAmounts), the two balance columns give way to it: a confirm
   sheet is read to check the amount, and the holdings are on the screen behind it.
   Same geometry as the Sequence pane's amount column, for the same kind of figure. */
.preflight-amt { --act-wt-cols: minmax(0, 1.6fr) minmax(0, 1fr); }
.preflight-amt .preflight-wallet-rows .sc-wallet-card {
  grid-template-areas: "name amt" "addr amt";
}
.preflight-amt .sc-wallet-fig {
  grid-area: amt; text-align: right; min-width: 0;
  font-family: var(--num); font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ONE COLUMN PER ASSET, when the run moves both — the same two the wallet tables
   everywhere else in the app carry, and the same two the sheet's tiles total. The
   figures are bare: the column heading names the asset, as it does in the rail. */
.preflight-amt-two { --act-wt-cols: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr); }
.preflight-amt-two .preflight-wallet-rows .sc-wallet-card {
  grid-template-areas: "name amt amt2" "addr amt amt2";
}
.preflight-amt-two .sc-wallet-fig + .sc-wallet-fig { grid-area: amt2; }
/* Three classes deep, not !important: the shared row geometry above is
   `.preflight-wallet-rows .sc-wallet-card` (0,2,0), so this needs to out-specify it
   rather than shout over it. */
.preflight-seq .preflight-wallet-rows .sc-wallet-card {
  grid-template-areas: "num name amt amt2 side" "num addr amt amt2 side";
}
/* The position, in a column two digits wide. Muted and tabular — it labels the row
   rather than being one of its values. */
.preflight-seq-num {
  grid-area: num; align-self: center; justify-self: start;
  color: var(--muted-2); font-family: var(--num); font-variant-numeric: tabular-nums;
  font-size: var(--cap-size); line-height: 1;
}
/* The figure from scWalletCard's `cells` path carries no grid-area of its own — the
   panes place it, and none of their selectors reach into a dialog. */
.preflight-seq .sc-wallet-fig {
  grid-area: amt; text-align: right; min-width: 0;
  font-family: var(--num); font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The SECOND figure. .preflight-amt-two carries the same rule for the Makers tab,
   but it also carries that tab's three-track --act-wt-cols — put both classes on one
   element and the later of two equally specific rules decides the grid, which is how
   a five-column table ends up drawn on three tracks. So this table states its own. */
.preflight-seq .sc-wallet-fig + .sc-wallet-fig { grid-area: amt2; }
.preflight-seq .sc-wallet-act {
  grid-area: side; justify-self: end; align-self: center; display: inline-flex;
}
.preflight-side { font-size: var(--body-size); line-height: 20px; white-space: nowrap; }
.preflight-side-buy { color: var(--green); }
.preflight-side-sell { color: var(--red); }
/* Same as the card's Sequence table: the leading # pushed every heading one class
   along, and the shared alignment is keyed by position — so this table states its
   own. # and Wallet read left, Amount and Side right, over columns whose contents do. */
.preflight-seq .act-wt-head > .act-wt-col { text-align: left; }
.preflight-seq .act-wt-head > .act-wt-c2,
.preflight-seq .act-wt-head > .act-wt-c3,
.preflight-seq .act-wt-head > .act-wt-c4 { text-align: right; }
/* margin-left: auto takes the far end of the title row, opposite the name and count —
   the same arrangement .sw-sequence-hint has on the card. */
.preflight-cap-note {
  margin-left: auto; flex: 0 0 auto;
  font-size: var(--cap-size); color: var(--muted-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.preflight-notes-in {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--background); padding: 10px 12px;
}
@media (prefers-reduced-motion: reduce) {
  .preflight-notes { transition: none !important; }
}
.preflight-note-p {
  margin: 0; font-size: var(--cap-size); line-height: 1.5; color: var(--muted);
}
.preflight-note-p + .preflight-note-p { margin-top: 6px; }

/* ============================================================
   Loops — repeating step-chain builder using shared Gotham tokens.
   ============================================================ */
.loops-accent { color: var(--violet); font-weight: 400; }
.loops-head .h1row { width: 100%; }
.loops-add { margin-left: auto; }
.loops-list { display: flex; flex-direction: column; }

.loops-empty {
  border: 1px dashed var(--border-strong); background: var(--card);
  padding: 34px 20px; text-align: center; display: flex; flex-direction: column;
  align-items: center; gap: var(--sp-1);
}
.loops-empty-title { font-weight: 400; font-size: var(--title-size); }
.loops-empty-sub { color: var(--muted-2); font-size: var(--body-size); max-width: 46ch; margin-bottom: 8px; }

.loop { background: var(--card); border: 1px solid var(--border); margin-bottom: 16px; }
.loop-head { display: flex; align-items: center; gap: var(--sp-2); padding: 11px 14px; border-bottom: 1px solid var(--border); }
.loop-name-input {
  background: transparent; border: 1px solid transparent; color: var(--foreground);
  font-size: var(--title-size); font-weight: var(--title-weight); padding: 3px 6px; min-width: 90px; max-width: 240px;
  border-radius: var(--radius);
}
.loop-name-input:hover { border-color: var(--border); }
.loop-name-input:focus { border-color: var(--ring); background: var(--surface); box-shadow: 0 0 0 1px var(--ring); }
.loop-badge { font-size: var(--cap-size); font-weight: 400; padding: 2px 9px; border-radius: var(--radius); background: var(--surface); color: var(--muted-2); }
.loop-badge.run { background: #12241c; color: var(--green); }
.loop-badge.done { background: var(--surface); color: var(--muted); }
.loop-badge.error { background: #261417; color: var(--red); }
/* .loop-head's own gap spaces these; only the two `auto` pushes are layout. */
.loop-head .btn.small { margin-left: 0; }
.loop-head .loop-dry { margin-left: auto; }
.loop-head .loop-stop { margin-left: auto; color: var(--red); border-color: #4a2427; }
.loop-head .loop-stop:hover { background: var(--red); border-color: var(--red); color: #fff; }

.loop-body { padding: 14px; }
.loop-body .eyebrow {
  font-size: var(--eyebrow-size); letter-spacing: var(--eyebrow-track); text-transform: uppercase;
  color: var(--muted-2); font-weight: var(--eyebrow-weight); margin-bottom: 8px;
}

/* start wallet dropzone */
.loop-startw {
  display: flex; align-items: center; gap: var(--sp-2); background: var(--surface);
  border: 1px solid var(--border); padding: 8px 11px; margin-bottom: 16px; min-height: 40px;
  flex-wrap: wrap;
}
.loop-startw.drop-hover { border-color: var(--violet); background: var(--surface-2); }
.loop-startw-empty { color: var(--muted-2); font-size: var(--body-size); }
.loop-startw-bal { color: var(--muted); }
.loop-startw .dz-chip-x { background: none; border: none; color: var(--muted); cursor: pointer; font-size: var(--body-size); }
.loop-startw .dz-chip-x:hover { color: var(--foreground); }
/* SET of seed wallets: one chip per seed, wrapping; each chain is one chip. */
.loop-startw-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); flex: 1; min-width: 0; }
/* One seed = one SINGLE-LINE chip, so it takes the compact control rung and the
   shared inset/gap exactly like .dz-chip. (.loop-startw itself is the drop-zone
   CONTAINER these sit in — a slot, not a chip — so it keeps its own box.) */
.loop-startw-chip {
  display: inline-flex; align-items: center; gap: var(--chip-gap); background: var(--card);
  border: 1px solid var(--border); padding: 0 var(--chip-pad-x); min-width: 0;
  height: var(--control-h-sm);
}
.loop-startw-add { margin-left: auto; flex: none; }

/* steps list */
.loop-steps { display: flex; flex-direction: column; border: 1px solid var(--border); }
.loop-step {
  display: flex; align-items: center; gap: var(--sp-3); padding: 8px 11px;
  border-bottom: 1px solid var(--border); font-size: var(--body-size); background: var(--card);
}
.loop-step:last-of-type { border-bottom: 0; }
.loop-step-dragging { opacity: .5; }
.loop-step-over { box-shadow: inset 0 2px 0 var(--violet); }
.loop-grip { color: var(--muted-2); cursor: grab; font-size: var(--body-size); flex: none; }
.loop-stype { display: inline-flex; align-items: center; gap: var(--sp-2); font-weight: 400; width: 104px; flex: none; }
.loop-ic {
  width: 20px; height: 20px; border-radius: var(--radius); display: inline-flex; align-items: center;
  justify-content: center; font-size: var(--body-size); font-weight: 400; flex: none;
}
.loop-ic.buy { background: #12241c; color: var(--green); }
.loop-ic.sell { background: #261417; color: var(--red); }
.loop-ic.xfer { background: #1a1830; color: var(--violet); }
.loop-ic.unwrap { background: #1a1830; color: var(--violet); }
.loop-ic.wait { background: var(--surface-2); color: var(--muted); }
.loop-sconf { display: flex; align-items: center; gap: var(--sp-1); flex-wrap: wrap; color: var(--muted); }
.loop-sconf-tail { color: var(--muted-2); font-size: var(--body-size); }
/* Inline step numbers ("wait 30 s", "every 3 loops"): they read as part of a
   sentence, so they stay CENTRED — but on the shared ch ladder and numeric face
   so every one of them is the same box. */
.loop-num {
  width: var(--num-w-sm); height: var(--control-h-sm); background: var(--background); border: 1px solid var(--border);
  color: var(--foreground); text-align: center; font-size: var(--body-size); border-radius: var(--radius);
  font-family: var(--num); font-variant-numeric: tabular-nums;
}
.loop-num:focus { border-color: var(--ring); box-shadow: 0 0 0 1px var(--ring); }
.loop-dest-sel { height: var(--control-h-sm); font-size: var(--body-size); width: auto; max-width: 190px; }

/* inline mode toggles inside a step editor */
.loop-seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.loop-seg-btn {
  background: var(--surface); border: none; border-right: 1px solid var(--border);
  color: var(--muted); font-size: var(--body-size); padding: 0 8px; cursor: pointer; height: var(--control-h-sm);
}
.loop-seg-btn:last-child { border-right: 0; }
.loop-seg-btn.active { background: var(--surface-2); color: var(--foreground); font-weight: 400; }
.loop-seg-btn:hover { color: var(--foreground); }
.loop-newpill { background: #1a1830; color: var(--violet); font-size: var(--cap-size); font-weight: 400; padding: 2px 8px; border-radius: var(--radius); }
.loop-sx { margin-left: auto; color: var(--muted-2); cursor: pointer; font-size: var(--body-size); background: none; border: none; flex: none; }
.loop-sx:hover { color: var(--red); }

/* + add step */
.loop-addstep {
  padding: 8px 11px; background: var(--surface); display: flex; align-items: center;
  gap: var(--sp-1); flex-wrap: wrap; font-size: var(--body-size);
}
.loop-addstep-lead { color: var(--muted-2); }
.loop-addstep-btn {
  background: transparent; border: 1px solid var(--border); color: var(--muted);
  font-size: var(--body-size); padding: 0 9px; cursor: pointer; border-radius: var(--radius); height: var(--control-h-sm);
}
.loop-addstep-btn:hover { background: var(--surface-2); color: var(--foreground); border-color: var(--border-strong); }

/* stop-when row */
/* run-order row (chain by chain | staggered) */
.loop-order {
  display: flex; align-items: center; gap: var(--sp-4); margin-top: 14px; flex-wrap: wrap;
}
.loop-order-lead { margin: 0; }
.loop-order-sep { color: var(--muted-2); }
.loop-order-tail { color: var(--muted-2); font-size: var(--body-size); }
.loop-stagger-num { width: var(--num-w-sm); margin-left: 2px; }

.loop-stop {
  display: flex; align-items: center; gap: var(--sp-4); margin-top: 16px; padding-top: 14px;
  border-top: 1px solid var(--border); flex-wrap: wrap;
}
.loop-stop-lead { margin: 0; }
.loop-opt { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--body-size); color: var(--muted); cursor: pointer; }
.loop-opt.on { color: var(--foreground); }
.loop-radio { width: 13px; height: 13px; border-radius: 50%; border: 1px solid var(--border-strong); flex: none; }
.loop-radio.on { border-color: var(--foreground); box-shadow: inset 0 0 0 3px var(--foreground); }
.loop-stop-num {
  width: var(--num-w-sm); height: var(--control-h-sm); background: var(--background); border: 1px solid var(--border);
  color: var(--foreground); text-align: center; font-size: var(--body-size); border-radius: var(--radius);
  font-family: var(--num); font-variant-numeric: tabular-nums;
}
.loop-stop-dir, .loop-stop-asset { height: var(--control-h-sm); font-size: var(--body-size); width: auto; }

/* live status line */
.loop-live { margin-top: 12px; font-size: var(--body-size); color: var(--muted); font-family: var(--num); min-height: 16px; }
.loop-live b { color: var(--foreground); }
.loop-live-sep { color: var(--muted-2); }
.loop-live-note { color: var(--muted-2); font-style: italic; }
.loop-neg { color: var(--red); }
.loop-pos { color: var(--green); }

/* gas reserve (on the stop-when row) */
.loop-gasres { display: inline-flex; align-items: center; gap: var(--sp-2); margin-left: auto; }
.loop-gasres-lead { margin: 0; }
.loop-gasres-num { width: var(--num-w-md); text-align: center; }
.loop-gasres-tail { color: var(--muted-2); font-size: var(--body-size); }

/* subsidiary family list */
.loop-subs { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }
.loop-subs-head { margin-bottom: 8px; display: flex; align-items: baseline; gap: var(--sp-2); }
.loop-subs-count { color: var(--muted-2); font-weight: 400; }
.loop-subs-selall {
  margin-left: auto; background: none; border: 1px solid var(--border-strong);
  color: var(--muted); font-family: inherit; font-size: var(--cap-size); padding: 1px 8px; cursor: pointer;
}
.loop-subs-selall:hover { color: var(--foreground); border-color: var(--muted); }
.loop-subs-list { display: flex; flex-direction: column; border: 1px solid var(--border); }
.loop-subs-list + .loop-subs-chain { margin-top: 10px; }
/* Per-chain label above each chain's subsidiary block (multi-chain loops). */
.loop-subs-chain {
  display: flex; align-items: baseline; gap: var(--sp-0); margin-bottom: 5px;
  font-size: var(--eyebrow-size); letter-spacing: var(--eyebrow-track); text-transform: uppercase;
  color: var(--muted-2);
}
.loop-subs-chain-nm { font-weight: var(--eyebrow-weight); color: var(--muted); }
.loop-subs-chain-ct { font-weight: var(--eyebrow-weight); }
/* A subsidiary row is the SINGLE-LINE wallet again (dot · name · address ·
   balance), so it takes the compact rung, the shared gap and the shared inset. */
.loop-sub {
  display: flex; align-items: center; gap: var(--chip-gap); padding: 0 var(--chip-pad-x);
  height: var(--control-h-sm);
  border-bottom: 1px solid var(--border); background: var(--card);
}
.loop-sub:last-child { border-bottom: 0; }
.loop-sub { cursor: pointer; user-select: none; }
.loop-sub:hover { background: var(--surface); }
.loop-sub.selected { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--ring); }
.loop-sub.dusted { opacity: .5; cursor: default; }
.loop-sub.dusted:hover { background: none; }
.loop-sub-addr { color: var(--muted-2); }
.loop-sub-bal { margin-left: auto; color: var(--muted); }

/* ---- Quick actions: right-click wallet menu + buy/sell params modal ---- */
.qa-menu {
  /* A FIXED WIDTH, not a minimum. Every hint in here is a fact about the selection —
     a count, an amount — and one long one used to stretch the menu to fit it, so the
     same menu came out a different size depending on what was selected. 210px holds
     the longest label and hint pair; anything longer is the hint's problem to shorten
     (see the unwrap entry, which prints a figure and no unit). */
  position: fixed; z-index: 1000; width: 210px;
  background: var(--float); border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-pop); padding: 4px;   /* items carry their own inset */
}
.qa-menu-head {
  font-size: var(--eyebrow-size); letter-spacing: var(--eyebrow-track); text-transform: uppercase;
  color: var(--muted-2); font-weight: var(--eyebrow-weight); padding: 6px 9px 5px;
}
.qa-menu-item {
  display: flex; align-items: baseline; gap: var(--sp-3); width: 100%;
  background: none; border: 0; text-align: left; cursor: pointer;
  padding: 8px 9px; color: var(--foreground); font-family: inherit; font-size: var(--body-size);
}
.qa-menu-item:hover { background: var(--surface); }
.qa-menu-label { font-weight: 400; }
.qa-menu-sub { margin-left: auto; font-size: var(--cap-size); color: var(--muted-2); }
.qa-menu-sep { height: 1px; background: var(--border); margin: 4px 2px; }

.qa-params { min-width: 380px; display: flex; flex-direction: column; gap: var(--sp-3); }
/* ---- THE AMOUNTS ARE TRANSFER'S CONFIGURE BOX ----
   The same three rows it carries — Variance, Total ETH, Total <coin> — because this is
   that question from the other end: Transfer splits one wallet's total across many, a
   gather pulls a total out of many into one. It was a panel of its own here (one big
   figure on a draggable ground, the Swap box's), which made the one percentage you set
   from a dialog the one you set differently from every other.
   The rows carry .act-row, so the geometry comes from the panels rather than from a
   second copy of it that can drift; only the width is stated here. */
.qa-params-fields { width: 100%; display: flex; flex-direction: column; gap: var(--sp-2); }
.qa-params-fields .act-row { padding: 0; }
/* THE COLUMNS ARE THE PANEL'S COLUMNS. .act-row is a flex line — label, track, figure
   — and what lines those up across rows is the Configure BOX around them: a fixed 84px
   label, a track that takes the slack, and a readout reserve. Every one of those rules
   is scoped to .act-pane .act-box-cfg, so in a dialog each row sized its own label and
   every track started and ended somewhere different. The same three, for the one place
   outside a pane that shows these rows. 56px is Transfer's reserve, and for the same
   reason: these readouts are bare figures with the unit in the label beside them. */
.qa-params-fields { --act-read-w: 56px; }
.qa-params-fields .act-row > .act-label { flex: 0 0 84px; width: 84px; }
.qa-params-fields .act-row > .act-track { flex: 1 1 auto; min-width: 0; }
.qa-params-fields .act-row > .act-read {
  flex: 0 0 var(--act-read-w); text-align: right; white-space: nowrap;
}
/* (The gather dialog no longer has a field of its own — its figures are .act-read
   readouts you click to type into. .qa-params-amt is the loop-preset name modal's
   text input, which has always borrowed this dialog's shell, and it keeps the app's
   ordinary input box.) */
.qa-params-hint { font-size: var(--cap-size); color: var(--muted-2); line-height: 1.45; }
.qa-params-foot { display: flex; justify-content: flex-end; gap: var(--sp-2); }

/* Rail footer: selected count + Clear (deselect all) */
.rail-foot { display: flex; align-items: center; gap: var(--sp-2); }
.rail-foot-count { flex: 1; }
.rail-foot-clear {
  background: none; border: 1px solid var(--border-strong); color: var(--muted);
  font-family: inherit; font-size: var(--cap-size); padding: 2px 10px; cursor: pointer;
}
.rail-foot-clear:hover { color: var(--foreground); border-color: var(--muted); }

/* ---------------------------------------------------------------- LAUNCH */
.launch-signer {
  display: flex; align-items: flex-end; gap: var(--sp-3); flex-wrap: wrap;
  margin-bottom: 18px; padding-bottom: 16px; border-bottom: 1px solid var(--border);
}
.launch-signer .field { flex: 1 1 320px; margin: 0; }
.launch-token-badge {
  font-family: var(--mono); font-size: var(--body-size); color: var(--foreground);
  background: var(--surface); border: 1px solid var(--border-strong);
  padding: 6px 10px; border-radius: 0; align-self: center;
  display: flex; align-items: center; gap: var(--sp-2); word-break: break-all;
}
.launch-token-badge b { color: var(--muted); font-family: inherit; font-weight: 400; }
.launch-step {
  border: 1px solid var(--border); background: var(--card);
  padding: 16px 16px 14px; margin-top: 14px;
}
.launch-step-head { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: 12px; }
.launch-step-head h3 { margin: 0; font-size: var(--title-size); font-weight: var(--title-weight); }
.launch-step-n {
  flex: 0 0 auto; width: 22px; height: 22px; border-radius: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface); border: 1px solid var(--border-strong);
  color: var(--muted); font-size: var(--body-size); font-weight: 400; font-family: var(--num); font-variant-numeric: tabular-nums;
}
.launch-step .fields { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.launch-step .field { flex: 1 1 180px; margin: 0; }
.launch-step .field.wide { flex-basis: 100%; }
.launch-step .run-actions { margin-top: 14px; }
.launch-step.launch-locked { opacity: 0.55; }
.launch-step.launch-locked input,
.launch-step.launch-locked select,
.launch-step.launch-locked .btn { pointer-events: none; }

/* Template picker (choose the contract template to deploy) */
.launch-template {
  display: flex; align-items: flex-end; gap: var(--sp-3); flex-wrap: wrap;
  margin-bottom: 18px; padding-bottom: 16px; border-bottom: 1px solid var(--border);
}
.launch-template .field { flex: 1 1 320px; margin: 0; }
.launch-template-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; align-self: center; }
/* Constructor-input type tag next to each generated field label */
.ctor-type { color: var(--muted); font-size: var(--cap-size); font-weight: 400; }
/* Add-template modal: name + artifact/ABI/bytecode textareas */
.tpl-modal { max-width: 620px; width: 92vw; }
.tpl-help { color: var(--muted); font-size: var(--body-size); line-height: 1.5; margin-bottom: 12px; }
.tpl-modal .field { margin: 0 0 12px; }
.tpl-ta {
  width: 100%; resize: vertical; font-size: var(--body-size); line-height: 1.4;
  background: var(--surface); border: 1px solid var(--border); border-radius: 0;
  color: var(--foreground); padding: 8px 10px;
}

/* ---------------------------------------------------------------- MANAGE */
.manage-target {
  margin-bottom: 4px; padding-bottom: 16px; border-bottom: 1px solid var(--border);
}
.manage-target .fields { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.manage-target .field { flex: 1 1 320px; margin: 0; }
.manage-target .field.wide { flex-basis: 100%; }
.manage-target .run-actions { margin-top: 12px; }

/* Read-only status strip painted after Load. */
.manage-status {
  margin-top: 14px; border: 1px solid var(--border); background: var(--card);
  padding: 12px 14px; display: flex; flex-wrap: wrap; gap: var(--sp-2) 20px;
}
.manage-status .ms-item { display: flex; flex-direction: column; gap: var(--sp-0); min-width: 90px; }
.manage-status .ms-k { font-size: var(--cap-size); font-weight: 400; color: var(--muted); text-transform: uppercase; letter-spacing: var(--eyebrow-track); }
.manage-status .ms-v { font-size: var(--body-size); color: var(--foreground); font-variant-numeric: tabular-nums; }
.manage-status .ms-v.mono { font-family: var(--mono); word-break: break-all; }
.manage-status .ms-warn {
  flex-basis: 100%; margin-top: 4px; padding: 8px 10px;
  border: 1px solid var(--danger, #c0392b); color: var(--danger, #c0392b);
  background: color-mix(in srgb, var(--danger, #c0392b) 8%, transparent);
  font-size: var(--body-size);
}
.manage-status .ms-ok { color: var(--green); }
.manage-status .ms-off { color: var(--muted); }

.manage-group { margin-top: 22px; }
.manage-group-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: 6px; }
.manage-group-head h3 {
  margin: 0; font-size: var(--cap-size); font-weight: 400; text-transform: uppercase;
  letter-spacing: var(--eyebrow-track); color: var(--muted);
}
.manage-action {
  border: 1px solid var(--border); background: var(--card);
  padding: 14px 16px 12px; margin-top: 10px;
}
.manage-act-head { margin-bottom: 10px; }
.manage-act-title { font-size: var(--body-size); font-weight: 400; color: var(--foreground); }
.manage-impact { font-size: var(--body-size); color: var(--muted); margin-top: 2px; }
.manage-action .fields { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.manage-action .field { flex: 1 1 180px; margin: 0; }
.manage-action .field.wide { flex-basis: 100%; }
.manage-action .run-actions { margin-top: 12px; }
.manage-current {
  padding: 8px 10px; border: 1px solid var(--border-strong); background: var(--surface);
  color: var(--foreground); font-size: var(--body-size);
}
/* Owner banner "IS the owner" (positive) variant of .ms-warn. */
.manage-status .ms-warn.ms-ok-banner {
  border-color: var(--green); color: var(--green);
  background: color-mix(in srgb, var(--green) 8%, transparent);
}

/* ABI-driven Read panel: one row per view function. */
.manage-read-row {
  border: 1px solid var(--border); background: var(--card);
  padding: 12px 14px; margin-top: 10px;
}
.manage-read-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: 8px; }
.manage-read-fn { font-size: var(--body-size); font-weight: 400; color: var(--foreground); }
.manage-read-row .fields { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.manage-read-row .field { flex: 1 1 180px; margin: 0; }
.manage-read-row .field.wide { flex-basis: 100%; }
.manage-read-row .run-actions { margin-top: 10px; }
.manage-read-result {
  margin-top: 8px; padding: 8px 10px; border: 1px solid var(--border-strong);
  background: var(--surface); color: var(--foreground); font-size: var(--body-size);
  word-break: break-all; min-height: 1.4em;
}
.manage-read-val { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-1); }
.manage-read-val + .manage-read-val { margin-top: 4px; }
.manage-read-name { color: var(--muted); font-size: var(--body-size); }
.manage-read-type { color: var(--muted); font-size: var(--cap-size); }
.manage-read-out { color: var(--foreground); font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------- LOOPS: max-wallets, presets, gather+dust */
/* Max-wallets cap row reuses .loop-order layout; number field matches stop-num. */
.loop-walletcap-num { width: var(--num-w-sm); text-align: center; }

/* Card-head "Save preset" + toolbar "Load preset…" (btn base styles the rest). */
.loop-savepreset { white-space: nowrap; }

/* Preset picker modal. */
.loop-preset-picker { min-width: 340px; max-width: 92vw; display: flex; flex-direction: column; gap: var(--sp-3); }
.loop-preset-list { display: flex; flex-direction: column; border: 1px solid var(--border); max-height: 50vh; overflow-y: auto; }
.loop-preset-row { display: flex; align-items: stretch; border-bottom: 1px solid var(--border); }
.loop-preset-row:last-child { border-bottom: 0; }
.loop-preset-pick {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-0); text-align: left;
  background: none; border: 0; color: var(--foreground); font-family: inherit; cursor: pointer; padding: 9px 11px;
}
.loop-preset-pick:hover { background: var(--surface); }
.loop-preset-nm { font-weight: 400; font-size: var(--body-size); }
.loop-preset-sub { color: var(--muted-2); font-size: var(--body-size); }
.loop-preset-del {
  flex: 0 0 auto; background: none; border: 0; border-left: 1px solid var(--border);
  color: var(--muted-2); cursor: pointer; font-size: var(--body-size); padding: 0 11px;
}
.loop-preset-del:hover { color: var(--red); background: var(--surface); }

/* Sub-panel "Gather + dust" (sits after "select all", accented). */
.loop-subs-gatherdust {
  background: none; border: 1px solid var(--ring);
  color: var(--foreground); font-family: inherit; font-size: var(--cap-size); padding: 1px 8px; cursor: pointer;
}
.loop-subs-gatherdust:hover { background: var(--surface-2); }

/* Left-rail group "Gather + dust" action row (below an open group's header). */
.rail-group-actions { padding: 4px 8px 6px 26px; }
.rail-group-gatherdust {
  background: none; border: 1px solid var(--border-strong); color: var(--muted);
  font-family: inherit; font-size: var(--cap-size); padding: 1px 8px; cursor: pointer;
}
.rail-group-gatherdust:hover { color: var(--foreground); border-color: var(--ring); }

/* ---------------------------------------------------------------- WALLET VAULT: dusted dropdown */
.vault-dust { border-top: 1px solid var(--border); margin-top: 8px; }
.vault-dust-head {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%; padding: 9px 4px;
  background: none; border: 0; cursor: pointer; color: var(--muted);
  font-family: inherit; font-size: var(--body-size); text-align: left;
}
.vault-dust-head:hover { color: var(--foreground); }
.vault-dust-caret { width: 10px; color: var(--muted-2); }
.vault-dust-label { text-transform: uppercase; letter-spacing: var(--eyebrow-track); font-weight: var(--eyebrow-weight); }
.vault-dust-count {
  color: var(--muted-2); font-variant-numeric: tabular-nums;
  border: 1px solid var(--border-strong); border-radius: var(--radius); padding: 0 7px; font-size: var(--cap-size);
}
.vault-dust-list { display: flex; flex-direction: column; opacity: .82; }

/* ---------------------------------------------------------------- VAULT GATE (locked popup) */
/* Full-screen blocker: nothing behind it is reachable until the vault unlocks. */
.vault-gate {
  position: fixed; inset: 0; z-index: 6000;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--background) 78%, transparent);
  backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px);
}
.vault-gate-card {
  width: 340px; max-width: 92vw; padding: var(--pad-modal);
  background: var(--float); border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-modal);
  display: flex; flex-direction: column; gap: var(--sp-3);
}
.vault-gate-brand {
  font-size: var(--title-size); font-weight: var(--title-weight); color: var(--foreground);
  margin-bottom: 2px;
}
.vault-gate-title { margin: 0; font-size: var(--title-size); font-weight: var(--title-weight); color: var(--foreground); }
.vault-gate-sub { margin: 0 0 6px; font-size: var(--body-size); line-height: 1.45; color: var(--muted); }
.vault-gate-input {
  width: 100%; height: var(--control-h); padding: 0 11px;
  background: var(--surface); border: 1px solid var(--border); color: var(--foreground);
  font-family: inherit; font-size: var(--body-size);
}
.vault-gate-input::placeholder { color: var(--muted-2); }
.vault-gate-input:focus { border-color: var(--ring); box-shadow: 0 0 0 1px var(--ring); }
.vault-gate-btn { width: 100%; margin-top: 5px; }
.vault-gate-err { color: var(--red); font-size: var(--body-size); min-height: 1em; }

/* ---------------------------------------------------------------- WETH-HOLDING WALLETS */
/* A wallet holding any WETH shows its balance amber/yellow everywhere it appears
   (WETH is folded into the ETH figure). Selectors are per-site so this wins over
   each place's base text colour. */
.bal-weth,
.rail-wallet-eth.bal-weth,
.wallet-chip-bal.bal-weth,
.loop-sub-bal.bal-weth,
.loop-startw-bal.bal-weth { color: var(--amber); }

/* ---------------------------------------------------------------- SHORTCUTS */
#shortcuts-body { display: flex; flex-direction: column; gap: var(--sp-4); }

/* Rail create/import buttons. */
.rail-head-actions { display: flex; align-items: center; gap: var(--sp-1); }

/* Small create/import modal (rail). */
.sc-modal-backdrop {
  position: fixed; inset: 0; z-index: 1000;
  /* THE SAME GROUND THE CONFIRM SHEET DIMS TO (.picker-overlay). It was 70% of
     --background over whatever was behind it, which is a different, lighter dim than the
     sheet's — so opening a dialog and opening a sheet darkened the app by two different
     amounts and read as two different kinds of interruption. */
  background: rgba(0, 0, 0, .6);
  display: flex; align-items: center; justify-content: center;
}
.sc-modal {
  width: min(92vw, 380px); background: var(--float);
  border: 1px solid var(--border-strong); border-radius: var(--radius, 0px); padding: var(--pad-modal);
  /* NO DROP SHADOW. The dim behind it is what separates it from the page; a shadow on
     top of that is a second answer to a question already answered, and at --shadow-modal
     it spread far enough to read as a smudge around the box rather than as depth. */
  display: flex; flex-direction: column; gap: var(--sp-3);
}
.sc-modal-title { margin: 0; font-size: var(--title-size); font-weight: var(--title-weight); }
/* ---- one dialog chrome, built by scModalShell ------------------------------
   Title on the left, ✕ on the right — the same corner the confirm sheet puts it in, so
   a dialog is closable from the same place wherever you meet one. */
.sc-modal-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.sc-modal-x { flex: 0 0 auto; }
.sc-modal-body { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; }
/* 70 : 30, FILLING THE WIDTH. The two used to sit at whatever their words measured, so
   "Generate" and "Cancel" came out nearly identical and the dialog's action did not read
   as the action. GRID, not flex: the buttons are border-box, so a flex-basis of 0 is
   floored at padding+border and each one enters the split already carrying 26px — which
   lands at 65:33, not 70:30. Grid tracks know nothing about an item's padding.
   (Same reasoning as .act-cluster, which splits Start / Recover.) */
.sc-modal-foot {
  /* HUGGING, and left-aligned under the fields. Stretched 70:30 across the dialog these
     two became the loudest thing in a box whose job is to take a name — a full-width
     accent bar for "Generate" is the weight a confirm sheet's action earns, not a
     one-field dialog's. Content width puts them back in proportion, and they start on
     the same edge every label and field above them starts on. */
  display: flex; align-items: center; gap: var(--sp-2);
  /* On top of .sc-modal's own 12px gap, so the break between the FIELDS and the ACTION
     is 24px — one rung up the scale from the gap that separates one field from the next.
     At the bare gap the buttons read as a third field rather than as what you press when
     the fields are done. */
  margin-top: var(--sp-3);
}
/* The dialog arrives and leaves like the confirm sheet, on the same two keyframes —
   see the note beside .picker-overlay. */
.sc-modal-backdrop {
  animation: picker-in .14s ease-out both;
  transition: opacity .14s ease-in;
}
.sc-modal-backdrop > .sc-modal { animation: picker-rise .18s cubic-bezier(.22, 1, .36, 1) both; }
.sc-modal-backdrop.is-closing { animation: none; opacity: 0; pointer-events: none; }
.sc-modal-backdrop.is-closing > .sc-modal {
  animation: none;
  transition: opacity .14s ease-in, transform .14s ease-in;
  opacity: 0; transform: translateY(6px) scale(.99);
}
@media (prefers-reduced-motion: reduce) {
  .sc-modal-backdrop, .sc-modal-backdrop > .sc-modal { animation: none; transition: none; }
}
/* The count row: label, a track that takes the slack, then the typed box. Same anatomy
   as every .act-row in Configure, so the two read as one control vocabulary. */
.sc-modal-amount { display: flex; align-items: center; gap: var(--sp-2); }
.sc-modal-amount > .act-label { flex: 0 0 auto; }
.sc-modal-amount > .act-range,
.sc-modal-amount > .act-track:not(.act-numbox) { flex: 1 1 auto; min-width: 0; }
.sc-modal-amount > .act-numbox { flex: 0 0 72px; }

/* Shortcut cards grid. */
.sc-cards {
  display: grid; gap: var(--sp-4);
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
}
/* ONE interior inset on all four sides — the card used to be 16px top/bottom
   against 18px left/right, so nothing inside lined up with the title. */
.sc-card {
  border: 1px solid var(--border);
  border-radius: var(--radius, 0px);
  background: var(--surface);
  padding: var(--pad-card);
  display: flex; flex-direction: column; gap: var(--sp-3);
}
.sc-card-head { display: flex; align-items: center; gap: var(--sp-2); }
/* h3. prefix REQUIRED. `.panel h3` (0,1,1) sets 14px and every shortcut card's
   title is an <h3>, so a bare `.sc-card-title` (0,1,0) loses to it — which is why
   Logs rendered at 14 while Activity/Sweep/Transfer rendered at 20: those three
   were rescued by a scoped rule in the overrides layer that simply did not list
   Logs. Matching the element here fixes it at the root, for every card. */
h3.sc-card-title, .sc-card-title { font-size: var(--title-size); font-weight: var(--title-weight); line-height: var(--title-line); margin: 0; }
.sc-card-sub { margin: -6px 0 2px; }
.sc-card-body { display: flex; flex-direction: column; gap: var(--sp-3); }

/* Drop slots with a big +. */
.sc-two-slots { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.sc-dropzone {
  position: relative;
  min-height: 56px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius, 0px);
  background: var(--background);
  padding: 0;
  overflow-x: hidden; overflow-y: auto; scrollbar-width: none;
  display: flex; flex-wrap: wrap; align-items: center; gap: 0;
}
.sc-dropzone::-webkit-scrollbar { display: none; }
/* Same rule for the shortcut zones: the highlight lives inside the box. This one used
   its own border, which at least stayed in bounds — but a zone whose border is zeroed
   by a pane (Sweep, Transfer, the rail drawer) had nothing left to recolour. */
/* THE ACCENT, matching .wallet-dropzone.drop-hover — these are the same gesture landing
   on two kinds of container, and they were drawn in two different colours. (It was
   violet before that, from an older palette; then dimmed white. A drop target is not a
   category, it is "here", and "here" is the accent everywhere else in the app.)
   The fill is a wash rather than --surface-2 for the same reason the selected row's is:
   an opaque grey buries the box's own hairline at the exact moment you are aiming at
   its edges. */
.sc-dropzone.drop-hover {
  border-color: var(--accent); border-style: solid;
  /* Outline, inset — same reason as .wallet-dropzone.drop-hover above: an inset
     shadow paints under the cards and disappears behind a full list. */
  outline: 2px solid var(--accent); outline-offset: -2px;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
/* ---- a wallet in hand, and the slot it would land in ----------------------
   The lifted row is `position: fixed` at the rect it had (set inline by
   scWireCardLift), so it is out of flow the moment you pick it up and the list closes
   over the gap on its own. Above the sticky column header — see --z-wallet-lift —
   because the one thing the pointer is holding must not slide under the table it came
   out of. pointer-events: none is load-bearing rather than cosmetic: the hit test has
   to read the row UNDERNEATH the one in hand or the placeholder never moves. */
.sc-card-lift {
  z-index: var(--z-wallet-lift); pointer-events: none; cursor: grabbing;
  box-shadow: var(--shadow-pop);
  transition: none;
}
/* The slot. A plain gap, not a dashed outline: the rows sliding around it already say
   where the drop goes, and an outlined box in the middle of a list reads as one more
   row rather than as the absence of one. It animates its own height so appearing and
   disappearing — which is what leaving and re-entering a box does — is a movement
   rather than a jump. */
.sc-card-ph {
  flex: 0 0 auto; width: 100%; box-sizing: border-box;
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--accent) 55%, transparent);
  pointer-events: none;
}
/* The rows that are NOT in hand slide to make room. The FLIP in scWireCardLift sets a
   start offset and clears it; this is the transition it rides home on. */
.sc-wallet-list > .sc-wallet-card { transition: transform .16s ease; }
.sc-wallet-list > .sc-wallet-card.sc-card-lift { transition: none; }
.sc-dropzone-multi { min-height: 84px; align-content: flex-start; }
/* The min-height belongs to the EMPTY state — it is the size of the target you
   have to hit. Once a zone holds cards it sizes to them, so one or two wallets
   no longer float in a reserved void. */
.sc-dropzone:has(.sc-wallet-list) { min-height: 0; align-items: stretch; align-content: stretch; }
/* Empty state: one centred + / hint pair on a sane internal scale, so a tall
   zone reads as a deliberate target instead of content floating in a void. */
.sc-drop-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-1); width: 100%; padding: 16px 12px; color: var(--muted-2); user-select: none;
}
.sc-plus { font-size: var(--title-size); line-height: 1; font-weight: 400; color: var(--muted-2); }
/* SAME MARK SIZE, DIFFERENT GLYPH. Every empty drop slot in the app puts one
   character over its hint, and the Sequence list's is an arrow rather than a plus —
   which at the same font-size is not the same size at all: measured at 20px, "+"
   paints 10px of ink and "↕" paints 19, because a plus sits in the middle of its em
   box and an arrow fills it. It read as a different, louder kind of mark.
   The BOX stays 20px so the empty state's height matches the other slots exactly;
   only the glyph inside it comes down to where its ink measures the same 10. */
.sc-plus-tall {
  display: inline-flex; align-items: center; justify-content: center;
  height: var(--title-size); font-size: 11px;
}
.sc-drop-hint { font-size: var(--body-size); line-height: 1; }

/* ---- Wallet cards in drop slots ----
   THE SAME chip as .dz-chip, in list form. The second line is the ADDRESS, not
   filler, and reads identity-left / holdings-right like the rail row.

   The card is ALWAYS a grid whose parts sit in named areas — `display: contents`
   dissolves the .sc-wallet-id wrapper so name and balance are grid items in both
   modes, instead of a flex pair fighting over one column (which is what
   truncated a name to "A 1…"). ✕ and the type scale are identical in both, so a
   wallet reads the same wherever it is dropped. */
.sc-wallet-list { width: 100%; display: flex; flex-direction: column; }
.sc-wallet-card {
  display: grid; align-items: center;
  grid-template-columns: minmax(0, 1fr) auto auto var(--chip-x);
  grid-template-areas: "name bal eth x";
  column-gap: var(--chip-gap);
  height: var(--control-h-sm); padding: 0 var(--chip-pad-x);
  border-bottom: 1px solid var(--border);
  transition: background .12s;
}
.sc-wallet-card:last-child { border-bottom: none; }
/* --surface, not --surface-2: #191919, the tone the rail's own rows hover to. The two
   lists sit side by side and were hovering to different greys. */
.sc-wallet-card:hover { background: var(--surface); }
.sc-wallet-id { display: contents; }
/* The name cell holds TWO things now — the title and, when the wallet belongs to
   one, its group badge — so the cell lays them out and the TITLE is the part that
   ellipsises. Same arrangement as the rail's name line, for the same reason: the
   badge keeps its shape and the name gives way. */
.sc-wallet-name {
  grid-area: name; min-width: 0;
  display: flex; align-items: center; gap: var(--sp-1);
}
.sc-wallet-title {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Address — rendered by scWalletCard() in BOTH densities, shown only where there
   is a second line to put it on. `display: none` (not a missing element) so the
   one-line grid, which has no `addr` area, can't auto-place it into a stray
   column. Unhidden by the .sc-dropzone rule below. */
.sc-wallet-addr {
  grid-area: addr; display: none; min-width: 0;
  font-family: var(--mono); color: var(--muted); letter-spacing: .01em;
  font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sc-wallet-bal {
  grid-area: bal; min-width: 0; text-align: right;
  color: var(--muted); font-family: var(--num);
  font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sc-wallet-bal.dim { color: var(--muted-2); }
.sc-wallet-eth {
  grid-area: eth; text-align: right; white-space: nowrap;
  font-family: var(--num); font-variant-numeric: tabular-nums; color: var(--text);
}
.sc-wallet-eth.dim { color: var(--muted-2); }

/* Narrow density — EVERY drop slot, single or multi, so the two slots of one
   card (Transfer's FROM and TO, Sweep's BUYERS and SELLERS) always match.
   Two lines, each reading identity-left → holdings-right:
     line 1  NAME .................. ETH · ✕
     line 2  0x1234…5678 ............ token
   Row 2 stops at the figure column, so the token figure and the ETH figure stack
   into ONE right edge instead of one ending under the ✕. */
.sc-dropzone .sc-wallet-card {
  grid-template-columns: minmax(0, 1fr) auto var(--chip-x);
  grid-template-areas:
    "name eth x"
    "addr bal .";
  height: var(--chip-h-2); row-gap: var(--sp-0); column-gap: var(--chip-gap);
  padding: 0 var(--chip-pad-x);
}
.sc-dropzone .sc-wallet-card .sc-wallet-addr { display: block; }
.sc-dropzone .sc-wallet-x { align-self: center; }

.sc-wallet-x {
  grid-area: x; justify-self: end;
  width: var(--chip-x); height: var(--chip-x); flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: none; cursor: pointer;
  color: var(--muted-2); font-size: var(--cap-size); line-height: 1; padding: 0;
  opacity: 0; transition: opacity .12s, color .12s;
}
.sc-wallet-card:hover .sc-wallet-x { opacity: 1; }
.sc-wallet-x:hover { color: var(--red); background: var(--surface-2); }


/* Form rows. Every control in a row lands on ONE bottom edge (align-items:end)
   and every field hugs its control, so a row of three fields does not stretch a
   two-digit input to a third of the card. Buttons share the control rung. */
.sc-row { display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-2); align-items: flex-end; }
.sc-row > .btn { flex: 0 0 auto; height: var(--control-h); }
.sc-field { display: flex; flex-direction: column; gap: var(--sp-1); flex: 0 1 auto; min-width: 0; }
.sc-field-label {
  font-size: var(--label-size); font-weight: var(--label-weight);
  line-height: var(--body-line); color: var(--foreground);
  white-space: nowrap;
}
/* A hint under a control must not set the field's width — it wraps inside it. */
.sc-field-hint { font-size: var(--cap-size); color: var(--muted); max-width: 34ch; }
/* Recessed field on a --surface card. `background-color` (not the `background`
   shorthand) so a <select class="sc-input"> keeps the app-wide chevron image.
   Height comes from the ladder, so only the horizontal inset is set here — and
   the select's chevron gutter (--sel-gut) survives untouched. */
.sc-input {
  background-color: var(--background); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius, 0px);
  padding: 0 10px; font: inherit; font-size: var(--body-size); height: var(--control-h);
}
/* …and the shorthand above must not eat that gutter back off a <select>. */
select.sc-input { padding-right: var(--sel-gut); }
.sc-input:focus { outline: 2px solid var(--ring); outline-offset: -1px; border-color: var(--ring); }
.sc-select { width: auto; min-width: 132px; padding-left: 10px; }
.sc-calc { margin: 0; font-size: var(--body-size); color: var(--muted); font-variant-numeric: tabular-nums; }

/* Shared IO wrappers — Sweep & Transfer re-parent their two wallet slots into
   the SAME structure so one rule styles both identically. .sc-io-row = the two
   captioned boxes side by side (equal widths); .sc-io-box = one box (caption +
   its existing drop slot); .sc-io-cap = the small uppercase caption above the
   slot; .sc-fields = the settings-field group below the slots. */
.sc-io-row { display: flex; gap: var(--sp-3); align-items: stretch; }
.sc-io-box { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.sc-io-cap {
  font-size: var(--label-size); font-weight: var(--label-weight);
  line-height: var(--body-line); color: var(--foreground);
}
.sc-fields { display: flex; flex-direction: column; gap: var(--sp-3); }
@media (max-width: 560px) { .sc-io-row { flex-direction: column; } }

/* Live net-position readout (Activity). */
.sc-net {
  font-size: var(--body-size); font-variant-numeric: tabular-nums;
  min-height: 1em; margin-bottom: 2px;
}
.sc-net-label {
  color: var(--muted); font-weight: var(--eyebrow-weight); text-transform: uppercase;
  font-size: var(--eyebrow-size); letter-spacing: var(--eyebrow-track); margin-right: 8px;
}
.sc-net-eth, .sc-net-tok { font-weight: 400; font-family: var(--mono); }

/* Net steering slider (Activity). */
.sc-lean-row { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.sc-lean { flex: 1 1 180px; min-width: 140px; accent-color: var(--violet); cursor: pointer; }
.sc-lean-cap { font-size: var(--cap-size); color: var(--muted); }
.sc-lean-read { font-size: var(--body-size); font-variant-numeric: tabular-nums; color: var(--text); min-width: 96px; }

/* Per-wallet net projection panel (Activity). */
.sc-proj { display: flex; flex-direction: column; gap: var(--sp-0); margin: 2px 0 2px; }
.sc-proj-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2);
  font-size: var(--body-size); font-variant-numeric: tabular-nums;
}
.sc-proj-name { font-weight: 400; min-width: 90px; }
.sc-proj-net { font-family: var(--mono); font-weight: 400; }
.sc-proj-final { color: var(--muted); font-family: var(--mono); }

/* Run button + status. A run button that is READY is the strongest thing in its
   box: it takes the app's white primary fill, the same treatment the vault gate's
   Continue button uses. It used to take a neutral --surface-2 chip instead, which
   is very nearly what :disabled paints — so "ready to run" and "cannot run" looked
   alike and the primary action did not read as primary at all. Disabled keeps the
   dimmed rule below; sw-armed keeps its own quiet outline. */
.sc-run { align-self: flex-start; }
/* ---- the run button names its engine ----
   Words at one end, the engine's own glyph at the other — the same mark the rail's quick
   action for that destination carries, so "the ⇄ one" means Swap whether you meet it on
   a wallet row or on the button that runs it. `space-between` rather than a gap: the
   button spans its Configure box now, and a centred label+icon pair in a 306px control
   reads as a word with a symbol stuck to it, with dead space on both flanks. Pushed
   apart, the glyph parks in the corner and the label keeps the left edge every label in
   the box above it already shares. */
.btn.sc-run-iconed { justify-content: space-between; }
/* The label can ellipsize (Swap's grows to "Run swap on 11 wallets"); the glyph never
   shrinks — a 14px icon would read as a different icon. */
.sc-run-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.sc-run svg {
  width: 16px; height: 16px; display: block; flex: 0 0 auto;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}
/* Transport marks are SOLID. A hollow triangle at 16px reads as a cursor or a "next"
   chevron; play is a filled wedge everywhere it has ever appeared. The stroke stays on
   underneath so the corners carry the same 1.5px round join as the outlined three. */
.sc-run svg.sc-run-ico-solid { fill: currentColor; }
.sc-run.primary:not(:disabled):not(.sw-armed) {
  background: var(--primary); border-color: var(--primary); color: var(--primary-fg);
  font-size: var(--body-size); font-weight: 400;
}
.sc-run.primary:not(:disabled):not(.sw-armed):hover {
  background: var(--primary-hover); border-color: var(--primary-hover);
}
.sc-run.primary:not(:disabled):not(.sw-armed):focus-visible {
  outline: 2px solid var(--ring); outline-offset: 1px;
}
/* min-height keeps a written status from shifting the layout as it changes length —
   but an EMPTY one was still reserving that line, and in the wallet modals the status
   sits AFTER the buttons, so every dialog opened with ~40px of dead space under its
   actions. Nothing to say, nothing to occupy. */
/* (size + layout now live with .sc-status-icon further down — this keeps the colour
   and the reserved line, so a message appearing does not shift the box.) */
.sc-status { min-height: 1em; color: var(--muted); }
.sc-status:empty { display: none; min-height: 0; }
/* IN PROGRESS IS NOT A WARNING. "Checking balances & price…" was amber, which is the
   app's caution colour — so the one line that appears every single time you press
   Start read as something having gone wrong, next to a button that had just gone
   disabled. It is the same --muted the status line already sits at when it has nothing
   in particular to say, because that is exactly what "still working" is: narration,
   not a verdict. The two states that ARE verdicts keep their colours. */
.sc-status-run { color: var(--muted); }
.sc-status-ok { color: var(--green); }
.sc-status-err { color: var(--red); }
.sc-error { color: var(--red); font-size: var(--body-size); }

@media (max-width: 560px) {
  .sc-two-slots { grid-template-columns: 1fr; }
}

/* ============================================================
   Activity page — instrument panel (Quiet Data redesign).
   Squared hairlines, mono figures, semantic color only.
   ============================================================ */
/* ONE vertical rhythm for the whole panel: the stack owns the spacing (gap),
   nothing inside carries its own margin. Fixes the run buttons sitting flush
   against the wallet drop zone's bottom edge. */
.act-panel { display: flex; flex-direction: column; gap: var(--sp-4); }
/* The band separator is GONE. The info line under the tiles already marks the
   turn from "what happened" to "what will happen", and a hairline on top of it was
   two devices doing one job inside a box whose border is the real boundary. Kept as
   a spacing element so the rhythm survives. */
.act-sep { border: 0; margin: 0; }

/* Config slider rows — ONE grid shared by every row, including NET:
     label │ gutter │ track │ gutter │ readout
   The two narrow gutter columns exist on EVERY row, so all tracks share one
   left and right edge. The NET row's inline BUY/SELL captions live in those
   gutters (its wrapper uses display:contents to join this same grid) instead of
   eating into its own track — which is what made it a different shape.
   The readout column is fixed-width, right-aligned and tabular, so 4 / 6 /
   ±25% / 0–2s / even all land on one right edge. */
/* ONE geometry for every slider row. Previously each row had its own: the unit
   ("eth") and the NET row's BUY/SELL captions lived in GUTTER COLUMNS between
   the label and the track, which shortened those tracks — so no two tracks
   started or ended on the same x. Three columns now, and nothing may sit
   between the label and the track:
     [ LABEL 140px ] [ TRACK — identical start/end on every row ] [ VALUE 90px ]
   Units moved INTO the value cell; BUY/SELL became axis captions UNDER the
   track ends (.act-axis), so the NET track is full width like the others. */
.act-row {
  position: relative;   /* the click-to-type editor is absolutely placed over the readout */
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); min-height: var(--control-h);
}
/* Label takes what it needs, control sits flush right, nothing in between. The row
   used to be a three-column grid with a fixed 140px label gutter and a stretching
   middle track — correct when the middle held a slider that filled it, and ~350px
   of dead air per row once the sliders became typed boxes. A ruler-straight gutter
   full of nothing is not alignment. */
.act-row > .act-label { flex: 0 1 auto; min-width: 0; }
.act-row > .act-read { flex: 0 0 auto; }
.act-row .act-track { flex: 0 0 auto; }
/* NET is the one row whose control still wants the slack — it holds a slider. */
.act-row > .act-net-body { flex: 1 1 auto; min-width: 0; }
/* (.act-row-num used to re-shape the grid so a typed row's box landed hard right.
   The row is space-between now, so every control is already flush right and the
   modifier has nothing left to do. The class is still SET in the markup — it marks
   "this row's control is its own value cell", which the readout rules read.) */
/* Causality grouping: per-trade (size, delay) then distribution (wallets, net).
   A single larger gap between the pairs, instead of even spacing throughout. */
.act-row.act-group-start { margin-top: var(--sp-3); }
/* controls sharing a slider row match the compact rung, not the 34px default */
.act-row > select, .act-row > input:not(.act-slider) {
  height: var(--control-h-sm); font-size: var(--body-size);
}
.act-label {
  font-family: inherit; font-size: var(--label-size); font-weight: var(--label-weight);
  line-height: var(--body-line); color: var(--foreground);
}
/* The value cell owns the UNIT too ("0.001 ETH", "3-60 s"), so every row ends on
   the same right edge instead of one row floating its unit inline mid-track. */
.act-read {
  font: var(--fig-inline)/1 var(--num); font-variant-numeric: tabular-nums;
  color: var(--foreground); text-align: right; white-space: nowrap;
}
.act-read .act-read-unit { color: var(--muted); margin-left: 4px; }
.act-read.dim { color: var(--muted-2); }
/* ---- a readout you can type into ----
   scReadEdit turns the figure at the end of a slider row into a field. At rest it
   is still just the number: the affordance is a quiet pill on hover, drawn with a
   box-shadow spread rather than padding so nothing in the row moves when it
   appears. The editor itself is positioned over the readout's reserved column —
   see scReadEdit for why it is parented to the ROW and not to the readout. */
/* THE PILL IS THE SAME SIZE BEFORE AND AFTER. The readout is a line of text — a 14px
   box — and the field that opens over it is a 20px control, so the hover pill grew by
   6px the moment you clicked it: the one thing that was supposed to stay put. The
   readout takes the field's height here, at rest, so the shape you hover is the shape
   you get. inline-flex rather than a padding, because the row reserves this column by
   width and a padding would have pushed the digits off its right edge. */
.act-read-live {
  display: inline-flex; align-items: center; justify-content: flex-end; height: 20px;
  cursor: text; border-radius: var(--radius);
  transition: background-color .12s ease, box-shadow .12s ease;
}
.act-read-live:hover,
.act-read-live:focus-visible {
  background-color: var(--control-bg-hover);
  box-shadow: 0 0 0 4px var(--control-bg-hover);
}
.act-read-live:focus-visible { outline: 1px solid var(--ring); outline-offset: 3px; }
/* The number is hidden while its field is open — the field carries its own ground,
   but a value showing through the edge of it would be the OLD one. */
.act-row.is-typing > .act-read { visibility: hidden; }
/* (The FIELD is drawn in the unlayered tail at the end of this file. It has to be:
   `.act-box-cfg input[type="text"]` down there is unlayered and would repaint its
   ground, and unlayered beats every layer at any specificity.) */

/* slider base — hairline track, thin playhead thumb */
.act-track { position: relative; display: flex; align-items: center; height: 20px; }
.act-slider { -webkit-appearance: none; appearance: none; width: 100%; height: 20px; background: transparent; cursor: ew-resize; margin: 0; }
/* --act-fill names the FILLED part of the track, so a slider that stands for one asset
   can be drawn in that asset's colour — Transfer's ETH row in --accent and its token
   row in --alloc-tok, the same two the portfolio chart and the allocation bar use, so
   a blue line means ETH everywhere in the app. Defaults to --foreground, which is what
   every slider that stands for a quantity rather than an asset keeps.
   The THUMB stays --foreground in all cases: it is the same grab handle on every
   slider in the app, and tinting it would make the control itself look different
   rather than the value it has reached. */
.act-slider::-webkit-slider-runnable-track { height: 2px; border-radius: 0;
  background: linear-gradient(90deg, var(--act-fill, var(--foreground)) 0 var(--pct, 0%), var(--border) var(--pct, 0%)); }
.act-slider::-moz-range-track { height: 2px; border-radius: 0; background: var(--border); }
.act-slider::-moz-range-progress { height: 2px; background: var(--act-fill, var(--foreground)); }
/* A grabbable handle. This was a 3px tick - visually a scale mark, not a
   control, and a hard target to hit with a mouse. */
/* 12x12 — one grab target on every slider in the app. It was a 10x18 bar, which read
   as a tick mark on the track rather than something to take hold of. margin-top is
   -(12 - 2)/2: the runnable track is 2px, so this centres the square on it. */
.act-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; margin-top: -5px; border-radius: 0;
  background: var(--foreground); border: none; cursor: ew-resize;
  transition: box-shadow .08s ease, background .08s ease; }
.act-slider::-moz-range-thumb { width: 12px; height: 12px; border-radius: 0; background: var(--foreground); border: none; cursor: ew-resize; }
.act-slider:hover::-webkit-slider-thumb { box-shadow: 0 0 0 4px color-mix(in srgb, var(--ring) 28%, transparent); }
.act-slider:active::-webkit-slider-thumb { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ring) 40%, transparent); }
.act-slider:focus-visible { outline: none; }
.act-slider:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 45%, transparent); }
/* ---- a LOCKED track has to look locked ----
   Activity disables its three settings for the length of a run — it is the only panel
   that does, because the run is reading them while it works — and until these rows
   were sliders that state was drawn by the typed field's own :disabled rule. A track
   with no disabled state looks exactly like a live one that has stopped responding,
   which is the worst of both. Dim the fill, the handle and the figure beside it, and
   say so with the cursor. */
.act-slider:disabled { cursor: not-allowed; }
.act-slider:disabled::-webkit-slider-runnable-track {
  background: linear-gradient(90deg, var(--muted-2) 0 var(--pct, 0%), var(--border) var(--pct, 0%));
}
.act-slider:disabled::-moz-range-progress { background: var(--muted-2); }
.act-slider:disabled::-webkit-slider-thumb { background: var(--muted-2); cursor: not-allowed; }
.act-slider:disabled::-moz-range-thumb { background: var(--muted-2); cursor: not-allowed; }
.act-slider:disabled:hover::-webkit-slider-thumb { box-shadow: none; }
/* (The figure beside a locked track dims with it. That half of the rule is in the
   unlayered tail — `.act-box-cfg .act-read` down there forces --foreground on every
   readout in the box, and no layered selector at any specificity can reach past it.) */


/* NET steering slider — same row grid as the others; the wrapper dissolves so
   its BUY / SELL captions land in the shared gutter columns and its track lines
   up with the four tracks above. Center-origin fill via --a/--b. */
/* BUY / SELL are AXIS captions under the track's two ends, not gutter content.
   Inside the row grid they stole width from the track; underneath it, the NET
   track spans exactly the same measure as the three above it. */
.act-net-body { display: flex; flex-direction: column; gap: 2px; }
.act-axis { display: flex; justify-content: space-between; }
.act-axis .act-cap { white-space: nowrap; }
.act-cap {
  font-family: inherit; font-size: var(--label-size); font-weight: var(--label-weight);
  line-height: var(--body-line); color: var(--muted);
}
/* The buy/sell captions under the NET track are axis ticks, not labels — they
   annotate the two ENDS of a control rather than naming it, so they keep the small
   marker treatment the tile captions just gave up. */
.act-axis .act-cap {
  font-size: var(--eyebrow-cap-size); font-weight: var(--eyebrow-weight);
  line-height: 1; text-transform: uppercase; letter-spacing: var(--eyebrow-track);
  color: var(--muted-2);
}
/* Center detent — the "even" position has to be findable by eye and by feel. */
.act-net-track::before { content: ""; position: absolute; left: 50%; top: 50%; width: 2px; height: 14px;
  transform: translate(-50%, -50%); background: var(--muted); pointer-events: none; z-index: 1; }
.act-slider-net { cursor: ew-resize; }
.act-slider-net::-webkit-slider-runnable-track { height: 2px; border-radius: 0;
  background: linear-gradient(90deg, var(--border) 0 var(--a, 50%), var(--fill, var(--border)) var(--a, 50%) var(--b, 50%), var(--border) var(--b, 50%)); }
.act-slider-net::-moz-range-track { height: 2px; background: var(--border); }
.act-slider-net::-moz-range-progress { background: transparent; }

/* Outcome cards — four even columns split by hairlines; 2x2 when narrow.
   ONE anatomy for all four: caption row, baseline-aligned figure row, then a
   fixed-height sparkline, so the columns read as one set. */
/* WHO OWNS THE LEFTMOST INSET. The band is a bordered box with its own 16px all
   round, and the first tile used to drop its own left inset to sit on that line —
   which made its column 16px wider ON THE INSIDE than the other three. Every
   sparkline is drawn to the width it is handed, so one chart in four ran long, in
   both panes, for the whole life of this band.
   The inset is handed to the TILE instead: the band gives up its left padding in
   4-up and all four tiles keep an identical one, so the leftmost caption lands on
   exactly the same pixel it always did (the wallet box below aligns to it) and the
   four charts are finally the same length with the same 32px between them.
   2-up puts a tile on both outer edges and zeroes those two itself, so there the
   band keeps its padding and nothing moves. */
.act-cards {
  --act-card-pad-y: 14px;
  --act-band-pad-l: 0px;
  display: grid; grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 1240px) {
  .act-cards { grid-template-columns: 1fr 1fr; --act-band-pad-l: var(--pad-card); }
  /* (no internal rules between tiles — see .act-card + .act-card) */
}
/* Each tile carries its own left/right inset as a variable, so the ONE place
   that zeroes an outer edge also moves anything pinned to that edge (the pill). */
.act-card {
  --act-pad-l: var(--pad-card);
  --act-pad-r: var(--pad-card);
  position: relative; min-width: 0;
  padding: var(--act-card-pad-y) var(--act-pad-r) var(--act-card-pad-y) var(--act-pad-l);
}
/* No rule between tiles. The four are ONE readout of one run — the card's own
   border already says where the group starts and stops, and an internal line per
   tile re-litigated that boundary three more times. Whitespace groups them. */
/* (The first tile used to zero its own left inset to reach that edge. The band
   gives up its left padding instead — see --act-band-pad-l above — because a tile
   with one inset missing is a tile 16px wider than its neighbours, chart included.) */
/* (last child keeps its right inset: the row needs a gutter matching the left
   margin, or the final tile bleeds into the panel edge.) */
/* 2-up: the outer edge of each ROW is the card edge. (After the 4-up rules above
   so it wins at equal specificity.) */
@media (max-width: 1240px) {
  .act-cards > .act-card:nth-child(odd) { --act-pad-l: 0px; }
}
.act-card > .act-cap {
  display: block; min-height: var(--eyebrow-cap-size);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.act-fig-line { display: flex; align-items: baseline; gap: var(--sp-1); margin-top: 10px; min-height: 24px; }
.act-fig { font: 400 var(--fig-card)/1 var(--num); font-variant-numeric: tabular-nums; color: var(--foreground); }
.act-fig.dim { color: var(--muted-2); }   /* same dim as .act-read.dim / .*-bal.dim */
.act-fig-net { font-size: var(--fig-card); }
.act-unit { font-size: var(--body-size); color: var(--muted); font-variant-numeric: tabular-nums; }
.act-word { font-size: var(--cap-size); color: var(--muted); }

/* stat-tile sparkline — 12-point trend: colored line + gradient area fade.
   Taller than wide-and-squished so peaks read; the NET tile reads against a
   solid zero axis at mid-height, --sell above zero, --buy below. */
.act-spark { position: relative; height: 54px; margin-top: 14px; }
/* ---- hover on a sparkline (scWireSparkHover builds these three) ----
   The overview chart's treatment at spark scale: a dashed column, a ringed dot on the
   trace, a value chip over it. All three are screen-space DIVs rather than SVG nodes —
   the spark's viewBox is stretched horizontally by the card's width, which would turn
   a circle into an ellipse and a 1px rule into several. */
.act-spark-cross,
.act-spark-dot,
.act-spark-chip { position: absolute; opacity: 0; pointer-events: none; transition: opacity .1s; }
.act-spark.is-hover .act-spark-cross,
.act-spark.is-hover .act-spark-dot,
.act-spark.is-hover .act-spark-chip { opacity: 1; }
/* 1-on/6-off, the same rhythm the canvas crosshair is dashed at. */
.act-spark-cross {
  top: 0; bottom: 0; width: 1px; transform: translateX(-0.5px);
  background-image: repeating-linear-gradient(to bottom,
    var(--border-strong) 0 1px, transparent 1px 7px);
}
/* The fill is written per-trace at hover time (Net recolours itself by sign); the ring
   is what separates the point from the line it sits on, which is the same colour. */
.act-spark-dot {
  width: 10px; height: 10px; box-sizing: border-box; border-radius: 50%;
  border: 2px solid var(--foreground); background: var(--muted);
  transform: translate(-50%, -50%);
}
.act-spark-chip {
  transform: translate(-50%, calc(-100% - 10px));
  padding: 3px 8px; white-space: nowrap;
  background: var(--float); border: 1px solid var(--border);
  color: var(--foreground); font-size: var(--cap-size); line-height: 16px;
  font-variant-numeric: tabular-nums;
}
.act-spark svg { display: block; width: 100%; height: 100%; overflow: visible; }
.act-spark polyline { fill: none; stroke-width: 1.5;
  stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
/* One visual weight across all four bars. There used to be a second rule here
   knocking the NET tile back to .67, correcting for a heavier fill it no longer
   has: NET was a FLAT tint while its neighbours were gradients, and no amount of
   opacity could make a solid block read like a fade. It is a gradient now, on
   the same .22 stop as the others (fading toward its zero axis rather than the
   floor — see buildNetSpark), so it needs no separate correction. */
.act-spark polygon { stroke: none; opacity: .85; }
.act-spark .spark-zero { stroke: var(--muted-2); stroke-width: 1;
  vector-effect: non-scaling-stroke; }

/* action row */
/* THE ROW IS THE BUTTON. A 90px "Run sweep" sitting at the left edge of a 306px box
   read as one more control among the rows above it, and its width was an accident of
   how many characters the verb happened to have — "Run send" and "Run gather" landed
   at different sizes in the same pane. The thing the box exists to press is now as
   wide as the box that configures it, so it terminates the column instead of
   floating in it. min-width stops being the width and becomes the shrink floor.
   (Activity has TWO buttons and splits the same full-width row — see .act-cluster.) */
.act-actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.act-actions > .btn,
.act-actions > .act-cluster { flex: 1 1 auto; }
.act-actions .btn { min-width: 88px; }
/* A link, not a button — for the control that HANDS YOU OFF rather than doing
   something here. It takes the far end of the row, away from the buttons that act on
   this card, so the row reads as "what you can do" on the left and "where else you
   can go" on the right. (Recover used to wear this and does not any more: it acts on
   the run, so it is a button beside Start.) */
.act-actions > .act-link { margin-left: auto; }
/* NO UNDERLINE, and the quietest ink in the box. This is the one control in the row
   that does not act on the card — it hands you off to the wallet — so it recedes
   behind the two buttons rather than competing with them, and the ↗ carries the
   affordance on its own. Hover is what confirms it is live, and with the rule gone it
   is now the only state change, so it goes all the way to --foreground. */
.act-link {
  background: none; border: 0; padding: 0;
  color: var(--muted-2); font: inherit; font-size: var(--body-size);
  text-decoration: none; cursor: pointer;
}
/* THE ARROW IS OPTICALLY CENTRED, not baseline-aligned. ↗ has no descender — its ink
   stops 2.52px ABOVE the baseline at 14px, where the words reach 2.10px below it — so
   sharing a baseline put the arrow's visual centre 2.3px higher than the text's and it
   read as raised. Nudged down by that difference, as a ratio of the font size (2.3/14)
   so it holds if the type scale moves. */
.act-link-go { padding-left: 6px; position: relative; top: 0.17em; }
.act-link:hover { color: var(--foreground); }
/* (its disabled look lives with the app-wide "EVERY disabled button" section) */
/* The drop zone runs the FULL height of the band (no 180px cap any more — it is
   the primary column now, and capping it left dead card under a half-full list).
   It scrolls internally once the wallets outrun the box, so a long roster never
   pushes the action cluster off the bottom. */
.act-mid > .sc-dropzone, .act-mid .sc-dropzone,
.act-band .sc-dropzone {
  max-height: none; height: 100%; min-height: 0; overflow-y: auto;
}
/* Empty state, CENTRED in that tall box. .sc-dropzone-multi sets
   `align-content: flex-start` — correct for a list of cards, but it pinned the
   "+ drop wallets" prompt to the top edge of a 320px box with nothing under it.
   Only while empty: once there are cards they must start at the top. */
.act-mid .sc-dropzone:not(:has(.sc-wallet-list)),
.act-band .sc-dropzone:not(:has(.sc-wallet-list)) { align-content: center; }
.act-mid .sc-drop-empty, .act-band .sc-drop-empty { margin: auto 0; }

/* ============================================================
   ACTIVITY IS FOUR SIBLING BOXES, NOT ONE CARD HOLDING FOUR.
   The frame lays them out as siblings on the page: tiles (y=434 h=232), then
   Wallets (y=677) with Configure (y=677) and Run (y=946) beside it — no outer
   container around the set. Ours had every one of them inside the pane's
   .sc-card, so each box drew its border INSIDE a fourth border. Activity is the
   only pane that is more than one box, so it is the only one whose card gives up
   its chrome; Sweep, Transfer and Logs are single boxes and keep theirs.
   ============================================================ */
.act-pane[data-act="activity"] .sc-card {
  border: 0; background: none; padding: 0; border-radius: 0;
}
/* ...which means the tiles need to become a box in their own right, since the
   card that used to draw one around them no longer does. */
.act-loop { display: flex; flex-direction: column; gap: var(--sp-2); }
.act-loop > .act-cards {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface);
  padding: var(--pad-card) var(--pad-card) var(--pad-card) var(--act-band-pad-l);
}

/* ============================================================
   THE THREE BOXES — Wallets | Configure + Run.
   Same 8px rhythm the dashboard uses between any two boxes, and each one carries
   the card chrome so it reads as its own container rather than a region of one
   big panel.
   ============================================================ */
.act-band {
  /* ONE declaration of the Configure column's width, read by the band's own track list
     AND by anything that has to line up with it (Transfer's direction switch spans the
     first track, so it subtracts this). Changing it here moves both. */
  --act-cfg-w: 340px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(260px, var(--act-cfg-w));
  gap: var(--sp-2); align-items: stretch;
  flex: 1 1 auto; min-height: 320px;
}
@media (max-width: 860px) {
  .act-band { grid-template-columns: 1fr; min-height: 0; }
}
/* Configure and Run stack in the right column on the SAME 8px gap, so all three
   boxes sit on one grid rather than the right pair using a rhythm of their own. */
.act-band-right { display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; }
.act-box {
  --act-inset: 20px;                 /* one inset for every table in the app */
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface);
  /* Even inset on all four sides. It used to be 12px on top because a 48px title
     row sat there; with the titles gone that left the first control riding high
     against the border. */
  padding: var(--act-inset);
  display: flex; flex-direction: column; gap: var(--sp-3);
  min-width: 0; min-height: 0;
}
/* Run holds the buttons, so it must not stretch — Configure takes the slack and
   Run stays exactly as tall as its contents, keeping Start on a stable line. */
.act-band-right > .act-box-cfg { flex: 1 1 auto; }
.act-band-right > .act-box-run { flex: 0 0 auto; }
.act-box-wallets { padding: 0; gap: 0; --act-inset: 20px; }     /* the table owns its own insets */
/* A 48px title ROW, not a 20px line: 12px + 24px of text + 12px, matching the
   frame's Frame 2147224515 (h=48, label 24px tall at y=12). The 12px below is a
   margin so it ADDS to the box gap rather than replacing it. */
.act-box-title {
  /* --body-size, not the frame's 16: the ramp is 12/14/20/30 and 16 is not a step
     on it. Weight 600 carries the emphasis the extra 2px used to. */
  margin: 0 0 12px; font-size: var(--body-size); font-weight: 400;
  line-height: 24px; color: var(--foreground);
}

/* ---- the wallet table ----
   Column titles over the drop zone. The rows underneath are the SAME
   .sc-wallet-card the drop zone builds; the grid below lines their parts up under
   these headings, so drag, pick and the per-wallet split chip are untouched. */
/* 32px tall, 16px text on 8px padding, 24px left inset — read off the frame
   (Frame 2147224518, y=56 h=32, label at y=8 h=16, x=24). */
.act-wt-head {
  display: grid; grid-template-columns: var(--act-wt-cols);
  align-items: center; gap: 0 24px;   /* 24px floor between columns, as in Logs */
  box-sizing: content-box; height: 32px; flex: 0 0 auto; padding: 0 var(--act-inset);
  border-bottom: 1px solid var(--border);
}
.act-wt-col {
  font-size: var(--cap-size); font-weight: var(--label-weight);
  color: var(--muted-2); white-space: nowrap;
}
.act-wt-c1, .act-wt-c2 { text-align: right; }
/* ONE Actions track now, not a pair, so the heading no longer spans two. */
.act-wt-c3 { text-align: right; grid-column: 4; }
/* ONE column definition, read by the head and every row — they cannot drift.
   FIXED widths, not auto: head and rows are separate grids, so `auto` sized each
   track to its own content and the headings landed over nothing in particular
   ("Balance" measured 47px in the head and 26px in the rows). The last two tracks
   are the Actions pair — split chip, then remove — which the heading spans. */
/* One column definition for EVERY wallet list in the action tabs, so a row looks
   the same whichever tab it is in. */
/* BALANCED, the same way the Logs table is: Wallet at 1.6x, Balance and Holdings
   an equal share each, and only the two Actions tracks fixed — they hold icons, not
   text, so they have nothing to balance. The old `1fr 112px 124px` gave Wallet every
   spare pixel and pinned the two figures to their own widths, which is what bunched
   them against the Actions pair on a wide table. */
.act-pane {
  /* Wallet | Balance | token | Actions. The Actions pair collapsed into a single
     78px track: the stepper and the ✕ share one cell now (.sc-wallet-act), and
     78px is what that content measures — 16 + 6 + 18 + 6 + 16 for the stepper,
     then 6 + 16 for the glyph — so the column is sized to its contents rather
     than to two tracks with a 24px gutter between them. */
  --act-wt-cols: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr) 78px;
  --act-inset: 20px;
}
.act-box-wallets > .sc-dropzone { border: 0; border-radius: 0; padding: 0; }
.act-pane .sc-dropzone .sc-wallet-list { gap: 0; }
/* FOUR classes deep on purpose. shortcuts-activity.js injects
   `.act-loop .sc-dropzone .sc-wallet-card` at runtime for the old two-line
   density — same specificity as a three-class rule here and appended after the
   stylesheet, so it won on order and the row kept its old template. */
.act-pane .sc-dropzone .sc-wallet-card {
  display: grid; grid-template-columns: var(--act-wt-cols);
  grid-template-rows: auto auto;
  align-items: center; gap: 0 24px;   /* 24px floor between columns, as in Logs */
  /* 68px: 12px + a 44px two-line identity block + 12px. The frame's rows are
     68px (Frames 2147224519/520/523/521, all h=68) — mine were 44px, which is the
     CONTENT height with no padding at all, so the table read as a dense list
     rather than the roomy roster the frame shows. */
  /* height:auto is LOAD-BEARING: the chip density above fixes height to
     --chip-h-2 (44px), and a card whose height is definite has its two auto rows
     split to the box rather than sized to the text — under the ≤1240px
     min-height:56px that resolved them to 16px each, so the 20px name and
     address lines overlapped by 4px ("rows on top of each other"). The table
     row sizes to its content; min-height stays the floor. */
  height: auto;
  min-height: 68px; padding: 12px var(--act-inset);
  border: 0; border-bottom: 1px solid var(--border); border-radius: 0;
  background: none;
}
/* Placement is by NAMED AREA, not grid-column. The card's parts already carry
   `grid-area: name / addr / eth / bal / split / x` (the two-line dropzone density
   in shortcuts-activity.js), and grid-area sets row+column — so any grid-column
   written here is overwritten by them. Re-mapping the areas onto the table's five
   tracks lets the existing placement do the work: eth, bal, split and x each
   repeat across both rows, so they span and centre on their own. */
.act-pane .sc-dropzone .sc-wallet-card {
  grid-template-areas: "name eth bal split x"
                       "addr eth bal split x";
}
.act-pane .sc-dropzone .sc-wallet-name,
.act-pane .sc-dropzone .sc-wallet-addr { text-align: left; }
/* The token balance is a CHILD of .sc-wallet-id, so it can only reach the grid if
   that wrapper stops being a box. Without this it stayed inside the name column
   and sat 208px left of the "Holdings" heading that was meant to label it. */
.act-pane .sc-dropzone .sc-wallet-card .sc-wallet-id { display: contents; }
.act-pane .sc-dropzone .sc-wallet-card .sc-wallet-bal { grid-area: bal; }
.act-pane .sc-dropzone .sc-wallet-card .sc-wallet-name { grid-area: name; }
.act-pane .sc-dropzone .sc-wallet-card .sc-wallet-addr { grid-area: addr; }
.act-pane .sc-dropzone .sc-wallet-card:last-child { border-bottom: 0; }
.act-pane .sc-dropzone /* --surface, not --surface-2: #191919, the tone the rail's own rows hover to. The two
   lists sit side by side and were hovering to different greys. */
.sc-wallet-card:hover { background: var(--surface); }
/* Name over address, the way the reference stacks a wallet's identity. */
.act-pane .sc-dropzone .sc-wallet-id {
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
}
.act-pane .sc-dropzone .sc-wallet-name {
  font-size: var(--body-size); color: var(--foreground);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.act-pane .sc-dropzone .sc-wallet-addr {
  font-size: var(--cap-size); color: var(--muted-2);
}
/* Balance and Holdings are their own columns now, not two figures crowded into
   the name block — which is what the headings above them promise. */
.act-pane .sc-dropzone .sc-wallet-bal,
.act-pane .sc-dropzone .sc-wallet-eth {
  font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap;
}

/* ---- row furniture: drag grip, Holdings unit, Actions slot ----
   The ACTIONS column is deliberately left empty for now — the track is reserved
   (see --act-wt-cols) so whatever goes in later lands in a column that already
   lines up with its heading, instead of the heading moving when the first button
   arrives. */
.act-pane .sc-dropzone .sc-wallet-card { position: relative; }
/* (The grip was defined HERE as well as on .rail-wallet — two copies of one idea,
   which is exactly how they came to sit at different insets and reveal on different
   conditions. One definition now, for every surface, at the end of this file.) */
/* The " tokens" suffix is GONE. The column heading already says Holdings and the
   figure already carries its ticker ("43 VERTAI"), so the word was a third label on
   one number. Rule kept, emptied, so the reason it went is on the record. */
.act-pane .sc-dropzone .sc-wallet-bal:not(.dim)::after {
  content: none;
}

/* The ETH mark in a wallet row: FLAT WHITE. Everywhere else it takes
   currentColor at .85 so it tints with its text — correct for a chip inside a
   coloured readout, wrong here, where the balance figure can be muted or dim and
   the mark went grey with it. In the table it is an asset badge, not part of the
   number, so it holds one colour regardless of the row's state. */
.act-pane .sc-dropzone .sc-wallet-eth::before {
  background-color: var(--foreground);
  opacity: 1;
}

/* ---- the Run box ---- */
/* (.act-run-stats held three read-only chips between the settings and Start: a
   trade tally, a chain count and the word "Server". The first two are what the
   status line directly below them already says while a run is live, and the third
   was a constant — this panel only ever plans on the server. Removed.) */
/* (Start no longer pushes itself right on its own — .act-cluster does it for all
   three at once, so the group stays together.) */
.act-livedot { width: 5px; height: 5px; border-radius: 50%; background: var(--green); animation: actPulse 1.4s ease-in-out infinite; }
@keyframes actPulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* Middle band: wallet drop zone on the LEFT, config column on the right.
   NOT 1fr 1fr. The config column used to be half the card while holding a 100px
   label, a 52px typed box and a 62px readout — once the sliders went away (their
   tracks were what justified a stretching middle column) that half was mostly
   empty track. It is sized to its content now and the remainder goes to the drop
   zone, which is the half that actually grows as you add wallets.
   `align-items: stretch` makes the two match heights on their own — the old
   `min-height: 100%` hack did the same job from the child side. */
.act-mid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
  column-gap: var(--sp-4); align-items: stretch;
  /* The band takes the card's slack, so the drop zone runs all the way down to
     the action cluster instead of stopping at whatever the four control rows
     happen to measure. min-height is what gives it something to fill on a card
     that sizes to content. */
  flex: 1 1 auto; min-height: 320px;
}
/* Narrow canvas: stack rather than crush the config column under its own labels.
   No min-height when stacked — the two are full width and size themselves. */
@media (max-width: 860px) {
  .act-mid { grid-template-columns: 1fr; row-gap: var(--sp-4); min-height: 0; }
}
.act-controls { display: flex; flex-direction: column; gap: var(--sp-1); }
.act-review { min-width: 88px; }
/* The two BUTTONS as one cluster, spanning the box the way the single run button
   does in every other pane. Start is the thing this whole box exists to press, so it
   starts where the eye already is — under the left edge of every label above it, and
   Recover sits beside it because it acts on the same run.
   SEVEN TO THREE, not halves. They are not peers: Start is the press, Recover is what
   you reach for on the rare run that stranded something. The old shared min-width
   made them literally identical, which said they mattered equally. The ratio divides
   the space AFTER the gap (flex-basis 0), and `.act-actions .btn`'s 88px min-width
   still holds Recover legible when the config column is at its 260px floor — there
   Start gives up the difference rather than Recover clipping.
   GRID, not flex, and that is the whole reason for the switch. `flex: 7/3 1 0` does
   NOT give 70:30 here: the buttons are border-box, so a 0 basis is floored at
   padding+border (26px), each button enters the split carrying that 26px, and only
   the remaining 246px gets divided — which lands at 65:33. Grid tracks are plain
   lengths that know nothing about the item's padding, so 7fr 3fr is exactly 70:30 of
   the row after the gap, and the buttons stretch to their tracks. */
.act-cluster { display: grid; grid-template-columns: 7fr 3fr; align-items: center; gap: var(--sp-2); }
/* Read-only line under the tiles: what the settings PRODUCE (trade size, address
   footprint). Quiet — it is a consequence, not a control — and it wraps rather
   than truncating, since every clause in it is a number you wanted. */
.act-info {
  font-size: var(--cap-size); line-height: 1.45; color: var(--muted);
  font-variant-numeric: tabular-nums; max-width: 100%;
}

/* Server Activity keeps the shipped Activity presentation, but no longer loads
   the legacy module that used to inject these UI-only rules at runtime. Keep the
   table/control chrome in the canonical stylesheet; none of this chooses a trade. */
/* (The typed .act-numbox field and its hover steppers were dressed here. Activity's
   three settings are sliders now, so every rule that drew that box went with them.
   The class itself lives on in app.js's batch-mint amount modal, which sits outside
   this scope and so never wore any of it.) */
.act-server-client .sc-dropzone.act-locked { opacity: .72; pointer-events: none; }
.act-server-client .sc-dropzone .sc-wallet-card {
  grid-template-columns: var(--act-wt-cols);
  grid-template-areas: "name eth bal act" "addr eth bal act";
}
.act-server-client .sc-dropzone .sc-wallet-eth,
.act-server-client .sc-dropzone .sc-wallet-bal { text-align: right; align-self: center; }
.act-server-client .sc-dropzone .sc-wallet-act {
  grid-area: act; align-self: center; justify-self: end;
  display: flex; align-items: center; justify-content: flex-end; gap: 6px;
}
.act-server-client .act-split {
  display: inline-flex; align-items: center; gap: 6px; height: 18px;
  border: 0; background: none; color: var(--muted); line-height: 1;
  font-size: var(--cap-size); font-variant-numeric: tabular-nums;
}
.act-server-client .act-step {
  width: 16px; height: 16px; padding: 0; display: inline-flex; align-items: center;
  justify-content: center; border: 1px solid var(--border); border-radius: var(--radius);
  background: transparent; color: var(--muted); cursor: pointer; line-height: 1;
  font: inherit; font-size: var(--cap-size); opacity: 0;
  transition: opacity .12s, color .12s, border-color .12s, background .12s;
}
.act-server-client .act-step:hover { color: var(--foreground); border-color: var(--border-strong); background: var(--surface-2); }
.act-server-client .act-step:disabled { opacity: 0; cursor: default; }
.act-server-client .act-split-n {
  font-family: var(--num); width: 18px; text-align: center; flex: 0 0 auto;
  cursor: default; transition: opacity .12s;
}
/* The default reads "auto" (the planner splits a two-sided maker itself), a
   word rather than a digit, so at rest the figure sizes to it instead of the
   18px a number needs. Still hidden until hover, like the steppers beside it. */
.act-server-client .act-split-n.is-default { opacity: 0; width: auto; min-width: 18px; }
.act-server-client .sc-wallet-card:hover .act-step,
.act-server-client .sc-wallet-card:focus-within .act-step { opacity: 1; }
.act-server-client .sc-wallet-card:hover .act-step:disabled,
.act-server-client .sc-wallet-card:focus-within .act-step:disabled { opacity: .35; }
.act-server-client .sc-wallet-card:hover .act-split-n.is-default,
.act-server-client .sc-wallet-card:focus-within .act-split-n.is-default { opacity: 1; }
}  /* end @layer components */

@layer overrides {
/* One full-width section, but ON the shared gray card box (title + everything
   inside it). The hub's Activity slot (.sc-activity-slot, now also
   [data-page="activity"]) and the .dash-half Sweep/Transfer boxes host the
   cards, so they share every rule below and stay pixel-consistent. */
.sc-cards[data-page="activity"],
.sc-activity-slot,
.dash-half .sc-cards[data-page="sweep"],
.dash-half .sc-cards[data-page="transfer"] {
  display: block;                           /* was auto-fill grid → card was capped near 340px */
  gap: 0;
}
.sc-cards[data-page="activity"] .sc-card,
.sc-activity-slot .sc-card,
.dash-half .sc-cards[data-page="sweep"] .sc-card,
.dash-half .sc-cards[data-page="transfer"] .sc-card {
  max-width: none; width: 100%; gap: var(--sp-4);  /* full width; keep the default gray card box */
}
/* Title reads as a proper section heading (big), keeping the "?" help beside it. */
.sc-cards[data-page="activity"] .sc-card-head,
.sc-activity-slot .sc-card-head,
.dash-half .sc-cards[data-page="sweep"] .sc-card-head,
.dash-half .sc-cards[data-page="transfer"] .sc-card-head { gap: var(--sp-3); }
.sc-cards[data-page="activity"] .sc-card-title,
.sc-activity-slot .sc-card-title,
.dash-half .sc-cards[data-page="sweep"] .sc-card-title,
.dash-half .sc-cards[data-page="transfer"] .sc-card-title {
  font-size: var(--title-size); font-weight: var(--title-weight); line-height: var(--title-line);
}

/* ---- Hub spacing: every box FLOATS -------------------------------------
   MEASURED from the reference: 8px between every pair of boxes, vertically and
   horizontally, and 8px from the page edge. Each box is its own framed region.
   (An earlier pass collapsed these seams so the hub read as one continuous
   instrument. That is not what the reference does — it floats every container —
   and merging them lost the boundary between one tool and the next.) */
.tab-panel[data-tab="hub"] > .stack { gap: var(--sp-2); }

/* ============================================================
   ACTION TABS — Activity / Sweep / Transfer / Logs in ONE box.
   Geometry copied from the reference's own portfolio tabs (36px row, 8px/16px
   per tab, 14px/20px text, muted -> foreground on select, a 2px accent underline
   on the ACTIVE tab only and no rule under the row itself).
   The box's chrome lives HERE rather than on the .sc-card inside it: the tab
   strip and the pane it switches are one component, so one border wraps both.
   The inner card gives its own border/fill up (below) — two nested boxes drawn
   1px apart read as a rendering bug, not a hierarchy.
   ============================================================ */
/* NOT a box. The strip floats on the page's dark plane and the CARD below it is
   the box — measured off the reference: transparent row, no rule under it, 8px
   down to the card's top edge, and the row's left edge flush with the card's so
   each tab's own 16px padding lands its label on the card's content inset.
   Wrapping both in one bordered container (the first cut) put the tabs INSIDE the
   lit surface, which is the opposite of floating them. */
.act-tabbed {
  /* gap:0 — the strip SITS ON the card, it does not hover above it. With a gap,
     the underline was a short orange line stranded in the dark band between the
     Overview card and this one, attached to nothing: the reference gets away with
     a gap because its tabs sit under a page header on open background, where they
     read as navigation. Ours are sandwiched between two boxes, so they need to be
     joined to the panel they switch. */
  /* Grid, and every pane shares ONE cell (2/1). The container then takes the
     height of the TALLEST pane — Activity — so Sweep, Transfer and Logs match it
     instead of each collapsing to its own content and making the box jump height
     on every tab click. */
  display: grid; grid-template-rows: auto 1fr; gap: 0;
  border: 0; background: none; padding: 0;
}
.act-pane { grid-area: 2 / 1; min-width: 0; }
/* Inactive panes stay in LAYOUT (that is what equalises the height) but are
   invisible, inert, and out of the accessibility tree and tab order. */
.act-pane.is-off { visibility: hidden; pointer-events: none; }
.act-pane > .sc-cards, .act-pane > .sc-cards > .sc-card,
.act-pane > #dash-logs, .act-pane > #dash-logs > .sc-card { height: 100%; }
.act-tabs {
  position: relative;            /* the offsetParent the ink measures against */
  display: flex; align-items: flex-end; gap: 0;
  overflow-x: auto; scrollbar-width: none;
}
.act-tabs::-webkit-scrollbar { display: none; }
.act-tab {
  position: relative; flex: 0 0 auto;
  padding: 8px var(--pad-card);      /* 16px sides = the card's own inset, so the
                                        first label starts on the content's edge */
  background: none; border: 0; border-radius: 0;
  font-family: inherit; font-size: var(--body-size); line-height: var(--body-line);
  /* 400, NOT --label-weight (500). Measured off the reference: its tabs are plain
     body weight, and the selected one is marked by COLOUR and the underline alone.
     At 500 every tab read as semi-emphasised, which flattened that distinction —
     the strip looked like five headings instead of one selected item among four. */
  font-weight: 400;
  color: var(--muted); white-space: nowrap; cursor: pointer;
  transition: color .12s;
}
.act-tab:hover { color: var(--foreground); }
.act-tab.active { color: var(--foreground); }
/* ONE underline for the whole strip that SLIDES to the selected tab, not a
   ::after on each tab that blinks off one and on at the next. The reference marks
   selection with a single shared element (framer-motion layoutId) and animates it
   between positions; per-tab pseudo-elements cannot do that — there is nothing
   continuous to interpolate, so the mark can only teleport.
   transform + width, both GPU-cheap, and the easing is a decelerating curve that
   approximates the reference's layout spring without its overshoot. */
.act-tab-ink {
  /* bottom:-1px so the mark OVERLAPS the card's 1px top border rather than
     resting a hair above it — under the active tab the orange replaces that
     border, which is what joins the tab to its panel instead of leaving a line
     floating over a seam. */
  position: absolute; left: 0; bottom: -1px;
  height: 2px; width: 0;
  background: var(--accent);
  pointer-events: none;
  transform: translateX(0);
  transition: transform .32s cubic-bezier(.22, 1, .36, 1),
              width     .32s cubic-bezier(.22, 1, .36, 1);
}
/* First paint and re-measures (resize, font load) must LAND, not glide in from
   the left — JS adds this, commits the position, then removes it. */
.act-tab-ink.jump { transition: none; }
@media (prefers-reduced-motion: reduce) {
  .act-tab-ink { transition: none; }
}
.act-tab:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
/* The card inside a pane keeps its OWN chrome — border, --surface fill, padding.
   It is the box the strip sits above, so it must look like every other card on
   the page. (An earlier cut stripped it because .act-tabbed drew the box instead;
   with the strip floating, that would leave the content on bare page.) */

/* Dashboard (hub): the half/half Sweep + Transfer row between the Activity box
   and the Logs box — two equal columns, stacking to full width on narrow canvas
   (mirrors the .grid utility). Each .dash-half is a chromeless panel; the visible
   gray box is the .sc-card moved into it (styled Activity-identical above). */
.dash-2col { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sp-2); align-items: stretch; }
@media (max-width: 960px) { .dash-2col { grid-template-columns: 1fr; } }

/* A half-width box only has room for two metric columns. The 4-up → 2x2 switch
   was viewport-driven (max-width 1240px), so on a wide screen Sweep's four
   tiles were crushed into a half column; bind it to the container instead. */
.dash-half .act-cards { grid-template-columns: 1fr 1fr; --act-band-pad-l: var(--pad-card); }
/* (no internal rules between tiles — see .act-card + .act-card) */
/* …and the same 2-up edge rule, so a half-card's tiles line up with its title. */
.dash-half .act-cards > .act-card:nth-child(odd) { --act-pad-l: 0px; }
.dash-half .act-cards > .act-card:nth-child(even) { --act-pad-r: 0px; }

/* Hub uniform box height — the Activity, Sweep and Transfer boxes share ONE
   fixed height (--dash-box-h). Sweep==Transfer via the grid stretching each
   .dash-half column + the half card flexing to fill it; Activity is pinned to
   that same height. The card BODY becomes the scroll region (flex:1 + overflow)
   so tall content fits without clipping or pushing the box past --dash-box-h. */
.dash-half { display: flex; gap: var(--sp-4); }   /* same 16px rhythm as .stack and .bchart-tri */
.dash-half > .sc-cards { flex: 1 1 auto; }
/* Activity is NOT on the shared height. Sweep and Transfer sit side by side in
   .dash-2col, so they must match EACH OTHER or the row looks broken — that is
   what --dash-box-h is for. Activity is a full-width box on its own row with
   nothing to match, and pinning it to Sweep's floor left 156px of empty card
   under its controls (measured: it needs 556 of 712). It sizes to content now. */
.dash-half .sc-cards[data-page="sweep"] .sc-card,
.dash-half .sc-cards[data-page="transfer"] .sc-card { height: var(--dash-box-h); }
.dash-half .sc-cards[data-page="sweep"] .sc-card-body,
.dash-half .sc-cards[data-page="transfer"] .sc-card-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
}
}  /* end @layer overrides */

@layer components {
/* Transfer tree visualizer — a themed container matching the balance-chart
   cards: recessed --background fill (like the drop slots in this card), a 1px
   hairline border, squared corners. Sits inside the Transfer card body under the
   Disperse|Collect toggle; window.mountTransferTree draws an inline SVG that
   fills it. flex:0 0 auto keeps its height inside the scrolling card body. */
/* Tiny flow glyph tucked into the top-right of the Transfer "From" (sender) box.
   Sized to match the chart's line/dot language (see transfer-tree.js). Absolute
   + pointer-events:none so it's purely decorative and never blocks the slot. */
.sc-io-box { position: relative; }
.transfer-tree {
  position: absolute;
  top: 4px; right: 4px;
  width: 76px; height: 56px;
  pointer-events: none;
  overflow: visible;
}
.transfer-tree svg { display: block; width: 100%; height: 100%; overflow: visible; }
}  /* end @layer components */

@layer overrides {
/* ============================================================
   CLEAN PASS — scoped in-page overrides that can't be made in place.
   Kept minimal; the .app-main token block does the heavy lifting.
   ============================================================ */
/* Balances chart hero figure: its rule is injected into <head> by charts.js
   (not editable here), so scope under .app-main to win on specificity and
   bring the hero number down to the shared ~30px hero size (was 40px). */
.app-main .bchart-value { font-size: var(--fig-hero); font-weight: 400; }
/* Its segmented controls read as one control tier (13px/500) with the others. */
.app-main .bchart-seg button,
.app-main .bchart-tf button { font-weight: 400; }

/* ---- Dashboard balance triptych (TOTAL ETH / TOTAL <TOKEN> / <PAIR> VALUE) ---
   charts.js injects .bchart-tri-* into <head>, so its rhythm is corrected here
   (scoped under .app-main to win on specificity). The tile was a stack of five
   loosely-spaced blocks with a 120px empty chart in the middle, so the
   empty-state line floated in a void. One scale now:
     caption (fixed height) → figure (fixed height) → chart → timeframe → range
   and all three tiles are identical, so their timeframe pill rows line up. */
/* The three cards must be ONE height. The grid was align-items:start, so each
   card sized to its own content — and they do not render the same parts: a card
   with history draws canvas + timeframe bar + footer, while one without draws a
   short "Not enough history" line instead. Same row, three heights.
   stretch makes the row size to the tallest; the card is a column flexbox so
   its empty state can fill the space rather than leaving a hole. */
/* ---- OVERVIEW: a rail of four values, and ONE chart ----------------------
   The values stack VERTICALLY on the left at a third of the card; the chart takes
   the rest. FULL-BLEED inside the card: negative margins cancel .sc-card's inset
   so every divider runs edge to edge and meets the card's own border, instead of
   stopping --pad-card short at both ends and leaving the lines floating. Each item
   puts that inset back as its own padding, so only the LINES bleed, not the text. */
/* THE ROW HEIGHT IS DECLARED, and that is what makes everything below it simple. It
   used to be derived — the row took the taller of its two columns, and the taller one
   was the pane, whose height was whatever the plot had grown to. That loop is why the
   plot had to be pinned to a hand-fitted pixel value: nothing else could break it.
   Stated here, both columns get 360, the plot fills whatever the pane's header leaves,
   and the rail lays its own content out inside the same 360. Change this number and
   both sides follow. */
.app-main .bchart-ov {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  grid-auto-rows: 360px;
  gap: 0; align-items: stretch; min-height: 0;
  margin: calc(-1 * var(--pad-card)) calc(-1 * var(--pad-card));
}
.app-main .sc-card:has(> #dash-charts) { background: var(--background); }
.app-main .bchart-ov-rail {
  display: flex; flex-direction: column; min-width: 0;
  border-right: 1px solid var(--border);
}
.app-main .bchart-ov-item {
  display: flex; flex-direction: column; justify-content: center;
  flex: 1 1 0; min-height: 0;
  padding: var(--sp-2) var(--pad-card);
  background: none; border: 0; border-top: 1px solid var(--border);
  border-radius: var(--radius);
  text-align: left; cursor: pointer; font: inherit; color: inherit;
  transition: background .12s;
}
.app-main .bchart-ov-item:first-child { border-top: 0; }
.app-main .bchart-ov-item:hover { background: var(--surface-2); }
.app-main .bchart-ov-item.active {
  background: var(--surface-2);
  box-shadow: inset 2px 0 0 var(--foreground);
}
.app-main .bchart-ov-item:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
.app-main .bchart-ov-cap {
  display: block; max-width: 100%;
  font-size: var(--label-size); font-weight: var(--label-weight);
  line-height: var(--body-line); color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.app-main .bchart-ov-item.active {
  background: var(--surface-2);
  box-shadow: inset 2px 0 0 var(--foreground);
}
.app-main .bchart-ov-item.active .bchart-ov-cap { color: var(--foreground); }
/* TEXT COLUMN LEFT, miniature RIGHT. The row is only as tall as the caption+figure
   block, and the miniature stretches to exactly that — so the graph's height is set
   by the two lines beside it, not by the box's leftover space, and all four
   miniatures match. The row is centred in the box by .bchart-ov-item. */
/* Grid, not flex, for ONE reason: the miniature's height is a percentage of this
   row (see .bchart-ov-spark), and a percentage height only resolves against a
   definite box. A grid row track is definite once sized; an auto-height flex line
   is not, so under flex the same `height: 80%` fell back to the svg's intrinsic
   size. Track height still comes from the text column, so the graph is still
   measured against the caption+figure block — just at 80% of it. */
.app-main .bchart-ov-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: var(--sp-3); min-width: 0;
}
.app-main .bchart-ov-main { flex: 1 1 auto; min-width: 0; }
/* margin-top/min-height are the Activity tile's numbers verbatim — caption + figure
   here are the same object as an .act-cap + .act-fig-line pair and must measure the
   same. The figure now owns this line alone, so it keeps the text column's left
   edge and stacks under its caption. */
.app-main .bchart-ov-figline {
  display: flex; align-items: flex-end; min-width: 0;
  margin-top: 10px; min-height: 24px;
}
.app-main .bchart-ov-fig {
  flex: 0 0 auto;
  font: 400 var(--fig-card)/1.2 var(--num);
  font-variant-numeric: tabular-nums; color: var(--foreground);
}
/* A fixed 96px, never a share of the row: the four miniatures then start and end
   on the same two edges regardless of how long each figure prints, which is what
   makes them scan as one column of trends. `0 0` (no shrink) holds that even when a
   long number crowds the row — the text column ellipses instead.
   Keep in step with MINI_W in charts.js, which sizes the viewBox to match.
   80% of the row, centred: squeezed a fifth flatter than the caption+figure block
   it sits beside, so the trace reads as an annotation to the number rather than
   competing with it for the row's full height. Still DERIVED from that block
   rather than a hardcoded pixel height, so it tracks any change to the type scale.
   preserveAspectRatio=none means the viewBox stretches to whatever box it lands in
   — the shape is a trend, not a measurement, so the vertical squash is fine. */
.app-main .bchart-ov-spark {
  width: 96px; height: 80%; align-self: center; display: block;
  opacity: .8;
}
.app-main .bchart-ov-item.active .bchart-ov-spark { opacity: 1; }
.app-main .bchart-ov-fig.dim { color: var(--muted-2); }
.app-main .bchart-ov-item:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }

/* THE PANE IS THE BOX, so it carries both the inset and the ground. The card inside it
   is stripped to a bare grid by the rule below, which is why neither can live there.
   24px all round: the headline reads off the same edge as everything under it.
   The dot field is CSS, not canvas, and it belongs to the PANE rather than the plot —
   painted onto the canvas it stopped dead at the headline and the axis, which read as
   a panel inside a panel instead of as the surface the whole box stands on. A
   background tile is also free, where several hundred fillRects would be repainted on
   every hover frame.
   `circle at 50% 50% … 1px, transparent 0` is a hard-edged dot, not a blurred one (the
   second stop sits AT the first, so there is no ramp to soften it into a smudge at this
   size) and it is centred in its tile, so a tile the box cuts short drops its dot
   entirely rather than showing half of one.
   The field is masked to its OWN box, 8px inside the border, on a pseudo-element —
   which is what lets it run past the 24px the header is inset by without being tied to
   the pane's padding the way background-clip would be. Absolutely positioned, so the
   card is given a z-index to sit over it.
   Barely-there on purpose: --border already only just separates itself from the ground,
   and this is at half of that. It is a texture, not a grid you read values off. */
.app-main .bchart-ov-pane { position: relative; min-width: 0; min-height: 0; padding: 24px; }
.app-main .bchart-ov-pane::before {
  content: ""; position: absolute; inset: 8px; pointer-events: none;
  background-image: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--border) 35%, transparent) 1px, transparent 0);
  background-size: 16px 16px;
  background-position: center;
}
.app-main .bchart-ov-pane .bchart-card {
  padding: 0; border: 0; background: none;
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-content: start;
  column-gap: var(--sp-3);
  /* over the dot field, which is an absolutely positioned pseudo on the pane */
  position: relative; z-index: 1;
}
/* THE PLOT SITS CLOSER IN THAN THE HEADER DOES. The pane's 24px is the inset for the
   figure and the timeframe switch — type, which wants that much air off an edge. The
   canvas is not type: it is the content, and at 24px it was reading as a small chart
   inside a large frame. Pulled back to 16px on the three sides that meet a border by
   cancelling 8px of the pane's padding; the top is not a border, it is the header.
   width and height BOTH have to give way for that to work. charts.js sizes the wrap
   `width: 100%; height: 210px`, and a box with a definite width does not widen under a
   negative margin — it slides, which put the left edge at 16px and the right at 32.
   With both auto the grid stretches it, so the margins open it out symmetrically.
   The 1fr row is the other half: the pane is stretched to the rail beside it, and at a
   fixed 210px the plot left that slack sitting under itself, so no bottom margin could
   ever reach the border.
   THE PLOT'S HEIGHT IS DECLARED, and it is what makes the two columns end level.
   228 + the 32px readout + the 34px sub + 48px of padding − the 8px it is pulled down
   by = 334, which is the allocation rail's own content height (212 + 121). Both
   columns finish on the same line with nothing dead below either.
   It is a NUMBER because it cannot be derived. Letting the plot fill instead — a 1fr
   row — is circular: the grid row is sized from the taller column, the pane is one of
   them, and the pane's height is whatever the plot took. The browser settles that at
   the pane's figure, not the rail's, so the plot ate every spare pixel and came out
   2:1 on a 936px canvas. minmax(0, 1fr) does not break the loop either; the row is
   already definite by the time it applies.
   THAT LOOP IS NOW BROKEN UPSTREAM: .bchart-ov states its row height, so the 1fr row
   below resolves against a number instead of against its own result, and the plot can
   simply take what the header leaves. No fitted height, nothing to re-tune when the
   rail's content changes. Its bottom lands on the pane's padding minus the 8px pull —
   the 16px inset. */
.app-main .bchart-ov-pane .bchart-card { height: 100%; grid-template-rows: auto auto 1fr; }
/* height:100%, NOT auto. The canvas inside is `height: 100%` and chartDraw sizes its
   backing store from wrap.clientHeight — so an auto-height wrap gives the canvas no
   definite parent to resolve against, it falls back to the intrinsic height chartDraw
   last wrote, and the two feed each other upward. 100% of the grid area is a number,
   and the loop never starts.
   +8px, because a stated height does not grow under a negative margin the way a
   stretched auto one does — the margin only pulls what FOLLOWS. Adding the 8 back is
   what carries the bottom edge past the pane's 24px padding to the 16px inset the
   sides sit on. */
.app-main .bchart-ov-pane .bchart-canvas-wrap {
  width: auto; height: calc(100% + 8px); min-height: 0;
  margin: 0 -8px -8px;
}
.app-main .bchart-ov-pane .bchart-topline { display: none; }
/* Original order, restored: figure, subtext, plot, footer. */
.app-main .bchart-ov-pane .bchart-readout { grid-column: 1; grid-row: 1; }
.app-main .bchart-ov-pane .bchart-tfbar { grid-column: 2; grid-row: 1; margin: 0; align-self: start; }
.app-main .bchart-ov-pane .bchart-sub { grid-column: 1 / -1; grid-row: 2; }
.app-main .bchart-ov-pane .bchart-canvas-wrap { grid-column: 1 / -1; grid-row: 3; }
.app-main .bchart-ov-pane .bchart-foot { grid-column: 1 / -1; grid-row: 4; }
/* The figure sits DOWN ON its subtext. .bchart-readout reserves 46px while the figure is
   32px tall and baseline-aligned to the top of that box, so ~14px of dead space sat
   between the number and the line that qualifies it — which is what read as the number
   being stuck up in the corner. No min-height here: the row is the figure's own height,
   so the subtext comes straight up under it. */
.app-main .bchart-ov-pane .bchart-readout { min-height: 0; margin: 0; }

@media (max-width: 900px) {
  .app-main .bchart-ov { grid-template-columns: 1fr; }
  .app-main .bchart-ov-rail { border-right: 0; border-bottom: 1px solid var(--border); }
}
}  /* end @layer overrides */

/* REOPENED, not a second block by choice: the `}` above closes the layer, but the
   rules below were written for it and the file's next `}` is labelled "end @layer
   overrides" — so the layer was meant to run on from here. Without this line that
   closer is a STRAY BRACE at top level, and Chrome's error recovery swallows the
   whole rule that follows it. That is not theoretical: it is why the .rail-bar-name
   block below had to be written twice (see the duplicate further down) — the first
   copy never parsed. */
@layer overrides {
/* The three balance boxes were transparent, so they floated on the page with
   only a border — the one thing in the hub that was NOT a card. On the preset's
   ramp that reads as a hole. Put them on the Card plane like every other box. */
/* A tile, not a card: no border, no fill, no card inset — it sits INSIDE the
   Overview card the way an .act-card sits inside Activity. Only the right-hand
   gutter is kept so four tiles breathe. */
.app-main .bchart-tri-card {
  padding: 0 var(--pad-card) 0 0;
  background: transparent;
  border: 0;
  display: flex; flex-direction: column; min-height: 0;
}
.app-main .bchart-tri > .bchart-tri-card:last-child { padding-right: 0; }
/* The chart is a SPARKLINE at the Activity tiles' height, not a full-height
   panel chart — the point of the tile is the figure; the line is its trend. */
.app-main .bchart-tri-canvas-wrap { flex: 0 0 auto; height: 54px; margin-top: 14px; }
/* Timeframe pills and the min/max foot belong to a full chart, not to a stat
   tile. Activity's tiles carry a caption, a figure and a line — nothing else. */
.app-main .bchart-tri .bchart-tf,
.app-main .bchart-tri .bchart-foot { display: none; }
/* (The chart used to absorb the tile's vertical slack so three full cards had
   level feet. It is a fixed-height sparkline now — see the .bchart-tri-canvas-wrap
   rule above — and there are no feet to level.) */
/* When a tile has nothing to plot, the empty MESSAGE owns the chart slot — the
   canvas is hidden rather than left drawing under it. .bchart-empty is absolutely
   positioned inside the (position:relative) wrap, so without this the stale line
   from a previous session renders straight through the words telling you there is
   nothing to show. The wrap also stops being a fixed 54px, since the message is
   two lines where the sparkline is one. */
.app-main .bchart-tri-canvas-wrap:has(.bchart-empty:not([hidden])) {
  height: auto; min-height: 54px;
}
.app-main .bchart-tri-canvas-wrap:has(.bchart-empty:not([hidden])) .bchart-canvas {
  visibility: hidden;
}
/* an empty card centres its message in the space a chart would have taken */
.app-main .bchart-tri .bchart-empty {
  flex: 1 1 auto; display: flex; align-items: center; justify-content: center;
  text-align: center;
}
/* caption = EXACTLY the Activity .act-cap treatment, which is now the sentence-case
   label rather than a micro-eyebrow. charts.js still injects its own 10px uppercase
   rule; this override is what actually renders, and the two should agree. */
.app-main .bchart-tri-cap {
  font-size: var(--label-size); font-weight: var(--label-weight); line-height: var(--body-line);
  letter-spacing: normal; text-transform: none;
  min-height: var(--body-line);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ONE value-row pattern on all three tiles. The caption already names the unit
   ("TOTAL ETH" / "TOTAL USDC" / "ETH / USDC VALUE"), and the third tile's unit
   is suppressed in JS — which left cards 1-2 reading "— ETH" and card 3 a bare
   "—". Dropping the redundant unit chip makes all three caption + figure. */
.app-main .bchart-tri-fig-line { margin-top: 10px; min-height: 24px; gap: var(--sp-1); }
/* The figure sits on the Activity tiles' rung. charts.js injects 24px — a fourth
   size on a ladder of three (--fig-inline 14 / --fig-card 20 / --fig-hero 30) —
   which is why the Overview figures read a step louder than Activity's beside
   them. Same token, same weight, same face. */
.app-main .bchart-tri-fig { font-size: var(--fig-card); }
.app-main .bchart-tri-unit { display: none; }
/* (A 104px floor and the tfbar/foot spacing belonged to the full-card tile. The
   sparkline height and its 14px step are set with the rest of the tile above; the
   pills and foot are hidden entirely.) */
.app-main .bchart-tri .bchart-empty { font-size: var(--body-size); padding: 0 12px; }
/* timeframe pills: the compact control rung — the comment already claimed "one
   compact control rung" while hardcoding 22px, which is not a rung at all. */
.app-main .bchart-tf button { height: var(--control-h-sm); padding: 0 9px; line-height: 1; }
}  /* end @layer overrides */

/* The Logs table's COLUMN GRID lives in logs.js's injected sheet — one copy of
   the track list, in the file that also writes the cells. That sheet opens with
   `@layer components` (it says so, and the note at the top of this file requires
   it), so it is reachable from here after all: this tail is UNLAYERED, and
   unlayered beats every layer. Only the frame is set below — never the tracks. */

/* ============================================================
   TABLE HEADERS — one treatment, three tables.

   The Activity tab's wallet table is the reference: a gray header band
   (--surface) over a darker body (--background), with 12px labels in
   sentence case. The Logs table and the wallet rail's balance columns are
   brought onto it here.

   Grouped selectors, not a shared class: .dlog-head is built by logs.js at
   runtime, so a class would mean editing JavaScript. Every name below is one
   the markup or the JS already writes — nothing new to keep in sync.
   ============================================================ */
.act-wt-head,
.dlog-head,
.rail-bal-head {
  /* Opaque on purpose. These headers are sticky, and rows scrolling under an
     inherited-transparent band show straight through it. */
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 2;
}
/* Column labels are sentence case. The eyebrow treatment (uppercase + .08em
   tracking) belongs to section titles, not to column headings — and on the rail
   those two labels are TICKERS the API supplies (#rail-bal-head-tok carries
   TOKEN_SYMBOL), which have to render exactly as they arrive. */
.act-wt-col,
.dlog-h,
.rail-bal-head span {
  font-size: var(--cap-size); font-weight: var(--label-weight);
  color: var(--muted-2);
  text-transform: none; letter-spacing: normal;
}

/* ---- Logs: a table, not a list ----
   The rows already carried the Date/Wallet/Action/Amount/Tx/Status grid. What
   made the pane read as a list was the frame around it: the card's 16px padding
   held the table off every edge, and the header shared one background with the
   rows, so nothing marked the headings as a header. Full-bleed, darker body,
   gray band on top — the Activity table's anatomy, applied here. */
.dlog-card { padding: 0; }
.dlog-card .sc-card-body { flex: 1 1 auto; min-height: 0; }

/* The status strip above the table is GONE. It held nothing but a running dot on
   an otherwise empty 32px band, which read as a broken toolbar over the header.
   Kept in the DOM, and kept VISIBLE for the one case that carries information:
   the poll's error path appends a "not updating" marker (.dlog-stale) to this
   head, and a table that has silently stopped refreshing has to say so. */
.dlog-card .sc-card-head { padding: 12px 20px 0; }
.dlog-card .sc-card-head:not(:has(.dlog-stale)) { display: none; }

/* ---- cell type: 14px, like every other table ----
   The injected sheet puts the whole table on --cap-size, header and cells alike,
   so the rows read at heading size. Cells are body copy: 14. Only .dlog-h (the
   column labels, above) stays at 12. The `*` is needed because that sheet sets
   the size on cell CHILDREN too, so they do not inherit. */
.dlog-cell,
.dlog-cell * { font-size: var(--body-size); }
/* The timestamp is the one cell that stays at --cap-size. It is a fixed-width
   stamp rather than something you read across, and at 12px the column narrows to
   56px — the space that buys goes to Wallet. Two classes deep because the rule
   above is one class deep and would otherwise win on order. */
.dlog-cell.dlog-date { font-size: var(--cap-size); }
/* Amount is a plain text cell — logs.js only ever sets its textContent. The
   injected `display: inline-flex` is a leftover from the old two-line layout, and
   on a flex box `text-align: right` does nothing: the text became an anonymous
   flex item packed to the start, so the figures sat left under a right-aligned
   "Amount" heading. Back to a normal box, and the alignment applies. */
.dlog-amt { display: block; }
/* Event has a bounded grid track and must never grow the table. Job labels and
   notice labels stay reachable through their title while clipping in place. */
.dlog-action {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Tx carries the short hash AND a copy button, so it is a flex row like the Wallet
   cell — the injected `text-align: right` cannot place two children, only text.
   justify-content does the right-alignment instead, and the link needs its own
   min-width/overflow because a flex child does not inherit the cell's clipping. */
.dlog-tx {
  display: flex; align-items: center; justify-content: flex-end; gap: 6px;
}
.dlog-tx .feed-tx {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The table ends at the BOTTOM OF THE SCREEN and scrolls its own rows.

   --dlog-max is measured and written by logs.js (fitToBottom), because the height
   wanted here — "from wherever this list starts down to the viewport floor" — is
   the one thing CSS cannot express: it depends on the list's own y position. The
   60vh fallback only applies for the frame before the first measurement.

   The card must WRAP this list rather than stretch past it, hence the height: auto
   below. All four .act-pane siblings share one grid area (2 / 1) and the inactive
   three are only `visibility: hidden`, so the row is as tall as the TALLEST pane —
   Activity, not Logs. Left stretching to that row, the card stood taller than a
   bounded table and the difference showed as a strip of bare --surface under the
   last row, which is the gray bar. */
.dlog-list {
  /* min AND max on the same measurement, so the table occupies the whole budget
     whether or not it has the rows to fill it. Without the min, a short log left the
     table ending at its last row with bare canvas below it down to the floor — a
     hole that looks like a layout bug rather than "no more transactions". Now the
     table's own ground runs to the bottom and the rows simply stop. */
  min-height: var(--dlog-max, 0);
  max-height: var(--dlog-max, 60vh);
  overflow-y: auto;
  background: var(--background);
}
/* Overrides `.act-pane > #dash-logs, … { height: 100% }` in the components layer:
   with the list bounded, filling the pane is what re-creates the gray bar. */
.act-pane > #dash-logs,
.act-pane > #dash-logs > .sc-card { height: auto; }

/* ---- while Logs is the active tab, ONLY Logs sizes the box ----
   The panes deliberately share one grid cell and all stay in layout, so the box
   takes the height of the TALLEST of them (Activity) and does not jump as you
   switch tabs — see the note on .act-pane.is-off above. That equalising is what
   left dead space under this table: the box stood at Activity's height while the
   table was capped to the screen, so the page still scrolled and revealed bare
   card below the last row.
   Taking the three hidden panes OUT OF FLOW for this one state lets row 2 size to
   the Logs pane alone, so the box ends exactly where the table ends and there is
   nothing underneath to scroll to. `inset: 0` rather than a bare `position:
   absolute` on purpose: it keeps each hidden pane the full width of the box, so
   anything that measures its own width while inactive (the charts do) still reads
   the same number it read before. Every other tab keeps the equalised height. */
/* GENERALISED to every tab, not just Logs. Sharing the cell meant the box stood at
   the height of the tallest pane whichever one you were looking at — so filling
   Activity with wallets lengthened the canvas on Sweep, Transfer and Swap too, and
   nothing short of emptying it and reloading brought them back. A pane's scroll extent
   is its own business; an invisible one has no business setting it.
   The height it was buying — panes not jumping as you switch — is bought instead by
   the fill chain each pane already has: a pane with room to spare stretches to the
   canvas, so they agree at rest and differ only when one genuinely has more in it,
   which is the moment they SHOULD differ.
   `inset: 0` rather than a bare `position: absolute`: it keeps each hidden pane the
   full width of the box, so anything measuring its own width while inactive (the
   charts do) still reads the number it read before. */
.act-tabbed > .act-pane.is-off {
  position: absolute; inset: 0;
  /* And CLIPPED. Out of flow is not enough on its own: a hidden pane taller than the
     active one still hung past the bottom and, being overflow rather than layout,
     still lengthened the canvas's scroll range. */
  overflow: hidden;
}

/* ---- Logs active: the canvas does not scroll, the TABLE does ----
   The scroll is LOCKED rather than merely balanced. Every previous attempt worked
   by making the content measure exactly one viewport, which is a knife edge: a
   fractional row height, a scrollbar appearing, a font loading a pixel late, and a
   few pixels of scroll come back. There is nothing below the table worth reaching,
   so the canvas simply does not scroll here — the table's own scroller is the one
   that moves.

   The canvas keeps the same frame inset while the table fills the remaining
   height and owns the only active scroller. */
.app-main:has(.act-pane[data-act="logs"]:not(.is-off)) {
  overflow-y: hidden;
}


/* ---- one copy button for every dense row ----
   .wcopy (app.js iconCopyBtn) is used beside the address in the wallet table and in
   the rail. The Logs table's .dlog-copy draws the same glyph at the same size from
   its own injected sheet, so all three read as one control. */
.wcopy {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 0; margin-left: 6px;
  background: none; border: 0; border-radius: var(--radius);
  color: var(--muted-2); cursor: pointer;
  transition: color .12s, background .12s;
}
.wcopy svg {
  width: 13px; height: 13px;
  fill: none; stroke: currentColor; stroke-width: 1.3;
  stroke-linecap: round; stroke-linejoin: round;
}
.wcopy:hover { color: var(--foreground); background: var(--surface-2); }
.wcopy:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
.wcopy.ok { color: var(--green); }
/* The address cell becomes a row so the glyph sits on the baseline beside the text
   instead of being clipped by the cell's ellipsis. */
.act-pane .sc-dropzone .sc-wallet-addr {
  display: inline-flex; align-items: center; min-width: 0;
}

/* ---- the ETH mark is gone, everywhere ----
   .eth-lead::before drew a masked octahedron in front of a figure. Killing the
   pseudo-element here removes it from EVERY instance in one place — the wallet
   table, the rail, drop-zone chips, loop rows, the gas reserve tail, session
   totals — rather than unpicking the class from a dozen builders. The class stays
   on those elements and keeps its spacing role; only the glyph is dropped, so
   nothing needs to be renamed and the JS that toggles `.eth-lead` on and off
   (app.js: "only shows in front of an actual number") stays correct.
   NOTE: a few spans exist ONLY to carry this mark (app.js loopEthMark()) and are
   now empty — harmless, and they disappear visually, which is the intent. */
.eth-lead::before { content: none; }

/* ---- type renders at Regular, not just declared as it ----
   Every weight in the app is already 400 and only the 400 face is loaded — that was
   measured. What still read heavier than Regular is the RASTERISER: on macOS,
   `-webkit-font-smoothing: auto` is subpixel AA, which fattens light-on-dark text.
   `antialiased` is grayscale AA and renders the same 400 face at its true weight.
   This overrides the deliberate `auto` in @layer base (see the note there, which
   reads it off the reference) — that is the one thing left that can make Regular
   look like Medium, and it has been asked for three times. Revert this block to get
   the reference's own rasterisation back. */
body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ---- the tab strip joins the box below it, and does not scroll ----
   .act-tabs sets `overflow-x: auto` for many tabs, and CSS then computes
   `overflow-y` to AUTO as well — `visible` on one axis is not allowed beside a
   scrolling one. That turned the strip into a scroll container in BOTH directions,
   with two consequences from the same cause:
     1. the ink's `bottom: -1px` no longer painted over the card's top border, it
        became 1px of SCROLLABLE CONTENT — the strip could be scrolled vertically by
        exactly that pixel;
     2. so the seam stayed visible under the active tab, the border it was supposed
        to replace still drawn.
   Fixed by moving the overlap from the ink to the STRIP: the strip itself now sits
   1px into the box below (negative margin), and the ink sits flush at its bottom
   edge. No negative offset, so nothing overflows and there is nothing to scroll.
   z-index is required with it — the pane comes later in the DOM, so without it the
   card paints over the pixel the strip just moved into. */
.act-tabs { margin-bottom: -1px; z-index: 1; }
.act-tab-ink { bottom: 0; }

/* ---- the expand / minimize control ----
   Pinned to the right end of the tab strip's row. Absolute rather than a flex child
   of .act-tabs: it lives outside the tablist for the ARIA reason given in
   index.html, and this also keeps it still while the strip scrolls horizontally. */
.act-tabbed { position: relative; }
.act-expand {
  /* ABOVE the card's help badge (z-index 3 in help-guide.js). This button carries a
     z-index, so it opens a stacking context — its hover hint is trapped inside and is
     ordered against the badge by the BUTTON's own value, not the hint's. At 1 the hint
     rendered under the "?" it sits beside. */
  position: absolute; top: 2px; right: 0; z-index: 5;
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0;
  background: none; border: 1px solid transparent; border-radius: var(--radius);
  color: var(--muted-2); cursor: pointer;
  transition: color .12s, background .12s, border-color .12s;
}
.act-expand:hover { color: var(--foreground); background: var(--surface-2); }
.act-expand:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
.act-expand svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}
/* One button, two glyphs — arrows out to expand, arrows in to restore. Driven off
   aria-expanded so the state lives in the attribute assistive tech already reads,
   with no second class to keep in sync. */
.act-expand .ic-in { display: none; }
.act-expand[aria-expanded="true"] .ic-out { display: none; }
.act-expand[aria-expanded="true"] .ic-in { display: block; }
/* Expanded: the overview (tiles + charts) is out of the way and the tab strip —
   with the table under it — starts at the top of the canvas. */
/* COLLAPSES, it does not vanish. display:none took 362px out of the layout in one
   frame, so the tabs jumped up the page with nothing to follow. Height and opacity are
   transitioned instead, and the negative margin cancels the stack's own gap on the way
   so the panel leaves no trace of itself at the end.
   max-height rather than height: the panel has no fixed size to animate from, and the
   cap only has to exceed the real height (~362px) for the ease to read correctly. */
.stack > .panel#act-overview {
  overflow: hidden; max-height: 1200px; opacity: 1;
  transition: max-height .3s cubic-bezier(.4, 0, .2, 1), opacity .18s ease, margin-bottom .3s cubic-bezier(.4, 0, .2, 1);
}
.stack.is-act-expanded > .panel#act-overview {
  max-height: 0; opacity: 0; margin-bottom: calc(-1 * var(--sp-2));
}
@media (prefers-reduced-motion: reduce) {
  .stack > .panel#act-overview { transition: none; }
}


/* Wallet rail title: 24px line box, regular weight, and subdued foreground. */
.rail-bar-name {
  /* inline-flex + explicit height: as a plain inline span its box measured the
     glyphs (16px), not the 24px line the frame specifies. */
  display: inline-flex; align-items: center; height: 24px;
  font-size: var(--body-size); font-weight: 400; line-height: 24px;
  color: rgba(255, 255, 255, .6863); white-space: nowrap;
}
/* The live wallet count, as a chip beside the title (no copy button — the title
   names the list, it is not something you copy). */
.rail-bar-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 6px; box-sizing: border-box;
  border-radius: var(--radius); background: var(--surface-2);
  font-size: var(--cap-size); font-weight: var(--label-weight); line-height: 1;
  color: var(--muted); font-variant-numeric: tabular-nums;
}
.rail-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 32px; padding: 0 12px; box-sizing: border-box; flex: 0 0 auto;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-2); color: var(--foreground);
  font-family: inherit; font-size: var(--body-size); font-weight: 400;
  line-height: 1; white-space: nowrap; cursor: pointer;
  transition: background .12s, border-color .12s;
}
.rail-btn:hover { background: var(--surface); border-color: var(--border-strong, var(--border)); }
.rail-btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
/* The primary: solid orange, no border, dark label — the one button on the bar
   that makes a new thing. */
.rail-btn-primary {
  background: var(--accent); border-color: var(--accent); color: #0d0d0d; font-weight: 400;
}
.rail-btn-primary:hover { background: var(--accent); border-color: var(--accent); filter: brightness(1.08); }

/* ---- Actions column: THE STAMP AT REST, THE ICONS ON HOVER ----
   One cell, two occupants, and they swap. At rest it shows how long ago this wallet
   traded, in the colour of the side it traded on — that plus the row's left edge is
   the whole "this wallet is hot" signal. Hover and the stamp steps aside for what you
   can do to the wallet, because a row cannot afford a second right-hand column and the
   two are never both urgent.
   The track (--fig-col-act, 120px) is fixed and wider than either occupant, so the
   swap moves nothing else on the row. */
.rail-wallet-acts { display: none; align-items: center; gap: 2px; }
.rail-wallet:hover .rail-wallet-ago,
.rail-wallet:focus-within .rail-wallet-ago { display: none; }
.rail-wallet:hover .rail-wallet-acts,
.rail-wallet:focus-within .rail-wallet-acts { display: flex; }

/* The profile glyph is an inline <svg> with no width/height attributes — in the
   rail it inherited a size from .hdr-iconbtn's old context and rendered blank
   once the button moved into the header. State it here. */
.app-header .hdr-iconbtn svg { width: 16px; height: 16px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round; }
/* SQUARE, and the same square for all three. The sizing rule these had was scoped to
   .rail-head / .shell-top — the two places this cluster used to live — so once it moved
   into .app-header nothing set a width and .btn's own padding sized each button to its
   glyph: 42x32 boxes pretending to be icon buttons. One rule, one dimension. */
.app-header .hdr-iconbtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--ctx-ctl-h); height: var(--ctx-ctl-h);
  min-width: var(--ctx-ctl-h); padding: 0; flex: 0 0 auto;
  background: transparent; color: var(--muted);
  border: 1px solid var(--border); border-radius: var(--radius);
  line-height: 1;
}
.app-header .hdr-iconbtn:hover {
  background: var(--surface-2); color: var(--foreground); border-color: var(--border-strong);
}
.app-header .hdr-iconbtn:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
/* Header controls share one height. This scoped rule deliberately follows the
   unlayered 32px .rail-btn base so Trade/Overview cannot drift onto that larger
   action-button ladder while the context controls remain compact. */
.app-header .app-brand,
.app-header .app-brand-vector,
.app-header .app-nav .rail-btn,
.app-header #ctx-token .cs-btn,
.app-header .ctx-addbtn { height: var(--ctx-ctl-h); }

/* The reserved Actions cell on a rail row. Sits in the row's right-hand column
   under the balances; fixed width so the figures do not move when icons land. */
.rail-wallet-actions {
  grid-column: 2; grid-row: 3; justify-self: end; align-self: center;
  display: flex; align-items: center; gap: 4px;
  min-width: 24px; min-height: 20px;
}

/* ============================================================
   RAIL SEARCH ROW — 56px tall, 16px inset, field 32px, "Show archived" right.
   Measured off the reference frame (Frame 2147224514: h=56, field 300x32 at
   x=16 y=12, right-hand button 121x24 at y=16).
   ============================================================ */
.rail-search-row {
  display: flex; align-items: center; gap: var(--sp-3);
  height: 56px; box-sizing: border-box; padding: 0 16px;
  border-bottom: 1px solid var(--border);
}
.rail-search-field {
  position: relative; flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; height: 32px;
}
.rail-search-icon {
  position: absolute; left: 0; width: 15px; height: 15px; pointer-events: none;
  fill: none; stroke: var(--muted-2); stroke-width: 1.5; stroke-linecap: round;
}
.rail-search-row #rail-search {
  width: 100%; height: 32px; box-sizing: border-box;
  padding: 0 0 0 24px; margin: 0;
  background: none; border: 0; border-radius: 0;
  color: var(--foreground);
  font-family: inherit; font-size: var(--body-size); line-height: 32px;
}
.rail-search-row #rail-search::placeholder { color: var(--muted-2); }
/* No ring on focus. `outline: none` alone never did it: the box drawn round this
   field was a BOX-SHADOW, from the global `input:focus` rule that gives every form
   control a 1px --ring halo (style.css ~1202). That rule is right for a bordered
   input in a form; this one is a chromeless field sitting in a 56px bar, and a
   rectangle appearing round it on click is just noise — the caret already says it has
   focus. Both the shadow and the border-colour it also sets are cleared. */
.rail-search-row #rail-search:focus,
.rail-search-row #rail-search:focus-visible {
  outline: none;
  box-shadow: none;
  border-color: transparent;
}
.rail-search-field:focus-within .rail-search-icon { stroke: var(--foreground); }
/* Right-hand toggle. Text + eye-slash, no chrome — it is a filter switch, not a
   button competing with + Group / + Create above it. */
.rail-archived-btn {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px;
  height: 24px; padding: 0; border: 0; background: none; cursor: pointer;
  color: var(--muted); font-family: inherit; font-size: var(--body-size);
  line-height: 1; white-space: nowrap;
}
.rail-archived-btn svg {
  width: 15px; height: 15px; fill: none; stroke: currentColor;
  stroke-width: 1.4; stroke-linecap: round;
}
.rail-archived-btn:hover { color: var(--foreground); }
/* ON = orange, matching every other "this is active" mark in the app (the tab ink, the
   selected group's bar). The open eye says the dust is in the list; the colour says the
   filter is doing something. */
.rail-archived-btn.active { color: var(--accent); }
.rail-archived-btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }


/* Rows and column labels share the same 16px inset as the toolbar above them. */
.rail-wallet { padding-left: 16px; padding-right: 16px; }

/* ============================================================
   WALLET RAIL — Gotham typography, alpha hierarchy, and fixed data columns.
   Keep row and header geometry synchronized so labels align with values.
   ============================================================ */
/* THE TWO TOKENS .rail-btn ITSELF NEEDS ARE GLOBAL — the rest of the fig-* set stays
   scoped below, because it is the rail's own column widths and text ramp.
   .rail-btn is the app's SECONDARY BUTTON and it is used well outside this column (the
   header nav, and now the list headers on Transfer), but it painted itself with a
   --fig-alpha-3 declared here — so anywhere else it arrived with no fill at all and no
   hover, because the hover swaps to a value it could not reach either. That is a bug in
   the component, not something each caller should patch: two callers had already worked
   around it, one by restating the value and one by giving up and using --surface-2.
   Declared on :root, restated below so the rail keeps owning its own set. */
:root {
  --fig-alpha-3: rgba(255, 255, 255, .08);
  --fig-accent-9: var(--accent);
}
.wallet-rail {
  --fig-alpha-3:  rgba(255, 255, 255, .08);
  --fig-alpha-9:  rgba(255, 255, 255, .44);
  --fig-alpha-11: rgba(255, 255, 255, .6863);
  --fig-neutral-11: #b4b4b4;
  --fig-accent-9: var(--accent);
  --fig-col-bal: 80px;
  --fig-col-hold: 80px;
  --fig-col-act: 78px;   /* was 120px: the same Actions width as the wallet table */
  --fig-col-gap: 8px;
}

/* ---- toolbar: 16/12 padding, title + badge left, buttons right ---- */
#rail-groups.rail-groups {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; box-sizing: border-box; height: 56px; padding: 0 16px;
  border-bottom: 1px solid var(--border);
}
.rail-bar-title { display: flex; align-items: center; gap: 12px; margin-right: auto; min-width: 0; }
/* Typography/3/Regular — 16px, NOT 14. This is the scale error: the title is one
   step up the ramp from body text, which is why it read small next to the frame. */
.rail-bar-name {
  display: inline-flex; align-items: center; height: 24px;
  /* --body-size, not the frame's 16: 16 is not a step on the 12/14/20/30 ramp.
     The duplicate of this block further up the file already used --body-size. */
  font-size: var(--body-size); font-weight: 400; line-height: 24px;
  color: var(--fig-alpha-11); white-space: nowrap;
}
/* Badge — Typography/1/Medium on neutral-alpha/3, 6px/2px padding = 20px tall. */
.rail-bar-count {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 2px 6px; box-sizing: border-box;
  border-radius: var(--radius); background: var(--fig-alpha-3);
  font-size: 12px; font-weight: 400; line-height: 16px;
  color: var(--fig-alpha-11); font-variant-numeric: tabular-nums;
}
/* Buttons — h32, px12, gap8, Typography/2/Medium. NO BORDER: both are solid
   fills in the design; the bordered secondary I invented was never there. */
.rail-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 32px; padding: 0 12px; box-sizing: border-box; flex: 0 0 auto;
  border: 0; border-radius: var(--radius);
  background: var(--fig-alpha-3); color: var(--foreground);
  font-family: inherit; font-size: 14px; font-weight: 400; line-height: 20px;
  white-space: nowrap; cursor: pointer; transition: background .12s, filter .12s;
}
.rail-btn:hover { background: rgba(255, 255, 255, .12); }
.rail-btn:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
/* One accent, not two. This was #ff6500 — the reference's own orange, a shade redder
   than the app's — kept separate so the rail's + Create could match the mock while the
   tab underline used ours. Two near-identical accents is a bug you cannot see and can
   never keep in step, so it now reads from --accent like everything else. */
.rail-btn-primary { background: var(--fig-accent-9); color: #fff; }
.rail-btn-primary:hover { background: var(--fig-accent-9); filter: brightness(1.08); }
#rail-groups .rail-btn + .rail-btn { margin-left: 0; }

/* ---- column header: px16/py8 = 32 tall, Typography/1/Regular, alpha/9 ---- */
.rail-bal-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--fig-col-gap); box-sizing: border-box; padding: 8px 16px;
  border-bottom: 1px solid var(--border);
}
.rail-bal-head span {
  font-size: 12px; font-weight: 400; line-height: 16px;
  color: var(--fig-alpha-9); white-space: nowrap;
}

/* ---- rows: px16/py12 = 68 tall ---- */
/* THREE columns: identity | balances | actions — the design's own shape. The
   actions cell had been placed on a third grid ROW, which stretched a 68px row to
   87. It belongs beside the balances, centred, spanning both text lines. */
.rail-wallet {
  grid-template-columns: minmax(0, 1fr) auto var(--fig-col-act);
  padding: 12px 16px; min-height: 68px;
  column-gap: var(--fig-col-gap);
}
.rail-wallet-bals { grid-column: 2; grid-row: 1 / span 2; align-self: center; justify-self: end; }
.rail-wallet-actions { grid-column: 3; grid-row: 1 / span 2; align-self: center; }
/* Typography/2/Regular for the name, /1/Regular for the address. */
.rail-wallet-name { font-size: 14px; font-weight: 400; line-height: 20px; }
.rail-wallet-addr { font-size: 12px; font-weight: 400; line-height: 16px; color: var(--fig-neutral-11); }
.rail-wallet-bal, .rail-wallet-eth {
  font-size: 14px; font-weight: 400; line-height: 20px; color: var(--fig-alpha-11);
}
/* Fixed column widths, straight from the design. */
.rail-wallet-bals { display: grid; grid-template-columns: var(--fig-col-bal) var(--fig-col-hold);
  column-gap: var(--fig-col-gap); }
.rail-wallet-actions { width: var(--fig-col-act); min-width: var(--fig-col-act);
  justify-content: flex-end; }

/* ============================================================
   THE RAIL IS THE SAME TABLE.

   It was a different shape from every other table here: two headings that were
   TICKERS ("TOKEN" / "ETH") rather than column names, and its two figures in the
   OPPOSITE order to the wallet table (token first, then ETH). Now it carries the
   same four columns in the same order — Wallet | Balance | Holdings | Actions —
   with the same 1.6 : 1 : 1 ratio.

   TWO grid rows per wallet, not three: name over address, everything else centred
   against them. The third row was the LAST TX line, and losing it is the point —
   see the Actions cell below, which now carries the recency instead.

   The two figures are placed in the MAIN grid (their .rail-wallet-bals wrapper is
   dissolved with display: contents, the same trick .rail-wallet-id already uses)
   instead of living in a nested 80/80px grid of their own.
   ============================================================ */
.rail-bal-head, .rail-wallet {
  /* THE IDENTITY COLUMN TAKES A LITTLE MORE, because it holds two things now. A
     name and its group badge were sharing 1.6fr against two 1fr balance columns,
     and at the rail's width that came to 124px — "Maker 04" plus "Warmers" is 126,
     so both ends clipped by a couple of pixels and the badge looked broken. At 2fr
     the balances give up ~7px each (they print five characters into seventy) and
     the pair fits. The header reads the same variable, so the columns stay columns. */
  --rail-cols: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr) var(--fig-col-act);
  /* 12px, not the 24px the wide tables use: the rail is a narrow fixed column and
     three 24px gutters plus a 120px Actions track leave the name nothing to sit in. */
  --rail-gap: var(--sp-3);
}
.rail-wallet {
  grid-template-columns: var(--rail-cols);
  column-gap: var(--rail-gap);
}
.rail-bal-head {
  display: grid;
  grid-template-columns: var(--rail-cols);
  column-gap: var(--rail-gap);
  align-items: center;
}
/* Dissolved, so the two figures become items of .rail-wallet's own grid and can
   each take a column of their own. */
.rail-wallet-bals { display: contents; }
/* Balance = ETH, Holdings = token — the wallet table's order, which is the reverse
   of the order the rail emits them in. Placed on the .rail-wallet-balcell WRAPPERS,
   not on the figures inside them: each figure sits in a cell of its own (figure +
   its caption), so with .rail-wallet-bals dissolved it is the WRAPPER that becomes
   the grid item, and a grid-column on the inner span does nothing at all.
   Order is by position, since the two wrappers carry the same class: the first
   holds the token figure, the second the ETH one (app.js appends them in that
   order). Both span the name/address rows and centre against them, exactly as the
   nested 80/80 pair used to. */
.rail-wallet-bals > .rail-wallet-balcell {
  grid-row: 1 / span 2; align-self: center; justify-self: end; min-width: 0;
}
.rail-wallet-bals > .rail-wallet-balcell:nth-child(1) { grid-column: 3; }  /* Holdings */
.rail-wallet-bals > .rail-wallet-balcell:nth-child(2) { grid-column: 2; }  /* Balance  */
.rail-wallet-actions { grid-column: 4; grid-row: 1 / span 2; align-self: center; }
/* ============================================================
   SWEEP — full width, and its two wallet lists ARE the Activity table.

   The pane's .sc-cards is the shared `repeat(auto-fill, minmax(340px, 1fr))` grid,
   so the single Sweep card landed in one of two 482px columns and the other stood
   empty — the card read as half the tab. One column here, so it fills the pane.

   The table itself is not a copy: shortcuts-activity.js's row rules are scoped to
   .act-panel, which is Activity and Sweep and nothing else, so both panes render
   from one implementation. All that differs is the track list below.
   ============================================================ */
.act-pane[data-act="sweep"] .sc-cards,
.act-pane[data-act="transfer"] .sc-cards,
.act-pane[data-act="swap"] .sc-cards { grid-template-columns: minmax(0, 1fr); }
/* Sweep is three boxes now, so — exactly as for Activity — the pane's card gives up
   its chrome, or every box would draw its border inside a fourth one. */
.act-pane[data-act="sweep"] .sc-card,
.act-pane[data-act="transfer"] .sc-card,
.act-pane[data-act="swap"] .sc-card {
  border: 0; background: none; padding: 0; border-radius: 0;
}
/* The panel stacks tiles then band on the page's 8px rhythm, and the tiles become a
   box in their own right now that the card no longer draws one round them. */
.act-pane[data-act="sweep"] .act-panel,
.act-pane[data-act="transfer"] .act-panel,
.act-pane[data-act="swap"] .act-panel {
  display: flex; flex-direction: column; gap: var(--sp-2);
}
.act-pane[data-act="sweep"] .act-panel > .act-cards {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface);
  padding: var(--pad-card) var(--pad-card) var(--pad-card) var(--act-band-pad-l);
}
/* WALLETS box: the two lists fill it edge to edge, split by one hairline, each
   owning its own 24px insets — the same deal .act-box-wallets makes on Activity. */
/* Activity is in these lists too: its Makers box took Sweep's anatomy the day it
   grew a Sequence view — .sw-wallets > .sw-view-bar + .sc-io-row > .sw-list, one
   list in the row instead of two — so the same rules size both. */
.act-pane[data-act="activity"] .sw-wallets,
.act-pane[data-act="sweep"] .sw-wallets,
.act-pane[data-act="transfer"] .sw-wallets,
.act-pane[data-act="swap"] .sw-wallets { padding: 0; gap: 0; }
.act-pane[data-act="activity"] .sc-io-row,
.act-pane[data-act="sweep"] .sc-io-row,
.act-pane[data-act="transfer"] .sc-io-row,
.act-pane[data-act="swap"] .sc-io-row { gap: 0; flex: 1 1 auto; min-height: 0; }
.act-pane[data-act="activity"] .sw-list,
.act-pane[data-act="sweep"] .sw-list,
.act-pane[data-act="transfer"] .sw-list,
.act-pane[data-act="swap"] .sw-list { gap: 0; min-height: 0; }
.act-pane[data-act="activity"] .sw-list + .sw-list,
.act-pane[data-act="sweep"] .sw-list + .sw-list,
.act-pane[data-act="transfer"] .sw-list + .sw-list,
.act-pane[data-act="swap"] .sw-list + .sw-list { border-left: 1px solid var(--border); }
/* The caption is the list's title row, on the same 48px rhythm as .act-box-title. */
/* (The caption's own type and padding used to be declared here at 16px — a size that
   is not on the 12/14/20/30 ramp, and a second copy of what the shared
   `.act-box-title, .sc-io-cap` rule now sets for all three box headers. Removed so
   there is one definition; only the pane-specific bits stay below.) */
/* The list scrolls inside its own box rather than stretching the band. */
.act-pane[data-act="activity"] .sw-list > .sc-dropzone,
.act-pane[data-act="sweep"] .sw-list > .sc-dropzone,
.act-pane[data-act="transfer"] .sw-list > .sc-dropzone,
.act-pane[data-act="swap"] .sw-list > .sc-dropzone {
  border: 0; border-radius: 0; padding: 0; flex: 1 1 auto; min-height: 0;
}
/* Sweep row 1 is one aligned Wallets | Configure band. Buyers and Sellers fill
   the band's stretched Wallets box, so all three visible boxes share its height.
   Row 2 reuses .sc-io-row for Price path | Sequence across the same full width. */
.act-pane[data-act="activity"] .act-band > .sw-wallets,
.act-pane[data-act="sweep"] .act-band > .sw-wallets { height: 100%; }
/* CONFIGURE HUGS ITS CONTROLS, in every pane. The band aligns its grid items to
   stretch, which is right for the wallet column — that is the one that should
   absorb the leftover height — and wrong for this one: beside a long wallet table
   Configure grew a tall empty tail under its Run button. Both the stretch and the
   height:100% this column used to carry are undone here; align-self alone loses
   to an explicit height. */
.act-pane[data-act="activity"] .act-band > .act-band-right,
.act-pane[data-act="sweep"] .act-band > .act-band-right,
.act-pane[data-act="transfer"] .act-band > .act-band-right,
.act-pane[data-act="swap"] .act-band > .act-band-right { align-self: start; height: auto; }
.act-pane[data-act="activity"] .act-band > .act-band-right > .act-box-cfg,
.act-pane[data-act="sweep"] .act-band > .act-band-right > .act-box-cfg,
.act-pane[data-act="transfer"] .act-band > .act-band-right > .act-box-cfg,
.act-pane[data-act="swap"] .act-band > .act-band-right > .act-box-cfg { flex: 0 0 auto; height: auto; }
/* The box chrome lives on the VIEW BAR and the LISTS, not on the wrapper: the
   Makers | Sequence switch carries the top edge and each list frames itself (see
   scInstallSequenceStyles in shortcuts.js). Sweep and Activity both — Activity's
   Makers table is one such list now, so its .act-box border comes off the same way. */
.act-pane[data-act="activity"] .act-band > .sw-wallets,
.act-pane[data-act="sweep"] .act-band > .sw-wallets { border: 0; background: none; }
.act-pane[data-act="activity"] .act-band > .sw-wallets > .sc-io-row,
.act-pane[data-act="activity"] .act-band > .sw-wallets > .sc-io-row > .sw-list,
.act-pane[data-act="sweep"] .act-band > .sw-wallets > .sc-io-row,
.act-pane[data-act="sweep"] .act-band > .sw-wallets > .sc-io-row > .sw-list { height: 100%; }
.act-pane[data-act="activity"] .act-band > .sw-wallets > .sc-io-row > .sw-list,
.act-pane[data-act="sweep"] .act-band > .sw-wallets > .sc-io-row > .sw-list {
  border: 1px solid var(--border); background: var(--surface);
}
.act-pane[data-act="activity"] .act-band > .sw-wallets > .sc-io-row > .sw-list + .sw-list,
.act-pane[data-act="sweep"] .act-band > .sw-wallets > .sc-io-row > .sw-list + .sw-list { border-left-width: 0; }
.act-pane[data-act="sweep"] .act-panel > .sc-io-row { width: 100%; gap: var(--sp-2); align-items: stretch; }
.act-pane[data-act="sweep"] .act-panel > .sc-io-row > .sw-sequence-box { flex: 1 1 0; width: auto; }
/* Run sweep sits INSIDE Configure, under the controls, so the third column is one
   box — Sweep has no separate Run box to put beside it the way Activity does. */
.act-pane[data-act="sweep"] .act-box-cfg > .act-actions,
.act-pane[data-act="transfer"] .act-box-cfg > .act-actions,
.act-pane[data-act="swap"] .act-box-cfg > .act-actions { margin-top: auto; }
/* STAGGER has to be able to shrink. `.act-row .act-track { flex: 0 0 auto }` is right
   on Activity, where every track is a typed box at its natural width — but Sweep's
   Stagger track is a two-handle SLIDER, so at a fixed basis it kept its ~258px
   intrinsic width in a 340px column, shoved the readout 33px past the box edge and
   squeezed the label to nothing. Scoped to this box so Activity's rows are untouched. */
.act-pane[data-act="sweep"] .act-box-cfg .act-row > .act-track,
.act-pane[data-act="transfer"] .act-box-cfg .act-row > .act-track,
.act-pane[data-act="swap"] .act-box-cfg .act-row > .act-track {
  flex: 1 1 auto; min-width: 0;
}
.act-pane[data-act="sweep"] .act-box-cfg .act-row > .act-label { flex: 0 0 auto; }
.act-pane[data-act="sweep"],
.act-pane[data-act="transfer"],
.act-pane[data-act="swap"] {
  /* Wallet | Balance | token | ✕. The last track is 24px and unlabelled — Sweep has
     no split stepper, so the Actions cell is one remove glyph, and a heading over it
     would be naming nothing. */
  --act-wt-cols: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr) 16px;
}
/* Areas end in `x`, not `act`: with no cardExtra there is no .sc-wallet-act wrapper
   to place, so the ✕ is a direct child of the card and takes the last column itself. */
.act-pane[data-act="sweep"] .sc-dropzone .sc-wallet-card,
.act-pane[data-act="transfer"] .sc-dropzone .sc-wallet-card,
.act-pane[data-act="swap"] .sc-dropzone .sc-wallet-card {
  grid-template-areas: "name eth bal x" "addr eth bal x";
}
.act-pane[data-act="sweep"] .sc-dropzone .sc-wallet-x,
.act-pane[data-act="transfer"] .sc-dropzone .sc-wallet-x,
.act-pane[data-act="swap"] .sc-dropzone .sc-wallet-x {
  grid-area: x; align-self: center; justify-self: end;
}
/* The two lists sit side by side already (.sc-io-row); this keeps their headings and
   rows on one baseline when one side is empty and the other is not.
   The transparent side borders are what ALIGNS the columns. The rows live inside
   .sc-dropzone, which carries a 1px border, so their content box is 2px narrower than
   the heading's and starts 1px further in — enough to walk every column off by ~1px.
   Giving the heading the same 1px each side (invisible) makes both content boxes
   identical, so head and rows resolve the same track widths from --act-wt-cols.
   Activity needs none of this: its lists zero the dropzone's border, and its heads
   already sit on the rows to the pixel — Activity's Makers and Sequence tables are
   .sc-io-box lists now too, and are deliberately left OUT of this rule so that
   alignment is kept. */
.act-pane[data-act="sweep"] .sc-io-box > .act-wt-head,
.act-pane[data-act="transfer"] .sc-io-box > .act-wt-head,
.act-pane[data-act="swap"] .sc-io-box > .act-wt-head {
  flex: 0 0 auto;
  border-left: 1px solid transparent;
  border-right: 1px solid transparent;
}

/* Inline error boxes used inside operation results remain readable as rows. */
.error-box:not([hidden]) {
  display: flex; align-items: flex-start; gap: var(--sp-3);
}

/* ============================================================
   ONE TITLE ROW for every box on the band — Buyers, Sellers, Configure.
   All three were different: Buyers/Sellers carried a 16px caption with only padding
   under it, Configure had a bare margin, and nothing separated any of them from the
   rows below. Same height, same type, and a hairline under each, so the three read
   as one row of boxes.
   ============================================================ */
/* 16px, NOT the tables' 20px. The 20px inset belongs to the table and its column
   headings, where it has to leave room for the drag grip that sits in it; a box's own
   title row has no grip to clear, so it takes the tighter inset. */
.act-box-title,
.sc-io-cap {
  display: flex; align-items: center; gap: var(--sp-2);
  box-sizing: border-box; height: 48px; margin: 0; flex: 0 0 auto;
  padding: 0 16px;
  border-bottom: 1px solid var(--border);
  font-size: var(--body-size); font-weight: 400; line-height: 24px;
  color: var(--foreground);
}
/* The count badge is BRIGHTER than the surface it sits on — at --surface-2 it was
   the same fill as the chips and boxes around it, so the number read as unboxed.
   The bottom-right Activity popup reuses this exact badge beside its title. */
.sc-io-cap > .rail-bar-count,
.txm-heading > .rail-bar-count {
  flex: 0 0 auto;
  background: var(--border-strong); color: var(--foreground);
}
/* A populated count doubles as clear-all. The number remains in layout while it
   becomes invisible, so revealing the same ✕ used on wallet rows cannot resize the
   chip or nudge the copy control beside it. Empty and fixed-capacity counts stay inert. */
.sc-list-clear {
  position: relative; border: 0; appearance: none;
  font-family: inherit; cursor: default; opacity: 1;
}
.sc-list-clear.is-populated { cursor: pointer; }
.sc-list-clear-x {
  position: absolute; inset: 0;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--red); font-size: var(--cap-size); line-height: 1;
  opacity: 0; pointer-events: none;
}
.sc-list-clear.is-populated:hover .sc-list-clear-count,
.sc-list-clear.is-populated:focus-visible .sc-list-clear-count { visibility: hidden; }
.sc-list-clear.is-populated:hover .sc-list-clear-x,
.sc-list-clear.is-populated:focus-visible .sc-list-clear-x { opacity: 1; }
.sc-list-clear:focus-visible { outline: 1px solid var(--ring); outline-offset: 1px; }
/* Configure has box padding of its own, so its title has to bleed back out to the
   box edges for its rule to run the full width. */
.act-box-cfg > .act-box-title {
  margin: calc(var(--cfg-pad) * -1) calc(var(--cfg-pad) * -1) var(--cfg-pad);
}
/* CONFIGURE is inset 16, not 20: it is a column of controls rather than a table, and
   at the tables' inset the fields had less room than the labels beside them. */
.act-box-cfg {
  --cfg-pad: 16px;
  padding: var(--cfg-pad);
}
/* (The list titles' inset and their count/copy rules are declared ONCE, with the
   shared `.act-box-title, .sc-io-cap` block above. A second copy here was overriding
   that inset back to the table's 20px.) */
.sc-io-cap-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Wallet-list titles start quiet like their zero count, then come up to full
   foreground when the shared count state marks their header populated. Quick
   swap uses the rail title anatomy, so it reads the same state through the same
   header class without changing the permanent Trading Wallets heading. */
.sc-io-cap > .sc-io-cap-name,
.rail-quickswap .rail-bar-title > .rail-bar-name {
  color: var(--muted); transition: color .12s;
}
.sc-io-cap.is-populated > .sc-io-cap-name,
.rail-quickswap .rail-bar-title.is-populated > .rail-bar-name { color: var(--foreground); }
.sc-io-cap > .wcopy { margin-left: 0; }
/* "+ Create" IS "+ Group" — it wears .rail-btn, so its height, inset, fill and 14px
   type all come from that one definition and cannot drift from the button it is
   supposed to match. SECONDARY for the same reason "+ Group" is: the rail's "+ Create"
   is the app's primary blue chip and the front door for making a wallet; this is a
   shortcut into one particular list.
   All that is left here is the one thing specific to living in a list header — which
   END of it to sit at. The name/count/copy cluster keeps the left, this keeps the
   right, however long the label runs. It fits the 48px header with 8px to spare, so
   nothing about the row's height moves. */
.sc-io-cap > .sc-io-create { margin-left: auto; }
.sc-io-cap > .sc-io-create:disabled { color: var(--muted-2); cursor: default; filter: none; }

/* ---- the control rows line up on ONE track start ----
   Every label used to be `flex: 0 1 auto`, so each row's control began wherever that
   row's word happened to end — "Net" started its slider 30px left of where "Max
   wallets" started its box. A fixed label column gives every track, box and select
   the same left edge. 84px fits "Max wallets" at 14px, the longest of them. */
.act-box-cfg .act-row > .act-label {
  flex: 0 0 84px; width: 84px;
}
/* (Activity used to opt OUT of this column. Its three names carried their units —
   "Max wallets / chain" runs 128px — so no fixed column could hold them and the
   box sized labels from content instead. The units moved into the readouts beside
   the sliders, the names came back under 84, and the exception went with them:
   every slider in every Configure box is now the same length.) */
/* ...and the control takes all the remaining width, so the right edges line up too.
   :empty on the readout matters: Order's is a placeholder, and left in the flow it
   held a space-between gap that pushed the select off the right edge. */
.act-box-cfg .act-row > .act-read:empty { display: none; }

/* ---- ONE vertical rhythm in Configure ----
   The gaps were not random by accident but they read that way: .act-controls set a
   6px gap, then `.act-row.act-group-start` added a 12px top margin to Max wallets to
   "group" the rows into pairs, so the column ran 6, 6, 18, 6. And the box's own
   padding is 16, so the first row sat closer to the title than the rows sat to each
   other. One gap throughout, equal to the box padding, and the grouping margin is
   dropped — the class stays in the markup, it just no longer changes the spacing. */
/* Named, because one rule needs to CANCEL it: Swap's trigger and its swap widget are
   two siblings that have to touch, and the only way back across a flex gap is a
   negative margin of exactly its size. Through the variable that margin cannot drift
   from the gap it undoes. */
.act-box-cfg .act-controls { --act-ctl-gap: 16px; gap: var(--act-ctl-gap); }
.act-box-cfg .act-row.act-group-start { margin-top: 0; }
/* The BOX's gap carries the 16px, and the title adds none — with both, the space under
   the title was 28px (its own 16 plus .act-box's 12px gap) while the rows sat 16
   apart, which is the "some gaps bigger" part. One value, set in one place. */
.act-box-cfg { gap: 16px; }
.act-box-cfg > .act-box-title { margin-bottom: 0; }

/* The Order select is a FIXED 130px — it holds two short phrases ("Buy first",
   "Sell first"), and stretched to the full column it was the widest control in the
   box for the least content. Everything else still takes the slack. */
.act-box-cfg .act-row > select,
.act-box-cfg .act-row > .cs { flex: 0 0 130px; width: 130px; min-width: 0; }
/* :not(.cs) matters — custom-select.js copies the select's classes onto its wrapper so
   existing width rules still find it, which means `.cs` is also an `.sc-input` here and
   this rule would stretch the dropdown back out to the full column. */
.act-box-cfg .act-row > .sc-input:not(select):not(.cs) { flex: 1 1 auto; min-width: 0; }

/* ============================================================
   THE DROPDOWN — one control, replacing the browser's <select> popup.
   custom-select.js wraps every native select in `.cs` and hides it; the button and
   the menu below are what you see. The native element is still there and still the
   value — see that file's header.
   Written against the same tokens as every other control, so a dropdown now matches
   the inputs beside it instead of being drawn by the OS.
   ============================================================ */
/* The wrapper LAYS OUT, it never paints. custom-select.js copies the select's classes
   onto it so the width rules written against those classes still find it — but that
   also brings the class's border, fill and padding (`.sc-input` has all three), and
   with .cs-btn drawing its own the result was a box inside a box. Everything visual is
   cleared here and owned by the button; only sizing passes through. */
.cs {
  position: relative; display: inline-flex; min-width: 0;
  border: 0; background: none; padding: 0; box-shadow: none;
  border-radius: 0; height: auto; min-height: 0;
}
/* CLIPPED, not display:none — a display-none select drops out of form submission in
   some browsers and its label stops resolving. It keeps its box, it just cannot be
   seen or tabbed to (custom-select.js sets tabindex=-1). */
select.cs-native {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0; pointer-events: none;
}
.cs-btn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  width: 100%; min-width: 0; box-sizing: border-box;
  height: var(--control-h-sm); padding: 0 10px;
  background: var(--background); color: var(--foreground);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-family: inherit; font-size: var(--body-size); line-height: 1;
  cursor: pointer; text-align: left;
  transition: border-color .12s, background .12s;
}
.cs-btn:hover { border-color: var(--border-strong); }
.cs-btn:focus-visible { outline: none; border-color: var(--ring); box-shadow: 0 0 0 1px var(--ring); }
.cs-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The chevron is drawn, not a glyph, so it matches the one the inputs use and turns
   when the menu is open. */
.cs-chev {
  flex: 0 0 auto; width: 10px; height: 6px; background: currentColor; opacity: .65;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3C/svg%3E") no-repeat center / contain;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3C/svg%3E") no-repeat center / contain;
  transition: transform .12s;
}
.cs-open .cs-chev { transform: rotate(180deg); }
.cs-open .cs-btn { border-color: var(--ring); }
.cs-disabled .cs-btn { opacity: .45; cursor: not-allowed; }
/* GEOMETRY IS SET IN JS, not here: the menu is moved to <body> and positioned fixed
   from the button's rect while open (see custom-select.js `place`), because as a child
   of .cs any ancestor with overflow clipped it and any stacking context capped its
   z-index. This rule owns only how it looks. */
.cs-menu {
  max-height: 260px; overflow-y: auto; box-sizing: border-box;
  padding: 4px;
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--shadow-modal);
}
.cs-menu[hidden] { display: none; }
.cs-opt {
  padding: 0 8px; height: 28px; display: flex; align-items: center;
  border-radius: var(--radius); color: var(--muted);
  font-size: var(--body-size); line-height: 1; white-space: nowrap; cursor: pointer;
}
.cs-opt:hover { background: var(--surface-2); color: var(--foreground); }
/* Permanent action rows sit after selectable values and lead to Gotham Wallet. */
.cs-opt-action {
  margin-top: 4px; border-top: 1px solid var(--border); color: var(--foreground);
}
/* The selected row is marked the way selection is marked everywhere else here: a
   filled row with the orange bar, not a tick. */
.cs-opt[aria-selected="true"] {
  color: var(--foreground); background: var(--surface-2);
  box-shadow: inset 2px 0 0 var(--accent);
}
.cs-opt[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
.cs-opt[aria-disabled="true"]:hover { background: none; color: var(--muted); }

/* ---- a dismissible error ----
   The ✕ is pushed to the far right of the message it closes, and only errors carry
   one (see scSetStatus). The box becomes a row so the glyph can sit opposite the
   text without wrapping under it on a narrow pane. */
.sc-status.sc-status-err {
  display: flex; align-items: flex-start; gap: var(--sp-3);
}
.sc-status-x {
  flex: 0 0 auto; margin-left: auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 0;
  border: 0; border-radius: var(--radius); background: none;
  color: currentColor; opacity: .7;
  font-family: inherit; font-size: var(--cap-size); line-height: 1; cursor: pointer;
  transition: opacity .12s, background .12s;
}
.sc-status-x:hover { opacity: 1; background: var(--surface-2); }
.sc-status-x:focus-visible { opacity: 1; outline: 2px solid var(--ring); outline-offset: 1px; }

/* ---- hover hint ----------------------------------------------------------
   A small styled label for icon-only controls — the rail's row actions, the header's
   refresh and wallet buttons, the overview's expand. They all carried a native title,
   which the OS draws in its own chrome, a second late, in a style that belongs to no
   part of this app. data-tip is the opt-in: only an element that has the attribute
   gets the ::after, and only that element is positioned.
   BELOW the control, not above: the row actions live inside the rail's scroller, and a
   bubble above the topmost row would be clipped by it.
   The delay is on opacity only, and visibility flips instantly on hover so the bubble
   cannot be left mid-fade when the pointer moves on. */
/* :where(), so this carries ZERO specificity. A plain `[data-tip]` selector is (0,1,0)
   and was declared after the components — which quietly beat `.act-expand`'s
   `position: absolute` and dropped the expand button out of the tab strip's corner into
   the flow. An element that already positions itself is its own containing block and
   needs nothing from here; this is only for the ones that do not. */
:where([data-tip]) { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute; z-index: 60;
  top: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  width: max-content; max-width: 240px; padding: 5px 8px;
  background: var(--float); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-pop, 0 6px 20px rgba(0, 0, 0, .45));
  color: var(--foreground); font-family: var(--sans, inherit); font-size: var(--cap-size);
  font-weight: 400; line-height: 16px; text-align: left; white-space: normal;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .12s ease .35s, visibility 0s linear .47s;
}
[data-tip]:hover::after,
[data-tip]:focus-visible::after {
  opacity: 1; visibility: visible; transition-delay: .35s, 0s;
}
/* A tip on a control at the far right would run off the panel; anchor those to their
   own right edge instead of their centre. */
[data-tip].tip-end::after { left: auto; right: 0; transform: none; }
/* The rail's actions are ALWAYS at the far right — the third one sits 26px from the
   edge, and a centred bubble on a 24px button reached past it. That overflow did two
   things at once: it clipped the bubble, and because .rail-body is a scroll container
   it put a horizontal scrollbar under the whole wallet list. */

/* ---- the rail's tips go BESIDE the row, not under it ----
   Under it is where they were, and .rail-body is a scroll container: whichever row sits
   at the bottom edge is flush with it, so its bubble was cut in half by the Quick swap
   bar every time. Flipping to `above` only moves the casualty to the top row. A bubble
   BESIDE the icons is the only placement that survives both edges — vertically it is
   centred on a 24px control inside a 68px row, so the whole 26px of it stays inside the
   row's own band and there is nothing left to clip.
   The containing block is the ACTIONS CELL, not the button: `.rail-act` gives up its
   position (it beats the zero-specificity :where() above on its own), so all three tips
   land in one place — just left of the cluster — instead of each one covering the icons
   to its left. */
.rail-wallet-actions { position: relative; }
.rail-act { position: static; }
.rail-act[data-tip]::after {
  top: 50%; bottom: auto;
  left: auto; right: calc(100% + 8px);
  transform: translateY(-50%);
}

/* ---- the status line is a NOTE ----
   12px with an info mark, the same treatment as .sc-note: it is an aside about the box
   it sits in, not one of its controls, and at 14px it was the same size as the labels
   above it. The mark takes the line's own colour, so an error's ✕ and its icon agree. */
.sc-status {
  display: flex; align-items: flex-start; gap: 6px;
  font-size: var(--cap-size); line-height: 18px;
}
.sc-status:empty { display: none; }
.sc-status-text { min-width: 0; }
.sc-status-icon {
  flex: 0 0 auto; width: 13px; height: 13px; margin-top: 2.5px;
  fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round;
}

/* ============================================================
   TRANSFER — the direction switch reads as the wallets box's header row, and the
   Configure column is one field per line.
   ============================================================ */
/* No flow diagram. transfer-tree.js is no longer mounted (see shortcuts-disperse.js);
   this hides any copy that a cached build might still seat. */
.transfer-tree { display: none !important; }

/* The bar sits on the same 48px rhythm as the Source/Destination headings under it,
   with the same hairline, on the box fill — so the three read as one header band
   rather than a control hovering over the band. It used to hold the Send|Receive
   toggle; it now READS the operation the wallet counts infer. */
.act-pane[data-act="transfer"] .act-panel,
.act-pane[data-act="transfer"] .sc-card-body { --act-cfg-w: 340px; }
/* The inferred-operation readout: the operation word at body weight, the live
   "N sources → 1 destination" detail quiet beside it. */
/* The count-rule note ("Only one box can hold multiple wallets…"): appears under
   the boxes it governs when a drop is capped; empty = occupies nothing. */
/* Transfer's shared wallet pair: the wrapper is LAYOUT ONLY. Its former box
   chrome double-framed the two inner boxes — their headers started one inset
   below Configure's title and their drop zones ended above its bottom. With the
   chrome gone the pair top-aligns with Configure, and the row stretches so the
   drop zones run the full height of the band's Configure column. */
.tf-wallets { border: 0; background: transparent; padding: 0;
  display: flex; flex-direction: column; gap: var(--sp-1); min-height: 0; }
.tf-wallets > .sc-io-row { flex: 1 1 auto; align-items: stretch; min-height: 0; }
.tf-wallets .sc-io-row > * { display: flex; flex-direction: column; min-height: 0; }
.tf-wallets .sc-dropzone { flex: 1 1 auto; }
/* The box chrome lives on the TWO INNER BOXES, not the wrapper — stripping the
   wrapper had taken the area's only border and surface with it, leaving Source
   and Destination floating on bare page background. Same deal Sweep's fused
   pair makes: each list framed and filled like Configure (.act-box-cfg), the
   pair sharing ONE hairline — the second box drops its left border, overriding
   the generic transfer divider above, so the middle stays 1px, not 2. Borders
   are inside the boxes' border-box geometry, so the pair's top/bottom stay
   flush with Configure's. */
.tf-wallets > .sc-io-row > .sw-list {
  border: 1px solid var(--border);
  background: var(--surface);
}
.tf-wallets > .sc-io-row > .sw-list + .sw-list { border-left-width: 0; }
.tf-count-note { font-size: var(--cap-size); color: var(--amber); }
.tf-count-note:empty { display: none; }
.sw-switch-row {
  display: flex; align-items: center;
  /* 12px on the left, tighter than the 16px a title row uses: the control has its own
     border, so at 16 it read as further in than the headings below it. */
  box-sizing: border-box; height: 48px; padding: 0 16px 0 12px; margin-bottom: -1px;
  background: var(--surface);
  border: 1px solid var(--border); border-bottom: 0;
  border-radius: var(--radius) var(--radius) 0 0;
  /* ONLY over the wallets column, not the whole row. It is that box's header — the
     Configure box has its own — so it stops where the band's first track stops.
     The width mirrors .act-band's own track list: everything except the right column
     and the gap between them. --act-cfg-w is declared with the band so the two cannot
     disagree. */
  width: calc(100% - var(--act-cfg-w) - var(--sp-2));
}
/* When the band collapses to one column (narrow canvas), the switch spans it again. */
@media (max-width: 860px) {
  .sw-switch-row { width: 100%; }
}
/* (The segmented look this row needs is the BASE .seg look now — see the note there —
   so there is nothing left to declare here.) */

/* ---- segments FILL their track ----
   .seg-field fixes the track at --control-h (32px) while a .seg-btn carries its own
   height (30px inside the base track's borders), so the buttons sat against the top
   edge with a gap under them — which is what read as "not centred". Stretching them
   removes the gap entirely rather than centring a short button in a taller box. */
.seg { align-items: stretch; }
.seg-field .seg-btn { height: auto; align-self: stretch; }

/* ---- ONE TRACK, TWO HANDLES ON IT ----
   This was two half-width sliders laid side by side, each running the full scale over
   its own half, with the band drawn in two pieces that met at the seam. It looked
   right at a glance and was not a range control at all: a handle's position did not
   mean its value, and the two halves gave the handles separate territories they could
   never leave, so they could not be brought together. Setting "exactly 20s" — both
   handles on one number, the thing this control is for — drew them half a track apart.
   Both inputs are stacked across the WHOLE track now. Their own runnable tracks go
   away and the band is painted once, here, between the two percentages — so the fill
   is one continuous piece and the geometry has a single source. */
.act-range { display: block; }
.act-range .act-slider {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  background: none;
  /* The track underneath is a picture, not a control: clicks belong to the thumbs, or
     the top input would swallow every press on the one beneath it. */
  pointer-events: none;
}
.act-range .act-slider::-webkit-slider-runnable-track { background: none; }
.act-range .act-slider::-moz-range-track { background: none; }
.act-range .act-slider::-moz-range-progress { background: none; }
.act-range .act-slider::-webkit-slider-thumb { pointer-events: auto; }
.act-range .act-slider::-moz-range-thumb { pointer-events: auto; }
/* The handle being reached for is raised by scActRange on hover, so a pair sitting on
   top of each other can always be taken apart again. */
.act-range .act-slider { z-index: 1; }
.act-range .act-slider.is-front { z-index: 2; }
/* The band is grabbable when there is enough of it to grab — scActRange sets these
   from the same test that decides whether the press would take, so the cursor never
   promises a gesture that is not armed. */
.act-range.can-grab-band { cursor: grab; }
.act-range.is-band-drag { cursor: grabbing; }
.act-range.is-band-drag .act-slider { pointer-events: none; }
/* The band. Accent because this is a chosen SPAN, the same accent selection uses
   everywhere else here — the single-value sliders keep their white fill, which is a
   level, not a range. */
.act-range::before {
  content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 2px; margin-top: -1px;
  background: linear-gradient(90deg,
    var(--border) 0 var(--lo-pct, 0%),
    var(--accent) var(--lo-pct, 0%) var(--hi-pct, 100%),
    var(--border) var(--hi-pct, 100%) 100%);
}

/* ---- Configure's number fields sit on BLACK ----
   They were on --surface, the same fill as the box around them, so each field read as a
   bordered area of the panel rather than something you type into. --background is the
   same ground every other input in the app uses.
   (.act-num was in this list until Activity's three settings became sliders; the only
   one left in the app is in a modal, not in a Configure box.) */
.act-box-cfg .sc-input,
.act-box-cfg input[type="text"] { background-color: var(--background); }

/* ---- Configure: one control per line ---- */
/* (Four rules lived here — two sizing .act-row > .sc-input and two overriding the
   label off its 84px column, all written when this panel was typed number fields
   under labels as long as "Mint N receivers". Its rows are three SLIDERS now, so the
   84px column is exactly what they want: it is the thing that makes every track start
   at one x and resolve to one length. The overrides were what made Transfer's three
   sliders start at three different places, and the .sc-input rules match nothing in
   this pane any more — Gather's Asset select is an .sc-row/scField, not an .act-row.
   Deleted rather than re-scoped; the shared rules already say it.) */

/* The card's "?" USED TO NEED A SEAT HERE — one rule for all four Actions cards,
   because help-guide.js pinned the badge into whichever box the card opened with and
   seated it off a 40px card header these cards do not have. There is one badge now,
   and it lives in the tab strip beside Expand rather than inside any card, so the
   rule that reconciled four corners has nothing left to reconcile. */
/* A note belongs to the row above it, so it sits tight under that row and carries the
   label column's indent rather than starting at the box edge. */
.act-box-cfg .sc-note {
  margin-top: -8px; padding-left: 84px;
  color: var(--muted); font-size: var(--cap-size); line-height: var(--body-line);
}
/* An action that acts on the row above it takes the full column. */
.act-box-cfg .sc-wide { width: 100%; justify-content: center; }
/* Two halves of one decision, filling the line evenly. */
.sc-duo { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
.sc-duo > .sc-field { min-width: 0; }
.sc-duo .cs, .sc-duo select { width: 100%; }
/* In this column a field is label-over-control, full width — the .sc-row default puts
   them side by side, which at 340px squeezed both. */
.act-box-cfg .sc-field {
  display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; width: 100%;
}
.act-box-cfg .sc-field > .sc-input,
.act-box-cfg .sc-field > .cs { width: 100%; }
.act-box-cfg .sc-field-label {
  font-size: var(--label-size); color: var(--foreground); line-height: var(--body-line);
}
.act-box-cfg .sc-field-hint { color: var(--muted); font-size: var(--cap-size); }

/* ---- one left inset for the whole rail: 24px ----
   Every section had its own 16px, so the rail sat tighter to the window edge than
   the wallet table does inside its card (--act-inset: 24px). Only the horizontal
   padding is set here — each rule keeps its own block padding (the search row's 0,
   the header's 8px, a wallet row's 12px), which is what holds their heights. */
.rail-search-row,
#rail-groups.rail-groups,
.rail-bal-head,
.rail-wallet {
  padding-left: 20px;
  padding-right: 16px;
}

.rail-wallet { position: relative; }
/* (The grip and the picked-up state are declared once, for every wallet surface, at
   the end of this file. They lived here AND under .act-pane .sc-dropzone, which is
   how they drifted apart.) */

/* Figure columns and headings are right-aligned; wallet identity stays left. */
.rail-bal-col { min-width: 0; }
.rail-bal-col:first-child { text-align: left; }
.rail-bal-col:nth-child(2), .rail-bal-col:nth-child(3) { text-align: right; }
.rail-bal-col-act { text-align: right; }

/* ---- and the rail's type is the wallet table's type ----
   Same sizes already, but three values were the rail's own: the address sat at
   #b4b4b4 on a 16px line, and both figures shared one alpha-white. In the table the
   address is --muted-2 on the row's 20px line, the ETH figure is --foreground (it is
   the balance you act on) and the token figure is --muted (context). Matching them is
   what makes the two lists read as one component rather than two. */
.rail-wallet-name { color: var(--foreground); }
.rail-wallet-addr { color: var(--muted-2); line-height: var(--body-line); }
.rail-wallet-eth { color: var(--foreground); }
.rail-wallet-bal { color: var(--muted); }
/* WETH-holding wallets stay AMBER here too. The flag rule (.bal-weth, WETH-HOLDING
   WALLETS section) sits inside a cascade layer; this block is unlayered, so the
   --foreground line above silently beat it and a wallet holding only WETH read as
   ordinary ETH — the amount folded in, the warning colour lost. Same cascade
   position, higher specificity: the flag wins again. */
.rail-wallet-eth.bal-weth { color: var(--amber); }

/* ---- the wallet toolbar: two rows ----
   Row 1 is fixed furniture — the title on the left, [+ Group][+ Create] pinned
   right. Row 2 holds the group chips and collapses to nothing when there are none,
   so a rail with no groups looks exactly as it did. The bar's height is content-led
   now (it was a hard 56px, which could not hold two rows). */
#rail-groups.rail-groups {
  display: flex; flex-direction: column; align-items: stretch;
  height: auto; gap: var(--sp-2);
  /* 24px left, with the rest of the rail. Written longhand-in-shorthand rather than
     left to the shared inset rule below, because this shorthand would otherwise
     reset it — it is declared later in the file. */
  padding: var(--sp-3) 16px var(--sp-3) 20px;
}
.rail-bar-top { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
/* margin-right: auto on the title is what pins the buttons right, whatever the
   title says. */
.rail-bar-top .rail-bar-title { margin-right: auto; }
/* --sp-2, the gap Sweep's Buyers/Sellers headers use, so "Trading Wallets · 5 · copy"
   is spaced exactly like "Buyers · 2 · copy". This row was on a 12px gap of its own,
   and the copy button's default 6px left margin stacked on top of it. */
.rail-bar-title { gap: var(--sp-2); }
.rail-bar-title > .wcopy { margin-left: 0; }
/* Same again on the rail row. .wcopy carries its own 6px for callers that just drop it
   after a text node; .rail-wallet-addr-row is a flex row with a gap, so the two stacked
   and the copy square sat 12px off an address that every other surface has it 6px from. */
.rail-wallet-addr-row > .wcopy { margin-left: 0; }
.rail-bar-chips { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); }
.rail-bar-chips:empty { display: none; }

/* ---- the group chip, and its delete control ----
   The chip stands the same height as the + Group / + Create buttons it sits beside,
   so the toolbar reads as one row of controls rather than chips of a different size
   floating next to buttons. */
/* SHORTER than the buttons above them, not matched to them. They sit on their own
   row now, so they no longer have to line up with [+ Group]/[+ Create] — and at the
   buttons' 32px a row of chips read as a second row of controls competing with the
   first. 24px is the chip height used elsewhere in the app (.mode-badge, the rail's
   count badge), and it still clears the 12px name and the bordered count inside. */
.rail-group-chip {
  height: 24px; box-sizing: border-box;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 0 4px 0 8px; flex: 0 0 auto;
}
/* 12px, the same step the counts and the column headings use — the name was on
   --label-size (14px), which made a chip read as loud as a wallet name. */
/* CAPPED, so one long group name can't run the width of the rail and push every
   other chip onto its own line. The cap is in `ch` because the constraint is about
   how much of a NAME you need to recognise it, not about pixels: ~14 characters is
   enough to tell your groups apart, and the ellipsis says there is more. The full
   name stays on the chip's title, and its wallets are one click away regardless. */
.rail-group-chip .rail-group-name {
  font-size: var(--cap-size); line-height: 1;
  max-width: 14ch;
}
/* A group holding NO wallets is dimmer than one that does: it is a label you have
   made but not filled, and at full brightness it competed with the groups that
   actually have something in them. Set from renderRailGroups, which knows the count. */
/* Darker FILL, not just dimmer text: an empty group drops to the page colour so it
   reads as an inert slot next to the chips that hold something, which sit a step up
   on --surface-2. */
.rail-group-chip.is-empty {
  background: var(--background);
  border-color: var(--border);
}
.rail-group-chip.is-empty .rail-group-name { color: var(--muted-2); }
.rail-group-chip.is-empty .rail-group-count { color: var(--muted-2); opacity: .7; }

/* ---- SELECTED, in the language the rest of the app already uses ----
   Every other "this one is selected" in here is a filled row with a 2px inset accent
   bar down its left edge — .rail-wallet.selected, .loop-sub.selected,
   .sc-wallet-pick.selected all say exactly that. A full bright stroke around the chip
   was not part of that vocabulary and read as a rectangle stamped over the top of it.
   So: the fill lifts to --surface-2, the stroke stays quiet, and the state is carried
   by an orange bar — the same orange as the active tab's ink and + Create, which is
   what "you are looking at this one" means everywhere else in this UI.
   Stated once, for empty and filled groups alike, so a group with no wallets shows
   selection exactly as loudly as one with fifty. */
.rail-group-chip.active,
.rail-group-chip.is-empty.active {
  background: var(--surface-2);
  border-color: var(--border);
  box-shadow: inset 2px 0 0 var(--accent);
  padding-left: 10px;               /* 8px + the 2px bar, so the name does not shift */
}
.rail-group-chip.active .rail-group-name,
.rail-group-chip.is-empty.active .rail-group-name { color: var(--foreground); }
.rail-group-chip.active .rail-group-count,
.rail-group-chip.is-empty.active .rail-group-count { color: var(--muted); opacity: 1; }
/* Dust keeps its own amber — it is a different KIND of filter, not another group. */
.rail-dust-chip.active { box-shadow: inset 2px 0 0 var(--amber); }
.rail-dust-chip.active .rail-group-name { color: var(--amber); }

/* The count was a bordered box inside a bordered box — two rectangles to show one
   small number. It is just a muted tabular figure now; the chip is the container. */
.rail-group-chip .rail-group-count {
  border: 0; padding: 0; min-width: 0;
  color: var(--muted-2); font-size: var(--cap-size); line-height: 1;
}

/* ALWAYS VISIBLE. Hiding it until hover was a mistake — it read as the button
   vanishing, and it hid the only affordance for removing a group. The reason it
   didn't work was never the styling (deleteGroup() has been wired all along): the
   chip is a drag source, so the press was becoming a drag instead of a click. See
   the draggable=false in app.js. 20x20 keeps it a real hit target — it was 9x14. */
.rail-group-del {
  display: inline-flex; align-items: center; justify-content: center;
  /* 18px inside a 24px chip — still a real hit target (it was 9x14), just no longer
     taller than the chip has room for. */
  width: 18px; height: 18px; padding: 0; flex: 0 0 auto;
  border: 0; border-radius: var(--radius); background: none;
  color: var(--muted-2); font-size: var(--body-size); line-height: 1; cursor: pointer;
  transition: color .12s, background .12s;
}
.rail-group-del:hover { color: var(--sell); background: var(--surface-2); }
.rail-group-del:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }

/* ============================================================
   WALLET ROWS DO NOT RULE OFF. LOGS DOES.

   Every wallet row in the app carried a 1px bottom border — the rail rows
   (.rail-wallet) and the wallet-table rows the shortcut panels build
   (.sc-wallet-card, used by Activity, Sweep and Transfer alike). Stacked, they
   turned a scan-and-pick surface into a ledger: each wallet boxed off from its
   neighbours, and a two-line rail row read as two separate records.

   A wallet is already a block — name over address, figures beside it, 56-68px of
   air. That is the separation. Logs is the one place the line earns its keep: it
   is one line per row, six narrow columns, read across rather than picked from,
   so the rule is what keeps the eye on its row. Its borders live in logs.js
   (.dlog-row) and are deliberately untouched here.

   Unlayered, at the tail, so it beats both the layered declarations above and the
   stylesheets the JS modules inject. Table HEADERS keep their bottom border —
   that separates the head from the body, not one wallet from the next — and so do
   the .rail-group / .rail-dust section rules, which mark where a collapsible
   section starts. Neither is a wallet divider.
   ============================================================ */
.rail-wallet,
.sc-wallet-card,
.act-pane .sc-dropzone .sc-wallet-card {
  border-bottom: 0;
}

/* A CAPACITY, NOT A COUNT.
   The badge beside Source/Destination normally tracks the list. On the side that
   holds exactly one wallet it states the limit instead, and has to LOOK unlike its
   live neighbour or it just reads as "one wallet in here" — which is wrong when the
   list is still empty. So it drops back to the dim fill the live badge was
   deliberately lifted off (see .sc-io-cap > .rail-bar-count above) and takes muted
   text: same size, same shape, plainly not something that is going to change.
   --surface-2, NOT --fig-alpha-3: the fig-* tokens are declared on .wallet-rail and
   only exist inside the rail, so out here that resolved to nothing and the badge lost
   its box entirely — a bare grey digit rather than a greyed-out field. */
.sc-io-cap > .rail-bar-count.is-fixed {
  background: var(--surface-2);
  color: var(--muted-2);
}

/* ============================================================
   HOT WALLETS — recency as a STAMP, and nothing else.

   Every row used to carry a "LAST TX spent 0.009 Ξ · 2h ago" line beneath it: a
   whole extra row, on every wallet, to say in words what a colour and a timestamp
   say at a glance. It doubled the height of the list you scan most and it read as
   two records per wallet once the dividers came off.

   Now the row says it with ONE mark: the time in the Actions column, in the colour of
   the side it traded on — green for a buy, red for a sell. A FRESH wallet gets nothing
   there; absence is the signal, which is what makes the traded rows findable.

   NO LEFT EDGE. It was there first and it had to go: with the row dividers gone, a run
   of adjacent same-side rows fused into one unbroken 2px stripe down the list, so the
   bar stopped reading per-wallet and started reading as a bracket around a group that
   did not exist. It also competed with the selection edge for the same 2px slot. The
   stamp carries the whole signal on its own, and the left edge belongs to selection.

   These now just point at the global side colours. The red was written out as a hex
   here when --sell was still orange and the rail needed a red of its own; --sell IS
   that red now, so the local copy is gone — one red for every sell in the app, and a
   change to the token reaches the rail with it.
   ============================================================ */
.wallet-rail {
  --rail-hot-buy: var(--buy);
  --rail-hot-sell: var(--sell);
}

/* The stamp. 12px on the type ramp, tabular so the column does not jitter as rows
   tick over from "50m ago" to "1h ago". */
.rail-wallet-ago {
  font-size: var(--cap-size); font-weight: 400; line-height: 16px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.rail-wallet.hot-buy .rail-wallet-ago { color: var(--rail-hot-buy); }
.rail-wallet.hot-sell .rail-wallet-ago { color: var(--rail-hot-sell); }

/* ---- The three action buttons (swap · sweep · transfer) ----
   24px hit targets holding a 16px stroked glyph — the size the header icon buttons
   and the row's own copy icon use, so the rail has one icon-button language. Muted at
   rest so three of them do not shout over the wallet name they sit beside; the fill
   only appears under the pointer, marking which one you are about to press. */
.rail-act {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0; flex: 0 0 auto;
  border: 0; border-radius: var(--radius); background: none;
  color: var(--muted); cursor: pointer;
  transition: color .12s, background .12s;
}
.rail-act:hover { color: var(--foreground); background: var(--surface-2); }
.rail-act:focus-visible { outline: 1px solid var(--ring); outline-offset: 1px; }
.rail-act svg {
  width: 16px; height: 16px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}
/* The disabled treatment stays even though all three actions are live now: a row action
   can still come up disabled (nothing to act on), and a control that looks pressable
   when it is not is the failure this rule exists to prevent. */
.rail-act:disabled { color: var(--muted-2); cursor: default; }
.rail-act:disabled:hover { color: var(--muted-2); background: none; }

/* ============================================================
   SWAP — Transfer's band with ONE list.

   The pane picks up the whole sweep/transfer treatment above (full-width card, the
   wallet table, the shrinkable slider track in Configure) by being named alongside
   them in those selectors. Only what is unique to this card lives here.

   There is no second list because a swap has no counterparty wallet: each wallet
   trades with the pool and keeps what it gets. So .sc-io-row holds one .sw-list and
   the `+ .sw-list` divider above simply never matches.
   ============================================================ */
/* (The .sw-pair rules that stood here are gone with the control they sized — Amount
   was a typed box beside a unit picker, and is a percentage slider now. Its track is
   covered by the shared `.act-box-cfg .act-row > .act-track` rule above, the same one
   Delay and Randomize use.) */
/* Direction is a sentence ("Sell — spend VERTAI"), not a token, so its control gets
   the slack too rather than being sized by the longest option. */
.act-pane[data-act="swap"] .act-box-cfg .act-row > .cs { flex: 1 1 auto; min-width: 0; }
/* The note belongs to the Amount row above it: full width, no label indent, and it
   breaks the row rhythm on purpose — it explains the one thing about this card that
   is genuinely easy to get wrong (per wallet, not split between them). */
.act-pane[data-act="swap"] .act-box-cfg .sc-note {
  padding-left: 0; margin: calc(var(--sp-2) * -1) 0 0;
}

/* ---- Swap: Market / Limit ----
   The mode switch is the app's segmented control, so it needs the same slack the
   Direction dropdown takes — at its intrinsic width two short words sat marooned in
   the middle of a 340px column. */
.act-pane[data-act="swap"] .act-box-cfg .act-row > .seg {
  flex: 1 1 auto; min-width: 0;
}
.act-pane[data-act="swap"] .act-box-cfg .act-row > .seg > .seg-btn { flex: 1 1 0; }
/* The limit block is one unit — the price row and the two notes under it appear and
   disappear together with the mode. Stated explicitly because `hidden` only wins by
   default: any `display` on this element would beat the UA rule and strand a Market
   run with a limit price on screen. */
/* THE TRIGGER IS A BLOCK, not a row. It is the one setting here that is a sentence —
   "fires at or below 0.0₇1 ETH" — and in the label/field/unit rhythm of a slider row
   the number came out the smallest thing in it. Stacked instead: the condition reads as
   the heading, the figure is the size of the decision, the presets that write it sit
   underneath. Bordered, because it is a group and the rows around it are not.
   Stated explicitly that `hidden` wins: any `display` here would beat the UA rule and
   strand a Market run with a limit price on screen. */
/* THREE ROWS, AND THE FIGURE HANGS OFF THE HEADING.
   It used to be `1fr auto 1fr` — two flexible rows of necessarily equal height, which
   centres the figure in the panel by construction. Centring is not what this reads as
   though: the caption NAMES the figure ("Sell" → 25 % ETH), so the two belong together
   at the top of the panel and the slack belongs underneath. So the rows are
   `auto auto 1fr` and the figure is set a fixed --sw-cap-gap below its caption, in all
   three panels — one distance, stated once, rather than a centring that happened to
   resolve to 8px here and would resolve to something else the moment a panel changed
   height. The 1fr is last: it takes whatever is left, and the presets ride its bottom
   edge (align-self: end), so a preset row that wraps to a second line grows DOWN into
   its own row instead of up through the figure. */
.sw-limit {
  display: grid; grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto 1fr; row-gap: 0;
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: var(--sw-pad); margin: 0;
  min-height: var(--sw-row-h);
}
/* The trigger block is a field too — same press-anywhere, same answer to the
   pointer as the Sell side below it. */
.sw-limit { cursor: text; transition: background-color 120ms ease; }
/* ONE FILL, from the left, exactly like the Sell side below it. The figure is a price
   relative to the live one rather than a quantity, so an earlier pass drew it as a bar
   growing out of the middle — correct about the arithmetic, and a second kind of bar
   in a stack of two panels that are otherwise the same gesture. Both grounds fill the
   same way now; what the fill MEANS is still on the scale, which runs 75..125 with the
   live price at its middle (see LIMIT_DRAG_LO in shortcuts-swap.js). */
.sw-limit:hover, .sw-limit:focus-within, .sw-limit.is-scrubbing {
  background-image: linear-gradient(90deg,
    var(--surface-2) 0 var(--sw-fill, 50%), transparent var(--sw-fill, 50%));
}
.sw-limit.is-scrubbing { cursor: ew-resize; }
.sw-limit.is-scrubbing > * { pointer-events: none; }
.sw-limit > .sw-limit-head { align-self: start; }
.sw-limit > .sw-limit-value { margin-top: var(--sw-cap-gap, 6px); }
.sw-limit > .sw-presets { align-self: end; }
.sw-limit[hidden] { display: none; }
/* ONE STACK, THREE PANELS. The trigger, the sell side and the buy side are one
   object — a limit swap is "at this price, sell that, buy this" — so they read as a
   single bordered column instead of a box floating above a box. The trigger keeps
   its border and gives up its bottom corners; the widget below drops its TOP border
   rather than being pulled up by a negative margin, so the seam is one 1px rule and
   not two stacked on each other.
   :not([hidden]) on both halves, because Market mode removes the trigger entirely
   and the widget has to get its own top back when it is on its own.
   Adjacent-sibling, so this is only ever true when the two really are touching —
   buildControls appends them in that order. */
.sw-limit:not([hidden]) {
  border-bottom-left-radius: 0; border-bottom-right-radius: 0;
}
.sw-limit:not([hidden]) + .sw-widget {
  margin-top: calc(-1 * var(--act-ctl-gap, 0px));
  border-top: 0; border-top-left-radius: 0; border-top-right-radius: 0;
}
/* THE STACK'S GEOMETRY, and it is now stated rather than solved. The panels used to
   share ONE height and derive the gap from it, because the figures were centred and a
   centred figure only lands on the same line in every panel while the arithmetic
   balances. They are not centred any more (see .sw-limit above), so the dependency is
   the other way round: the gap is the given, and each panel's height is what its own
   contents add up to.
     trigger  12 + 20 head + 6 + 30 figure + [30] + 12 = 110
     side     12 + 20 head + 6 + 30 figure + [ 0] + 12 =  80
   The trigger keeps 110 because it has a third row to hold; the sides drop the whole of
   it. SIX, not twelve: the caption names the figure, so it wants to be the nearest thing
   to it — and the leading inside a 30px row holding 20px glyphs already adds a few
   optical pixels on top of whatever this says, which is why 12 read as a gap rather than
   as a pair. The trigger's [30] is 6 more than its 24px preset row needs, and the
   presets take the BOTTOM of it (align-self: end), so the same 6 lands under the figure
   as above it — where before the figure sat flush on the buttons.
   min-height, not height: at a narrow Configure the presets wrap to a second line, and
   a fixed height would put them through the seam — the panel grows instead, and because
   the figure hangs off the heading rather than floating in the middle, nothing above the
   wrap moves when it happens. */
.act-pane[data-act="swap"] .act-box-cfg .act-controls {
  --sw-row-h: 110px;  /* the trigger: heading, figure, presets */
  --sw-side-h: 80px;  /* pay / receive: heading, figure */
  --sw-fig-h: 30px;
  --sw-pad: 12px;
  --sw-cap-gap: 6px;  /* heading → figure, the same in all three */
}
/* Heading: the condition, its ?, and the live price pushed to the far end — the
   reference the figure below is judged against, kept on screen without a line of its
   own. */
.sw-limit-head { display: flex; align-items: center; gap: 0; }
.sw-limit-cap {
  font-size: var(--body-size); font-weight: 400; line-height: 20px; color: var(--muted);
}
.sw-limit-head > .sw-now {
  margin-left: auto; font-size: var(--cap-size); color: var(--muted-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.sw-limit-head > .sw-now.dim { opacity: .7; }
/* The figure. 20px — the ramp's title step — and borderless, because a box around it
   would make it a form field again. It is still an input: it takes focus, and the ring
   says so. */
/* The unit is an APPENDIX to the figure, not a column: "0.00000001 ETH" reads as one
   quantity, where the same two things at opposite ends of a row read as two. So the
   field is sized by its CONTENT and the unit sits immediately after it, wherever that
   lands. `field-sizing: content` is what does it — without it an input is a fixed box
   and the unit would be stranded at whatever width the box happened to have.
   flex-start matters: the pair is one object, and it starts at the block's left edge
   so a shorter price does not float. */
/* The figure row is a FIXED height, the same one the widget's two use — it is the row
   the panel's centring is measured around, so the three have to agree on it or the
   flexible rows above and below resolve differently in each panel. */
.sw-limit-value {
  display: flex; align-items: baseline; justify-content: flex-start;
  gap: 6px; min-width: 0; height: var(--sw-fig-h, 30px);
}
/* Specificity, deliberately: `.act-box-cfg input[type="text"]` (0,2,1) paints every
   typed field in this column with --background, and a plain `.sw-limit-input` (0,1,0)
   lost to it — which is where the black slab behind the number came from. The pane
   selector takes it to 0,3,0. */
.act-pane[data-act="swap"] .sw-limit-input {
  /* width: auto is not redundant — a `width: 100%` further up the sheet reaches this
     field through the shared typed-input rules, and a fixed width defeats
     field-sizing outright: the box stayed 236px whatever was in it, so the unit was
     pinned to the far edge exactly as before. */
  /* 1ch, not 2. The floor is a floor, not a default: `field-sizing: content` sizes the
     box to what is in it, so a two-character minimum held a one-character price ("2")
     in a two-character box and parked a blank column between the figure and its unit.
     An EMPTY field does not collapse to 1ch — content sizing falls back to the
     placeholder, which is the full "0.00000001". */
  flex: 0 1 auto; width: auto; min-width: 1ch; max-width: 100%;
  field-sizing: content;
  background: none; border: 0; padding: 0; border-radius: 0; box-shadow: none;
  color: var(--foreground); font-family: inherit;
  font-size: var(--title-size); font-weight: 400; line-height: 28px;
  font-variant-numeric: tabular-nums;
}
.act-pane[data-act="swap"] .sw-limit-input:hover,
.act-pane[data-act="swap"] .sw-limit-input:focus { background: none; box-shadow: none; outline: none; }
/* NO BOX WHILE TYPING. The 2px ring drew a grey rectangle round the figure the moment
   it took focus, which is the one thing this block was restyled to get rid of — and it
   was the only outlined control in a card with no outlined controls.
   Focus still has to be findable though, so it becomes a 1px rule UNDER the number:
   an inset shadow rather than a border, so nothing shifts by a pixel when it appears.
   The caret does the rest. */
.act-pane[data-act="swap"] .sw-limit-input:focus-visible {
  outline: none;
  box-shadow: inset 0 -1px 0 var(--border-strong);
}
.sw-limit-input::placeholder { color: var(--muted-2); }
/* Pressing anywhere on either block selects what is in its field, so these two are
   the fields in the app that show a selection WITHOUT anyone having dragged over
   text. The platform blue made that read as an error state; the app's own wash
   reads as what it is. */
.sw-limit-input::selection, .sw-widget-input::selection { background: var(--select-wash); color: var(--foreground); }
.act-pane[data-act="swap"] .sw-limit-input:disabled { color: var(--muted); background: none; }
.sw-limit-unit {
  flex: 0 0 auto; font-size: var(--title-size); font-weight: 400; color: var(--muted-2);
}
/* A token price is a long thin number ("0.00000001"), so it gets the row's slack
   rather than a 96px box, and reads right-aligned like every other figure. */
.act-pane[data-act="swap"] .sw-limit .act-row > .sc-input {
  flex: 1 1 auto; min-width: 0; text-align: right;
}
/* The live price sits directly under the field it is the reference for — brighter
   than the explanatory note beneath it, because it is data, not prose, and it
   changes while you are looking at it. */
.sw-now { color: var(--muted); font-variant-numeric: tabular-nums; }
/* Armed: the button now CANCELS, and it should not look like the thing that starts
   a run. Drops the primary fill for the quiet outline the rest of the app uses for
   "stop", and the status line beside it carries the live watch. */
.sc-run.sw-armed {
  background: none; color: var(--foreground);
  box-shadow: inset 0 0 0 1px var(--border-strong);
}
.sc-run.sw-armed:hover { background: var(--surface-2); }

/* ---- Swap's wallet table has FOUR columns, not two ----
   Wallet | Total | Spend | Receive | ✕. The other panes show two balances; this one
   shows a projection of the run — what each wallet puts in and what that buys — so it
   needs a track for each and the shared 1.6/1/1/16px list does not fit.
   The figures are direct children of the card (scWalletCard's `cells` path), so they
   take these tracks in source order rather than by grid-area name. */
.act-pane[data-act="swap"] {
  --act-wt-cols: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) 16px;
}
/* NAMED AREAS, like every other pane here. Source order is not enough: .sc-wallet-id
   is `display: contents`, so the name and the address are grid ITEMS of the card, and
   without areas they took the first two tracks and pushed every figure one column
   right of its heading. */
.act-pane[data-act="swap"] .sc-dropzone .sc-wallet-card {
  grid-template-areas: "name f0 f1 f2 x" "addr f0 f1 f2 x";
}
.act-pane[data-act="swap"] .sw-f-total { grid-area: f0; }
.act-pane[data-act="swap"] .sw-f-in    { grid-area: f1; }
.act-pane[data-act="swap"] .sw-f-out   { grid-area: f2; }
.act-pane[data-act="swap"] .sc-wallet-fig {
  align-self: center; justify-self: end;
  text-align: right; white-space: nowrap;
  font-size: var(--body-size); font-weight: 400;
  font-variant-numeric: tabular-nums; color: var(--foreground);
}
.act-pane[data-act="swap"] .sc-wallet-fig.dim { color: var(--muted-2); }
/* RECEIVE is the estimate, and it reads as one: quieter than the two figures beside
   it, which are things the wallet actually holds or will actually spend. */
.act-pane[data-act="swap"] .sw-f-out { color: var(--muted); }
.act-pane[data-act="swap"] .sc-dropzone .sc-wallet-x { grid-area: x; }

/* ---- ONE MARK SEAT on an action tab ----
   Two things can want to speak from a tab: something ARRIVED here while you were
   elsewhere, and something is RUNNING here now. They used to sit in different places
   — the arrival square above the label's top-right corner, the state square inline
   after it — and two 6px squares in two positions read as an inconsistency rather
   than as a distinction. One seat now, and colour carries the difference: blue for
   an arrival, amber and pulsing for a run.

   THE SEAT IS THE TAB'S OWN RIGHT PADDING, which is 16px and was always going to be
   sitting there empty. Both marks are absolutely positioned into it, so a tab is
   exactly as wide with a mark as without one and the label stays centred between two
   equal insets — nothing about the strip moves when a mark appears or clears. That is
   what lets the arrival mark live here at all: it was in the corner precisely because
   it must never change a tab's width, and an inline mark would have shifted every tab
   to its right on each drop. It also means scSetTabState no longer has to re-measure
   the sliding underline.
   (Reserving a slot with padding on the label does NOT work, even though it also
   holds the width: padding on one side only stops the label being centred in its tab,
   which is visible on every tab at once, mark or no mark.)

   A 6px SQUARE, not a circle — the app marks state with boxes (the balance chart's
   .bchart-alloc-dot legend keys are the same 6px, border-radius:0). The exact wallet
   count stays in the accessible label and the hover title. */
.act-tab { position: relative; }
.act-tab-label { display: inline-block; }
.act-tab-notice,
.act-tab-dot {
  /* Against the TAB, not the label — the label is unpositioned so these resolve to
     .act-tab's padding box, which is what puts them in its padding and on its
     midline. 5px centres the 6px square in the 16px inset, leaving it ~5px after the
     word and ~21px from the next tab's first letter, so it groups with its own. */
  position: absolute; right: 5px; top: 50%; margin-top: -3px;
  width: 6px; height: 6px;
  box-sizing: border-box; pointer-events: none;
  border: 0; border-radius: 0;
}
/* Arrival: static, and it clears when you open the tab. */
.act-tab-notice { background: var(--accent); }
/* Running: amber, and it outranks an arrival for the seat while it lasts. A run is
   happening NOW; an arrival is something you missed, and it will still be true when
   the run ends — at which point the blue mark comes back on its own. */
.act-tab-dot { display: none; }
.act-tab.is-working .act-tab-notice { display: none; }
.act-tab.is-working .act-tab-dot {
  display: block;
  background: var(--amber);
  /* SLOW on purpose — 2s, and it fades rather than blinking off. A fast blink on a
     tab strip reads as an alarm; this is "still going". */
  animation: act-tab-pulse 2s ease-in-out infinite;
}
/* No "done" mark: a tab that has finished is just a tab, and a persistent green tick
   is noise once it has been read. .is-done still says "finished" in the aria-label. */
@keyframes act-tab-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
/* Respect a system preference for less motion: the colour still says "working". */
@media (prefers-reduced-motion: reduce) {
  .act-tab.is-working .act-tab-dot { animation: none; }
}

/* ---- limit-price presets ----
   Buttons, not another segmented control: they SET a value and let go, they do not
   hold a state — the field above them holds it. 24px tall at 12px, the size the rail's
   group chips use.
   SQUARE. --radius is 0 in this preset and every box, field, chip and button in the
   app is a rectangle; the 999px pill I first gave these was the only rounded thing on
   the page and it read as imported from somewhere else.
   RIGHT-ALIGNED, so the row sits under the price box rather than under its label — the
   presets fill that box, and a control should start where the thing it edits starts. */
.sw-presets { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.sw-preset {
  height: 24px; padding: 0 10px; flex: 0 0 auto;
  background: none; border: 1px solid var(--border-strong); border-radius: var(--radius);
  color: var(--muted); font-family: inherit; font-size: var(--cap-size); line-height: 1;
  cursor: pointer; transition: color .12s, background .12s, border-color .12s;
}
.sw-preset:hover { color: var(--foreground); background: var(--surface-2); }
.sw-preset:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
/* (its disabled look lives with the app-wide "EVERY disabled button" section) */

/* ---- field-level "?" ----
   The explanation for a control, hung off its label instead of parked under the row.
   Three lines of prose under Limit price took more height than every control it sat
   beneath, and it was prose you read once. The badge matches the card-level ? in
   help-guide.js — same glyph, same muted-until-hover treatment — just smaller,
   because this one belongs to a row rather than to the whole card. */
.sw-info {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; margin-left: 6px; padding: 0; flex: 0 0 auto;
  vertical-align: middle;
  background: none; border: 1px solid var(--border-strong); border-radius: var(--radius);
  color: var(--muted-2); font-family: inherit; font-size: var(--cap-size); line-height: 1;
  cursor: pointer; transition: color .12s, background .12s, border-color .12s;
}
.sw-info:hover { color: var(--foreground); background: var(--surface-2); border-color: var(--border-strong); }
.sw-info:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
/* The popover it opens. Reuses .qa-menu's floating shell (border, shadow, fixed
   position, one-at-a-time dismissal) — it is the same kind of object, so it should not
   be a second implementation of one. */
.qa-info { max-width: 320px; padding: 12px; }
.qa-info-text {
  margin: 0; color: var(--muted); font-size: var(--cap-size); line-height: var(--body-line);
  white-space: normal;
}
/* The live price line is DATA — it keeps the brighter muted tone and one line. */
.sw-now { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* The "?" adds ~22px to a label, and the fields in this column take the slack
   (flex: 1) — so "Limit price ?" broke onto two lines and the badge landed under its
   own label. The label takes what it needs here and the control takes the rest. */
.act-pane[data-act="swap"] .act-box-cfg .act-row > .act-label {
  flex: 0 0 auto; white-space: nowrap;
}

/* SELECTED preset: full white text, the way every picked thing in this app reads. The
   border lifts with it so the button does not look like white text floating in a grey
   outline; the fill stays off, because these set a value and let go — they are not a
   segmented control holding a mode. */
.sw-preset.is-on {
  color: var(--foreground);
  border-color: var(--foreground);
}
.sw-preset.is-on:hover { color: var(--foreground); background: var(--surface-2); }

/* BEGIN DASHBOARD-ONLY SWAP WIDGET */
@layer components {
/* The Configure panel and Quick swap rail mount this same component. All controls
   retain the app's .btn/.sc-input vocabulary; these rules only provide the swap
   widget's pay/receive composition, which has no existing app equivalent. */
.sw-widget {
  position: relative; width: 100%;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--background); overflow: hidden;
}
/* The SAME three rows as the trigger, with the third one empty — heading, figure,
   nothing — and the same --sw-cap-gap holding the figure under its caption, so the two
   halves of the stack are set to one rhythm rather than each arriving at 8px by its own
   arithmetic. The empty third row is what absorbs any extra height a caller hands this
   panel, which keeps the caption/figure pair pinned to the top instead of drifting.
   The fallbacks cover a mount outside the Configure box, which declares none of these
   (the Quick swap drawer). */
.sw-widget-side {
  box-sizing: border-box; min-height: var(--sw-side-h, 80px); padding: var(--sw-pad, 12px);
  display: grid; grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto 1fr; row-gap: 0;
  opacity: 1; transform: translateY(0);
  transition: opacity 140ms cubic-bezier(.25,.46,.45,.94),
              transform 140ms cubic-bezier(.25,.46,.45,.94);
}
.sw-widget-side > .sw-widget-head { align-self: start; }
.sw-widget-side > .sw-widget-value { margin-top: var(--sw-cap-gap, 6px); }
/* The heading, matching .sw-limit-head exactly: what the section is, then the
   reference figure pushed to the far end — the equivalent for the side you pay, the
   quote qualifier for the side you receive. */
.sw-widget-head { display: flex; align-items: center; gap: 0; }
.sw-widget-cap {
  font-size: var(--body-size); font-weight: 400; line-height: 20px; color: var(--muted);
}
.sw-widget-head > .sw-widget-unit-toggle,
.sw-widget-head > .sw-widget-detail { margin-left: auto; }
/* +1px so the two sides are the same height BETWEEN THE RULES. min-height is a
   border-box measure, and this is the one of them carrying a divider of its own, so at
   a flat --sw-side-h its interior came out a pixel short of its neighbour. */
/* The DARK panel is the one you RECEIVE into. Two tones separate what goes in from
   what comes out, and which of them gets the darker ground is a call about where the
   eye should settle — it settles on the result. */
.sw-widget-pay {
  min-height: calc(var(--sw-side-h, 80px) + 1px);
  border-bottom: 1px solid var(--border); background: var(--surface);
}
.sw-widget-receive { background: var(--background); }
/* ---- the two sides answer the pointer ----
   The Sell side IS a field — its whole box takes a press and puts the caret in the
   number (see buildSwapWidget) — so it lifts a step and shows a text caret, the way
   any input does under the pointer. The Buy side is a RESULT and never takes a
   value, but a panel that does nothing at all under the pointer reads as switched
   off; it lifts too, by less, and keeps the arrow. Two different answers to the same
   gesture, which is the honest way to say that one of them is typed and the other is
   worked out.
   The transition is written on the sides themselves — .sw-widget-side already
   animates opacity and transform for the switch, and background has to join that
   list rather than replace it. */
.sw-widget-side { transition: opacity 140ms cubic-bezier(.25,.46,.45,.94),
              transform 140ms cubic-bezier(.25,.46,.45,.94),
              background-color 120ms ease; }
/* ---- the ground IS the control ----
   Hovering these two used to wash the whole panel one shade lighter — a flat "you
   can press this" and nothing more. The wash is a FILL now: it runs from the left
   edge to the value the panel holds, so the lighter part of the box is how much of
   your balance this sell is, read at a glance without parsing the number. Press and
   move and it follows the pointer; press and release without moving and the caret
   goes in the field, which is what a press on a number should do.
   --sw-fill is written by paintSwapWidget. The gradient only appears on hover (or
   while a drag is live), so at rest the panel is still just a panel. */
.sw-widget-pay { cursor: text; }
.sw-widget-pay:hover,
.sw-widget-pay.is-scrubbing {
  background-image: linear-gradient(90deg,
    var(--surface-2) 0 var(--sw-fill, 0%), transparent var(--sw-fill, 0%));
}
/* Drag reads as a drag: an east-west grip, and the text caret gives way to it. */
.sw-widget-pay.is-scrubbing { cursor: ew-resize; }
/* Nothing inside the panel takes the pointer during a scrub — a stray press landing
   on the number or the unit toggle mid-drag would end it. */
.sw-widget-pay.is-scrubbing > * { pointer-events: none; }
/* BARELY THERE. The Sell side answers a press with a value; this one has nothing
   to answer with, so its hover is the smallest mark that still says the panel is
   awake — a third of the wash any real control gets. */
.sw-widget-receive:hover { background: color-mix(in srgb, var(--foreground) 3%, var(--background)); }
/* ...and a press that lands on the box focuses the field, so the box wears the focus
   the field has. Without it the caret appears in a panel that looks untouched. */
.sw-widget-pay:focus-within {
  background-image: linear-gradient(90deg,
    var(--surface-2) 0 var(--sw-fill, 0%), transparent var(--sw-fill, 0%));
}
.sw-widget-value {
  display: flex; align-items: center; gap: var(--sp-2); min-width: 0;
  height: var(--sw-fig-h, 30px);
}
.sw-widget-amount {
  display: inline-flex; align-items: baseline; gap: var(--sp-1); min-width: 0;
}
/* ONE QUIET GREY for every reading, and specifically the grey `no price yet` is
   printed in on the trigger's heading two panels up — these sit in the same slot
   saying the same kind of thing, so they read as one voice: "impact est." when the
   projection is depth-aware, "not quoted" when the pair has not answered. Amber on the
   degraded ones made a routine "we have no quote yet" look like a fault.
   --muted, not --muted-2, because that is what .sw-now resolves to inside a Configure
   box: the unlayered typography tail sets it there, so matching the token in the
   layered rule would NOT have matched what is on screen. */
.sw-widget-detail {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--muted); font-size: var(--cap-size); line-height: 20px;
}
.sw-widget-detail:empty { display: none; }
/* 20px, matching the caption beside it and `.sw-now` over the trigger. It is a .btn,
   so it arrived with the button min-height — which made this heading 28px tall while
   the trigger's was 20, and since the flexible rows above and below the figure are
   sized from the tallest thing in one of them, an 8px-taller heading pushed this
   panel's figure 4px off the line the other two sit on. */
.sw-widget .sw-widget-unit-toggle {
  display: inline-flex; align-items: baseline; gap: var(--sp-1);
  margin-left: auto; width: auto; max-width: 50%; padding: 0; flex: 0 1 auto;
  height: 20px; min-height: 0;
  overflow: hidden; white-space: nowrap; border-color: transparent;
}
/* CAPTION TIER, not a second headline. The equivalent moved out of the value line and
   into the heading, where it plays the part `now 0.0₄25` plays over the trigger: the
   reference the figure below is read against. At 18px beside a 20px figure the two
   competed; at 12px on the heading's line the figure is unambiguously the value and
   this is the note on it. */
.sw-widget-equivalent {
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
  color: var(--muted-2); font-family: var(--num); font-size: var(--cap-size); line-height: 20px;
  font-variant-numeric: tabular-nums;
}
.sw-widget-equivalent-unit { color: var(--muted-2); font-size: var(--cap-size); line-height: 20px; }
.act-pane[data-act="swap"] .sw-widget-input,
.rail-quickswap .sw-widget-input {
  /* 30px, not 32 — the same box the receive side's span occupies. Two pixels taller
     than its baseline-aligned neighbour made this row 33px against the other's 32,
     which is another pixel of drift between two figures meant to sit on one line. */
  /* SIZED BY WHAT IS IN IT, exactly like the trigger's price above it, so the unit
     sits immediately after the figure instead of at the end of a fixed box. The width
     used to come from paintSwapWidget, one `ch` per character — and a `ch` is the
     width of a zero, so "59.4" reserved four digit-widths for three digits and a
     decimal point and left a phantom half-digit of air before "% ETH". */
  width: auto; min-width: 1ch; max-width: 100%; flex: 0 1 auto; field-sizing: content;
  height: 30px; padding: 0;
  border: 0; border-radius: 0; box-shadow: none;
  color: var(--foreground); font-family: var(--num); font-size: var(--title-size);
  font-weight: 400; line-height: 30px; text-align: left;
  font-variant-numeric: tabular-nums;
}
.act-pane[data-act="swap"] .sw-widget-input:focus,
.rail-quickswap .sw-widget-input:focus { border: 0; box-shadow: none; outline: none; }
/* ONE FIGURE SIZE across the stack. The trigger's price is 20px — the ramp's title
   step — and the two quantities under it are the same kind of thing, so they read at
   the same size. They were 24px over an 18px unit, which made the widget shout over
   the block it is now joined to, and made "0.369 ETH" two sizes where the trigger's
   "0.00000001 ETH" is one. */
.sw-widget-output {
  flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  color: var(--foreground); font-size: var(--title-size); line-height: 30px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.sw-widget .sw-widget-asset {
  display: inline-flex; align-items: center;
  height: 30px; padding: 0; flex: 0 0 auto;
  color: var(--muted-2); font-size: var(--title-size); line-height: 30px;
}
.sw-widget .sw-widget-switch {
  position: absolute; z-index: 1; left: 50%; top: 50%;
  width: 32px; height: 32px; padding: 0;
  transform: translate(-50%, -50%);
  background: var(--background); border-color: var(--border);
}
.sw-widget-switch-icon {
  width: 16px; height: 16px; display: block; fill: currentColor;
  transform: rotate(0deg);
  transition: transform 300ms cubic-bezier(.4,0,.2,1);
}
.sw-widget-switch:hover .sw-widget-switch-icon { transform: rotate(-180deg); }
.sw-widget.is-switching-out .sw-widget-side {
  opacity: 0; transform: translateY(4px);
}
.sw-widget.is-switching-in .sw-widget-side {
  opacity: 0; transform: translateY(-4px); transition: none;
}
/* The drawer has ONE view now — the wallets — so it is sized to the rail's own
   rows (3 x 68px) rather than to the trade widget's old 210px footprint. The
   settings moved to the Swap tab; the cog is the door. */
.rail-quickswap > .qs-body {
  box-sizing: border-box; height: 0; max-height: 0; min-height: 0;
  overflow: hidden; background: var(--background);
  transition: height 180ms ease, max-height 180ms ease;
}
.rail-quickswap.is-open > .qs-body { height: 204px; max-height: 204px; }
.qs-body > .qs-list {
  display: flex; box-sizing: border-box;
  width: 100%; height: 204px; min-height: 0; max-height: 204px;
  overflow-y: auto; border: 0; border-radius: 0; padding: 0;
}
.qs-count { cursor: pointer; }
/* Cog hover: a read-only summary of what Run swap would do. Fixed, because the
   rail is a scrolling column and a panel inside it would be clipped by the
   drawer it sits above. */
.qs-hover {
  position: fixed; z-index: 10030; pointer-events: none;
  min-width: 208px; max-width: 280px; padding: 10px 12px;
  border: 1px solid var(--border-strong); background: var(--surface-2);
  box-shadow: var(--shadow-pop); font-size: var(--cap-size); line-height: 1.45;
}
.qs-hover[hidden] { display: none; }
.qs-hover-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  margin-bottom: 6px; padding-bottom: 6px; border-bottom: 1px solid var(--border);
  color: var(--foreground);
}
.qs-hover-count { color: var(--muted-2); }
.qs-hover-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.qs-hover-k { color: var(--muted); }
.qs-hover-v { color: var(--foreground); text-align: right; }
.qs-hover-foot {
  margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border);
  color: var(--muted-2);
}
/* (.qs-clear is sized with .qs-cog below — the bar's two icon buttons are one rule) */
.qs-clear[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .sw-widget-side, .sw-widget-switch-icon,
  .rail-quickswap > .qs-body { transition-duration: 0ms; }
}
} /* end @layer components */
/* END DASHBOARD-ONLY SWAP WIDGET */

/* ============================================================
   QUICK SWAP — the Swap engine docked at the foot of the rail.

   A drawer, not a panel: at rest it is one toolbar row sitting on the bottom edge of
   the wallet list, and the list keeps the rest of the rail. Its bar IS the rail's own
   toolbar row (.rail-bar-top, same classes), so the two are the same height and
   "Quick swap [2] ⧉" is spaced exactly like "Trading Wallets [11] ⧉" without a single
   number being restated here.
   ============================================================ */
.rail-quickswap {
  position: relative;
  flex: 0 0 auto;
  border-top: 1px solid var(--border);
  background: var(--surface);
}
/* The rail's toolbar band, copied exactly: 12px above and below a 32px row, and the
   SAME asymmetric inset — 20px on the left, 16px on the right. The 20 is what lines
   "Quick swap" up under "Trading Wallets"; at a symmetric 16 it sat 4px proud of it,
   which is the kind of misalignment you see without being able to name. */
.rail-quickswap > .qs-bar {
  box-sizing: border-box; height: 56px; padding: 12px 16px 12px 20px;
}
/* The caret straddles the top edge, and is INVISIBLE until the pointer is on the bar:
   a permanent tab hanging off the wallet list is furniture. Focus reveals it too, or it
   would be unreachable from the keyboard. */
.qs-caret {
  position: absolute; top: -11px; left: 50%; transform: translateX(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 20px; padding: 0;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--muted-2); cursor: pointer; z-index: 1;
  opacity: 0; transition: opacity .12s, color .12s, background .12s;
}
.rail-quickswap:hover .qs-caret,
.qs-caret:focus-visible { opacity: 1; }
.qs-caret:hover { color: var(--foreground); background: var(--surface-2); }
.qs-caret:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
.qs-caret svg {
  width: 14px; height: 14px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform .16s ease;
}
/* The chevron points where pressing it SENDS the drawer: up while it is shut (press to
   raise it), down once it is open (press to put it away). The path is drawn pointing
   down, so the closed state is the rotated one. */
.qs-caret svg { transform: rotate(180deg); }
.rail-quickswap.is-open .qs-caret svg { transform: rotate(0deg); }

/* The clear, cog and Run sit hard right, in that order: the thing you press is last
   on the line. (.rail-bar-title already carries margin-right: auto — the rail's own
   rule.) */
.qs-cog, .rail-quickswap .qs-run { flex: 0 0 auto; }
.rail-quickswap .qs-run { align-self: center; }
/* The bar's two icon buttons are ONE rule and ONE size: 32px square, the app's
   standard control height (.btn/.rail-btn), sitting flush beside the 32px Run chip.
   The ✕ was a 28px .btn.tiny next to a 32px cog — the pair read as two systems in a
   four-control row. Icon buttons stay square rather than taking a text button's
   inset. */
.qs-clear, .qs-cog {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; flex: 0 0 auto;
  background: var(--surface-2); border: 0; border-radius: var(--radius);
  color: var(--muted); cursor: pointer; transition: color .12s, background .12s;
}
.qs-clear:hover, .qs-cog:hover { color: var(--foreground); background: var(--border-strong); }
.qs-clear:focus-visible, .qs-cog:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
.qs-cog svg {
  width: 16px; height: 16px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round;
}

/* (The drawer itself is `.rail-quickswap > .qs-body` further up. Two rules that
   used to live here targeted `.rail-quickswap > .qs-list`, but the list has
   always been nested inside the body — they matched nothing, while reading like
   the authority on the drawer's height. Their intent survives on .qs-body:
   closed it has NO height rather than `display: none`, because a drop zone that
   does not exist cannot be dragged onto and a drag is what opens it; open it is
   exactly three rail rows, a fixed stop, so a big selection can never eat the
   wallet list above it.) */
/* The rows ARE rail rows and are left alone: the drag grip, the recency stamp, the
   hover swap between stamp and controls all come from the rail's own rules. The single
   addition is that they must FILL the drawer — .sc-dropzone is a flex container, so its
   children were sizing to content and leaving a 44px hole down the right-hand side. */
.qs-row { width: 100%; flex: 0 0 auto; }
/* The ✕ takes the place of the rail's three action icons, so it is one of them: same
   24px box, same muted-until-hover, and red on hover because it removes. */
.qs-x:hover { color: var(--sell); background: var(--surface-2); }

/* ---- the cog's popup: the same controls, centre screen ---- */
/* The dialog inset is --pad-modal now (16px, app-wide), so this only sets its width. */
.qs-modal { width: min(420px, calc(100vw - 32px)); }
/* The title is a HEADER: it spans the dialog edge to edge with its rule under it, which
   means bleeding back out through the dialog's own padding. Same treatment
   .act-box-cfg gives its own title. */
.qs-modal > .act-box-title {
  margin: calc(var(--pad-modal) * -1) calc(var(--pad-modal) * -1) var(--pad-modal);
}
.qs-modal-foot {
  display: flex; justify-content: flex-end; align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-4);
}
/* Cancel is the quiet one: it undoes, so it must not compete with Save for the eye. */
.qs-cancel {
  background: none; border: 1px solid var(--border-strong); color: var(--muted);
}
.qs-cancel:hover { color: var(--foreground); background: var(--surface-2); }
.qs-modal .act-controls { display: flex; flex-direction: column; gap: var(--sp-3); }

/* ---- the cog's hover preview ----
   The whole configuration, labelled, at caption size — the popup's content without the
   popup. It floats above the bar because the bar is at the foot of the rail and there
   is never room beneath it. Pointer-events off: it is a readout, and a panel that can
   swallow the pointer would flicker the moment it appeared under the cursor. */
.qs-pop {
  position: fixed; z-index: 1200; pointer-events: none;
  min-width: 232px; max-width: 300px; padding: 10px 12px;
  background: var(--float); border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-pop);
}
.qs-pop-row {
  display: flex; align-items: baseline; gap: var(--sp-3);
  font-size: var(--cap-size); line-height: 18px;
}
.qs-pop-k { flex: 0 0 auto; color: var(--muted-2); }
.qs-pop-v { margin-left: auto; text-align: right; color: var(--foreground); }
.qs-pop-foot {
  margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border);
  font-size: var(--cap-size); line-height: 16px; color: var(--muted-2);
}

/* (The app-wide "EVERY disabled button" section lives after the Configure typography
   tail below — it must be the file's last word on button colour, or the tail's
   `.act-box-cfg .btn { color: var(--foreground) }` re-lights disabled text.) */

/* ============================================================
   THE OVERVIEW'S LEFT PANEL IS AN ALLOCATION, NOT FOUR FIGURES.

   It was four stacked values, each with its own miniature — four numbers, ranked
   against nothing, and four shapes competing with the big chart that exists to show
   one. The question this card is asked is "where is the money", which is one number
   per asset plus their SHARE, so: a header carrying the pair's value, a split bar, a
   legend, and a two-row table.

   It is also the chart's tab strip. The header selects the ratio; each asset row
   selects its own series.

   Geometry is the reference frame's: 20px inset on the upper group, 48px from the
   figure to the bar, 24px from the bar to the legend, and a full-bleed table whose
   rows put the 20px back as their own padding so the rules run edge to edge.
   ============================================================ */
.app-main .bchart-alloc {
  display: flex; flex-direction: column; min-width: 0;
  padding: 0; gap: 0;
}
/* BOTTOM-ALIGNED. The rail is 360px and its content needs less, so the question is
   only where the slack goes — and it goes under the FIGURE, not under the table. The
   bar, its legend and the table below read as one block of allocation; leaving the gap
   beneath them would have floated that block in the middle of the column while the
   heading sat alone at the top.
   `margin-bottom: auto` on the head rather than `margin-top: auto` on the bar: the bar
   keeps its own designed 56px off the figure, and the free space is added ABOVE that
   rather than replacing it. An auto top margin would have collapsed the 56 to nothing
   the moment the column had no slack to give. */
.app-main .bchart-alloc-top {
  display: flex; flex-direction: column; padding: 20px;
  flex: 1 1 auto; min-height: 0;
}
.app-main .bchart-alloc-top > .bchart-alloc-head { margin-bottom: auto; }
/* The header row: caption over the figure, refresh hard right and top-aligned. A
   button, because it picks the ratio series — it just does not look like one until you
   hover it. */
/* align-self: stretch, not width: 100%. With a negative margin on both sides, a
   `width: 100%` box is exactly the parent's CONTENT width but shifted 8px left — so it
   ended 16px short of the right edge and the header's hover fill stopped before the
   panel did. Stretch sizes it to the parent and the margins then push it out evenly. */
.app-main .bchart-alloc-head {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  align-self: stretch; padding: var(--sp-2); margin: calc(var(--sp-2) * -1);
  background: none; border: 0; border-radius: var(--radius);
  text-align: left; cursor: pointer;
  transition: background .12s;
}
.app-main .bchart-alloc-headmain { flex: 1 1 auto; min-width: 0; }
.app-main .bchart-alloc-head:hover { background: var(--surface); }
/* NO selected state on the selectors. The chart names the series it is drawing, and a
   filled row here read as "this row is special" rather than "the chart is showing
   this" — on a two-row table that is a permanent highlight on half of it. Hover still
   answers "can I click this". */
.app-main .bchart-alloc-head:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
.app-main .bchart-alloc-cap {
  font-size: var(--body-size); font-weight: 400; line-height: 20px; color: var(--muted);
  margin-bottom: 4px;
}
.app-main .bchart-alloc-fig {
  font-size: var(--fig-hero); font-weight: 400; line-height: 1.05; color: var(--foreground);
  font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.app-main .bchart-alloc-fig.dim { color: var(--muted-2); }
.app-main .bchart-alloc-refresh {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--control-h-sm); height: var(--control-h-sm); padding: 0;
  background: var(--surface-2); border: 0; border-radius: var(--radius);
  color: var(--muted); cursor: pointer; transition: color .12s, background .12s;
}
.app-main .bchart-alloc-refresh:hover { color: var(--foreground); background: var(--border-strong); }
.app-main .bchart-alloc-refresh:focus-visible { outline: 2px solid var(--ring); outline-offset: 1px; }
.app-main .bchart-alloc-refresh svg {
  width: 16px; height: 16px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}

/* The split bar sits 48px under the figure — the gap is the frame's, and it is what
   stops the bar reading as part of the heading. */
.app-main .bchart-alloc-bar {
  display: flex; width: 100%; height: 16px; margin-top: 56px; overflow: hidden;
  border-radius: var(--radius); background: var(--surface-2);
}
/* The segments are <button>s (they select a series), so they give up every default a
   button carries and keep only the fill. */
.app-main .bchart-alloc-seg {
  display: block; height: 100%; padding: 0;
  border: 0; border-radius: 0; appearance: none;
  cursor: pointer;
  transition: width .2s ease, opacity .12s ease, filter .12s ease;
}
.app-main .bchart-alloc-seg:hover { filter: brightness(1.15); }
.app-main .bchart-alloc-seg:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
/* Hovering a segment does what hovering its row does — dims the other side and the
   other legend entry — so the bar, the legend and the table all answer "which is
   which" the same way whichever one you point at. */
.app-main .bchart-alloc:has(.bchart-alloc-seg.is-eth:hover) .bchart-alloc-seg.is-tok,
.app-main .bchart-alloc:has(.bchart-alloc-seg.is-tok:hover) .bchart-alloc-seg.is-eth {
  opacity: .25;
}
.app-main .bchart-alloc:has(.bchart-alloc-seg.is-eth:hover) .bchart-alloc-key.is-tok,
.app-main .bchart-alloc:has(.bchart-alloc-seg.is-tok:hover) .bchart-alloc-key.is-eth {
  opacity: .35;
}
.app-main .bchart-alloc-seg.is-eth { background: var(--accent); }
/* The token half has its own colour in the accent's family — not --green (a market
   side) and not a mix of --accent (which read as a disabled ETH).
   The divider is a border in the page tone, not a flex gap — a gap would push the pair
   past 100% and the last few pixels of the token segment off the end. */
.app-main .bchart-alloc-seg.is-tok {
  background: var(--alloc-tok);
  box-sizing: border-box; border-left: 3px solid var(--background);
}

.app-main .bchart-alloc-legend {
  display: flex; align-items: center; gap: var(--sp-4); margin-top: 24px;
}
.app-main .bchart-alloc-key { display: inline-flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.app-main .bchart-alloc-dot {
  width: 6px; height: 6px; border-radius: 0; flex: 0 0 auto;
}
.app-main .bchart-alloc-key.is-eth .bchart-alloc-dot { background: var(--accent); }
.app-main .bchart-alloc-key.is-tok .bchart-alloc-dot { background: var(--alloc-tok); }
.app-main .bchart-alloc-keyname {
  font-size: var(--cap-size); font-weight: 400; color: var(--foreground);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- the table: full-bleed, 20px insets per row ---- */
.app-main .bchart-alloc-table { display: flex; flex-direction: column; }
.app-main .bchart-alloc-row {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
  align-items: center; gap: var(--sp-3);
  width: 100%; box-sizing: border-box; padding: var(--sp-3) 20px;
  background: none; border: 0; border-top: 1px solid var(--border);
  text-align: left; font-family: inherit;
}
/* The same band every table in this app puts over its columns: 32px on --surface with
   a rule under it. It was inheriting the row padding and sitting transparent, so this
   one table's heading looked like a row. */
.app-main .bchart-alloc-thead {
  height: 32px; padding: 0 20px;
  background: var(--surface);
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
/* The heading already draws the line under itself, and every row draws one above
   itself — so the first row doubled it into a 2px rule. The row after the heading
   gives its border up. */
.app-main .bchart-alloc-thead + .bchart-alloc-item { border-top: 0; }
.app-main .bchart-alloc-thead .act-wt-col:not(:first-child) { text-align: right; }
.app-main .bchart-alloc-item { cursor: pointer; transition: background .12s; }
.app-main .bchart-alloc-item:hover { background: var(--surface); }
.app-main .bchart-alloc-item:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; }
.app-main .bchart-alloc-asset {
  font-size: var(--body-size); font-weight: 400; color: var(--foreground);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.app-main .bchart-alloc-total,
.app-main .bchart-alloc-share {
  font-size: var(--body-size); font-weight: 400; color: var(--muted);
  font-variant-numeric: tabular-nums; text-align: right;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.app-main .bchart-alloc-total.dim, .app-main .bchart-alloc-share.dim { color: var(--muted-2); }

/* ---- hovering a row lights up its share of the bar ----
   The table says 50.87% and the bar shows a split; nothing connected the two, so on a
   two-asset chart you had to infer which end was which from the legend's order. Hover a
   row and the OTHER segment recedes — dimming the rest is steadier than brightening the
   one, which on an already-saturated fill just looks like a flicker.
   `:has()` rather than JS: the rows and the bar are siblings inside .bchart-alloc, and
   this is a hover state, which belongs in the stylesheet. */
.app-main .bchart-alloc-seg { transition: width .2s ease, opacity .12s ease; }
.app-main .bchart-alloc:has(.bchart-alloc-item[data-series="eth"]:hover) .bchart-alloc-seg.is-tok,
.app-main .bchart-alloc:has(.bchart-alloc-item[data-series="tok"]:hover) .bchart-alloc-seg.is-eth {
  opacity: .25;
}
/* The legend follows the same cue, so the name and the colour are tied together too. */
.app-main .bchart-alloc:has(.bchart-alloc-item[data-series="eth"]:hover) .bchart-alloc-key.is-tok,
.app-main .bchart-alloc:has(.bchart-alloc-item[data-series="tok"]:hover) .bchart-alloc-key.is-eth {
  opacity: .35;
}
.app-main .bchart-alloc-key { transition: opacity .12s ease; }

/* Split tooltip on the portfolio-value chart. Positioned inside the canvas wrap
   (which is position:relative) and pinned to the top, so it never sits under the
   cursor or covers the crosshair it is describing. */
.bchart-canvas-wrap { position: relative; }
.bchart-tip {
  position: absolute; top: 8px; z-index: 5; pointer-events: none;
  min-width: 148px; padding: 8px 10px;
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: 0 6px 20px -8px rgba(0,0,0,.8);
  font-variant-numeric: tabular-nums;
}
.bchart-tip[hidden] { display: none; }
.bchart-tip-head {
  font-size: var(--cap-size); color: var(--muted);
  padding-bottom: 6px; margin-bottom: 6px; border-bottom: 1px solid var(--border);
}
.bchart-tip-row {
  display: grid; grid-template-columns: 1fr auto 46px;
  gap: 8px; align-items: baseline;
  font-size: var(--cap-size); line-height: 18px;
}
.bchart-tip-name { color: var(--muted); }
.bchart-tip-val  { color: var(--foreground); }
.bchart-tip-pc   { color: var(--muted); text-align: right; }
/* The two assets keyed to the same colours the split bar uses. */
.bchart-tip-row.is-eth  .bchart-tip-name::before,
.bchart-tip-row.is-tok  .bchart-tip-name::before {
  content: ""; display: inline-block; width: 6px; height: 6px;
  border-radius: 0; margin-right: 6px; vertical-align: middle;
}
.bchart-tip-row.is-eth .bchart-tip-name::before { background: var(--accent); }
.bchart-tip-row.is-tok .bchart-tip-name::before { background: var(--alloc-tok, var(--muted)); }
.bchart-tip-row.is-total {
  margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border);
}
.bchart-tip-row.is-total .bchart-tip-name,
.bchart-tip-row.is-total .bchart-tip-val { color: var(--foreground); }

/* 24h change, in place of the chart's descriptive subtitle. Same size and family
   as the sentence it replaced — only the colour carries the meaning, using the
   app's existing --green / --red rather than a third pair invented here. */
.bchart-delta { color: var(--muted); font-variant-numeric: tabular-nums; }
.bchart-delta.up   { color: var(--green); }
.bchart-delta.down { color: var(--red); }
.bchart-delta-win  { color: var(--muted); margin-left: 6px; }

/* Configure keeps its heading at the shared box-title size. Setting names use the
   same quiet caption colour as the metric labels, while editable/readout values stay
   fully foreground.
   SIZE: everything in a Configure box is on the 14px body tier. These used to step
   down to the 12px caption tier, which made the panel that carries the app's actual
   settings the smallest type on the page — labels, readouts, selects and every
   button all at caption size. The colour split below still separates a setting's
   NAME from its VALUE; only the size is back on the body tier. */
.act-box-cfg .act-label,
.act-box-cfg .sc-field-label,
.act-box-cfg .sw-limit-cap,
.act-box-cfg .sw-now {
  color: var(--muted); font-size: var(--body-size);
}
/* A NOTE is not a setting either — it is an aside ABOUT the row above it, so it sits a
   tier below the controls rather than level with them. It was in the group above, which
   put two lines of prose at the same size as the labels it was explaining.
   The info mark is what makes that reading immediate; it takes the caption colour and
   is pinned to the first line, so a note that wraps keeps its text in one column
   instead of running back under the glyph. */
.act-box-cfg .sc-note {
  display: flex; align-items: flex-start; gap: 6px;
  color: var(--muted); font-size: var(--cap-size); line-height: 18px;
}
.act-box-cfg .sc-note-text { min-width: 0; }
.sc-note-icon {
  flex: 0 0 auto; width: 13px; height: 13px; margin-top: 2.5px;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; color: var(--muted-2);
}
/* The "?" is NOT on the body tier — it is a footnote marker beside a heading, not a
   setting. It was in the group above, which pushed it to 14px on --muted and made a
   16px box the second-loudest thing on the trigger's caption line. Back to the caption
   tier and the quiet ink, in a box sized to that: 14px, on the plain --border rather
   than --border-strong. */
.act-box-cfg .sw-info {
  width: 14px; height: 14px;
  color: var(--muted-2); font-size: 11px;
  border-color: var(--border);
}
.act-box-cfg .sw-info:hover { color: var(--foreground); border-color: var(--border-strong); }
.act-box-cfg .act-read,
.act-box-cfg .sc-input,
.act-box-cfg .cs-btn,
.act-box-cfg .cs-text,
.act-box-cfg .sw-preset,
.act-box-cfg .btn {
  color: var(--foreground); font-size: var(--body-size);
}
/* A SEGMENTED CONTROL IS A CHOICE, so it takes the size from that rule and not the
   colour. It was in the list above, which is unlayered and therefore beats the three
   state colours .seg-btn declares in @layer components — so inside a Configure box
   every option was --foreground whether it was picked or not, and the control said
   nothing about which one was. Nothing to hover into, either: the destination
   colour was already there.
   The three states, restated here so nothing in this tail can flatten them again.
   They are the tab strip's exactly (.act-tab / .act-tab:hover / .act-tab.active) —
   same greys, same .12s — because a segmented control and a tab strip are the same
   gesture at two sizes. */
.act-box-cfg .seg-btn { font-size: var(--body-size); color: var(--muted); }
.act-box-cfg .seg-btn:hover,
.act-box-cfg .seg-btn.active { color: var(--foreground); }
/* A disabled option (Bundle without a relay) must not read as merely unpicked. */
.act-box-cfg .seg-btn:disabled,
.act-box-cfg .seg-btn.is-disabled { color: var(--muted-2); }
.act-actions .btn,
.act-actions .act-link,
.rail-quickswap .btn { font-size: var(--body-size); }
/* The READY run button keeps the dark ink its white fill needs. This has to be
   declared here, unlayered, rather than relying on `.sc-run.primary` in
   @layer components: `.act-box-cfg .btn` above is unlayered too and forces
   --foreground on every button in the box, which painted white text on the white
   primary and left the label invisible. */
.act-box-cfg .sc-run.primary:not(:disabled):not(.sw-armed) {
  background: var(--primary); border-color: var(--primary); color: var(--primary-fg);
}
.act-box-cfg .sc-run.primary:not(:disabled):not(.sw-armed):hover {
  background: var(--primary-hover); border-color: var(--primary-hover);
}
/* A SLIDER IS A FIXED LENGTH. The track was flex:1 1 auto and the readout beside
   it flex:0 0 auto, so the readout was sized to its text and the track absorbed
   whatever was left — which means the slider's length changed with the VALUE it
   was showing. "none" -> "0–20s" moved Sweep's track by 34px, and Swap's Delay
   jumped every time the range crossed from one digit to two. The track now holds
   a fixed basis and the readout takes the remainder, right-aligned, so neither
   the slider nor the number moves as the value changes.
   :has() scopes this to rows that really are sliders — the same .act-track wraps
   the Execution segmented control and the typed number boxes, which must keep
   their natural widths.
   The leading .act-pane is NOT redundant: the per-pane rule above
   (`.act-pane[data-act="sweep"] .act-box-cfg .act-row > .act-track`) sets
   flex:1 1 auto at a higher specificity than a selector without it, so without
   this the track kept absorbing the slack and the length still moved. */
/* The READOUT is the fixed part, not the track. It is the element whose content
   changes, so reserving its width is what stops the change propagating; the track
   then takes the remainder. Pinning the TRACK instead worked at a wide box and
   started truncating amounts ("1.187 ETH") at 1280 — and clipping a value is worse
   than the wobble this set out to fix. 76px clears the longest readout in these
   boxes (an ETH amount measures ~67). The label beside it is already a fixed 84px,
   so with both ends fixed every slider in a box resolves to the same length. */
.act-pane .act-box-cfg .act-row:has(input[type="range"]) > .act-track {
  flex: 1 1 auto; min-width: 0;
}
.act-pane .act-box-cfg .act-row:has(input[type="range"]) > .act-read {
  flex: 0 0 var(--act-read-w, 76px); text-align: right; white-space: nowrap;
}
/* The reserve is per PANE, sized to the longest readout that pane can show, so a
   box never holds back space it will not use. 76px is the default because an ETH
   amount ("1.187 ETH") measures ~67. Swap's only slider is a delay range, which
   tops out at "53–60s" (~50) — leaving it at 76 parked 26px of dead air between
   the slider and the number and made the track look stunted. */
.act-pane[data-act="swap"] { --act-read-w: 58px; }
/* Transfer's three readouts are BARE figures — the unit is in the label beside the
   slider ("Total ETH", "Total DEMO"), so nothing here carries a trailing " ETH". The
   widest it can print is "±100%" or a short-formatted token count, both under 52. At
   the 76px default a five-character number left a visible trench between the slider
   and itself; 56 closes it and hands the width back to the track. */
.act-pane[data-act="transfer"] { --act-read-w: 56px; }
/* The dim half of the locked-slider state (its track and handle are in @layer
   components, beside the rest of the slider). It has to be here: `.act-box-cfg
   .act-read` above is unlayered and sets --foreground on every readout in the box,
   and an unlayered rule beats every layer at any specificity. */
.act-pane .act-box-cfg .act-row:has(input[type="range"]:disabled) > .act-read {
  color: var(--muted-2);
}
/* ...and it stops offering to be typed into. scReadEdit refuses to open the field
   while the row is locked; this is the half that says so before you click. */
.act-pane .act-box-cfg .act-row:has(input[type="range"]:disabled) > .act-read-live {
  cursor: default; background-color: transparent; box-shadow: none;
}

/* ---- the field IS the readout ----
   A bordered box appearing where a number was is a second control arriving to do
   the first one's job: the row goes from three quiet figures to one framed input,
   which is a bigger event than changing a number deserves. So the field wears the
   hover pill the readout already wears — same box, same right edge, same tabular
   digits, same size — and the only things that change when it opens are the caret
   and the fact that you can type. No border, no ground of its own.
   Unlayered because two rules down here would otherwise beat it: the ground from
   `.act-box-cfg input[type="text"]`, and the --num-w rung every numeric input gets
   from `input[inputmode="decimal"]`. */
.act-pane .act-row > .act-read-input,
.qa-params-fields .act-row > .act-read-input {
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  width: var(--act-read-w, 76px); min-width: 0; flex: none;
  box-sizing: border-box; height: 20px; padding: 0;
  border: 0; border-radius: var(--radius);
  background-color: var(--control-bg-hover);
  box-shadow: 0 0 0 4px var(--control-bg-hover);
  color: var(--foreground);
  font-family: var(--num); font-variant-numeric: tabular-nums;
  font-size: var(--body-size); line-height: 20px; text-align: right;
  outline: none;
}
/* THE SELECTION IS NOT THE BROWSER'S. Opening a field selects what is in it, and
   the platform highlight is a saturated blue that belongs to no part of this
   palette — the loudest thing on the panel, for a state that means "start typing
   over this". A quiet wash of the app's own ink says the same. */
.act-pane .act-row > .act-read-input::selection,
.qa-params-fields .act-row > .act-read-input::selection { background: var(--select-wash); color: var(--foreground); }
/* Swap's limit box is a price READOUT, not a row of settings, so it is the one
   place in Configure that steps off the 14px body tier in both directions: the
   figure is the headline at the 20px title tier, and the chrome around it — the
   live-price note and the four percentage presets — drops to the 12px caption
   tier so nothing competes with the number. "Fires at or below" stays on the body
   tier with every other setting NAME in the box.
   Unlayered and sitting beside the tail it overrides, for the same reason that
   tail is: `.act-box-cfg .sw-now` / `.sw-preset` above are unlayered too, so no
   layered selector at any specificity could reach these. */
.act-pane[data-act="swap"] .act-box-cfg .sw-limit-input,
.act-pane[data-act="swap"] .act-box-cfg .sw-limit-unit { font-size: 20px; }
.act-pane[data-act="swap"] .act-box-cfg .sw-now,
.act-pane[data-act="swap"] .act-box-cfg .sw-preset { font-size: var(--cap-size); }

/* BEGIN DASHBOARD-ONLY SWAP WIDGET */
/* This typography tail is intentionally unlayered. The Configure typography tail
   above is also unlayered, so it beats @layer components regardless of specificity;
   these component exceptions must live beside the rule they override.
   All three panels of the stack now state their figure at the same 20px title step,
   and the notes on their headings at the same 12px caption step — see the trigger's
   pair just above. Repeated here rather than left to @layer components because
   `.act-box-cfg .sc-input` / `.btn` reach these elements from the unlayered tail. */
.act-pane[data-act="swap"] .act-box-cfg .sw-widget-input,
.act-pane[data-act="swap"] .act-box-cfg .sw-widget-output,
.act-pane[data-act="swap"] .act-box-cfg .sw-widget-asset,
.rail-quickswap .sw-widget-input,
.rail-quickswap .sw-widget-asset { font-size: var(--title-size); }
.act-pane[data-act="swap"] .act-box-cfg .sw-widget-cap { font-size: var(--body-size); }
/* NO SLAB BEHIND THE FIGURE. `.act-box-cfg input[type="text"]` (0,2,1, unlayered)
   paints every typed field in this column with --background, which read as a black
   block under the number the moment the Sell panel went to the lighter --surface. Same
   fix and same reasoning as `.sw-limit-input` further up — the figure is a headline
   that happens to be editable, not a form field — so the pane selector takes this to
   0,4,0 and the focus ring becomes a 1px rule UNDER the number rather than a box
   around it. Without that the field would have no affordance left at all. */
.act-pane[data-act="swap"] .act-box-cfg .sw-widget-input,
.act-pane[data-act="swap"] .act-box-cfg .sw-widget-input:hover,
.act-pane[data-act="swap"] .act-box-cfg .sw-widget-input:focus {
  background: none; box-shadow: none; outline: none;
}
.act-pane[data-act="swap"] .act-box-cfg .sw-widget-input:focus-visible {
  outline: none; box-shadow: inset 0 -1px 0 var(--border-strong);
}
.act-pane[data-act="swap"] .act-box-cfg .sw-widget-equivalent,
.act-pane[data-act="swap"] .act-box-cfg .sw-widget-equivalent-unit,
.act-pane[data-act="swap"] .act-box-cfg .sw-widget-detail { font-size: var(--cap-size); }
/* END DASHBOARD-ONLY SWAP WIDGET */

/* ---- EVERY disabled button ----
   The primary button is a near-white fill, and the old `.btn:disabled { opacity: .5 }`
   still left it the brightest thing in the bar — it read as the thing to press when it
   was the one thing you could not. Worse, dimming a DARK label on a near-white fill to
   50% produced light-grey-on-white: the text all but vanished.
   Disabled is a COLOUR here, not a dimming: muted-2 ink on the surface tone the rest
   of the inert chrome uses, and no border, so it sits flat rather than outlining an
   empty box. Run buttons opt into their neutral enabled chip above (foreground text
   the moment wallets land in the box); other enabled buttons remain unchanged.
   This grew in two steps: first from the Swap run button to every disabled primary,
   and now to EVERY button species on the action tabs — .btn and its variants, the
   segmented controls (Sweep's Bundle off mainnet, Swap's mode while armed), the
   limit presets and the Activity text links, which each carried their own ad-hoc
   fade or, worse, no disabled look at all. One rule, driven off the real :disabled
   state — a control that cannot act must look it, and it must not change size or
   grow chrome to do so. `opacity: 1` opts every species out of the base layer's
   :where() fade fallback (which stays, for controls this system does not name).
   Placement is load-bearing: unlayered AND after the Configure typography tail, so
   nothing re-lights a disabled control's text. */
.btn:disabled, .btn:disabled:hover {
  background: var(--surface-2); border-color: transparent; color: var(--muted-2);
  opacity: 1; filter: none; cursor: default;
}
/* Unfilled species (ghost/tiny/danger buttons, segments, presets, links) have no fill
   to keep: they go dim in place rather than gaining a chip by being turned off. */
.btn.ghost:disabled, .btn.ghost:disabled:hover,
.btn.tiny:disabled, .btn.tiny:disabled:hover,
.btn.danger:disabled, .btn.danger:disabled:hover {
  background: transparent; color: var(--muted-2);
}
.seg-btn:disabled, .seg-btn:disabled:hover,
.sw-preset:disabled, .sw-preset:disabled:hover,
.act-link:disabled, .act-link:disabled:hover {
  color: var(--muted-2); opacity: 1; cursor: default;
}
/* An armed Swap keeps its chosen segment recessed (.seg-btn.active's fill still
   applies) — the lock reads as "held", not erased. */
.sw-preset:disabled, .sw-preset:disabled:hover { background: none; border-color: var(--border); }
/* (.act-link:disabled no longer needs its underline removed — it does not have one.) */

/* ============================================================
   WALLET CANVAS + STACKED SHELL
   One selection vocabulary for every wallet row: a plain press DRAGS (a rail row
   alone, a selected row with its whole selection), and Shift SWEEPS a marquee.
   The cursor states below are the only thing telling the two apart, so they have
   to stay honest — grab where a press picks up, crosshair where it sweeps.
   ============================================================ */
.wallet-rail { position: relative; }
/* Crosshair ONLY while Shift is held (.rail-marquee itself is styled further
   down, beside .rail-drag-ghost). It used to be the rows' permanent cursor,
   which announced nothing: every row looked ready to sweep whether or not it
   was. Now it marks the one moment the gesture actually changes. */
.rail-body.marquee-armed,
.rail-body.marquee-armed *:not(button):not(a) { cursor: crosshair; }
.rail-wallet, .sc-wallet-card { user-select: none; }
/* While a sweep is in flight nothing on the page is text-selectable. Set on the
   BODY rather than per-element because the rectangle crosses everything — panel
   headings, table figures, tab labels — and each one it dragged over came back
   highlighted. Inputs keep their own selection: a sweep never starts in one
   (railGestureBlockedTarget) and typing must stay usable. */
/* NOTHING in the app chrome is text-selectable, with or without a modifier. This
   is a dense tool UI, not a document: any drag across it to reach a control left
   a trail of highlighted labels, figures and table cells, and none of it is prose
   anyone reads by selecting. Text still gets OUT — every address, hash and amount
   carries its own copy button, which is the supported route and puts the full
   value on the clipboard rather than the truncated one on screen.
   Two exceptions: form fields, because typing needs a caret and a selection, and
   [data-selectable] as the opt-in for anything that later turns out to want it.
   This also subsumes the scattered per-element `user-select: none` above (group
   chips, wallet rows, tabs) — those are now redundant, kept only because each
   states its own reason. */
/* The Logs band holds ONE control — the Txs|Events selector — so it takes an 8px
   inset that matches its own vertical one (48px band, 32px control). It has to be
   declared HERE rather than in logs.js's injected sheet: `.act-box-title` sets
   0 16px unlayered, and that beats anything logs.js writes inside
   @layer components no matter how specific. */
.act-box-title.dlog-band { padding-left: 8px; padding-right: 8px; }
/* The Logs box stacks its parts with no gap, the way Buyers/Sellers do (their
   .sw-list is gap:0). .sc-card's 12px gap applied between the view-selector band,
   an empty .sc-card-head and the table — 24px of dead space over the column
   headings once the band lost its title. */
.sc-card.dlog-card { gap: 0; }

/* ============================================================
   PAGE SCROLL, PINNED CONFIGURE  (Activity / Sweep / Transfer / Swap)
   Logs keeps its own model — that table scrolls inside its box, because a log is
   a feed you page through rather than a list you build. Everywhere else the
   wallet lists ARE the work: they grow as you drag wallets in, the PAGE scrolls
   to follow, and Configure stays put so you can set up and run without scrolling
   back up. Three rules do it.
   ============================================================ */
/* 1. The lists stop scrolling inside themselves and size to their content, so
      the page is what grows. Without this a long list hid its own tail behind an
      inner scrollbar while the page stayed still. */
.act-pane[data-act="activity"] .sw-list > .sc-dropzone,
.act-pane[data-act="sweep"] .sw-list > .sc-dropzone,
.act-pane[data-act="transfer"] .sw-list > .sc-dropzone,
.act-pane[data-act="swap"] .sw-list > .sc-dropzone,
.act-pane[data-act="activity"] .act-box-wallets > .sc-dropzone {
  overflow: visible; max-height: none;
}
/* 2. EMPTY, the band reaches the bottom by FILLING what the pane leaves — every
      wrapper between the card and the band passes its height down, so the band
      absorbs the slack. This used to be `calc(100vh - Npx)` per pane, which is
      the wrong tool twice over: the subtrahend had to be hand-tuned to whatever
      sat above the band, and on a short window it demanded more height than the
      viewport had, so an EMPTY page scrolled with nothing under the fold. Filling
      cannot overshoot — there is nothing to fill past. */
.act-pane[data-act="activity"] > .sc-cards > .sc-card,
.act-pane[data-act="sweep"] > .sc-cards > .sc-card,
.act-pane[data-act="transfer"] > .sc-cards > .sc-card,
.act-pane[data-act="swap"] > .sc-cards > .sc-card,
.act-pane[data-act="activity"] .sc-card-body,
.act-pane[data-act="sweep"] .sc-card-body,
.act-pane[data-act="transfer"] .sc-card-body,
.act-pane[data-act="swap"] .sc-card-body,
.act-pane[data-act="activity"] .act-panel,
.act-pane[data-act="sweep"] .act-panel,
.act-pane[data-act="transfer"] .act-panel,
.act-pane[data-act="swap"] .act-panel,
.act-pane[data-act="transfer"] .sc-tabpane:not([hidden]),
.act-pane[data-act="swap"] .sc-tabpane:not([hidden]) {
  flex: 1 1 auto; min-height: 0;
}
.act-pane[data-act="activity"] .act-band,
.act-pane[data-act="sweep"] .act-band,
.act-pane[data-act="transfer"] .act-band,
.act-pane[data-act="swap"] .act-band { flex: 1 1 auto; min-height: 0; }
/* 3. AND THE PANE HAS A HEIGHT TO PASS DOWN. The chain above only distributes what
      the pane is given, and the pane used to be given it by the tallest of its
      hidden siblings — which is why isolating the panes took the stretch with it.
      Anchored properly now: the canvas gives the hub's stack a full screen, the
      stack gives the tab area whatever the overview panel leaves, and the active
      pane fills that. Nothing depends on what the other four contain, so a full
      Activity can no longer stretch Sweep and an empty Sweep still reaches the
      floor. min-height, not height: content taller than a screen still grows the
      page rather than being trapped. */
.app-main > .tab-panel[data-tab="hub"] { min-height: 100%; }
.app-main > .tab-panel[data-tab="hub"] > .stack { min-height: 100%; }
.app-main > .tab-panel[data-tab="hub"] > .stack > .act-tabbed { flex: 1 1 auto; min-height: 0; }
/* 3. Configure pins to the top of the scroll region and rides the page down.
      It already hugs its content and aligns to the start, which is what leaves
      room for it to stick rather than stretch. */
.act-pane[data-act="activity"] .act-band > .act-band-right,
.act-pane[data-act="sweep"] .act-band > .act-band-right,
.act-pane[data-act="swap"] .act-band > .act-band-right,
.act-pane[data-act="transfer"] .act-band > .act-band-right,
/* The column headings stick to the SAME line. They were already sticky at top:0
   and used to stop against their own scrolling box, which had no padding; now
   that the lists grow and the PAGE scrolls, they stop against the canvas — so
   they need the same correction or the two would rest 8px apart. */
.act-pane[data-act="activity"] .act-wt-head,
.act-pane[data-act="sweep"] .act-wt-head,
.act-pane[data-act="swap"] .act-wt-head,
.act-pane[data-act="transfer"] .act-wt-head {
  /* NEGATIVE, to cancel the canvas's own 8px top padding. Sticky offsets are
     measured from the scrollport's padding box, so top:0 parked everything 8px
     below the viewport edge and left a strip of page background above it. */
  position: sticky; top: calc(-1 * var(--sp-2)); z-index: 3;
}
body { user-select: none; }
input, textarea, [contenteditable="true"], [data-selectable] { user-select: text; }
/* A sweep suppresses even those exceptions: the rectangle crosses inputs on its
   way past, and it must not drag a selection out of one. */
body.is-wallet-marquee, body.is-wallet-marquee * { user-select: none; }
/* Rail rows always drag; a drop-zone card only drags once selected. */
.rail-wallet { cursor: grab; }
.sc-wallet-card { cursor: pointer; }
/* SELECTION IS BLUE, AND ITS FILL IS AN ALPHA.
   It was --surface-2 (#232323) against a --border (#262626) hairline — three levels
   apart on a 255-point scale, so wherever a selected row met the edge of its box the
   box's own stroke stopped reading, worst in Sweep where Buyers and Sellers sit edge to
   edge and the shared seam is all that separates them. The problem was never that the
   row had a fill, it was that the fill was a NEUTRAL GREY sitting a hair off the grey
   of the stroke. A low-alpha accent solves it on both counts: it composites over
   whatever plane the row is on instead of replacing it, and it differs from the
   hairline in HUE rather than by three points of luminance, so the stroke stays legible
   straight through it.
   Same accent on the 2px bar. This is the app's "this one" mark, and the selected group
   chip already draws its bar from --accent for exactly that reason. (.loop-sub.selected
   still carries the old grey; it is a different list in a legacy surface and is left
   alone rather than swept up in a wallet-row change.) */
.rail-wallet.selected, .sc-wallet-card.selected {
  cursor: grab;
  background-color: color-mix(in srgb, var(--accent) 10%, transparent);
  box-shadow: inset 2px 0 0 var(--accent);
}
/* THE SAME MARK IN RED, for the rows an error is about. When a wallet sits in two
   lists that cannot both hold it, the toast states the rule and this says which rows
   broke it — same 2px bar, same 10% wash, so it reads as a state of the row rather
   than a new kind of object. Declared AFTER .selected, and at equal specificity, so a
   culprit that is also selected shows red: the error outranks the selection.
   (Set from scMarkConflict; cleared the moment any list changes.) */
.sc-wallet-card.sc-wallet-conflict {
  background-color: color-mix(in srgb, var(--red) 10%, transparent);
  box-shadow: inset 2px 0 0 var(--red);
}
.rail-wallet:active, .sc-wallet-card.selected:active { cursor: grabbing; }
/* SELECTION IS BLUE ALL THE WAY THROUGH. The row takes the accent bar and the accent
   wash, and then everything sitting ON it kept the grey it wears on a plain row: the
   group tag's fill and hairline, the address, and the grey square every icon button
   lights up with. Grey on blue reads as a piece of the unselected row that failed to
   change with it. Each of them resolves the SAME accent instead, at its own strength,
   so a selected row is one object in one state rather than a blue row with grey parts.
   Declared out here, unlayered, because the rules they have to beat are. */
.rail-wallet.selected .rail-wallet-group,
.sc-wallet-card.selected .sc-wallet-group {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  /* A HAIRLINE, not an outline. The tag's edge was the brightest thing on the row —
     brighter than the 2px bar that actually says "this one" — and a badge that
     out-shouts the mark it sits inside is a second selection. The edge is the row's
     own border colour with a hint of the accent mixed THROUGH it rather than an alpha
     of the accent over the wash, so it stays a hairline at any wash strength. */
  border-color: color-mix(in srgb, var(--accent) 22%, var(--border-mid));
}
/* (The ADDRESS keeps --muted. It was tinted with the rest of the row and it was the
   wrong one to tint: it is the wallet's identity, the one thing on the row you read
   character by character, and colouring it made it the row's loudest text. Selection
   is the bar, the wash and the tag — the identity reads the same in every state.) */
/* The hover square under every icon button a row carries — copy, the three actions,
   the ⋮. One tone for all of them, because they are one row's worth of controls. */
.rail-wallet.selected .wcopy:hover,
.rail-wallet.selected .rail-act:hover,
.rail-wallet.selected .wallet-more:hover,
.rail-wallet.selected .wallet-more:focus-visible,
.sc-wallet-card.selected .wcopy:hover,
.sc-wallet-card.selected .wallet-more:hover,
.sc-wallet-card.selected .wallet-more:focus-visible {
  /* Light enough to read as "the pointer is here" against the row's own 10% wash,
     rather than as a filled-in button sitting on it. */
  background: color-mix(in srgb, var(--accent) 13%, transparent);
}
/* Except the ✕, which is red on hover everywhere and stays the app's red mark. */
.sc-wallet-card.selected .sc-wallet-x:hover {
  background: color-mix(in srgb, var(--red) 20%, transparent);
}
/* A WALLET IN HAND IS OPAQUE, and this has to be declared out here to get that.
   .sc-card-lift lives in @layer components, and the selection rule directly above is
   unlayered — so an unlayered alpha beat the layered fill and every picked-up card was
   see-through, worst on a multi-wallet carry where the stack showed the list sliding
   about underneath it. The row is off the page while you hold it, so it takes a real
   surface: --surface-2, the app's "lifted" tone.
   A selected row keeps its blue by resolving the SAME 10% wash against that opaque
   ground instead of against transparency — the colour it appears to have in the list,
   made solid, rather than a second blue that only shows up mid-drag. */
.sc-wallet-card.sc-card-lift { background-color: var(--surface-2); }
.sc-wallet-card.sc-card-lift.selected {
  background-color: color-mix(in srgb, var(--accent) 10%, var(--surface-2));
}
.rail-sort-head {
  display: inline-flex; align-items: center; justify-content: inherit; gap: 4px;
  width: 100%; padding: 0; border: 0; background: none; color: inherit;
  font: inherit; line-height: inherit; cursor: pointer; white-space: nowrap;
}
.rail-bal-col:nth-child(2) .rail-sort-head,
.rail-bal-col:nth-child(3) .rail-sort-head { justify-content: flex-end; }
/* THE LABEL, not just the head. `.rail-bal-head span` paints every span in this
   header with the muted figure alpha at (0,1,1) — the same rule that was holding the
   arrow grey — so setting a colour on the BUTTON never reached the word inside it:
   the head went white and the only thing you could see stayed exactly as it was.
   (0,2,0) here, which is what it takes to win. */
.rail-sort-head:hover, .rail-sort-head.active { color: var(--foreground); }
.rail-sort-head:hover .rail-sort-label,
.rail-sort-head.active .rail-sort-label { color: var(--foreground); }
/* ONE ARROW, TURNED OVER. It used to be a ↑ swapped for a ↓ — two different
   characters, which reads as the mark being replaced rather than the same mark
   changing its mind, and cannot be animated at all. One glyph now, rotated for
   descending, faint under the pointer so the column admits it is sortable before
   you commit to finding out. */
/* Scoped through the head, and it has to be: `.rail-bal-head span` (0,1,1) sets
   every span in this header to the muted figure alpha, and a bare .rail-sort-arrow
   (0,1,0) loses to it — which is why the arrow used to fade in FROM the header's
   grey in the rail and from blue in the other tables, two different openings for the
   same gesture. */
.rail-sort-head .rail-sort-arrow {
  /* --foreground even while invisible: the arrow FADES IN, and a colour it has to
     travel from is a colour you see for the length of that fade. */
  width: 9px; flex: 0 0 9px; color: var(--foreground); text-align: center;
  opacity: 0; transform: rotate(0deg);
  transition: opacity .14s ease, transform .22s cubic-bezier(.25, .46, .45, .94), color .12s ease;
}
/* THREE STATES, ONE RULE, and the same three in every table on the page (see
   .sort-head below — the two blocks are deliberately identical).
     at rest       grey heading, no arrow
     under the     white heading, white arrow — the arrow ARRIVES rather than
       pointer     brightening, so hovering shows you exactly what pressing gives
     sorted        white heading, white arrow, and it stays when you move away
   Blue is this app's "act on this" — the primary button, a selected row's bar, a
   range's fill — and a column heading is not an action, it is a state, so the mark
   that says so reads in the same ink as the heading it belongs to. Telling hover
   from sorted is the pointer's job: one of them is under it. */
.rail-sort-head:hover .rail-sort-arrow { opacity: 1; color: var(--foreground); }
.rail-sort-head.active .rail-sort-arrow { opacity: 1; color: var(--foreground); }
.rail-sort-head.is-desc .rail-sort-arrow { transform: rotate(180deg); }

/* ---- the same heading, in every other table ----
   scSortHead builds these (shortcuts.js); the rail keeps its own markup and the
   rules above, because its heads are in index.html and predate the helper. */
.sort-head {
  display: inline-flex; align-items: center; gap: 4px; min-width: 0;
  cursor: pointer; user-select: none;
  transition: color .12s ease;
}
.sort-head > .sort-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sort-head > .sort-arrow {
  flex: 0 0 9px; width: 9px; text-align: center; line-height: 1;
  color: var(--foreground); opacity: 0; transform: rotate(0deg);
  transition: opacity .14s ease, transform .22s cubic-bezier(.25, .46, .45, .94), color .12s ease;
}
/* The same three states as the rail's, in the same ink — see the block above. */
.sort-head[role="button"]:hover > .sort-arrow { opacity: 1; color: var(--foreground); }
.sort-head[role="button"]:hover, .sort-head[role="button"]:hover > .sort-label,
.sort-head.is-on, .sort-head.is-on > .sort-label { color: var(--foreground); }
.sort-head.is-on > .sort-arrow { opacity: 1; color: var(--foreground); }
.sort-head.is-on.is-desc > .sort-arrow { transform: rotate(180deg); }
/* THE LABEL ENDS ON THE COLUMN'S OWN EDGE. A right-aligned column's figures are
   flush right, so its heading has to be too — which means the arrow leads. */
.sort-head.align-right { justify-content: flex-end; }
.sort-head.align-right > .sort-arrow { order: -1; }
.sort-head:focus-visible { outline: 1px solid var(--ring); outline-offset: 2px; }
/* THE ARROW LEADS on the right-aligned columns, so the LABEL ends on the column edge.
   Trailing, it reserved 9px + the 4px gap inside the head whether or not a sort was
   active, which held "Balance" and the token name 13px short of the right edge the
   figures under them are aligned to — a permanent misalignment caused by a mark that
   is invisible most of the time. Order, not markup: the Wallet column reads
   left-to-right and keeps its arrow after the word. */
.rail-bal-col:nth-child(2) .rail-sort-arrow,
.rail-bal-col:nth-child(3) .rail-sort-arrow { order: -1; }
.rail-sort-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.wallet-more {
  display: none; position: absolute; z-index: 1; left: 4px; top: 50%;
  width: 24px; height: 28px; padding: 0; transform: translateY(-50%);
  border: 0; background: none; color: var(--muted); cursor: pointer;
  font-size: 18px; line-height: 1; align-items: center; justify-content: center;
}
.wallet-more:hover, .wallet-more:focus-visible { color: var(--foreground); background: var(--surface-2); }
/* ---- the confirm sheet's wallet list is READ-ONLY ----
   Declared out here, unlayered, because both of the rules it has to beat are: .wallet-more
   and .sc-list-clear-x are defined in this same tail, so a copy inside @layer components
   lost on layer order however specific it was.
   The "…" opens rename/archive/dust and the populated count badge doubles as clear-all —
   both would let the sheet edit the run it exists to confirm. */
/* The column heads take the header block's fill too. Out here, unlayered, because
   `.act-wt-head, .dlog-head, .rail-bal-head { background: var(--surface) }` is declared
   in this tail — a copy inside @layer components lost on layer order however specific it
   was, and the head stayed --surface while the title row above it went to #282828. */
.preflight-wallets .act-wt-head,
.preflight-seq .act-wt-head { background: #282828; border-bottom-color: var(--border-mid); }
/* Same reason, one rule up: `.act-box-title, .sc-io-cap { border-bottom: 1px solid
   var(--border) }` lives in this tail too. */
.preflight-wallets > .sc-io-box > .sc-io-cap,
.preflight-seq > .sc-io-box > .sc-io-cap { border-bottom-color: var(--border-mid); }
/* And the row dividers, for a third time. A .sc-wallet-card is bordered by the PANES
   (`.act-pane .sc-dropzone .sc-wallet-card`), and outside a pane it has no divider at
   all — the rule that would give it one is in this tail, so the dialog's copy has to be
   here as well or the rows separate on nothing but their own height. */
.preflight-wallet-rows > .sc-wallet-card { border-bottom: 1px solid var(--border-mid); }
/* ---- THE PICKER'S LIST IS THE RAIL'S LIST, NOT THE SHEET'S ----
   Same markup, opposite job. The sheet's list previews a decision already made, so
   it reads locked in — one raised header block on #282828 over lighter rows. The
   picker's list is the vault, browsed: the surface the decision is still being made
   ON, and it takes the rail's tones instead — rows on --background with the header
   one step up on --surface.
   Here rather than beside the picker's other rules because those two lines above are
   unlayered, and a layered copy loses to them at any specificity. */
/* ============================================================
   ONE WALLET ROW, EVERY SURFACE
   A wallet is listed in four places — the left rail, a pane's drop zone, Sweep's
   sequence, and the dialogs — and the same row was being dressed by four sets of
   rules. They drifted, which is the only reason the grip sat at 8px in the rail and
   6px in a pane, appeared on hover in one and only on a SELECTED hover in the other,
   and why a row in hand looked lifted in one list and faded in another.
   Stated once here, for both class names, so there is nothing left to drift.
   Unlayered, and it has to be: `.act-pane .sc-dropzone .sc-wallet-card` is (0,3,0)
   and paints the row's ground, so a layered rule at any specificity loses to it.
   ============================================================ */
/* THE GRIP. Six dots in the row's own left inset, outside the Wallet column so it
   never shifts the name. Hidden at rest — a resting list stays quiet — and shown
   whenever the row can be picked up: under the pointer, under the keyboard, and for
   as long as one is in hand. */
.rail-wallet::before,
.sc-wallet-card::before {
  content: "";
  position: absolute; left: 8px; top: 50%; width: 7px; height: 14px;
  transform: translateY(-50%);
  opacity: 0; transition: opacity .12s;
  /* Two columns of three: a 4x5 tile with one dot, tiled across a 7x14 box. */
  background-image: radial-gradient(circle at 1px 1px, var(--muted-2) 1px, transparent 1.5px);
  background-size: 4px 5px; background-position: 0 1px;
  pointer-events: none;
}
.rail-wallet:hover::before,
.rail-wallet:focus-within::before,
.rail-wallet.dragging::before,
.rail-wallet.sc-card-lift::before,
.sc-wallet-card:hover::before,
.sc-wallet-card:focus-within::before,
.sc-wallet-card.dragging::before,
/* IN HAND COUNTS AS HELD, on every list. A card lifted by the app carries
   .sc-card-lift and NOT .dragging — .dragging belongs to the browser's own drag,
   which the lift deliberately never starts — so the pane lists showed no handle on
   the one row that was actually being held. The rail's rows were the only ones that
   looked right, and only because their copy wears both. */
.sc-wallet-card.sc-card-lift::before { opacity: 1; }
/* A ROW IN HAND IS LIFTED, not faded. Fading reads as "disabled" — the row greying
   out under the pointer that just picked it up. On the hover surface with the app's
   pop shadow it reads as carried, which is what Sweep's sequence rows have always
   done and what the rest now do too. */
.rail-wallet.dragging,
.sc-wallet-card.dragging {
  opacity: 1; background: var(--surface-2); box-shadow: var(--shadow-pop);
}
/* AND IT IS THE ROW, not a picture of it. A rail row now leaves the flow and rides
   the pointer the way every other wallet list's row does (railLiftBegin), so it takes
   the same opaque ground: over the rest of the list a see-through row shows the
   wallets sliding about underneath the one in your hand. A selected row keeps its
   blue by resolving the SAME 10% wash against that ground rather than against
   transparency. Its hover actions are gone on their own — the row in hand is
   pointer-events:none, so nothing is hovering it. */
.rail-wallet.sc-card-lift { background: var(--surface-2); }
.rail-wallet.sc-card-lift.selected {
  background: color-mix(in srgb, var(--accent) 10%, var(--surface-2));
}
/* WHAT IS IN HAND HAS TO LEAVE THE RAIL — over the wallet list, over the page, over
   the box it is being dropped into. The rail clips its own overflow and sits in its
   own stacking context, so a group chip (which travels in the flow of its row) simply
   vanished at the rail's edge, and anything lifted out of the rail could only ever
   rise as high as the rail itself. Both are true only while something is in hand. */
/* ...and the rail is raised ONLY FOR ITS OWN. A lifted rail row is a child of the
   rail, so it can rise no higher than the rail does — the rail has to come with it.
   A card lifted out of a PANE is not, and raising the rail for that one put the left
   menu over the very thing the pointer was holding. Two flags, because they are two
   different gestures: is-rail-lifting is set only by railLiftBegin. */
body.is-chip-dragging #wallet-rail,
body.is-rail-lifting #wallet-rail { overflow: visible; z-index: var(--z-wallet-lift); }
/* NOT IN A DIALOG. The confirm sheet's list previews a run already decided and the
   picker's list is filtered, not ordered — neither row can be dragged anywhere, so
   neither offers a handle for it. */
.preflight-wallet-rows .sc-wallet-card::before { display: none; }

.picker-wallets > .sc-io-box { background: var(--background); }
.picker-wallets > .sc-io-box > .sc-io-cap,
.picker-wallets .act-wt-head { background: var(--surface); }
.preflight-wallet-rows > .sc-wallet-card:last-child { border-bottom: 0; }
.preflight-wallet-rows .wallet-more { display: none; }
.preflight-wallets .sc-list-clear,
.preflight-seq .sc-list-clear { pointer-events: none; }
.preflight-wallets .sc-list-clear-x,
.preflight-seq .sc-list-clear-x { display: none; }
.sc-wallet-card { position: relative; }
.rail-marquee {
  position: fixed; z-index: 10020; pointer-events: none;
  border: 1px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 20%, transparent);
}
.rail-drag-ghost {
  position: fixed; left: 0; top: 0; z-index: 10021; pointer-events: none;
  padding: 7px 10px; border: 1px solid var(--border-strong);
  background: var(--surface-2); color: var(--foreground);
  font-size: var(--cap-size); line-height: 1; box-shadow: var(--shadow-pop);
}
/* At the same breakpoint where the metric cards become 2×2, snap directly to
   the stacked touch layout — there is no intermediate tablet shell. The app
   becomes two work surfaces: a scrolling canvas above and a slightly taller
   wallet canvas below. */
@media (max-width: 1240px) {
  :root { --dash-box-h: auto; }
  .app,
  .app:has(> .wallet-rail.collapsed),
  .app:has(> .wallet-rail:not(.collapsed)) {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1.65fr) minmax(230px, 1fr);
  }
  .app-header {
    --ctx-ctl-h: 30px;
    grid-column: 1; grid-row: 1; height: 44px; padding: 0 8px; gap: 6px;
  }
  .app-nav { gap: 2px; }
  .app-nav .rail-btn { padding: 0 8px; }
  .app-header .rail-head-top { gap: 6px; }
  .app-header .ctx-actions { gap: 2px; }
  .app-header .ctx-prof { display: none; }
  .app-header .hdr-iconbtn { width: 30px; height: 30px; }
  .app-header #ctx-token { min-width: 0; flex: 1 1 auto; }
  .app-header #coin-select { width: 100%; min-width: 0; height: 30px; }

  .app-main {
    grid-column: 1; grid-row: 2; padding: 6px 6px 24px;
    min-height: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain;
  }
  .app .wallet-rail,
  .app .wallet-rail.collapsed {
    grid-column: 1; grid-row: 3; min-width: 0; min-height: 0;
    display: flex; border-width: 1px 0 0; z-index: 80;
  }
  .wallet-rail.collapsed .rail-head,
  .wallet-rail.collapsed .rail-search-row,
  .wallet-rail.collapsed #rail-groups,
  .wallet-rail.collapsed .rail-body,
  .wallet-rail.collapsed .rail-foot { display: flex; }
  .wallet-rail .rail-head,
  .wallet-rail #rail-toggle,
  .wallet-rail .rail-strip,
  .wallet-rail .rail-quickswap { display: none !important; }

  #rail-groups.rail-groups { padding: 5px 8px; gap: 0; }
  .rail-bar-top { min-height: 30px; gap: 5px; }
  .rail-bar-name { font-size: var(--cap-size); }
  .rail-bar-top .rail-btn { height: 28px; padding: 0 8px; font-size: var(--cap-size); }
  .rail-bar-chips { display: none; }
  .rail-search-row { min-height: 40px; height: 40px; padding: 0 8px; }
  .rail-search-row .rail-archived-btn span { display: none; }
  .rail-search-field { min-width: 0; }
  /* Balance columns move left to give the complete action cluster one shaded
     region. The rightmost eighth inside that region remains the touch-scroll lane. */
  .wallet-rail {
    --fig-col-act: 100px;
    --wallet-action-w: 100px;
  }
  .rail-bal-head, .rail-wallet {
    --rail-cols: minmax(0, 1.35fr) 72px 72px var(--fig-col-act);
    --rail-gap: 6px;
  }
  .act-pane:not([data-act="swap"]) {
    --act-wt-cols: minmax(0, 1.25fr) minmax(60px, .75fr) minmax(60px, .75fr) 96px;
    --wallet-action-w: 96px;
  }
  .act-pane[data-act="swap"] {
    --act-wt-cols: minmax(80px, 1.2fr) repeat(3, minmax(42px, .7fr)) 48px;
    --wallet-action-w: 48px;
  }
  .rail-bal-head { height: 28px; padding: 6px 0 6px 34px; }
  .rail-bal-col-act { text-align: center; }
  .act-wt-head { padding-left: 34px; padding-right: 0; column-gap: 8px; }
  .act-wt-c3 { text-align: center; }
  .act-pane .sc-dropzone .sc-wallet-card { column-gap: 8px; }
  .rail-wallet-actions {
    justify-self: center; justify-content: center; width: var(--fig-col-act); min-width: var(--fig-col-act);
  }
  .act-pane .sc-dropzone .sc-wallet-act {
    justify-self: center !important; justify-content: center !important;
  }
  .act-pane .sc-dropzone .sc-wallet-x { justify-self: center; }
  /* The rightmost eighth is the scroll lane. pan-y is allowed globally, then the
     pointer controller prevents it only when a gesture starts in the left 7/8. */
  .rail-wallet, .sc-wallet-card { touch-action: pan-y; }
  .rail-wallet, .act-pane .sc-dropzone .sc-wallet-card {
    min-height: 56px; padding-left: 34px; padding-right: 0;
  }
  .wallet-more { display: inline-flex; }
  .rail-wallet::before,
  .act-pane .sc-dropzone .sc-wallet-card::before { display: none; }
  .rail-wallet-acts { display: flex; }
  .rail-wallet-ago { display: none; }
  .rail-body { overscroll-behavior: contain; scrollbar-width: none; }
  .rail-body::-webkit-scrollbar { display: none; }
  .rail-foot { min-height: 30px; padding: 4px 10px; }
  /* A quiet continuous lane replaces the thick thumb. It is painted on every
     row so the right eighth remains visible while the table scrolls. */
  .rail-wallet,
  .sc-wallet-card,
  .rail-bal-head,
  .act-wt-head {
    --wallet-shade-start: calc(100% - max(12.5%, var(--wallet-action-w, 78px)));
    background-image: linear-gradient(to right,
      transparent 0 calc(var(--wallet-shade-start) - 1px),
      color-mix(in srgb, var(--border) 75%, transparent) calc(var(--wallet-shade-start) - 1px) var(--wallet-shade-start),
      color-mix(in srgb, var(--foreground) 4%, transparent) var(--wallet-shade-start) 100%);
  }
  .sc-dropzone { overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none; }
  .sc-dropzone::-webkit-scrollbar { display: none; }

  /* The top work surface uses one stacked action layout instead of desktop
     columns squeezed narrower. Inactive action panes leave layout entirely, so
     each tab owns its natural height and cannot overlap a taller sibling. */
  .act-tabbed { display: flex; flex-direction: column; }
  .act-pane { grid-area: auto; }
  .act-pane.is-off { display: none; }
  .act-cards { grid-template-columns: 1fr 1fr; }
  .act-card { --act-pad-l: 8px; --act-pad-r: 8px; }
  .act-cards > .act-card:nth-child(odd) { --act-pad-l: 0; }
  .act-cards > .act-card:nth-child(even) { --act-pad-r: 0; }
  .act-band { grid-template-columns: minmax(0, 1fr); min-height: 0; align-items: start; }
  .act-band-right { width: 100%; }
  .act-pane[data-act="transfer"] .act-band-right { margin-top: 0; }
  .act-box { --act-inset: 12px; padding: 12px; overflow: hidden; }
  .act-box-cfg { --cfg-pad: 12px; padding: 12px; }
  .act-box-wallets { padding: 0; }
  .act-row { gap: 8px; }
  .act-tab { padding-inline: 12px; }

  /* Every wallet input is one 220px module. Desktop pairs stretch to a shared
     neighbouring column; stacked mode has no neighbour, so Buyers/Sellers,
     Source/Destination, Swap Wallets and Activity Makers all keep one size. */
  /* (Activity's Makers is one of these .sw-list modules now — same 220px, same
     rules — rather than a 220px box of its own.) */
  .act-pane[data-act="activity"] .sw-wallets,
  .act-pane[data-act="sweep"] .sw-wallets,
  .act-pane[data-act="transfer"] .sw-wallets,
  .act-pane[data-act="swap"] .sw-wallets {
    height: auto; min-height: 0; flex: 0 0 auto;
  }
  .act-pane[data-act="activity"] .sc-io-row,
  .act-pane[data-act="sweep"] .sc-io-row,
  .act-pane[data-act="transfer"] .sc-io-row,
  .act-pane[data-act="swap"] .sc-io-row {
    flex: 0 0 auto; min-height: 0; flex-direction: column;
  }
  .act-pane[data-act="sweep"] .act-panel > .sc-io-row > .sw-sequence-box { width: 100%; }
  .act-pane[data-act="activity"] .sw-list,
  .act-pane[data-act="sweep"] .sw-list,
  .act-pane[data-act="transfer"] .sw-list,
  .act-pane[data-act="swap"] .sw-list {
    flex: 0 0 220px; height: 220px; min-height: 220px;
  }
  .act-pane[data-act="activity"] .sw-list > .sc-dropzone,
  .act-pane[data-act="sweep"] .sw-list > .sc-dropzone,
  .act-pane[data-act="transfer"] .sw-list > .sc-dropzone,
  .act-pane[data-act="swap"] .sw-list > .sc-dropzone {
    flex: 1 1 auto; min-height: 0;
  }
  .act-pane[data-act="activity"] .sw-list + .sw-list,
  .act-pane[data-act="sweep"] .sw-list + .sw-list,
  .act-pane[data-act="transfer"] .sw-list + .sw-list,
  .act-pane[data-act="swap"] .sw-list + .sw-list {
    border-left: 0; border-top: 1px solid var(--border);
  }
}

/* ---- the title row as a SWITCH (two-sided runs) --------------------------------
   UNLAYERED, and it has to be: .act-tab and .act-tab-ink are both declared in
   @layer overrides, which outranks @layer components at any specificity — a rule
   next to the rest of the .preflight-* block could not touch either of them.

   When a run has two sides — Sweep's buyers and sellers, Transfer's source and
   destination — the confirm sheet's wallet box puts one name-and-count per side in
   its header and marks the one you are reading with the app's own sliding underline.
   The strip IS .act-tabs, so the mark and its easing come from the tabs at the top of
   the page rather than from a second tab idiom invented for a dialog. What does not
   carry over is .act-tab's padding: 16px a side is the CARD's inset, and inside this
   header it would indent the first name off the edge every row in the box starts on.

   The strip also has to give up .act-tabs' `align-items: flex-end`. Up there the tabs
   sit ON the panel's top edge and their own height is all there is; in a 48px header
   it pinned a 20px tab to the bottom and left 28px of empty air above the name, so the
   label sat low while the copy control beside it stayed centred. Stretched, each tab
   fills the header and centres its own contents — the name lands where the single-list
   version's name lands, and the mark still rides the bottom edge. */
.preflight-wt-tabs { align-self: stretch; align-items: stretch; gap: 0; overflow: visible; }
.preflight-wt-tabs > .act-tab {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 0 12px; font-size: var(--label-size); font-weight: var(--label-weight);
}
/* THE FIRST TAB OWNS THE HEADER'S LEFT INSET, so its mark runs out to the box's own
   edge instead of stopping where the word starts. The mark is measured off the tab,
   so the only way to give it that 16px is to give the TAB that 16px: the strip is
   pulled out into the header's padding and the first tab puts the same amount back as
   its own, which leaves the name exactly where it was and moves nothing else. An
   underline that stops short of the edge reads as underlining a word; one that reaches
   it reads as marking a section, which is what these are. */
.preflight-wt-tabs { margin-left: calc(var(--pad-card) * -1); }
.preflight-wt-tabs > .act-tab:first-child { padding-left: var(--pad-card); }
/* The ink takes the SIDE's colour, set per selection from the JS — one element that
   both travels and recolours, so green → red reads as the same mark changing sides
   rather than two marks swapping places. Transfer's ends stay on the accent default. */
.preflight-wt-tabs > .act-tab-ink { bottom: 0; background: var(--act-ink, var(--accent)); }
/* `.sc-io-cap > .rail-bar-count` is what gives the badge its brighter fill, and it is
   a DIRECT-child rule — inside a tab the badge is a grandchild, so it needs saying
   again. The inactive side's badge stays muted, the same recede its label makes. */
.preflight-wt-tabs > .act-tab > .rail-bar-count {
  flex: 0 0 auto; background: var(--border-strong); color: var(--foreground);
}
.preflight-wt-tabs > .act-tab:not(.active) > .rail-bar-count { color: var(--muted); }
/* One list is shown at a time and the rest carry `hidden`, so the stack places
   nothing — it dissolves and lets the box treat the visible list as its own slot. */
.preflight-wt-stack { display: contents; }

/* SAME RUNG AS THE SEARCH FIELD BESIDE IT. Every dropdown in the app is a .cs-btn at
   the small control height, which is right in a settings column but 4px short next to
   the 32px field it shares a row with in the picker — and two controls of different
   heights side by side read as a mistake however small the difference.
   Out here, unlayered, because the rule it has to beat is: `.cs-btn` sets that height
   at the end of the dropdown block with no layer around it, and an unlayered
   declaration beats every layer at any specificity. */
.picker-controls .cs-btn { height: var(--control-h); }

/* THE STRIP THE OVERLAY BAR IS DRAWN IN, on the other three tables that scroll.
   Out here, unlayered, because every padding it has to beat is: each of these rows
   sets its inset with the `padding` shorthand from @layer components, and a longhand
   in an earlier layer loses to it however specific it is.

   The same strip, on three tables that scroll. Each names its own inset —
   these lists were built at different times and do not share one — and adds the bar's
   width to it. The heads reserve it too: the rail's and the log's sit INSIDE their
   scroller, the panes' sits above theirs, and a head that did not reserve would put
   every column heading a bar's width right of the figures under it. */
.rail-body .rail-wallet,
.rail-body .rail-bal-head { padding-right: calc(var(--chip-pad-x) + var(--sc-bar-w)); }
.dlog-list .dlog-head,
.dlog-list .dlog-row { padding-right: calc(20px + var(--sc-bar-w)); }
.act-pane .sc-dropzone .sc-wallet-card,
.act-pane .act-wt-head { padding-right: calc(var(--act-inset) + var(--sc-bar-w)); }
