*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0b0e14;--panel:#161b22;--panel2:#0d1117;--border:#21262d;--border2:#30363d;--text:#c9d1d9;--text-hi:#f0f6fc;--muted:#8b949e;--faint:#828c99;--green:#238636;--green-hi:#2ea043;--green-soft:#3fb950;--blue:#58a6ff;--red:#f85149;--primary:#2ea043;--primary-strong:#238636;--danger:#da3633;--danger-strong:#b62b28;--link:#58a6ff;--accent:var(--primary);--card:#13181f;--th-bg:#1a212b;--row-hover:#1b222d;--code-bg:#1b222d;--header-bg:rgba(13,17,23,.78);--input-border:#2a3140;--input-border-hover:#3d444d;--control-h:2.4rem;--control-h-sm:1.9rem;--control-h-icon:calc(var(--control-h) - .4rem);--control-h-icon-sm:calc(var(--control-h-sm) - .2rem);--sp-0:.15rem;--sp-1:.25rem;--sp-2:.5rem;--sp-3:.75rem;--sp-4:1rem;--sp-5:1.5rem;--sp-6:2rem;--body-bg:radial-gradient(1200px 600px at 50% -10%,#10151f 0%,#0b0e14 55%) fixed,#0b0e14}:root{--menu-radius:8px;--r:9px;--r-ctl:var(--r);--r-card:12px;--r-pill:999px}
:root[data-theme="light"]{--bg:#ffffff;--panel:#f6f8fa;--panel2:#ffffff;--border:#d0d7de;--border2:#d8dee4;--text:#1f2328;--text-hi:#0f1419;--muted:#4a535e;--faint:#59626d;--green:#1a7f37;--green-hi:#1f883d;--green-soft:#1a7f37;--blue:#0969da;--red:#cf222e;--primary:#1f883d;--primary-strong:#1a7f37;--danger:#cf222e;--danger-strong:#a40e26;--link:#0969da;--accent:var(--primary);--card:#ffffff;--th-bg:#f6f8fa;--row-hover:#f3f4f6;--code-bg:#eff1f3;--header-bg:rgba(255,255,255,.85);--input-border:#d0d7de;--input-border-hover:#afb8c1;--body-bg:#ffffff}
:root[data-theme="light"] body{color:var(--text)}
html{-webkit-text-size-adjust:100%;overflow-x:clip}
/* overflow-x:clip (not hidden) kills any stray sub-pixel horizontal overflow on
   mobile without creating a scroll container — so the sticky header keeps working. */
body{font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--body-bg);color:var(--text);line-height:1.55;min-height:100vh;display:flex;flex-direction:column;-webkit-font-smoothing:antialiased;letter-spacing:.01em;overflow-x:clip}
header{background:var(--header-bg);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-bottom:1px solid var(--border);padding:.85rem 1rem;position:sticky;top:0;z-index:30}
header a{color:var(--blue);text-decoration:none;font-weight:600;font-size:1.1rem}
main{flex:1;padding:2rem 1.1rem 3rem;padding-left:max(1.1rem,env(safe-area-inset-left));padding-right:max(1.1rem,env(safe-area-inset-right));max-width:56rem;margin:0 auto;width:100%}
footer{background:var(--panel);border-top:1px solid var(--border);padding:1.1rem;text-align:center;font-size:.78rem;color:var(--faint);letter-spacing:.03em}
h1{font-size:1.7rem;color:var(--text-hi);margin-bottom:1.25rem;font-weight:700;letter-spacing:-.02em}
h2{font-size:1.15rem;margin:1.75rem 0 .9rem;color:var(--text-hi);font-weight:650;letter-spacing:-.01em}
h3{color:var(--text-hi);font-weight:650;letter-spacing:-.01em}
p{margin:.5rem 0}
/* Restore list indentation the universal *{padding:0} reset strips — otherwise
   ol/ul markers (the 1. 2. 3. numbers / bullets) jam against the container edge
   and look cramped inside cards. */
ol,ul{margin:.5rem 0;padding-left:1.5rem}li{margin:.25rem 0}
.muted{color:var(--muted)}
table{border-collapse:separate;border-spacing:0;width:100%;margin:1rem 0;background:var(--panel);border:1px solid var(--border);overflow:hidden}
td,th{padding:.62rem .8rem;text-align:left;font-size:.86rem;border-bottom:1px solid var(--border)}
th{background:var(--th-bg);color:var(--text-hi);font-weight:600;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em}
tbody tr:last-child td{border-bottom:none}
tbody tr{transition:background .12s ease}
tbody tr:hover{background:var(--row-hover)}
code{background:var(--code-bg);border:1px solid var(--border);padding:.12rem .4rem;font-size:.82em;overflow-wrap:anywhere}
input,select,textarea{background:var(--panel2);color:var(--text-hi);border:1px solid var(--input-border);padding:.55rem .7rem;font-size:.95rem;width:100%;font-family:inherit;transition:border-color .12s ease,box-shadow .12s ease;-webkit-appearance:none;appearance:none}
input:not([type=checkbox]):not([type=radio]),select{min-height:var(--control-h)}
/* Funnel H, S1 — control height. There is NO blanket :where() floor here, and that
   is the slice's finding rather than an omission.
   A floor rule was tried TWICE and reverted twice. It only ever grows an element,
   which is safe for anything already taller and DEFORMING for anything shorter:
   a control with an explicit height below the floor keeps its width and gains
   height, so every hand-sized round button becomes an ellipse. Measured on the
   second attempt: 61 plug-local classes fleet-wide declare a fixed height under
   2.4rem with no min-height -- .card-del, .dx-burger, .cht-ind-color,
   .lv-facet-chip -- and the reported symptom both times was the annotate palette
   rendering as ovals.
   Pinning each exception does not scale: it requires knowing every fixed-size
   control in 68 plugs, and a new one is a silent regression.
   So a control OPTS IN to the house height by taking .btn (or .btn.sm), which is
   an explicit choice at the call site. The tokens below are what make that choice
   one edit rather than a literal. */
/* .field-row pairs a single-line input with button(s) on ONE baseline: the input
   flexes to fill, controls sit flush right, and because .btn and inputs now share
   --control-h they line up EXACTLY. Use it (or webui.FieldRow) for any input+button
   pair so the heights never drift again. */
.field-row{display:flex;gap:var(--sp-2,.5rem);align-items:stretch;flex-wrap:wrap}
.field-row>input,.field-row>select{flex:1 1 12rem;width:auto;margin:0}
/* AN INLINE CONTROL — a select or input that sits in a TOOLBAR beside buttons
   rather than in a form. The base rule above gives every control width:100%
   because the common case is a form field, and that assumption has now been
   re-discovered three times: the ListView pager select dropped onto its own line
   until .lv-size select{width:auto} put it back, and charts' style picker
   stretched to 1898px in a 1920 viewport ("strange looking rounded elements" —
   a full-page-width rounded box among 28px buttons). Naming it once means the
   fourth caller does not have to work it out from the symptom. */
.ctl-inline,.ctl-inline select,select.ctl-inline{width:auto;max-width:100%}
.field-row>.btn{flex:0 0 auto}
/* .action-bar — the COMPACT row of actions at the top of a detail screen: the "what
   can I do with this thing" strip, as opposed to .field-row's "one control and its
   button". Promoted from plug-case, which is the surface the owner pointed at as the
   one that gets this right (case.zm.is/case/<id>), so the house now ships what was
   being copied by eye.
   A <form>-wrapped control is display:contents, so its select and button sit in the
   row as siblings of the plain buttons rather than as one lump of unknown height —
   the reason a status control used to stack above its own submit. Controls are
   width:auto here for the same reason .ctl-inline exists: the base input,select rule
   is width:100%, which stretches a status picker across the page. */
/* .fixed-field — a value the surface that linked here already chose, shown rather than
   offered. Reads as a control's sibling (same height, same padding) without being one,
   because it is not editable and must not look editable. */
.fixed-field{min-height:var(--control-h);display:flex;align-items:center;padding:var(--sp-2,.5rem) var(--sp-3,.75rem);margin-top:var(--sp-1,.25rem);color:var(--text-hi,#f0f6fc);background:var(--panel2,#0d1117);border:1px dashed var(--border2,#30363d)}
.action-bar{display:flex;gap:var(--sp-2,.5rem);flex-wrap:wrap;align-items:center;margin:var(--sp-2,.5rem) 0}
.action-bar>form{display:contents}
.action-bar select,.action-bar input{width:auto;max-width:100%;margin:0}
/* .btn-group is a SET of controls presented as one group — the row of actions on a
   list row, the choices under a card, a form's trailing buttons. Use it (or
   webui.ButtonGroup) wherever two or more buttons stand together.
   It is a GRID, not a flex row, and that is the whole point. A wrapping flex row
   gives each control its natural width, so the moment the set does not fit it
   becomes a ragged stack — three buttons at 126.5 / 89.5 / 83.7px on one list row,
   which is what "buttons need to be the same size and height" keeps being reported
   about. auto-fit + minmax(min,1fr) makes every column the same width whether the
   set lands on one line or several, so the group reads as a group in both states,
   and stretch gives them one height without any floor rule (see the note above on
   why a blanket height floor cannot be used).
   --btn-group-min is the width below which a column will not shrink before wrapping;
   raise it locally for long labels. A control wrapped in its own <form> (the usual
   shape for a POST action) is the grid item, so the form stretches and the button
   fills it. */
.btn-group{display:grid;grid-template-columns:repeat(auto-fit,minmax(var(--btn-group-min,7.5rem),1fr));gap:var(--sp-2,.5rem);align-items:stretch}
.btn-group>*{min-width:0}
.btn-group>.btn{width:100%}
/* .btn is white-space:nowrap by design — a toolbar label should not break mid-word — but
   inside a group the column width is decided by the GROUP, so a label longer than the
   column has to give somewhere. Wrapping is the right trade here and the equal heights
   survive it (stretch); overflowing is not, and it is what "Edit capabilities" did,
   spilling 4px past its own border. Same call plug-steps' widget CSS already makes. */
.btn-group .btn{max-width:100%;white-space:normal}
.btn-group>form{display:flex;margin:0}
.btn-group>form>.btn{flex:1 1 auto;width:100%}
input[type=checkbox],input[type=radio]{-webkit-appearance:auto;appearance:auto;width:auto;height:auto;min-height:0;padding:0;margin:0;border:initial;border-radius:0;background:initial;box-shadow:none;accent-color:var(--green-hi);cursor:pointer;flex:none}
input:hover,select:hover,textarea:hover{border-color:var(--input-border-hover)}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--green-hi);box-shadow:0 0 0 3px rgba(46,160,67,.22)}
input::placeholder,textarea::placeholder{color:var(--faint)}
select{background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%238b949e' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");background-repeat:no-repeat;background-position:right .7rem center;padding-right:2rem}
label{display:block;font-size:.85rem;margin:1rem 0 .25rem;color:var(--muted);font-weight:500}
/* Inside the grid-managed form layouts (.form-grid / the shared .dose-form-grid) the
   grid gap is the SINGLE source of row spacing — so zero the generic label margin
   above, which would otherwise double-count with the gap and inflate the line
   spacing. Root fix for the exp/dose + exp/ceremonies bigger-spacing drift vs taper's
   tight /dose/new (taper's page CSS already lacks the label margin; this makes every
   grid form match it regardless of the page's generic label rule). */
.form-grid>label,.dose-form-grid>label,.form-grid label,.dose-form-grid label{margin-top:0;margin-bottom:0}
/* Feature-form fields wrap the label text + control in ONE <label>; give the
   label text its own line + a gap so it never crowds the input below it. */
.form-grid label:not(.checkbox)>span{display:block;margin-bottom:.4rem}
button{display:inline-block;background:var(--panel2);color:var(--text-hi);cursor:pointer;font-weight:500;border:1px solid var(--border2);padding:.55rem 1rem;font-size:.88rem;font-family:inherit;transition:filter .12s ease,transform .08s ease,box-shadow .12s ease;box-shadow:none}
/* Toast UI editor toolbar buttons draw their icon as a background-image sprite;
   the generic :hover repaint below uses the background SHORTHAND, which would wipe
   the sprite and make the icon vanish on hover — so exempt them. The speed-dial
   buttons (.fb-launch/.fb-spoke/.fb-prod) own their colour and must NOT get this
   generic repaint — otherwise a hover/tap flips them to var(--panel) (looks like
   they "change colour when clicked"), so exempt them too. Keep the exclusions in
   :where(), whose zero specificity lets component rules such as .btn.danger:hover
   override this generic baseline. Chained :not() selectors made the baseline more
   specific than the components and caused white-on-white danger buttons. */
button:hover:not(:where([class*="toastui-"],.fb-launch,.fb-spoke,.fb-prod)){filter:none;border-color:var(--text-hi);background:var(--panel)}
button:active{transform:translateY(1px)}
button:focus-visible{outline:2px solid var(--green-hi);outline-offset:2px}
button.secondary,button.ghost{background:var(--border,#21262d);color:var(--text);border:1px solid var(--border2);box-shadow:none}
/* The hover was the literal #2d333b, which is a DARK-THEME grey with no light-theme
   counterpart: on a light page the control went from var(--border) #d0d7de to near
   black under the cursor, carrying var(--text) #1f2328 text — about 1.15:1, which is
   text you cannot read on a control you are pointing at.
   --border2 is the token that means "one step up from --border" in BOTH themes, which
   is what the literal was approximating: #30363d in dark (against the #2d333b it
   replaces — imperceptible) and #d8dee4 in light, where the hover finally reads as a
   lift rather than an inversion. */
button.secondary:hover,button.ghost:hover{filter:none;background:var(--border2);box-shadow:none}
button.danger,button.delete,button.del,.btn-danger{background:var(--danger,#da3633);color:#fff;border:1px solid rgba(0,0,0,.18)}
button.danger:hover,button.delete:hover,button.del:hover,.btn-danger:hover{filter:none;background:#f85149;box-shadow:0 3px 10px rgba(218,54,51,.3)}
/* Compact inline delete (dense table rows): a bare red trash/✕ glyph, no box. */
button.del-icon,.del-icon{background:none;border:none;color:var(--red,#f85149);box-shadow:none;cursor:pointer;font-weight:600;font-size:1rem;padding:.1rem .4rem;line-height:1}
button.del-icon:hover,.del-icon:hover{filter:none;background:none;color:#ff7b72;box-shadow:none}
/* ---- Unified .btn component system (the canonical way to render a button; see
   webui.Button in components.go). .btn is role-neutral; add .primary/.secondary/
   .danger for the role colour and .sm for the compact size. Works on both <button>
   and <a> so a link styled as a button matches a real button exactly. The bare
   <button> rules above remain (green by default) for un-classed buttons — .btn
   resets to the role colours so a `.btn`/`.btn.primary` element is unaffected by
   them. */
/* Sleek, flat, SHARP-edged button system (no rounded corners, no gradients) — a
   minimal/futuristic outline style: role colour sits in the border + text at rest,
   and fills with a soft glow on hover. Centrally managed here so a single change
   restyles every plug. Three semantic categories render via webui.IconButton /
   TextButton / IconTextButton; .btn-icon is the square icon-only variant. */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-2,.5rem);min-height:var(--control-h);padding:.4rem .9rem;font-weight:500;font-size:.84rem;letter-spacing:.03em;font-family:inherit;line-height:1;white-space:nowrap;border:1px solid var(--border2);background:var(--panel2);color:var(--text-hi);cursor:pointer;text-decoration:none;transition:background .13s ease,border-color .13s ease,box-shadow .13s ease,color .13s ease}
.btn:hover{text-decoration:none;border-color:var(--text-hi);background:var(--panel)}
.btn:focus-visible{outline:1px solid var(--primary);outline-offset:2px}
.btn.primary{background:transparent;color:var(--primary);border-color:var(--primary)}
.btn.primary:hover{background:var(--primary);color:#06210f;box-shadow:0 0 0 1px var(--primary),0 0 14px -3px var(--primary)}
.btn.secondary{background:transparent;color:var(--muted);border-color:var(--border2)}
.btn.secondary:hover{background:var(--panel);color:var(--text-hi);border-color:var(--text-hi)}
.btn.danger{background:transparent;color:var(--red);border-color:var(--danger)}
.btn.danger:hover{background:var(--danger);color:#fff;box-shadow:0 0 14px -3px var(--danger)}
/* .action-bar makes its CONTROLS compact — the caller cannot get the size wrong,
   because the container decides it. ONE declaration, listing the button AND the field:
   they are the same size because they are literally the same rule, not because two
   rules agree today.
   Owner review: "buttons need to be the same size as field". min-height alone was not
   enough — measured, the select came out 37.6px against the buttons' 30.4px, because
   its own padding (8.8px) and font-size (15.2px) set a natural height above the floor.
   A control shares a button's BOX or it does not match it.
   The input selector repeats the :not()s from FormControlsCSS deliberately: that rule
   is (0,2,1) and would otherwise keep the full-height floor here. */
.btn.sm,.action-bar .btn,.action-bar select,.action-bar input:not([type=checkbox]):not([type=radio]){min-height:var(--control-h-sm);padding:.28rem .6rem;font-size:.78rem}
.btn-icon{padding:.5rem;width:var(--control-h-icon);height:var(--control-h-icon);min-height:var(--control-h-icon);justify-content:center}
.btn-icon.sm{padding:.32rem;width:var(--control-h-icon-sm);height:var(--control-h-icon-sm);min-height:var(--control-h-icon-sm)}
.btn-icon>i{margin:0}
/* ---- DISABLED: the one control state the mesh never drew.
   Measured before this rule existed: a disabled button rendered PIXEL-IDENTICAL to an
   enabled one, cursor:pointer included, and repainted on hover like a live control. The
   only way to find out it was disabled was to press it and watch nothing happen. That
   is an affordance bug, not a missing nicety, and it was fleet-wide — three components
   had each invented a local opacity (.fb-btn .55, .fb-an-tool .4, .tgl .45) precisely
   because there was nothing to inherit.
   It quiets the SURFACE and dims the LABEL rather than putting opacity on the element.
   That is .lv-page.lv-disabled's lesson, already written down two files away: opacity
   fades the border and the background too, by different visible amounts, so a control
   whose whole point is to read as one box reads as three pieces.
   var(--panel) is deliberate over "keep whatever background the variant had": a
   disabled .primary must stop being green, and resolving per-variant would need one
   rule per variant that all say the same thing.
   The :hover twin is REQUIRED, not decorative. The generic button:hover baseline is
   (0,2,0) and .btn.danger:hover is (0,3,0), so without a (0,3,0) rule placed after
   them a disabled control still lights up under the cursor — which is the exact "this
   is pressable" signal being removed. aria-disabled is carried alongside :disabled
   because an <a class="btn"> cannot be :disabled at all, and a link styled as a button
   is the shape this system exists to make identical. */
button:disabled,.btn:disabled,button[aria-disabled="true"],.btn[aria-disabled="true"],
button:disabled:hover,.btn:disabled:hover,button[aria-disabled="true"]:hover,.btn[aria-disabled="true"]:hover{
  background:var(--panel);color:var(--faint);border-color:var(--border);
  cursor:default;box-shadow:none;filter:none;text-decoration:none}
/* Sliding on/off switch (webui.ToggleSwitch) — the canonical binary-setting
   control; spliced here like the button system so it works on every page. */
.tgl{display:inline-flex;align-items:center;gap:var(--sp-2,.5rem);cursor:pointer;user-select:none;font-size:.85rem;color:var(--text,#c9d1d9);margin:0}
.tgl input{position:absolute;opacity:0;width:0;height:0;margin:0;pointer-events:none}
.tgl-track{position:relative;width:38px;height:22px;background:var(--panel2,#0d1117);border:1px solid var(--border2,#30363d);transition:background .15s,border-color .15s;flex:none}
.tgl-track::before{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;background:var(--muted,#8b949e);transition:transform .15s,background .15s}
.tgl input:checked+.tgl-track{background:var(--panel,#161b22);border-color:var(--primary,#2ea043)}
.tgl input:checked+.tgl-track::before{transform:translateX(16px);background:var(--primary,#2ea043)}
.tgl input:focus-visible+.tgl-track{outline:1px solid var(--primary,#2ea043);outline-offset:2px}
.tgl input:disabled~.tgl-track,.tgl input:disabled~.tgl-lbl{opacity:.45;cursor:not-allowed}
.tgl input:checked~.tgl-lbl{color:var(--text-hi,#f0f6fc)}
.tgl-lbl{line-height:1.35}
/* Dark/light theme toggle button (webui.ThemeToggleHTML) in the shared header. */
.theme-toggle{display:none!important}
/* Header utility slot + shared header-button look (flowweb Page.HeaderUtil). */
.hdr-util{display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);flex:none}
.hdr-util:empty{display:none}
.hdr-btn{background:transparent;border:1px solid var(--border2);color:var(--muted);width:var(--control-h-icon);height:var(--control-h-icon);min-height:var(--control-h-icon);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;font-size:.9rem;flex:none;text-decoration:none;transition:color .12s ease,border-color .12s ease,background .12s ease}
.hdr-btn:hover{color:var(--text-hi);border-color:var(--text-hi);background:var(--panel);text-decoration:none}
.hdr-btn.active{color:var(--primary);border-color:var(--primary)}
.hdr-btn>i{margin:0}
.page-sub{color:var(--muted);font-size:.92rem;margin:-.75rem 0 1.25rem}
a{color:var(--link);text-decoration:none}a:hover{text-decoration:underline}
.ok{color:var(--green-soft)}.bad{color:var(--red)}.warn{color:#d2991d}
.form-grid{display:flex;flex-direction:column;gap:var(--sp-4,1rem)}
.card{border:1px solid var(--border);padding:1.25rem;margin:1.1rem 0;background:var(--card);box-shadow:0 1px 3px rgba(0,0,0,.3)}
.card h3:first-child,.card h2:first-child{margin-top:0}
/* Density + flow modifiers on the shared surface.
   MEASURED 2026-08-06: ~700 hand-rolled border+background+padding surface
   declarations across the fleet, under ~370
   one-off class names. Sampling them showed the overwhelming majority differ
   from .card in NOTHING but padding and margin — plug-fisk alone had eight, at
   five different paddings. These three modifiers absorb that variation, so a
   bespoke rule collapses to class="card tight" and the local CSS is deleted
   rather than overridden.
   Specificity is deliberate: .card.tight is 0,2,0, so it beats both the base
   .card and the 480px .card padding:1rem mobile rule (both 0,1,0) — a tight
   surface stays tight on a phone, which is the point of asking for it. */
.card.tight{padding:.55rem .7rem;margin:.5rem 0}
.card.snug{padding:.9rem 1rem;margin:.75rem 0}
/* Flow, not density: for a card that is already inside a spaced grid or panel
   and must not add its own outer margin. */
.card.flush{margin:0}
/* A surface that groups content without claiming visual lift. */
.card.plain{box-shadow:none}
.empty{padding:2.5rem 1.5rem;text-align:center;color:var(--muted);border:1px dashed var(--border2);background:var(--panel2);margin:1.25rem 0}
/* Inline status badge. vertical-align:middle + a fixed line-height keep it centred
   on a cell's text baseline and stop its inline-block box from growing the row's
   height, so a badge cell lines up with a plain-text cell. Spacing between STACKED
   badges is owned by the .badges group wrapper (a flex gap) — the pill itself
   carries NO margin, so 1 badge and N badges have identical cell padding.
   white-space:nowrap because a badge is ONE atomic token: it may wrap to the next
   line as a unit, but it must never break INSIDE. Without it a multi-word label
   ("Not started", "Pushed back", "from Ideas") splits across two lines and reads as
   two separate badges — the same defect, and the same fix, as .lv-facet-chip and
   .lv-num. Reported on plan.zm.is, where "💡 from Ideas" broke after "from". */
.pill{display:inline-block;vertical-align:middle;line-height:1.4;padding:.1rem .6rem;font-size:.72rem;font-weight:600;border:1px solid var(--border2);color:var(--text);margin:0;white-space:nowrap}
/* Canonical status-badge variants — border AND text share ONE colour so every badge
   reads as a coloured outline (the house look, matching fisk/alpha). Centralised HERE
   so plugs stop re-defining these locally (some getting the TEXT colour wrong → black);
   a plug may still add a bespoke variant. Semantic synonyms map to one hue. */
.pill.up,.pill.long,.pill.ok,.pill.success,.pill.good,.pill.protected,.pill.live,.pill.active,.pill.opened,.pill.filled{border-color:#3fb950;color:#3fb950}
.pill.down,.pill.short,.pill.bad,.pill.danger,.pill.error,.pill.unprotected,.pill.rejected,.pill.closed,.pill.failed{border-color:#f85149;color:#f85149}
.pill.info,.pill.open,.pill.pending,.pill.new,.pill.queued{border-color:#58a6ff;color:#58a6ff}
.pill.accent,.pill.best,.pill.special{border-color:#a371f7;color:#a371f7}
.pill.warn,.pill.warning,.pill.paused{border-color:#d29922;color:#d29922}
.pill.muted,.pill.neutral,.pill.off,.pill.done{border-color:var(--border2);color:var(--muted)}
/* Chat contact/conversation NAME cell (webui.ContactNameCell + ChatKindBadge): the
   name is always-visible primary text, the number a muted handle, and a group gets
   an amber fa-users badge (monolith-patchbay look). Shared so signal/whatsapp
   connector pages, storm outreach and mc bridge all render contacts identically. */
.contact-nm{color:var(--text-hi,#f0f6fc)}
.contact-num{color:var(--muted,#8b949e);font-size:.82rem;margin-left:.45rem}
.chat-kind{font-size:.66rem;letter-spacing:.04em;text-transform:uppercase;white-space:nowrap;margin-left:.5rem;color:var(--muted)}
.chat-kind i{margin-right:.2rem}
.chat-kind-group{color:#d29922}
/* .chip is the smaller sibling badge some plugs use (objective-race / flush cells).
   Normalized here to the SAME no-margin, vertically-centred model as .pill so a
   stack of chips no longer drifts the cell padding by its old per-chip margin.
   Plugs keep their own colour variants; this only fixes spacing/alignment.
   nowrap for the same reason as .pill: a badge is one atomic token and must never
   break inside, however narrow the cell it lands in. */
.chip{display:inline-block;vertical-align:middle;line-height:1.4;margin:0;white-space:nowrap}
/* Shared multi-badge cell wrapper (webui.Badges): an inline-flex group that spaces
   its badge children with a UNIFORM gap (not per-badge margins) and centres them
   vertically. Because the gap lives on the wrapper (not the badges) and the wrapper
   adds no padding of its own, a cell with N badges has the exact same effective
   padding + vertical rhythm as a cell with 1 badge or plain text — which is what
   keeps the board Signals / Actions / flush Outcome columns aligned row-to-row. */
.badges{display:inline-flex;align-items:center;flex-wrap:wrap;gap:var(--sp-1,.25rem);vertical-align:middle}
.badges>*{margin:0}
/* THE badge (webui.Badge): a small muted marker that labels an item — where it came
   from, what kind it is. Distinct from .pill, which is a filter CHIP and stays fully
   round; a badge takes the house card radius so it matches the listview it sits in.
   Promoted verbatim from plug-ideas' .src-badge, which is the rendering the owner
   pointed at ("these are our badges, use this element"): several plugs had each
   hand-rolled their own inline-styled span, so they matched no role selector and
   picked up neither the radius nor the never-break-the-icon-from-the-label rule.
   display / border-radius / white-space come from the ROLE rules below — do not
   restate them here or the two definitions will drift. */
.badge{font-size:.72rem;padding:.05rem .4rem;border:1px solid var(--border2);color:var(--muted);text-decoration:none;white-space:nowrap}
a.badge:hover{color:var(--text-hi);border-color:var(--border2)}
/* .badge.caps is the SHOUTED variant — the "ALPHA" / "ADD-ON" / "SIGN UP" markers that
   flag what a thing is rather than where it came from. It was hand-rolled identically
   in three plugs (docs's card corner pill, hub's alpha and add-on badges, habits' demo
   teaser), and docs' own comment said hub's was "styled to match" it — three copies of
   one intent, which is how they drifted to three different font sizes. Colour stays at
   the call site: dashed red means alpha, solid amber means add-on, and that meaning is
   the caller's, not the component's. */
.badge.caps{font-size:.62rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;background:var(--panel2)}
pre{background:var(--panel2);border:1px solid var(--border2);padding:1rem;overflow-x:auto;white-space:pre-wrap}
.modal{display:none;position:fixed;z-index:50;left:0;top:0;width:100%;height:100%;background:rgba(1,4,9,.7);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);padding:1rem}
.modal.active{display:flex;align-items:center;justify-content:center;animation:modal-fade .18s ease}
@keyframes modal-fade{from{opacity:0}to{opacity:1}}
.modal-content{background:var(--card);border:1px solid var(--border2);padding:1.6rem;max-width:32rem;width:100%;max-height:88vh;overflow-y:auto;box-shadow:0 24px 60px rgba(0,0,0,.55);animation:modal-pop .2s cubic-bezier(.4,0,.2,1)}
@keyframes modal-pop{from{opacity:0;transform:translateY(12px) scale(.98)}to{opacity:1;transform:none}}
.modal-content h3{color:var(--text-hi);margin-bottom:1rem;font-size:1.2rem}
.modal-close{float:right;background:none;border:none;color:var(--muted);font-size:1.4rem;cursor:pointer;padding:0;line-height:1;box-shadow:none;transition:color .12s ease}
.modal-close:hover{color:var(--text-hi);filter:none}
@media(max-width:640px){table{font-size:.8rem}td,th{padding:.45rem .55rem;font-size:.78rem}
table:not(.tbl-stack){display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}
table.tbl-stack,.tbl-stack thead,.tbl-stack tbody,.tbl-stack th,.tbl-stack td,.tbl-stack tr{display:block}
table.tbl-stack{border:none;background:none;overflow:visible}
.tbl-stack thead{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
.tbl-stack tbody tr{margin:0 0 .75rem;border:1px solid var(--border);background:var(--card);padding:.5rem .25rem;box-shadow:0 1px 3px rgba(0,0,0,.3)}
.tbl-stack tbody tr:hover{background:var(--row-hover)}
/* flex-wrap and min-width:0 are what make display:flex safe here. The rule was
   written for the two-item shape a stacked cell usually has — the data-label
   pseudo-element and one value — where nowrap is harmless. A cell holding SEVERAL
   inline items (a markdown table listing five code spans, say) turns into that many
   flex items on one unwrappable line: measured at 946px inside a 299px cell on a
   390px viewport, which then pushed the whole page to 985px. Since html and body are
   overflow-x:clip, the excess was not scrollable — it was simply unreachable, which
   is what "spillover" looked like on a plan. Flex items also default to min-width:auto
   and so refuse to shrink below their content, which is why wrapping alone is not
   enough for a long unbreakable token. */
.tbl-stack td{border:none;border-bottom:1px solid var(--border);display:flex;flex-wrap:wrap;min-width:0;justify-content:space-between;align-items:center;gap:var(--sp-4,1rem);padding:.5rem .85rem;text-align:right}
.tbl-stack td>*{min-width:0;max-width:100%}
.tbl-stack tbody tr td:last-child{border-bottom:none}
.tbl-stack td:before{content:attr(data-label);color:var(--muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.05em;font-weight:600;text-align:left}
.tbl-stack td:empty{display:none}
.tbl-stack td:not([data-label]),.tbl-stack td[data-label=""]{justify-content:flex-start;text-align:left}
.tbl-stack td:not([data-label]):before,.tbl-stack td[data-label=""]:before{content:none}
/* action cells (no header label) keep their buttons on ONE left-aligned row.
   .lv-actions-wrap is the ListView merged-actions wrapper (listview.go): on mobile the
   stacked action cell reads left-to-right like the other house action rows. */
.tbl-stack td[data-label=""] .acts,.tbl-stack td[data-label=""] .al-acts,.tbl-stack td[data-label=""] .fk-acts,.tbl-stack td[data-label=""] .lv-actions-wrap{flex-wrap:nowrap;justify-content:flex-start}}
/* DYNAMIC card collapse (NOT a mobile breakpoint): fitCards() in listViewJS adds
   .lv-cards to a .tbl-stack table whose row content would overflow its container —
   at ANY viewport width — so a wide table becomes cards instead of horizontal
   scroll. Mirrors the <=640px rules above; the media query stays as the no-JS /
   mobile fast path (no flash-of-wide-table before JS measures). .lv-cards is only
   ever set on .tbl-stack tables, so the bare thead/tbody/td selectors are safe. */
.lv-cards,.lv-cards thead,.lv-cards tbody,.lv-cards th,.lv-cards td,.lv-cards tr{display:block}
.lv-cards{border:none;background:none;overflow:visible}
.lv-cards thead{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
.lv-cards tbody tr{margin:0 0 .75rem;border:1px solid var(--border);background:var(--card);padding:.5rem .25rem;box-shadow:0 1px 3px rgba(0,0,0,.3)}
.lv-cards tbody tr:hover{background:var(--row-hover)}
.lv-cards td{border:none;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:var(--sp-4,1rem);padding:.5rem .85rem;text-align:right}
.lv-cards tbody tr td:last-child{border-bottom:none}
.lv-cards td:before{content:attr(data-label);color:var(--muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.05em;font-weight:600;text-align:left}
.lv-cards td:empty{display:none}
.lv-cards td:not([data-label]),.lv-cards td[data-label=""]{justify-content:flex-start;text-align:left}
.lv-cards td:not([data-label]):before,.lv-cards td[data-label=""]:before{content:none}
.lv-cards td[data-label=""] .acts,.lv-cards td[data-label=""] .al-acts,.lv-cards td[data-label=""] .fk-acts{flex-wrap:nowrap;justify-content:flex-start}
@media(max-width:480px){main{padding:1.25rem 1rem 2.5rem;padding-left:max(1rem,env(safe-area-inset-left));padding-right:max(1rem,env(safe-area-inset-right))}h1{font-size:1.45rem}.nav{gap:var(--sp-2,.5rem)}.form-grid{grid-template-columns:1fr!important}.card{padding:1rem}.modal-content{padding:1.2rem}}

/* ── CORNERS: the single definition of radius for the whole mesh ─────────────
   Adopted 2026-08-05 after the zm.is trial. Owner: "we have the same corner
   radius for all", "fewer custom hand rolled things, faster loading pages and
   greater consistency".

   Matched by ROLE, not by an enumerated list of class names. A hand-rolled card is
   still called something-card, a hand-rolled button still contains btn, and a field
   is still an input/select/textarea — so a bespoke component inherits the house
   radius without anyone writing a rule for it. Enumerating class names is what
   produced 74 zero-radius declarations in this package alone; they are now deleted,
   and this is the only place a radius is decided.

   :where() holds specificity at ZERO deliberately. A component that genuinely must
   be square overrides with one ordinary declaration — but it also means any stale
   zero-radius left behind still wins, which is why they had to be removed rather
   than edited. If something renders square unexpectedly, DELETE the leftover
   override; do not add a radius beside it.

   Checkbox and radio are excluded: they set appearance:auto, and a radius on a
   native control fights the platform widget. */
:where(button,[class*="btn"],input:not([type=checkbox]):not([type=radio]),
  select,textarea,pre,code,[class*="chip"],summary){border-radius:var(--r-ctl)}
/* "-row" is here for the same reason the two tbody-tr selectors are: a hoverable
   LIST ROW is a surface, and it only looks like one while hovered — which is
   exactly when a square corner next to round controls reads as a mistake. It was
   already named twice for the two ListView variants; the pattern generalises it so
   a hand-rolled row inherits like a hand-rolled card does. Checked against the real
   class names rather than guessed: field-row, xfn-row, ts-row, al-row, cat-row,
   plan-row, ds-row, planned-row … the ones with no background and no full border
   (field-row, lv-rowdata) take the radius as a no-op, so there is nothing to
   regress. 856 of these were the last square things on mcp.zm.is/capabilities. */
/* A CALLOUT is a surface too: a bordered block with a background that says
   something about the content next to it. There is no shared component for one —
   every plug writes its own — so the role has to catch it by name, and the naming
   turns out to be remarkably consistent across 103 distinct class names in the
   fleet: note / notice / callout / banner / warn / alert. The ones that are plain
   text rather than a block (.hint, .q-note) have no background and no border, so
   the radius is a no-op on them. "notice" needs its own pattern — it does not
   contain "note".
   A bordered <details> is a disclosure PANEL, matched by tag because it is
   routinely written with no class at all (tooleditor's permissions preview, which
   is what showed this gap on mcp.zm.is/capabilities). Its <summary> already takes
   the control radius above. */
:where(.card,[class*="-card"],[class*="card-"],table,.empty,.modal-content,.nav-tabs,
  [class*="stat"],details,
  [class*="note"],[class*="notice"],[class*="callout"],[class*="banner"],[class*="warn"],[class*="alert"],
  [class*="panel"],[class*="tile"],[class*="-row"],.tbl-stack tbody tr,.lv-cards tbody tr){border-radius:var(--r-card)}
/* Pills (filter chips) stay fully round. BADGES take the house radius so they match
   the listview they sit inside — owner's experiment this pass; revert by moving
   [class*="badge"] back into the pill rule above. */
:where(.pill,[class*="pill"]){border-radius:var(--r-pill)}
:where([class*="badge"]){border-radius:var(--r)}
/* A badge is one token: it must NEVER wrap between its icon and its label. Owner:
   "they should never break up the icon and the text". Measured on ideas: the
   src-badge "retrospective" rendered 96x39 across two lines with the emoji stranded
   on the first. Role-matched like the radius, so every badge in the mesh inherits it
   and no plug has to remember. inline-flex (not just nowrap) also keeps the glyph and
   text on a shared baseline when the badge sits in a wrapping row. */
:where(.pill,[class*="pill"],[class*="badge"]){display:inline-flex;align-items:center;gap:.3em;white-space:nowrap}
/* Icon-only controls are SQUARE IN SHAPE — equal sides, via the shared component's
   width/height — and then take the house radius like everything else. Owner: "the
   modals should be square with rounded corners, so the lengths of their sides are
   equal". I first read "make the modal icons square" as square-CORNERED and set
   border-radius:0 here; the point was the opposite. A 35x30 rounded rectangle looks
   wrong ("small rectangles dont look great with curved edges"), so the fix is equal
   sides, not sharp corners. webui.IconButton already sizes them 2rem x 2rem. */
/* Menus and popovers take the dedicated --menu-radius the drop shapes already use
   (MenuCSS, .sg-dd), so every floating surface agrees. menu-pop is the role class;
   [class*="menu"] alone would also catch table cells like .lv-colmenu-col. */
:where([class*="menu-pop"],[class*="dropdown"],[class*="popover"]){border-radius:var(--menu-radius,6px)}
/* Floating labels/tooltips are chips, not panels — they take the control radius.
   lbl/tip are the naming already in use (flowweb's .fb-slbl speed-dial label).
   NOT "-lbl": fb-slbl contains no such substring, which is exactly the trap of
   guessing a pattern instead of checking it against the real class. */
:where([class*="tooltip"],[class*="lbl"],[class*="-tip"]){border-radius:var(--r-ctl)}

.nav{display:flex;gap:var(--sp-2,.5rem);margin:1.5rem 0;flex-wrap:wrap}
.nav .btn{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--sp-2,.5rem);flex:1 1 5rem;min-width:4.6rem;background:var(--card,#13181f);color:var(--text,#c9d1d9);font-weight:600;border:1px solid var(--border2,#30363d);padding:.85rem .5rem;font-size:.74rem;text-align:center;text-decoration:none;transition:background .13s ease,border-color .13s ease,color .13s ease}
.nav .btn .ico{font-size:1.55rem;line-height:1}
.nav .btn:hover{background:var(--panel,#1c2230);border-color:var(--primary,#3fb950);color:var(--text-hi,#fff);text-decoration:none}
.nav .btn:active{background:var(--panel2,#10151c)}
.nav .btn:focus-visible{outline:2px solid #3fb950;outline-offset:2px}
.nav .btn.primary{background:var(--primary);border-color:rgba(0,0,0,.18);color:#06210f}
.nav .btn.primary:hover{filter:brightness(1.08);background:var(--primary);border-color:rgba(0,0,0,.18)}

/* ── toolbar ──────────────────────────────────────────────────────────────
   The action buttons form a FLUSH, equal-size grid: cols across, dropping to 2
   across when narrow. Every button fills its cell so they're all identically
   sized and aligned (ActionGrid renders .dash-actions; graphs' JS builds the
   same markup). */
.dash-toolbar { display: flex; flex-direction: column; gap: .6rem; margin: .2rem 0 1.1rem; }
.dash-titlebar { display: flex; align-items: center; gap: .5rem; }
.dash-actions { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .5rem; margin: .4rem 0 .5rem; }
.dash-actions.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 760px) { .dash-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.dash-actions .btn { width: 100%; min-width: 0; }
.dash-toolbar .dash-h { color: var(--text-hi); font-size: 1.05rem; }

/* Shared flat/square/outline button system (graphs' toolbar grammar, matches
   webui .btn): neutral by default, green outline for the import/primary action,
   fill+glow on hover. Both boards load this on their board page so their toolbars
   render identically. Buttons always carry a variant class alongside .btn. */
/* NO height and NO gap of its own — both were removed 2026-08-17, and the reasoning
   matters more than the values.
   The house expresses a control's size as a min-height FLOOR; this rule expressed it
   as a fixed "height: 36px". Measured on charts.zm.is, that combination does the
   opposite of what it looks like:
     .btn       38.4px — the house floor is HIGHER, so the 36px never applied at all
     .btn.sm    36px   — the floor is 30.4, so this INFLATED the compact button to
                         within 2px of a standard one
     .btn-icon  36px   — the floor is 32, so the square icon button was 4px too big
                         and 4px too wide
   So it was not sizing the button it named, and it was silently resizing two variants
   it never meant to touch. "gap: .45rem" was the same shape in miniature: 7.2px here
   against 8px everywhere else, for no stated reason.
   Scoping the override to boardkit's own variants would not have preserved 36px
   either — min-height floors those too — so "scope it" and "remove it" render
   identically, and one of them is a rule.
   The self-sufficiency argument does not survive contact either: this rule already
   reads --border2/--panel2/--text-hi, which only the house sheet defines, and all four
   consumers add boardkit.CSS() TO webui.AppShellCSS. It was never standalone, so
   there is nothing for a local height to protect. */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2, .5rem); padding: 0 .9rem; font-weight: 500; font-size: .84rem; letter-spacing: .03em; line-height: 1; border: 1px solid var(--border2); background: var(--panel2); color: var(--text-hi); cursor: pointer; text-decoration: none; box-sizing: border-box; white-space: nowrap; transition: background .13s ease, border-color .13s ease, box-shadow .13s ease, color .13s ease; }
.btn:hover { border-color: var(--text-hi); background: var(--panel); }
.btn i { font-size: .85em; }
.btn-import { background: transparent; color: #2ea043; border-color: #2ea043; }
.btn-import:hover { background: #2ea043; color: #06210f; box-shadow: 0 0 14px -3px #2ea043; }
.btn-add { background: transparent; color: var(--text-hi); border-color: var(--border2); }
.btn-add:hover { border-color: var(--text-hi); background: var(--panel); }
.btn-ghost { background: transparent; color: var(--muted); border: 1px solid var(--border2); }
.btn-ghost:hover { background: var(--panel); color: var(--text-hi); border-color: var(--text-hi); }
.btn-pin.is-on { color: var(--link); border-color: var(--link); background: rgba(31,111,235,.10); }
.btn-pin i { transform: rotate(30deg); }

/* ── grid + panels ───────────────────────────────────────────────────────── */
.grid-stack { background: transparent; min-height: 340px; }
/* Editable board title (dash + graphs identical): the page h1 IS the rename
   control — click, type, blur/Enter saves, Escape reverts. Dashed underline on
   hover/focus is the affordance. */
.board-title { outline: none; border-bottom: 1px dashed transparent; cursor: text; }
.board-title:hover { border-bottom-color: var(--border2); }
.board-title:focus { border-bottom-color: var(--link); }

/* Full-bleed grid: gridstack pads every item 6px, so the grid pulls -6px into
   main's padding — panels visually reach the same edge on every board plug
   (dash had this, graphs didn't: a 12px mobile width skew). */
.grid-stack { margin-left: -6px; margin-right: -6px; width: auto; }
.grid-stack-item-content.panel { display: flex; flex-direction: column; background: var(--panel2); border: 1px solid var(--border); ; overflow: hidden; }
.panel-head { display: flex; align-items: center; gap: .4rem; padding: .35rem .5rem .35rem .7rem; border-bottom: 1px solid var(--border); cursor: move; background: var(--panel); flex: none; }
.panel-head .spacer { flex: 1; }
.panel-title { background: transparent; border: 1px solid transparent; color: var(--text-hi); font-weight: 600; font-size: .88rem; padding: .15rem .3rem; ; min-width: 3rem; max-width: 60%; }
.panel-title:hover, .panel-title:focus { border-color: var(--border2); background: var(--panel2); outline: none; }
/* Static (non-editable) panel title — a board whose titles are fixed uses this
   in place of the editable input, same weight/size so the chrome is identical. */
.panel-title-static { color: var(--text-hi); font-weight: 600; font-size: .88rem; padding: .15rem .3rem; }
.icon-btn { background: transparent; border: none; color: var(--muted); cursor: pointer; font-size: .9rem; padding: .2rem .4rem; ; line-height: 1; }
.icon-btn:hover { background: var(--row-hover); color: var(--text-hi); }
/* Some hosts still render the remove control as a bare ✕ form button — give it the
   same icon-button feel inside a panel header. */
.panel-head .del-icon { background: transparent; border: none; color: var(--muted); cursor: pointer; font-size: .95rem; line-height: 1; padding: .2rem .4rem; }
.panel-head .del-icon:hover { color: #f85149; background: var(--row-hover); }
.panel-head form { display: inline-flex; margin: 0; }

/* The body fills the remaining panel height; the ECharts canvas fills the body. */
.panel-body { flex: 1; min-height: 0; display: flex; overflow: hidden; padding: 2px 4px 2px; }
.echart { flex: 1; width: 100%; height: 100%; min-height: 0; }
.panel-body iframe { flex: 1; border: 0; width: 100%; height: 100%; }

.grid-stack > .grid-stack-item > .grid-stack-item-content { inset: 0; }
.grid-stack-item.ui-draggable-dragging .panel { box-shadow: 0 8px 28px rgba(0,0,0,.5); }
.grid-stack .grid-stack-placeholder > .placeholder-content { border: 1px dashed #347d39; ; background: rgba(63,185,80,.06); }

/* Empty board prompt (no panels yet). */
.dash-empty { text-align: center; color: var(--muted); padding: 3rem 1rem; }
.dash-empty p { margin: 0 0 .9rem; }

/* ── panels the build agent just proposed ────────────────────────────────────
   They fade/slide in with an accent ring that pulses until the user interacts. */
.grid-stack-item.agent-added .panel {
  outline: 2px solid #bc8cff; outline-offset: -2px;
  box-shadow: 0 0 0 1px rgba(188,140,255,.35), 0 10px 30px -12px rgba(137,87,229,.6);
  animation: ab-reveal .5s cubic-bezier(.2,.7,.3,1) 1, ab-glow 1.8s ease-in-out .5s 4;
}
@keyframes ab-reveal { from { opacity: 0; transform: translateY(10px) scale(.985); } to { opacity: 1; transform: none; } }
@keyframes ab-glow { 0%,100% { outline-color: #bc8cff; } 50% { outline-color: rgba(188,140,255,.2); } }
@media (prefers-reduced-motion: reduce) { .grid-stack-item.agent-added .panel { animation: none; } }

/* ── touch scroll-lock (boardkit.js) ─────────────────────────────────────────
   A locked panel's chart/iframe ignores touches so swipes scroll the PAGE
   instead of panning the chart; the hand button (coarse pointers only) unlocks
   one panel at a time, and while one is active the page itself stops
   scrolling/zooming so every gesture belongs to that panel. */
.grid-stack-item-content.touch-locked .echart,
.grid-stack-item-content.touch-locked iframe { pointer-events: none; }
.touch-lock-btn.unlocked { color: #3fb950; }
body.touch-capture { overflow: hidden; touch-action: none; overscroll-behavior: none; }
body.touch-capture .grid-stack-item-content:not(.touch-locked) { touch-action: auto; }

/* Muted footnote line under a panel (chart notes, entity-click info). */
.chart-notes { padding: .15rem .5rem .35rem; font-size: .72rem; color: var(--muted); font-style: italic; }
.dash-entity-info { padding: .3rem .6rem; border-top: 1px solid var(--border); font-size: .78rem; color: var(--muted); min-height: 1.4em; }

/* Static and live boards get their cross-filter controls from filterkit.CSS(),
   appended by CSS(). Keeping a second copy here previously let the two palettes
   drift and briefly exposed the obsolete blue active-filter border. */
/* zoom: the panel-header expand button + the shared modal's chart sizing */
.panel-head .bk-zoom{background:transparent;border:none;color:var(--muted);cursor:pointer;font-size:.8rem;line-height:1;padding:.2rem .4rem}
.panel-head .bk-zoom:hover{color:var(--text-hi);background:var(--row-hover)}
/* click-mode pill (Filter | Attribution) — mirrors dashCSS so the static
   board's bar matches the live board exactly. */
.dash-clickmode{display:inline-flex;height:1.85rem;box-sizing:border-box;border:1px solid var(--border);border-radius:var(--r-pill);overflow:hidden}
.dash-clickmode .dcm-opt{display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);font-size:.74rem;padding:0 .6rem;background:transparent;color:var(--muted);border:none;cursor:pointer;white-space:nowrap}
.dash-clickmode .dcm-opt+.dcm-opt{border-left:1px solid var(--border)}
.dash-clickmode .dcm-opt.active{background:rgba(88,166,255,.18);color:var(--text-hi)}
.dash-clickmode .dcm-opt:hover:not(.active){color:var(--text)}
.bk-sb-modal-content{max-width:min(66rem,94vw);width:94vw}
#bk-sb-zoom{height:min(68vh,34rem);width:100%}

/* ── fixed top-right icon cluster (.bk-topicon) ───────────────────────────
   Flat 2.55rem squares pinned to the top-right so they add ZERO vertical
   height; hosts stack them right-to-left with an inline right: offset. This is
   the single definition of that size/shape — it used to live on the (now deleted) live
   toggle, and hosts' icon buttons copied the numbers from it by comment.
   .active / .on marks an engaged control (house green). */
.bk-topicon{position:fixed;top:.55rem;z-index:40;width:2.55rem;height:2.55rem;box-sizing:border-box;
  border:1px solid var(--border2);cursor:pointer;background:var(--panel);color:var(--muted);
  font-size:1rem;display:flex;align-items:center;justify-content:center;
  transition:color .13s ease,border-color .13s ease}
.bk-topicon:hover{color:var(--text);border-color:var(--green-soft)}
.bk-topicon.active,.bk-topicon.on{color:var(--green-soft);border-color:var(--green-soft)}

.dash-filter-bar{display:flex;flex-wrap:wrap;gap:var(--sp-2,.5rem);align-items:center;margin:.1rem 0 .7rem}
.dash-filter-lbl{font-size:.78rem;color:var(--muted);font-weight:600;margin-right:.1rem}
.dash-filter-chip{font-size:.78rem;height:1.85rem;box-sizing:border-box;padding:0 .5rem;border:1px solid var(--border,#30363d);background:var(--panel2);color:var(--text,#c9d1d9);border-radius:var(--r-pill);display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);text-decoration:none}
.dash-filter-chip:hover{border-color:#6e7681;text-decoration:none}
.dash-filter-chip .fc-txt{margin-right:.15rem}
.dash-filter-chip.negate{border-color:#f85149;background:rgba(248,81,73,.14)}
.dash-filter-chip.disabled{opacity:.5;border-style:dashed}
.dash-filter-chip.disabled .fc-txt{text-decoration:line-through}
.dash-filter-chip.pinned{border-color:#d29922}
.dash-filter-chip .fc-pin-ic{font-size:.66rem;margin-right:.2rem}
.dash-filter-chip .fc-btn{border:none;background:transparent;color:inherit;cursor:pointer;font-size:.72rem;line-height:1;padding:0 .1rem;opacity:.55;min-height:0;height:auto}
.dash-filter-chip .fc-btn:hover{opacity:1}
.dash-filter-add{display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);height:1.85rem;min-height:1.85rem;box-sizing:border-box;font-size:.76rem;padding:0 .6rem;border:1px dashed var(--border);background:transparent;color:var(--muted);border-radius:var(--r-pill);cursor:pointer;white-space:nowrap}
.dash-filter-add:hover{color:var(--text-hi,#f0f6fc);border-color:var(--link,#58a6ff);background:transparent}
.dash-filter-add-wrap{display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);flex-wrap:wrap}
.dash-filter-form{display:inline-flex;align-items:center;gap:var(--sp-2,.5rem);height:1.85rem;box-sizing:border-box;padding:0 .4rem;border:1px solid var(--border);background:var(--panel2)}
/* The global control system has a 38px minimum. Filter controls deliberately
   fit inside the 29.6px capsule, so both height and min-height are pinned. */
.dash-filter-form select,.dash-filter-form input.dff-val{font-size:.76rem;height:1.45rem;min-height:1.45rem;max-height:1.45rem;box-sizing:border-box;padding:0 .35rem;background:var(--panel);color:var(--text,#c9d1d9);border:1px solid var(--border);margin:0;line-height:1}
.dash-filter-form input.dff-val{width:9rem}
.dash-filter-form .dff-not{font-size:.72rem;color:var(--muted);display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);white-space:nowrap;margin:0}
.dash-filter-form .dff-not input{margin:0;width:auto;height:auto;min-height:0;vertical-align:middle}
.dash-filter-form .dff-add,.dash-filter-form .dff-cancel{display:inline-flex;align-items:center;height:1.45rem;min-height:1.45rem;box-sizing:border-box;font-size:.74rem;padding:0 .45rem;border:1px solid var(--border);background:var(--panel);cursor:pointer;line-height:1}
.dash-filter-form .dff-add{color:var(--link,#58a6ff);border-color:#1f6feb}
.dash-filter-form .dff-cancel{color:var(--muted)}
.dash-filter-clear{display:inline-flex;align-items:center;height:1.85rem;min-height:1.85rem;box-sizing:border-box;font-size:.74rem;padding:0 .6rem;border:1px solid var(--border);background:transparent;color:var(--muted);border-radius:var(--r-pill);cursor:pointer}
.dash-filter-clear:hover{color:var(--text-hi,#f0f6fc);border-color:var(--muted);background:transparent}

/* Charts is a FULL-BLEED board — the shell's generous 2rem/3rem main padding just
   pushed the board off the bottom of the viewport. Trim it so the whole board (chart
   + rail + trade tabs) clears a laptop screen without vertical scroll (owner "fit it
   all on one page", 2026-07-10). */
main{padding-top:.6rem;padding-bottom:.6rem}
/* No navbar (owner 2026-07-10) and no footer (owner 2026-07-11) on the board: the
   header held two links to the page you are already on plus a hub link, and the footer
   was the last ~56px pushing the board past the viewport into a slight vertical
   scroll. The fullscreen and Build-with-AI buttons are separate position:fixed
   overlays, so they stay. Both are now NOT RENDERED (flowweb Page.NoChrome) rather
   than hidden from here — hiding the footer never worked, because it styles itself by
   class and outranked the type selector that used to sit here. */
.cht-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2,.5rem);margin:.35rem 0 .5rem}
.cht-toolbar select{background:var(--panel2);color:var(--text,#c9d1d9);border:1px solid var(--border,#30363d);height:1.85rem;padding:0 .5rem;font-size:.82rem}
/* The pair dropdown is redundant now the Markets rail switches the pair (owner
   2026-07-10) — hidden, kept in the DOM as the switching plumbing charts.js drives. */
#cht-market{display:none}
/* Drop the chart panel's title bar ("mexc:X · candles"): the rail highlight, the axis
   key and the footer already name the pair, and the space is better spent on candles. */
.bk-static .grid-stack-item:has(#cht-root) .panel-head{display:none}
/* Compact SQUARE tabs for the bottom Trade/Positions panel (house theme: not rounded). */
.cht-tabs{display:flex;gap:var(--sp-1,.25rem);margin-bottom:.5rem;border-bottom:1px solid var(--border,#30363d)}
.cht-tab{appearance:none;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--muted,#8b949e);font-size:.8rem;font-weight:600;padding:.3rem .6rem;cursor:pointer;margin-bottom:-1px}
.cht-tab:hover{color:var(--text,#c9d1d9)}
.cht-tab.active{color:var(--text-hi,#c9d1d9);border-bottom-color:#1f6feb}
.cht-tabpane[hidden]{display:none}
/* .dash-filter-chip — NEUTRAL border (owner 2026-07-11: no solid blue on filter
   elements), subtle dark fill, matching the capsule controls. */
.dash-filter-chip{font-size:.78rem;height:1.85rem;box-sizing:border-box;padding:0 .5rem;border:1px solid var(--border,#30363d);background:var(--panel2);color:var(--text,#c9d1d9);border-radius:var(--r-pill);display:inline-flex;align-items:center;gap:var(--sp-1,.25rem)}
.dash-filter-chip .fc-txt{margin-right:.15rem}
/* Volume, Scroller and the Indicator add-button are each their own SINGLE-segment
   .dash-clickmode capsule (see the canonical block below) — grey/transparent when OFF,
   the dash filter BLUE fill + light text when ON, identical to the FVG|Magnets segments. */
/* FVG + Magnets adopt the SAME .dash-clickmode double capsule as the pyramid-tier
   switcher below (canonical block, copied verbatim from plug-dash) — grouping the two
   zone overlays into one segmented pill that reads identically to dash (owner 2026-07-11). */
.dash-filter-chip.disabled{opacity:.5;border-style:dashed}
.dash-filter-chip .fc-sub{color:var(--muted,#8b949e)}
.dash-filter-chip .fc-pin-ic{font-size:.66rem;margin-right:.2rem}
.dash-filter-chip .fc-btn{border:none;background:transparent;color:inherit;cursor:pointer;font-size:.72rem;line-height:1;padding:0 .1rem;opacity:.55}
.dash-filter-chip .fc-btn:hover{opacity:1}
.dash-filter-bar{display:flex;flex-wrap:wrap;gap:var(--sp-2,.5rem);align-items:center;margin:.1rem 0 .5rem}
.dash-filter-lbl{font-size:.78rem;color:var(--muted);font-weight:600;margin-right:.1rem}
.dash-filter-add-wrap{display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);flex-wrap:wrap}
.dash-filter-form{display:inline-flex;align-items:center;gap:var(--sp-2,.5rem);height:1.85rem;box-sizing:border-box;padding:0 .4rem;border:1px solid var(--border);background:var(--panel2)}
.dash-filter-form input{font-size:.76rem;height:1.45rem;box-sizing:border-box;padding:0 .35rem;background:var(--panel);color:var(--text,#c9d1d9);border:1px solid var(--border);margin:0;line-height:1}
.dash-filter-form .dff-add,.dash-filter-form .dff-cancel{display:inline-flex;align-items:center;height:1.45rem;box-sizing:border-box;font-size:.74rem;padding:0 .45rem;border:1px solid var(--border);background:var(--panel);cursor:pointer;line-height:1}
.dash-filter-form .dff-add{color:var(--link,#58a6ff);border-color:#1f6feb}
.dash-filter-form .dff-cancel{color:var(--muted)}
/* .dash-clickmode — copied verbatim from plug-dash/web.go (segmented pill). */
.dash-clickmode{display:inline-flex;height:1.85rem;box-sizing:border-box;border:1px solid var(--border);border-radius:var(--r-pill);overflow:hidden}
.dash-clickmode .dcm-opt{display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);font-size:.74rem;padding:0 .6rem;background:transparent;color:var(--muted);border:none;cursor:pointer;white-space:nowrap}
.dash-clickmode .dcm-opt+.dcm-opt{border-left:1px solid var(--border)}
.dash-clickmode .dcm-opt.active{background:rgba(88,166,255,.18);color:var(--text-hi,#f0f6fc)}
.dash-clickmode .dcm-opt:hover:not(.active){color:var(--text,#c9d1d9)}
.dash-clickmode .dcm-opt:disabled{opacity:.4;cursor:not-allowed}
.cht-chart{width:100%;min-height:240px;border:1px solid var(--border,#30363d);background:var(--panel2)}
/* Footer summary: brighter than the old muted grey (it was hard to read) and clipped
   with an ellipsis so the long window/date string never runs off the panel edge. */
.cht-summary{color:var(--muted,#8b949e);font-size:.78rem;margin:.3rem 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
/* per-bar inspector: SQUARE house cards, tabular numerals */
.cht-inspect{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:var(--sp-2,.5rem);margin:.8rem 0}
.cht-cell{border:1px solid var(--border,#30363d);background:var(--card);padding:.55rem .7rem;min-height:3.1rem;box-sizing:border-box}
.cht-cell .k{font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;color:var(--muted,#8b949e)}
.cht-cell .v{font-size:1.1rem;font-weight:600;margin-top:.12rem;font-variant-numeric:tabular-nums}
.cht-cell .v.up{color:var(--green-soft,#3fb950)}.cht-cell .v.down{color:var(--red,#f85149)}
.cht-empty{color:var(--muted,#8b949e);padding:2.5rem 0;text-align:center}
.cht-error{border:1px solid var(--red,#f85149);background:rgba(248,81,73,.08);color:var(--text-hi,#f0f6fc);padding:.6rem .8rem;margin:.8rem 0;font-size:.85rem}
/* boardkit's .panel-body is a flex ROW with overflow hidden; each panel's root
   must flex-fill it (min-width:0 so a wide chart can shrink). The trade/positions
   panels may scroll their own overflow, but the CHART panel must never inner-scroll
   (owner: all content shown) — it clips, and the chart itself flexes to absorb the
   space the toolbar+indicators leave, so it always fits the cell at any width. */
.bk-static .panel-body{overflow:auto}
.bk-static .panel-body:has(#cht-root){overflow:hidden}
#cht-root,.cht-trade,.cht-pos-wrap{flex:1;min-width:0}
#cht-root{display:flex;flex-direction:column;height:100%}
.cht-toolbar{margin:.2rem 0 .5rem}
/* html.fullscreen drops the shared shell's centred max-width; the board page
   force-adds it before first paint (head shim). The old floating .fs-toggle
   button is GONE (2026-07-13): its last use was the /focus back-to-board link,
   which now rides the chart toolbar in-flow (chartPanel's .cht-hdr-actions)
   instead of overlapping the chart as a position:fixed control on mobile. */
html.fullscreen main{max-width:none;margin:0;padding-left:14px;padding-right:14px}
.cht-brick{width:6rem;background:var(--panel2);color:var(--text,#c9d1d9);border:1px solid var(--border,#30363d);height:1.85rem;padding:0 .5rem;font-size:.82rem}
/* SQUARE HOUSE THEME (owner 2026-07-10: only filters + timeframes are rounded).
   Strip the number-input spinners from the paper-trade fields (their up/down arrows
   overlapped the digits) and keep every trade control square. */
.cht-trade input[type=number]{-moz-appearance:textfield;appearance:textfield}
.cht-trade input[type=number]::-webkit-outer-spin-button,.cht-trade input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.cht-trade input,.cht-trade select{}
/* display:contents flattens these nested group wrappers so every control
   (Indicator, annotations toggle, Line, Zone, each saved chip) becomes a direct
   flex child of the filter bar and they pack/wrap as ONE continuous row group —
   instead of each group being an atomic flex island that forced its own line. */
.cht-inds,.cht-anno-host,.cht-annobar,.cht-anno-tools,.cht-anno-list{display:contents}
/* Indicator chip = a COLLAPSIBLE dropdown (owner 2026-07-11): a head you click to
   expand a vertical panel where each version of the indicator has its own params,
   colour and stroke, with add/remove. Multiple versions (e.g. 3 EMAs) group here. */
.dash-filter-chip.cht-ind{position:relative;padding:0;overflow:visible}
.cht-ind-head{display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);height:100%;background:transparent;border:none;color:var(--text,#c9d1d9);cursor:pointer;font-size:.78rem;padding:0 .55rem}
/* The shared theme paints EVERY button a SQUARE var(--panel) bg on :hover; inside these
   rounded (1rem) chips — kept overflow:visible so the floating panel can escape — that
   square pokes past the corners. Keep the chip's inner buttons background-free (they carry
   colour-only hovers) and move the hover affordance onto the rounded chip border. NOTE the
   theme rule button:not([class*=toastui-]):hover has specificity (0,2,1) AND loads later,
   so the override must EXCEED it: the two-class ".dash-filter-chip.cht-ind button:hover" is
   (0,3,1) and wins outright. A bare ".cht-x:hover" (0,2,0) or ".cht-ind button:hover"
   (0,2,1, a tie the later theme rule wins) both failed earlier. */
.dash-filter-chip.cht-ind button:hover{background:transparent}
.dash-filter-chip.cht-ind:hover{border-color:#6e7681}
.cht-ind-title{font-weight:500}
.cht-ind-count{font-size:.62rem;color:var(--muted,#8b949e);background:var(--panel2);border:1px solid var(--border,#30363d);padding:0 .3rem;line-height:1.4}
.cht-ind-caret{font-size:.58rem;color:var(--muted,#8b949e)}
.cht-ind-panel{position:absolute;top:calc(100% + .3rem);left:0;z-index:30;min-width:15.5rem;max-width:calc(100vw - .75rem);background:var(--panel2);border:1px solid var(--border,#30363d);box-shadow:0 8px 24px rgba(1,4,9,.7);padding:.4rem;display:flex;flex-direction:column;gap:var(--sp-1,.25rem)}
.cht-ind-panel[hidden]{display:none}
.cht-ind-prow{padding-bottom:.34rem;border-bottom:1px solid var(--border,#30363d)}
.cht-ind-prow select{width:100%;background:var(--panel2);color:var(--text,#c9d1d9);border:1px solid var(--border,#30363d);font-size:.78rem;padding:.15rem .3rem}
.cht-ind-vrow{display:flex;align-items:center;gap:var(--sp-1,.25rem)}
/* 3+ params (e.g. MACD): stack the colour/width/style controls BELOW the params so
   they fit (owner 2026-07-11). */
.cht-ind-vrow.stack{flex-direction:column;align-items:stretch;gap:var(--sp-1,.25rem)}
.cht-ind-vparams{display:inline-flex;gap:var(--sp-0,.15rem);flex:0 0 auto}
.cht-ind-vstyle{display:flex;align-items:center;gap:var(--sp-1,.25rem);flex:1 1 auto}
.cht-ind-vrow input[type=number]{width:3rem;background:var(--panel2);color:var(--text,#c9d1d9);border:1px solid var(--border,#30363d);font-size:.74rem;padding:.08rem .25rem}
.cht-ind-color{width:1.7rem;height:1.5rem;padding:0;border:1px solid var(--border,#30363d);background:none;cursor:pointer;flex:0 0 auto}
.cht-ind-width,.cht-ind-dash{background:var(--panel2);color:var(--text,#c9d1d9);border:1px solid var(--border,#30363d);font-size:.72rem;padding:.1rem .15rem}
.cht-ind-del-inst{border:none;background:transparent;color:var(--muted,#8b949e);cursor:pointer;font-size:.95rem;line-height:1;padding:0 .15rem}
.cht-ind-del-inst:hover{color:var(--red,#f85149)}
/* Per-indicator visibility (eye) toggles — mirror the Alphas tab: a group eye on the
   chip head hides EVERY instance, a per-row eye hides one version. Hidden ⇒ dimmed +
   eye-slash, still fully clickable to restore. */
.cht-ind-eye-grp{border:none;background:transparent;color:var(--muted,#8b949e);cursor:pointer;font-size:.76rem;line-height:1;padding:0 .05rem 0 .5rem;align-self:center}
.cht-ind-eye-grp:hover{color:var(--text,#c9d1d9)}
.cht-ind.ind-off .cht-ind-title{opacity:.5;text-decoration:line-through}
.cht-ind.ind-off .cht-ind-eye-grp{color:#6e7681}
.cht-ind-eye{margin-left:auto;border:none;background:transparent;color:var(--muted,#8b949e);cursor:pointer;font-size:.82rem;line-height:1;padding:0 .15rem}
.cht-ind-eye:hover{color:var(--text,#c9d1d9)}
.cht-ind-vrow.ind-off .cht-ind-vparams,.cht-ind-vrow.ind-off .cht-ind-color,.cht-ind-vrow.ind-off .cht-ind-width,.cht-ind-vrow.ind-off .cht-ind-dash{opacity:.45}
.cht-ind-more{margin-top:.1rem;border:1px dashed var(--border,#30363d);background:transparent;color:var(--muted,#8b949e);cursor:pointer;font-size:.74rem;padding:.22rem;text-align:center}
.cht-ind-more:hover{color:var(--green-soft,#3fb950);border-color:var(--green-soft,#3fb950)}
/* No number spinners on the indicator param inputs. */
.cht-ind input[type=number]{-moz-appearance:textfield;appearance:textfield}
.cht-ind input[type=number]::-webkit-outer-spin-button,.cht-ind input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.cht-annobar.off .cht-anno-tools,.cht-annobar.off .cht-anno-list{display:none}
/* Keep each annotation chip a compact single-line pill so the bar stays sleek:
   alpha-source markers carry the full composite-signal reason as their label,
   which used to span the whole bar and force everything onto separate rows. */
.dash-filter-chip.cht-anno-item{max-width:13rem}
.dash-filter-chip.cht-anno-item .fc-txt{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dash-filter-chip.anno-auto{border-style:dashed}
/* Annotations toggle + Line/Zone now use the shared .dash-clickmode capsule (see the
   canonical block above); their old .dash-filter-chip/.dash-filter-add rules are gone. */
.cht-anno-note{font-size:.72rem;color:var(--muted,#8b949e)}
/* Annotation add-form is a floating popover CARD anchored under the Line/Zone
   button (top/left set by JS relative to the bar, which is position:relative) —
   absolutely positioned so opening it never reflows the filter bar into extra
   rows, which broke the layout on the narrow chart panel. */
#cht-filter-bar{position:relative}
#cht-filter-bar .cht-anno-form{position:absolute;z-index:40;display:flex;flex-direction:column;align-items:stretch;height:auto;min-width:9.5rem;gap:var(--sp-2,.5rem);padding:.55rem;box-shadow:0 8px 24px rgba(1,4,9,.6)}
#cht-filter-bar .cht-anno-form input{width:100%;box-sizing:border-box}
#cht-filter-bar .cht-anno-form .caf-actions{display:flex;gap:var(--sp-2,.5rem);margin-top:.1rem}
#cht-filter-bar .cht-anno-form .caf-actions .dff-add{flex:1;justify-content:center}
#cht-filter-bar .cht-anno-form .caf-actions .dff-cancel{justify-content:center;padding:0 .5rem}
/* The chart wrap is the flex child now (holds the chart + the floating historical
   badge overlay); the chart fills it. */
.cht-chart-wrap{flex:1 1 auto;min-height:240px;position:relative;display:flex;flex-direction:column}
.cht-chart{flex:1 1 auto;min-height:240px}
/* Historical badge: a small amber pill top-centred over the chart when the view is
   pinned to the past. Clicking it (or "go live") returns to the live tail. */
/* FVG zone explainer popover (click an imbalance box). Fixed-position, clamped in JS. */
.cht-fvg-pop{position:fixed;z-index:60;width:min(19rem,90vw);background:var(--panel2);border:1px solid var(--border2,#30363d);padding:.6rem .7rem;box-shadow:0 6px 22px rgba(1,4,9,.6);font-size:.8rem;color:var(--text,#c9d1d9)}
.cht-fvg-pop .fp-head{font-weight:700;display:flex;align-items:center;gap:var(--sp-1,.25rem);margin-bottom:.4rem}
.cht-fvg-pop .fp-x{margin-left:auto;background:none;border:none;color:#6e7681;font-size:1.1rem;line-height:1;cursor:pointer;padding:0 .1rem}
.cht-fvg-pop .fp-x:hover{color:var(--text-hi,#f0f6fc)}
.cht-fvg-pop .fp-row{display:flex;justify-content:space-between;gap:var(--sp-4,1rem);margin:.15rem 0}
.cht-fvg-pop .fp-row span{color:var(--muted,#8b949e)}
.cht-fvg-pop .fp-act{color:var(--green-soft,#3fb950);font-weight:700}
.cht-fvg-pop .fp-mit{color:var(--muted,#8b949e);font-weight:700}
.cht-fvg-pop .fp-blurb{margin-top:.45rem;color:var(--muted,#8b949e);line-height:1.4;border-top:1px solid var(--border,#21262d);padding-top:.4rem}
/* Mobile: give the chart a viewport-relative minimum so it is a usable height on phones.
   gridstack at column=1 stacks panels top-to-bottom and sets each to cellHeight×H px, but
   the chart must be large enough to read — 55vh covers most portrait phones without scrolling.
   The volume pane shares the same flex container so it also grows proportionally. */
@media (max-width:720px){.cht-chart{min-height:55vh}}
/* Touch-lock fix for the charts page: boardkit's CSS targets .echart and iframe inside a
   locked panel (.grid-stack-item-content.touch-locked) to drop pointer-events — but the
   charts panel uses #cht-chart (class .cht-chart), not .echart, so the CSS never matched
   and the hand button had no effect. Mirror the same rule for .cht-chart so the hand toggle
   properly captures touches on the candlestick canvas, making pan/zoom work while the page
   is locked. This is charts-local — no shared boardkit change required. */
.grid-stack-item-content.touch-locked .cht-chart{pointer-events:none}
/* EXCHANGE chrome v2 (Binance-inspired, owner 2026-07-10) ------------------- */
/* Ticker strip: pair price header above the toolbar — big coloured last price +
   window change/high/low/volume stats. Filled by charts.js from the loaded bars. */
.cht-ticker{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-5,1.5rem);padding:.2rem 0 .55rem;border-bottom:1px solid var(--border,#30363d);margin-bottom:.35rem}
.cht-ticker .ct-px{font-size:1.55rem;font-weight:700;font-variant-numeric:tabular-nums;line-height:1;align-self:center}
.cht-ticker .ct-px.up{color:var(--green-soft,#3fb950)}.cht-ticker .ct-px.down{color:var(--red,#f85149)}
.cht-ticker .ct-stat{display:inline-flex;flex-direction:column;gap:var(--sp-0,.15rem);font-variant-numeric:tabular-nums}
.cht-ticker .ct-stat small{color:var(--muted,#8b949e);font-size:.68rem;text-transform:uppercase;letter-spacing:.06em}
.cht-ticker .ct-stat b{font-weight:600;font-size:.86rem;color:var(--text-hi,#c9d1d9)}
.cht-ticker .ct-stat b.up{color:var(--green-soft,#3fb950)}.cht-ticker .ct-stat b.down{color:var(--red,#f85149)}
/* Markets rail: exchange watchlist — one row per stored pair, click to switch.
   STABLE SIZING (owner 2026-07-10 bug: the rail collapsed to ~half height for
   ~350ms during a chart update): the rail fills the panel body with a DEFINITE
   height and the watchlist is the flex-grow child, so its height is deterministic
   and can't transiently collapse when an echarts re-render triggers a reflow. */
/* flex:1 so the rail fills the panel body's full WIDTH (the body is a flex row) —
   without it the watchlist only took its natural content width and stopped ~3/4 across
   (owner 2026-07-10); the rows are width:100% of the rail, so they now reach the edge. */
.cht-rail{flex:1;min-width:0;display:flex;flex-direction:column;height:100%;min-height:0}
/* Watchlist takes its NATURAL height (flex:0 1 auto, scrolls if the pair list is long)
   so the Alpha-alert pane sits IMMEDIATELY under the last pair — no blank gap between
   them (owner 2026-07-10). The rail's DEFINITE height (100%) is what keeps this stable
   and prevents the earlier reflow-collapse; the flex:1 was only papering over it. */
.cht-watch{flex:0 1 auto;display:flex;flex-direction:column;overflow-y:auto;min-height:0}
.cht-watch-item{display:flex;width:100%;box-sizing:border-box;align-items:center;gap:var(--sp-2,.5rem);padding:.42rem .5rem;border:0;border-bottom:1px solid rgba(48,54,61,.55);background:transparent;color:var(--text,#c9d1d9);font-size:.82rem;cursor:pointer;text-align:left}
.cht-watch-item:hover{background:rgba(255,255,255,.05)}
.cht-watch-item.active{background:rgba(31,111,235,.12);border-left:2px solid #1f6feb;padding-left:.38rem}
.cht-watch-item .cw-pair{font-weight:600}
.cht-watch-item .cw-px{font-variant-numeric:tabular-nums;color:var(--muted,#8b949e)}
.cht-watch-item.active .cw-px{color:var(--text-hi,#c9d1d9)}
/* Badge pinned to the row's RIGHT edge, UNIFORM width so every row's badge is the
   same size + lines up vertically (owner 2026-07-10): fixed min-width, centred. */
.cht-watch-item .cw-badge{margin-left:auto;flex:0 0 auto}
.cw-badge{display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-0,.15rem);min-width:3rem;box-sizing:border-box;text-align:center}
/* One-level dropdown under the ACTIVE watchlist row (dash-attribution style): a
   nested list of the pair's live alpha signals, each row "↳ name · time · pill". */
.cw-nested:empty{display:none}
.cw-nested{background:rgba(1,4,9,.35);border-bottom:1px solid rgba(48,54,61,.55);padding:.15rem 0 .3rem}
.cw-sig{display:flex;align-items:center;gap:var(--sp-2,.5rem);padding:.24rem .5rem .24rem .55rem;font-size:.76rem;line-height:1.2;cursor:pointer}
.cw-sig:hover{background:rgba(255,255,255,.045)}
.cw-sig.open{background:rgba(255,255,255,.06)}
.cw-sig .cw-arw{color:var(--muted,#8b949e);flex:0 0 auto;transition:transform .12s ease}
.cw-sig.open .cw-arw{transform:rotate(90deg)}
/* Level-2 signal detail: price · exact fire time · source · reasons, indented under
   the signal row. */
.cw-sig-info{padding:.15rem .5rem .4rem 1.55rem;font-size:.72rem;background:rgba(1,4,9,.35)}
.cw-sig-info[hidden]{display:none}
.csi-row{display:flex;gap:var(--sp-2,.5rem);justify-content:space-between;padding:.06rem 0}
.csi-row .csi-k{color:var(--muted,#8b949e)}
.csi-row .csi-v{color:var(--text-hi,#c9d1d9);font-variant-numeric:tabular-nums;text-align:right}
.csi-reasons{display:flex;flex-direction:column;gap:var(--sp-0,.15rem);margin-top:.15rem}
/* Level-3: each reason is clickable to reveal a plain-language explanation. */
.csi-reason{cursor:pointer;padding:.1rem .2rem}
.csi-reason:hover{background:rgba(255,255,255,.045)}
.csi-reason .csi-rlead{color:var(--muted,#8b949e);margin-right:.3rem}
.csi-reason .csi-rtext{color:var(--text,#c9d1d9)}
.csi-reason.open .csi-rtext{color:var(--text-hi,#c9d1d9);font-weight:600}
.csi-rexp{margin:.2rem 0 .15rem 1rem;padding:.3rem .45rem;background:var(--panel2);border-left:2px solid var(--border2,#30363d);color:var(--muted,#8b949e);font-size:.7rem;line-height:1.35}
.csi-rexp[hidden]{display:none}
.cw-sig .cw-nm{color:var(--text-hi,#c9d1d9);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;flex:1 1 auto}
/* Time in a fixed right-aligned column and the direction pill a UNIFORM width, so
   both columns line up across rows regardless of LONG/SHORT/NEUTRAL length (owner). */
.cw-sig .cw-tm{color:var(--muted,#8b949e);flex:0 0 auto;font-variant-numeric:tabular-nums;min-width:2.3rem;text-align:right}
.cw-sig .cw-tag{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;min-width:4.4rem;box-sizing:border-box;font-size:.64rem;font-weight:700;letter-spacing:.03em;padding:.08rem .3rem;border:1px solid transparent;text-transform:uppercase}
.cw-sig .cw-tag.up{color:var(--green-soft,#3fb950);border-color:rgba(63,185,80,.5);background:rgba(63,185,80,.12)}
.cw-sig .cw-tag.down{color:var(--red,#f85149);border-color:rgba(248,81,73,.5);background:rgba(248,81,73,.12)}
.cw-sig .cw-tag.flat{color:var(--muted,#8b949e);border-color:rgba(139,148,158,.4)}
.cw-nested .cw-none{display:block;padding:.2rem .5rem .2rem 1.1rem;font-size:.74rem;color:var(--muted,#8b949e)}
/* Rail tabs: Markets / Alpha alert share the rail height; the ACTIVE pane fills it
   (flex:1) and scrolls, so the watchlist can be long without pushing anything. */
.cht-rail-tabs{flex:0 0 auto;align-items:center}
/* Board controls docked to the RIGHT of the Markets header (owner 2026-07-11): the
   fullscreen + Build-with-AI icons, in-flow (no viewport-fixed nav bar). */
.cht-hdr-actions{margin-left:auto;display:flex;align-items:center;gap:var(--sp-1,.25rem);padding-right:.15rem}
.cht-hdr-btn{width:1.65rem;height:1.65rem;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--border,#21262d);background:transparent;color:var(--muted,#8b949e);font-size:.82rem;cursor:pointer;text-decoration:none;transition:color .13s ease,border-color .13s ease}
.cht-hdr-btn:hover{color:var(--text,#c9d1d9);border-color:var(--green-soft,#3fb950)}
/* On /focus the shared theme toggle joins this cluster — size it to match the
   1.65rem action buttons (ThemeToggleCSS defaults it to 2rem for the header). */
.cht-hdr-actions .theme-toggle{width:1.65rem;height:1.65rem}
/* Fullscreen + AI: DESKTOP keeps them in the Markets header (.cht-hdr-actions-rail);
   MOBILE moves them to the RIGHT of the timeframe row (.cht-hdr-actions-tier) so it's
   all one line (owner 2026-07-11). Exactly one set shows per breakpoint. */
.cht-hdr-actions-tier{display:none}
@media(max-width:720px){
  .cht-hdr-actions-rail{display:none}
  .cht-hdr-actions-tier{display:flex}
}
.cht-rail-pane:not([hidden]){flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
.cht-rail-pane .cht-watch{flex:1 1 auto;min-height:0}
.cht-rail-pane .cht-alpha{flex:0 0 auto}
/* Per-symbol alpha badge (option 2): a small direction tag on the watchlist row when
   that pair has a recent alpha fire — for THIS or any other pair — WITHOUT reordering
   the list (a fire mid-click must not move the row under the pointer). */
.cw-badge{font-size:.66rem;font-weight:700;line-height:1;padding:.1rem .3rem;border:1px solid transparent;white-space:nowrap}
.cw-badge:empty{display:none}
.cw-badge.up{color:var(--green-soft,#3fb950);border-color:rgba(63,185,80,.5);background:rgba(63,185,80,.12)}
.cw-badge.down{color:var(--red,#f85149);border-color:rgba(248,81,73,.5);background:rgba(248,81,73,.12)}
.cw-badge.flat{color:var(--muted,#8b949e);border-color:rgba(139,148,158,.4)}
.cht-rail-sec{margin-top:.55rem;padding-top:.6rem;border-top:1px solid var(--border,#30363d);color:var(--muted,#8b949e);font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em}
/* Chart OVERLAYS (Binance-style, over the top of the candle canvas): the O/H/L/C/V
   legend top-left (fills from the hovered or newest bar) and the "nearest zone"
   readout top-right. Both are pointer-events:none so they never block chart drags.
   The canvas CHROME re-tints with the theme (xfchart.js chrome()) and .cht-chart's
   backdrop is var(--panel2), so these overlays use the theme tokens like everything
   else; only their dark text-shadow is dropped in light (rule below). */
/* Transient instructional banner while placing a Line/Zone annotation by clicking. */
.cht-place-hint{position:absolute;top:.4rem;left:50%;transform:translateX(-50%);z-index:20;background:var(--card,#13181f);border:1px solid var(--link,#58a6ff);color:var(--text,#c9d1d9);font-size:.74rem;padding:.2rem .6rem;pointer-events:none;white-space:nowrap;box-shadow:0 4px 14px rgba(1,4,9,.5)}
/* Readout sits IN LINE with the echarts series key (top:6) at the key's font size, but
   INDENTED past the left Y-axis label gutter so the leading "O" never overlaps the axis
   numbers (owner 2026-07-11). The key is right-anchored, the readout left, so the shared
   top line reads as one clean strip. */
.cht-legend{position:absolute;top:.35rem;left:2.9rem;right:.55rem;z-index:6;display:flex;flex-wrap:wrap;gap:var(--sp-0,.15rem) var(--sp-2,.5rem);pointer-events:none;font-variant-numeric:tabular-nums;font-size:.69rem;text-shadow:0 1px 3px rgba(1,4,9,.9)}
.cht-legend .lg{color:var(--muted,#8b949e)}
/* Fixed-width value slots so the O/H/L/C/V columns don't shift as you mouse across
   bars (owner 2026-07-10): each number occupies the same space regardless of digits. */
.cht-legend .lg b{color:var(--text-hi,#f0f6fc);font-weight:600;margin-left:.28rem;display:inline-block;min-width:4rem;text-align:left}
.cht-legend .lg b.up{color:var(--green-soft,#3fb950)}.cht-legend .lg b.down{color:var(--red,#f85149)}
/* The overlays' text-shadow is a dark halo tuned for the dark canvas — drop it in light. */
:root[data-theme="light"] .cht-legend,:root[data-theme="light"] .cht-near{text-shadow:none}
/* MOBILE: the O/H/L/C/V readout used to span the full top line — overlapping the chart's
   series key and pushing V off the right edge. Drop it to its OWN line below the key,
   pin it within the chart width (left+right) so it can never run off-page, let it wrap,
   and tighten the value slots (owner 2026-07-11). */
@media(max-width:720px){
  /* Narrow screens can't fit the readout beside the key, so keep it on its own line —
     but INDENT it past the Y-axis gutter so the "O" clears the axis numbers. */
  .cht-legend{top:1.75rem;left:2.6rem;right:.4rem;flex-wrap:wrap;gap:0 .3rem;font-size:.68rem}
  .cht-legend .lg b{min-width:2.4rem;margin-left:.16rem}
}
.cht-near{position:absolute;top:.4rem;right:.55rem;z-index:6;max-width:20rem;text-align:right;pointer-events:none;font-size:.76rem;text-shadow:0 1px 3px rgba(1,4,9,.9);color:var(--text,#c9d1d9)}
.cht-near .nz-dist{font-weight:700;font-variant-numeric:tabular-nums}
.cht-near .nz-dist.near{color:#d29922}.cht-near .nz-dist.hit{color:var(--green-soft,#3fb950)}
.cht-near .nz-lbl{color:var(--muted,#8b949e)}
/* The readout itself is hoverable (pointer-events:auto only on the text, so it doesn't
   block chart drags elsewhere) and shows a popup explaining what happens at the zone. */
.cht-near .nz-hit{display:block;margin-bottom:.12rem;pointer-events:auto;cursor:help;position:relative}
.cht-near .nz-pop{position:absolute;top:1.5rem;right:0;z-index:20;width:16rem;text-align:left;background:var(--panel2);border:1px solid var(--border2,#30363d);padding:.5rem .6rem;font-size:.74rem;line-height:1.35;color:var(--text,#c9d1d9);box-shadow:0 8px 24px rgba(1,4,9,.7);white-space:normal;display:none}
.cht-near .nz-hit:hover .nz-pop{display:block}
/* Live alpha alert pane (rail bottom): the current pair's newest screener/Alpha fire. */
.cht-alpha{flex:0 0 auto;display:flex;flex-direction:column;gap:var(--sp-1,.25rem);margin-top:.15rem}
.cht-alpha-empty{color:var(--muted,#8b949e);font-size:.8rem}
.cht-alert{border:1px solid var(--border,#30363d);border-left:2px solid #6e7681;background:var(--card);padding:.4rem .5rem;font-size:.78rem}
.cht-alert.up{border-left-color:#3fb950}.cht-alert.down{border-left-color:#f85149}
.cht-alert .ca-top{display:flex;justify-content:space-between;gap:var(--sp-2,.5rem);align-items:baseline}
.cht-alert .ca-name{font-weight:600;color:var(--text-hi,#c9d1d9)}
.cht-alert .ca-dir{font-size:.68rem;text-transform:uppercase;letter-spacing:.05em}
.cht-alert .ca-dir.up{color:var(--green-soft,#3fb950)}.cht-alert .ca-dir.down{color:var(--red,#f85149)}
.cht-alert .ca-note{color:var(--muted,#8b949e);margin-top:.15rem;line-height:1.3}
/* Alpha-alert tab: each signal's conditions as individual badges stacked VERTICALLY
   (like alpha.zm.is/?view=live), not crammed onto one line (owner 2026-07-10). */
.cht-alert .ca-badges{display:flex;flex-direction:column;align-items:flex-start;gap:var(--sp-1,.25rem);margin-top:.3rem}
.cht-alert .ca-badge{font-size:.72rem;line-height:1.2;padding:.12rem .4rem;border:1px solid var(--border2,#30363d);background:var(--panel2);color:var(--text,#c9d1d9);max-width:100%;white-space:normal}
.cht-alert.up .ca-badge{border-color:rgba(63,185,80,.45)}
.cht-alert.down .ca-badge{border-color:rgba(248,81,73,.45)}
.cht-alert .ca-when{color:var(--muted,#8b949e);font-size:.7rem;margin-top:.28rem}
.cht-trade{display:flex;flex-direction:column;gap:var(--sp-2,.5rem)}
/* Renovated bottom panel: liquidations/flow | order form | positions ------------ */
.cht-tradewrap{flex:1;min-width:0;display:flex;gap:var(--sp-5,1.5rem);align-items:stretch;height:100%;min-height:0;overflow:hidden}
.cht-tcol{display:flex;flex-direction:column;min-height:0;min-width:0}
/* Left (liquidations/flow) + right (positions) are EXACTLY the Markets side-pane width
   (--railw, measured in JS), so they align with it; left-aligned and right-aligned
   respectively. The order form flex-fills the remaining centre (owner 2026-07-10). */
.cht-tleft,.cht-tright{flex:0 0 var(--railw,25%);min-width:0}
.cht-tcenter{flex:1 1 auto;min-width:0;align-items:center;justify-content:flex-start;overflow-y:auto;padding:.15rem .85rem 0;border-left:1px solid var(--border,#30363d);border-right:1px solid var(--border,#30363d)}
/* Collapse the (usually empty) trade status line below Buy/Sell so it stops
   adding phantom height that scrolled the paper-trade pane (owner 2026-07-16). */
.cht-trade-msg:empty{display:none}
.cht-tcol .cht-tabpane:not([hidden]){flex:1 1 auto;min-height:0;overflow-y:auto}
/* The order ticket FILLS the wide centre column (owner 2026-07-11: don't cram it into
   a narrow strip). A generous cap keeps it sane on ultra-wide monitors; the 2-col body
   below spreads the fields so nothing is a lone stretched input. */
.cht-trade-form{width:100%;max-width:52rem;gap:var(--sp-2,.5rem)}
/* Two balanced columns under the order-type switcher. Left = price/size/chips, right =
   protection. Collapses to one column on mobile. */
.cht-tf-cols{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-2,.5rem) var(--sp-5,1.5rem);align-items:start}
.cht-tf-col{display:flex;flex-direction:column;gap:var(--sp-2,.5rem);min-width:0}
.cht-dim{color:var(--muted,#8b949e);font-size:.78rem}
/* Liquidation clusters: price · notional bar · long/short flush colour. */
.cht-liq{display:flex;flex-direction:column;gap:var(--sp-0,.15rem)}
.cl-row{display:grid;grid-template-columns:4.5rem 1fr 4rem;align-items:center;gap:var(--sp-2,.5rem);font-size:.76rem;font-variant-numeric:tabular-nums}
.cl-px{color:var(--text-hi,#c9d1d9)}
.cl-bar{height:.7rem;background:var(--panel2);border:1px solid var(--border,#30363d);position:relative;overflow:hidden}
.cl-bar>span{position:absolute;left:0;top:0;bottom:0}
.cl-bar>span.long{background:rgba(248,81,73,.55)}
.cl-bar>span.short{background:rgba(63,185,80,.55)}
.cl-not{text-align:right;color:var(--muted,#8b949e)}
/* Order-flow summary: delta / CVD / buy-sell volume. */
.cht-flow{display:flex;flex-direction:column;gap:var(--sp-2,.5rem);font-size:.8rem}
.cf-row{display:flex;justify-content:space-between;gap:var(--sp-2,.5rem)}
.cf-row .cf-k{color:var(--muted,#8b949e)}
.cf-row .cf-v{font-variant-numeric:tabular-nums;color:var(--text-hi,#c9d1d9)}
.cf-row .cf-v.up{color:var(--green-soft,#3fb950)}.cf-row .cf-v.down{color:var(--red,#f85149)}
.cht-liq,.cht-flow{padding:.4rem .55rem}
/* The mobile trade-section tab bar is DESKTOP-hidden (all three columns show at once);
   on mobile it mirrors the .cht-tabs underline style (buttons reuse .cht-tab). */
.cht-mtabs{display:none;gap:var(--sp-1,.25rem);margin-bottom:.5rem;border-bottom:1px solid var(--border,#30363d)}
/* MOBILE: the three trade columns used to stack into one long scroll — instead make them
   TABS (Trade / Flow / Positions), showing only the section the .cht-mtabs bar selects
   via data-mtab on the wrap (owner 2026-07-11). Each column fills the width. */
@media(max-width:720px){
  .cht-tradewrap{flex-direction:column;overflow:visible;gap:var(--sp-2,.5rem)}
  .cht-mtabs{display:flex}
  .cht-tradewrap>.cht-tcol{display:none;flex:0 0 auto;width:100%;max-width:none;min-width:0}
  .cht-tradewrap[data-mtab="trade"]>[data-col="trade"],
  .cht-tradewrap[data-mtab="liqflow"]>[data-col="liqflow"],
  .cht-tradewrap[data-mtab="pos"]>[data-col="pos"]{display:flex}
  .cht-tcenter{align-items:stretch;justify-content:flex-start;border-left:0;border-right:0;padding:0}
  /* Don't let the form STRETCH to fill the column (flex:1 spread its fields ~27px apart
     and forced a scroll) — content-height packs them at the tight form gap (owner 2026-07-11). */
  .cht-trade-form{max-width:none;flex:0 0 auto}
  /* COMPACT ORDER TICKET (owner 2026-07-13: "all the space we have" — fit it without
     scrolling on a phone). Size+chips stay full width (readable); the three protection
     fields collapse to ONE 3-across row (label above input) instead of three stacked rows,
     and every gap/height/pad is tightened so Market-switcher → Buy/Sell all fit at once. */
  .cht-tf-cols{grid-template-columns:1fr;gap:var(--sp-1,.25rem)}
  .cht-trade-form{gap:var(--sp-1,.25rem)}
  .cht-tf-col{gap:var(--sp-1,.25rem)}
  .cht-tf-col:last-child{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-1,.25rem)}
  .cht-tf-col:last-child .cht-field{flex-direction:column;align-items:stretch;gap:var(--sp-0,.15rem);font-size:.66rem}
  .cht-tf-col:last-child .cht-field input{width:100%;text-align:right}
  .cht-field input{height:1.55rem}
  .cht-chips{gap:var(--sp-1,.25rem)}
  .cht-chip{padding:.2rem 0}
  .cht-oseg-b{padding:.3rem .15rem}
  .cht-est{margin:.05rem 0;min-height:.9rem}
  /* Keep Buy/Sell a comfortable tap target (~44px) — the protection 3-across already
     recovered the vertical space, and these are the primary action. */
  .cht-btn{padding:.6rem .4rem;font-size:.86rem}
  .cht-mtabs{margin-bottom:.35rem}
  /* The Trade panel's boardkit header ("Trade" + drag handle) DUPLICATES the mtabs
     (Trade/Flow/Positions) right beneath it and eats ~its own height — which is exactly
     the vertical overflow the ticket had on a phone. Hide it on mobile (the chart panel
     already drops its head, line ~228) so Market-switcher → Buy/Sell fit without scrolling. */
  .bk-static .grid-stack-item:has(.cht-tradewrap) .panel-head{display:none}
  .cht-tcol .cht-tabpane:not([hidden]){overflow-y:visible}
}
/* Order-entry BAR under the chart (Binance places order entry below the chart):
   buttons + fields in one wrapping row; the risk hint rides inline at the end.
   Declared AFTER .cht-trade so the row direction wins the equal-specificity cascade. */
.cht-trade.cht-trade-bar{flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--sp-3,.75rem)}
.cht-trade-bar .cht-trade-row{flex:0 0 auto;min-width:16rem}
.cht-trade-bar .cht-field{flex:0 1 11rem}
.cht-trade-bar .cht-field input{width:5.5rem}
.cht-trade-bar .cht-hint{flex:1 1 16rem;font-size:.72rem;color:var(--muted,#8b949e)}
.cht-trade-bar .cht-trade-msg{flex-basis:100%;margin:0}
.cht-trade-row{display:grid;grid-template-columns:1fr 1fr;gap:var(--sp-2,.5rem)}
.cht-btn{font-size:.82rem;font-weight:600;border:1px solid var(--border,#30363d);background:var(--panel);color:var(--text,#c9d1d9);padding:.5rem .4rem;cursor:pointer}
.cht-btn:disabled{opacity:.45;cursor:not-allowed}
.cht-btn.buy{border-color:var(--green,#238636);background:rgba(63,185,80,.14);color:var(--green-soft,#3fb950)}
.cht-btn.sell{border-color:#8b2c22;background:rgba(248,81,73,.12);color:var(--red,#f85149)}
.cht-btn.buy:hover:not(:disabled){background:rgba(63,185,80,.24)}
.cht-btn.sell:hover:not(:disabled){background:rgba(248,81,73,.2)}
.cht-btn.close{font-size:.72rem;padding:.2rem .5rem;font-weight:500}
/* margin:0 overrides the shared label{margin:1rem 0 .25rem} (theme.go) — that ~20px per
   label was the "empty gaps between lines" in the order form; the form's own flex gap
   spaces the fields (owner 2026-07-11). */
.cht-field{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-2,.5rem);margin:0;font-size:.78rem;color:var(--muted,#8b949e)}
.cht-field input{width:5.5rem;background:var(--panel2);color:var(--text,#c9d1d9);border:1px solid var(--border,#30363d);height:1.7rem;padding:0 .45rem;font-size:.8rem;text-align:right}
.cht-hint{font-size:.72rem;color:var(--muted,#8b949e);margin:.1rem 0}
.cht-hint b{color:var(--green-soft,#3fb950)}
.cht-trade-msg{font-size:.78rem;margin:.1rem 0;min-height:1rem}
.cht-trade-msg.ok{color:var(--green-soft,#3fb950)}.cht-trade-msg.err{color:var(--red,#f85149)}
/* Order-type segmented switcher (square house theme): a 4-up button row that
   fills the form width; the active cell reads green like the chart's tabs. */
.cht-oseg{display:grid;grid-template-columns:repeat(4,1fr);gap:0;border:1px solid var(--border,#30363d);background:var(--panel2)}
.cht-oseg-b{font-size:.74rem;font-weight:500;background:transparent;color:var(--muted,#8b949e);border:0;border-right:1px solid var(--border,#30363d);padding:.35rem .2rem;cursor:pointer}
.cht-oseg-b:last-child{border-right:0}
.cht-oseg-b:hover{color:var(--text,#c9d1d9);background:var(--panel)}
.cht-oseg-b.active{color:var(--green-soft,#3fb950);background:rgba(63,185,80,.12);font-weight:600}
/* Conditional price rows use the same field layout but a wider input (a price, not a %). */
.cht-price-row input{width:8rem}
/* .cht-field sets display:flex, which (author sheet) beats the UA [hidden]{display:none};
   restore hiding for hidden fields so market mode drops the price rows (owner 2026-07-11). */
.cht-field[hidden]{display:none}
/* Quick-fill size chips: small square buttons under the size field. */
.cht-chips{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--sp-1,.25rem)}
.cht-chip{font-size:.72rem;background:var(--panel2);color:var(--muted,#8b949e);border:1px solid var(--border,#30363d);padding:.25rem 0;cursor:pointer}
.cht-chip:hover{color:var(--text,#c9d1d9);background:var(--panel)}
.cht-chip.active{color:var(--green-soft,#3fb950);border-color:var(--green,#238636);background:rgba(63,185,80,.12)}
/* Protection grid: three stacked-label columns so SL/TP/Trailing share one line. */
.cht-prot{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-2,.5rem)}
.cht-field.col{flex-direction:column;align-items:stretch;gap:var(--sp-1,.25rem);font-size:.7rem;text-align:left}
.cht-field.col input{width:100%;text-align:right}
/* Live cost/fill estimate line. */
.cht-est{font-size:.72rem;color:var(--muted,#8b949e);margin:.1rem 0;min-height:1rem;font-variant-numeric:tabular-nums}
.cht-est b{color:var(--text,#c9d1d9);font-weight:600}
/* Open-orders count badge on the right-column tab. */
.cht-ordn{display:inline-block;min-width:1.1rem;text-align:center;font-size:.68rem;font-weight:600;color:var(--panel2,#0d1117);background:#d29922;padding:0 .3rem;margin-left:.15rem}
/* Resting-order row (mirrors .cht-pos): type · side · price · Cancel. */
.cht-ord{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2,.5rem);border:1px dashed var(--border,#30363d);background:var(--card);padding:.4rem .6rem;font-size:.8rem}
.cht-ord-px{font-variant-numeric:tabular-nums;color:var(--text-hi,#c9d1d9)}
.cht-ord-ty{font-size:.68rem;text-transform:uppercase;letter-spacing:.03em;color:var(--muted,#8b949e)}
.cht-pill.limit{background:rgba(88,166,255,.16);color:var(--link,#58a6ff)}
.cht-pill.stop_market,.cht-pill.stop_limit{background:rgba(210,153,34,.16);color:#d29922}
/* open positions strip */
.cht-bal{font-size:.76rem;color:var(--muted,#8b949e);margin-bottom:.4rem;font-variant-numeric:tabular-nums}
.cht-positions{display:flex;flex-direction:column;gap:var(--sp-1,.25rem)}
.cht-pos{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2,.5rem);border:1px solid var(--border,#30363d);background:var(--card);padding:.4rem .6rem;font-size:.82rem}
.cht-pos-sz{font-variant-numeric:tabular-nums}
.cht-pos-pnl{font-variant-numeric:tabular-nums;font-weight:600;font-size:.8rem}
.cht-pos-pnl.up{color:var(--green-soft,#3fb950)}.cht-pos-pnl.down{color:var(--red,#f85149)}
.cht-pos-empty{color:var(--muted,#8b949e);font-size:.82rem}
.cht-pill{font-size:.68rem;font-weight:600;text-transform:uppercase;letter-spacing:.03em;padding:.05rem .4rem}
.cht-pill.long{background:rgba(63,185,80,.16);color:var(--green-soft,#3fb950);border:1px solid var(--green-hi,#2ea043)}
.cht-pill.short{background:rgba(248,81,73,.14);color:var(--red,#f85149);border:1px solid #8b2c22}
.cht-pill.protected{background:rgba(63,185,80,.12);color:var(--green-soft,#3fb950);border:1px solid var(--green-hi,#2ea043)}
.cht-pill.unprotected{background:rgba(210,153,34,.14);color:#d29922;border:1px solid #9e6a03}

/* THE SIDEBAR'S OWN TOKENS.
   Everything with a house equivalent now uses it: --panel2 --panel --border
   --border2 --muted --text --text-hi --blue --green-soft --red. Each of those
   house tokens' DARK value is byte-identical to the literal it replaced, so dark
   mode is unchanged by construction rather than by eye.

   These nine have NO house token with that dark value, so rather than round them
   to the nearest one — which would silently shift dark mode — the sidebar
   declares its own. The dark column below is the EXACT literal that was there
   before; only the light column is new.

   --ab-ink/-line/-glow/-sky/-icon are the AI identity, not theme chrome, and
   they stay purple in both themes. But they could not stay LITERAL: --ab-ink was
   #d2a8ff used as TEXT (the title sweep, the Build label, the live step), and a
   pale lilac on a white page is unreadable. So the hue is kept and the lightness
   is flipped — GitHub Primer's purple-600/500 against white, which is the same
   accent at the same brand, legible.

   Scoped to the two roots (.ab-sidebar, .ab-toggle) so nothing leaks into a host
   page, and mirrored on :root[data-theme="light"] — the same attribute
   webui.ThemeLightVars keys on, set before first paint by ThemeBootHead. */
.ab-sidebar,.ab-toggle{
  --ab-top:#111826;        /* gradient crown, above --panel2 */
  --ab-inset:#0a0e14;      /* recessed field/log wells, below --bg */
  --ab-field-ink:#e6edf3;  /* textarea text: brighter than --text, dimmer than --text-hi */
  --ab-chip-line:#2b3a55;  /* chip hairline, a blue-tinted --border */
  --ab-line:#8957e5;       /* AI purple: borders and the filled hover */
  --ab-ink:#d2a8ff;        /* AI purple as TEXT */
  --ab-glow:#bc8cff;       /* AI purple for glows and the live blip */
  --ab-sky:#79c0ff;        /* the blue half of the identity sweep */
  --ab-icon:#8b7bd8;       /* the wand glyph on a chip */
}
:root[data-theme="light"] .ab-sidebar,:root[data-theme="light"] .ab-toggle{
  --ab-top:#ffffff;
  --ab-inset:#f6f8fa;
  --ab-field-ink:#1f2328;
  --ab-chip-line:#d0d7de;
  --ab-line:#8250df;
  --ab-ink:#6639ba;
  --ab-glow:#8250df;
  --ab-sky:#0969da;
  --ab-icon:#8250df;
}
/* Visibility model (plug-api hamburger, mirrored right): the sidebar is ALWAYS
   rendered but OFF-CANVAS (translateX) until body.ab-open — transform-based
   hiding avoids the display-cascade bug that once made it full-screen and
   uncloseable on phones. The hamburger (.ab-toggle) is the only way in; the
   header ✕ closes. Desktop: 360px right panel. Below the breakpoint: the SAME
   panel is a full-screen bar. */
.ab-sidebar{position:fixed;top:0;right:0;bottom:0;width:360px;max-width:100vw;box-sizing:border-box;
  background:linear-gradient(180deg,var(--ab-top) 0%,var(--panel2) 42%);
  border-left:1px solid var(--border);padding:1rem 1rem 1.4rem;z-index:41;overflow-y:auto;
  display:flex;flex-direction:column;gap:var(--sp-3,.75rem);
  transform:translateX(105%);transition:transform .22s ease}
body.ab-open .ab-sidebar{transform:translateX(0)}
@media (max-width:1099px){.ab-sidebar{width:100vw;border-left:0}}
/* The sidebar OVERLAYS the page — it must not push content aside (the board is
   visible anyway; the padding shove caused a visual jump on every open/close). */
/* Accent gradient edge — the AI identity sweep. */
.ab-sidebar::before{content:"";position:absolute;top:0;left:0;bottom:0;width:3px;
  background:linear-gradient(180deg,var(--blue),var(--ab-glow));opacity:.9}
.ab-head{display:flex;align-items:center;justify-content:space-between;min-height:2.55rem;padding-right:3.4rem}
.ab-title{display:inline-flex;align-items:center;gap:var(--sp-2,.5rem);font-weight:650;font-size:1.15rem;letter-spacing:-.01em}
.ab-title-text{background:linear-gradient(90deg,var(--ab-sky),var(--ab-ink));-webkit-background-clip:text;background-clip:text;color:transparent}
.ab-spark{color:var(--ab-ink);filter:drop-shadow(0 0 6px rgba(188,140,255,.55))}
.ab-collapse{background:none;border:1px solid var(--border2);color:var(--muted);
  cursor:pointer;width:2.55rem;height:2.55rem;display:flex;align-items:center;
  justify-content:center;padding:0;font-size:1.4rem;line-height:1;
  position:absolute;top:.55rem;right:1rem}
.ab-collapse:hover{border-color:var(--ab-line);color:var(--ab-ink)}
.ab-intro{color:var(--muted);font-size:.78rem;line-height:1.5}
.ab-instruction{width:100%;box-sizing:border-box;resize:vertical;min-height:4.6rem;
  background:var(--ab-inset);border:1px solid var(--border2);color:var(--ab-field-ink);padding:.55rem .7rem;font:inherit;font-size:.95rem;
  transition:border-color .15s ease,box-shadow .15s ease}
.ab-instruction:focus{outline:none;border-color:var(--blue);box-shadow:0 0 0 3px rgba(88,166,255,.18)}
.ab-build{align-self:stretch;display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-2,.5rem);
  background:transparent;border:1px solid var(--ab-line);color:var(--ab-ink);
  padding:.62rem .8rem;cursor:pointer;font-weight:600;font-size:.84rem;letter-spacing:.03em;
  transition:background .13s ease,color .13s ease,border-color .13s ease}
.ab-build:hover{background:var(--ab-line);color:#fff}
.ab-build:disabled{opacity:.65;cursor:default}
.ab-build.ab-busy{opacity:.8}
/* suggested-prompt chips */
.ab-chips-wrap{display:flex;flex-direction:column;gap:var(--sp-1,.25rem)}
.ab-chips-lbl{font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--faint)}
.ab-chips{display:flex;flex-wrap:wrap;gap:var(--sp-1,.25rem)}
.ab-chip{display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);max-width:100%;text-align:left;
  background:rgba(88,166,255,.07);border:1px solid var(--ab-chip-line);color:var(--text);
  padding:.28rem .6rem;font-size:.78rem;cursor:pointer;line-height:1.2;
  transition:border-color .13s ease,background .13s ease,color .13s ease}
.ab-chip:hover{border-color:var(--ab-line);background:rgba(188,140,255,.12);color:var(--text-hi)}
.ab-chip .fa-wand-magic-sparkles{color:var(--ab-icon);font-size:.7rem}
/* progress + live step trace */
.ab-progress{font-size:.85rem;color:var(--muted);min-height:1.1em;font-weight:500}
.ab-progress.ab-run{background:linear-gradient(90deg,var(--ab-sky),var(--ab-ink),var(--ab-sky));background-size:200% auto;
  -webkit-background-clip:text;background-clip:text;color:transparent;animation:ab-shimmer 2s linear infinite}
.ab-progress.ab-ok{color:var(--green-soft)}
.ab-progress.ab-err{color:var(--red)}
.ab-steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--sp-1,.25rem)}
.ab-steps li{position:relative;padding-left:1.15rem;font-size:.78rem;color:var(--muted);line-height:1.3}
.ab-steps li::before{content:"";position:absolute;left:.15rem;top:.42em;width:.42rem;height:.42rem;border-radius:50%;
  background:var(--green-soft);box-shadow:0 0 0 2px rgba(63,185,80,.18)}
.ab-steps li.ab-step-live{color:var(--ab-ink)}
.ab-steps li.ab-step-live::before{background:var(--ab-glow);box-shadow:0 0 8px 1px rgba(188,140,255,.8);animation:ab-blip .9s ease-in-out infinite}
/* the run log: steps carrying an input/outcome preview expand on tap */
.ab-steps li.ab-has-io{cursor:pointer}
.ab-steps li.ab-has-io .ab-step-nm{border-bottom:1px dotted var(--border2)}
.ab-steps li.ab-has-io:hover .ab-step-nm{color:var(--text);border-bottom-color:var(--muted)}
.ab-step-t{margin-left:.45rem;font-size:.72rem;color:var(--faint)}
.ab-step-io{display:none;margin:.35rem 0 .2rem;flex-direction:column;gap:var(--sp-1,.25rem)}
.ab-steps li.ab-step-open .ab-step-io{display:flex}
/* one styled element per direction: sent (blue) / received (green) */
.ab-io{background:var(--ab-inset);border:1px solid var(--border);padding:.4rem .55rem .45rem}
.ab-io-in{border-left:3px solid var(--blue)}
.ab-io-out{border-left:3px solid var(--green-soft)}
.ab-io-tag{display:block;font-size:.62rem;font-weight:600;text-transform:uppercase;
  letter-spacing:.08em;color:var(--faint);margin-bottom:.25rem}
.ab-io-in .ab-io-tag{color:var(--blue)}
.ab-io-out .ab-io-tag{color:var(--green-soft)}
.ab-io pre{margin:0;color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:.72rem;line-height:1.5;white-space:pre-wrap;word-break:break-word;
  max-height:10rem;overflow-y:auto}
.ab-summary{color:var(--text);font-size:.85rem;margin:.3rem 0;line-height:1.55}
.ab-applied{display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);color:var(--green-soft);font-size:.78rem;font-weight:600}
.ab-err{color:var(--red);font-size:.85rem}
/* the right hamburger — the one-and-only way in; pulses once on load */
.ab-toggle{position:fixed;top:.55rem;right:1rem;z-index:40;width:2.55rem;height:2.55rem;
  border:1px solid var(--border2);cursor:pointer;
  background:var(--panel);color:var(--ab-ink);font-size:1.15rem;
  display:flex;align-items:center;justify-content:center}
body.ab-open .ab-toggle{display:none}
.ab-toggle:hover{color:#fff;border-color:var(--ab-line)}
@keyframes ab-shimmer{0%{background-position:0 0}100%{background-position:200% 0}}
@keyframes ab-blip{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.5);opacity:.55}}
@media (prefers-reduced-motion:reduce){
  .ab-build.ab-busy,.ab-progress.ab-run,.ab-steps li.ab-step-live::before,.ab-sidebar{animation:none;transition:none}
}
