*{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;--amber:#d29922;--purple:#a371f7;--gold:#e3b341;--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;--amber:#9a6700;--purple:#8250df;--gold:#8a6d00;--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)}
/* A card's STATE, as a tint of its own border. Moved here from GridCSS, which had it
   only for the apps catalogue: a tone modifies .card, and .card is defined here, so a
   plug that wanted "this one is going badly" either pulled the whole apps-grid sheet or
   — what actually happened — hand-rolled a 3px coloured stripe down the left edge. The
   owner on plug-life: "not a fan of the coloured lines on the left side of blocks …
   reminds me of 2019 bootstrap-esque era and generally looks dated."
   Tint the border the card already has; do not add an edge to it. */
.card.card-tone-pos{border-color:rgba(63,185,80,.45)}
.card.card-tone-info{border-color:rgba(88,166,255,.45)}
.card.card-tone-warn{border-color:rgba(210,153,34,.5)}
.card.card-tone-neg{border-color:rgba(248,81,73,.5)}
.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:var(--purple);color:var(--purple)}
.pill.warn,.pill.warning,.pill.paused{border-color:var(--amber);color:var(--amber)}
.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)}
.fb-dock{position:fixed;right:1.1rem;bottom:1.1rem;z-index:2147483000;display:flex;flex-direction:column;align-items:center;gap:var(--sp-2,.5rem);pointer-events:none}
.fb-launch,.fb-prod{pointer-events:auto;width:3.1rem;height:3.1rem;border-radius:50%;border:none;color:#fff;box-shadow:0 3px 12px rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;transition:box-shadow .12s ease,background .12s ease;user-select:none;-webkit-user-select:none}
.fb-launch{cursor:grab;background:var(--primary,#1f883d);touch-action:none}
/* Icons fill the button rather than sitting tiny in the middle. Explicit rem (not %,
   which doesn't resolve reliably for an SVG flex-item) + flex:none so the flex row
   can't collapse the SVG to a sliver. */
.fb-launch>svg{width:1.7rem;height:1.7rem;flex:none}
.fb-spoke>svg{width:1.5rem;height:1.5rem;flex:none}
.fb-prod{cursor:pointer;background:#b9503f}
.fb-launch:hover,.fb-prod:hover{box-shadow:0 4px 16px rgba(0,0,0,.5)}
.fb-launch.fb-dragging{cursor:grabbing;transition:none;box-shadow:0 6px 20px rgba(0,0,0,.55)}
/* Speed-dial spokes: a spoke is present only when the page carries that affordance
   (JS adds .fb-on); collapsed onto the FAB until .fb-expanded. The container is
   click-through so the collapsed dial's reserved column never shadows the page. */
.fb-spoke{pointer-events:none;position:relative;width:2.7rem;height:2.7rem;flex:none;border:none;border-radius:50%;background:var(--primary,#1f883d);color:#fff;display:none;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.4);opacity:0;transform:translateY(12px) scale(.85);transition:opacity .16s ease,transform .16s ease,background .12s ease}
.fb-spoke.fb-on{display:flex}
.fb-dock.fb-expanded .fb-spoke.fb-on{opacity:1;transform:none;pointer-events:auto}
.fb-spoke:hover{background:var(--primary-strong,#1a7f37);box-shadow:0 4px 14px rgba(0,0,0,.5)}
.fb-slbl{position:absolute;right:calc(100% + .45rem);top:50%;transform:translateY(-50%);white-space:nowrap;background:var(--card,#ffffff);color:var(--text,#1f2328);border:1px solid var(--input-border,#d0d7de);font-size:.76rem;font-weight:600;padding:.2rem .5rem;box-shadow:0 2px 8px rgba(0,0,0,.3);opacity:0;pointer-events:none;transition:opacity .12s ease}
.fb-spoke:hover .fb-slbl{opacity:1}
.fb-overlay{display:none;position:fixed;inset:0;z-index:2147483001;background:rgba(0,0,0,.55);align-items:center;justify-content:center;padding:1rem}
.fb-overlay.fb-open{display:flex}
.fb-card{background:var(--card,#ffffff);color:var(--text,#1f2328);border:1px solid var(--input-border,#d0d7de);max-width:30rem;width:100%;padding:1.25rem 1.35rem;box-shadow:0 8px 40px rgba(0,0,0,.6);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
.fb-card h3{margin:0 0 .15rem;font-size:1.1rem;color:var(--text-hi,#0f1419);display:flex;align-items:center;gap:var(--sp-2,.5rem)}
.fb-card p.fb-sub{margin:.1rem 0 .9rem;font-size:.85rem;color:var(--muted,#4a535e)}
.fb-card label{display:block;font-size:.78rem;color:var(--muted,#4a535e);margin:.55rem 0 .25rem;font-weight:600}
.fb-card input.fb-title,.fb-card textarea.fb-body{width:100%;box-sizing:border-box;background:var(--panel2,#ffffff);color:var(--text,#1f2328);border:1px solid var(--input-border,#d0d7de);padding:.5rem .6rem;font-size:.9rem;font-family:inherit}
.fb-card textarea.fb-body{min-height:7rem;resize:vertical}
.fb-card input.fb-title:focus,.fb-card textarea.fb-body:focus{outline:none;border-color:#3fb950}
.fb-actions{display:flex;gap:var(--sp-2,.5rem);justify-content:flex-end;margin-top:1rem}
.fb-btn{cursor:pointer;font-size:.85rem;font-weight:600;padding:.45rem .85rem;border:1px solid var(--input-border,#d0d7de);background:var(--panel2,#ffffff);color:var(--text,#1f2328)}
.fb-btn.fb-primary{background:var(--primary,#1f883d);border-color:rgba(0,0,0,.18);color:#fff}
.fb-btn:disabled{opacity:.55;cursor:default}
.fb-msg{font-size:.82rem;margin-top:.6rem;min-height:1rem}
.fb-msg.fb-ok{color:#3fb950}.fb-msg.fb-err{color:#f85149}
/* Idea capture — the DIAL ITSELF morphs into the input. #fb-morph is a right-aligned
   flex row wrapping the round launch button (its right cap). Collapsed it is exactly the
   3.1rem circle. Open, its WIDTH transitions to the pill width, growing LEFTWARD (the
   right edge — where the launch button sits — stays pinned at the dock's corner anchor),
   revealing the input in the opened rectangle. border-radius is half the height at every
   frame, so the ends are always semicircular (circle → pill, never a square corner). We
   animate width + the input's opacity only (no reflow jank). The morph is purely visual:
   it never writes to POS_KEY and dragging is disabled while open, so the persisted
   corner offset stays the collapsed dial's. */
.fb-morph{pointer-events:auto;display:flex;align-items:center;justify-content:flex-end;flex-direction:row;width:3.1rem;height:3.1rem;overflow:hidden;background:var(--primary,#1f883d);border-radius:var(--r-pill,999px);box-shadow:0 3px 12px rgba(0,0,0,.45);transition:width .2s ease,box-shadow .12s ease}
.fb-morph.fb-morph-open{width:min(22rem,calc(100vw - 2.4rem));box-shadow:0 4px 18px rgba(0,0,0,.5)}
/* Inside the morph the launch button loses its own round background/shadow — the morph
   pill is the surface now — so it reads as the pill's right cap, not a nested circle. */
.fb-morph .fb-launch{background:transparent;box-shadow:none;flex:none}
.fb-morph.fb-morph-open .fb-launch{cursor:pointer}
/* The idea input is a TRANSPARENT field sitting on the solid-green pill, so ANY
   background or box-shadow it paints shows as a lighter/different-green patch over the
   pill. Global app-shell rules give inputs a panel background (input{background:var(--panel2,#ffffff)})
   and a green focus glow (input:focus{box-shadow:rgba(46,160,67,.22) 0 0 0 3px}); a browser
   can also paint an autofill background. We pin background:transparent + box-shadow:none
   with !important across EVERY state (base/focus/hover/active/autofill) so the whole pill
   stays one flat colour with no seam or patch, regardless of theme or global input styling. */
.fb-idea-input,.fb-idea-input:focus,.fb-idea-input:hover,.fb-idea-input:active,.fb-idea-input:focus-visible,.fb-idea-input:-webkit-autofill{flex:1 1 auto;min-width:0;width:0;box-sizing:border-box;background:transparent!important;background-color:transparent!important;background-image:none!important;color:#fff;border:none;outline:none;box-shadow:none!important;-webkit-box-shadow:none!important;font-size:.92rem;font-family:inherit;padding:0 .2rem 0 1rem;height:100%;opacity:0;transition:opacity .16s ease}
/* Chrome paints autofilled fields with an internal yellow/green tint via a box-shadow
   trick; a long transition keeps that tint from ever compositing onto the pill. */
.fb-idea-input:-webkit-autofill{-webkit-text-fill-color:#fff!important;transition:background-color 9999s ease,opacity .16s ease}
.fb-morph.fb-morph-open .fb-idea-input{opacity:1}
.fb-idea-input::placeholder{color:rgba(255,255,255,.75)}
/* The right-cap glyph swaps: the info dial when collapsed, the submit arrow when open. */
.fb-launch .fb-glyph-send{display:none}
.fb-morph.fb-morph-open .fb-launch .fb-glyph-dial{display:none}
.fb-morph.fb-morph-open .fb-launch .fb-glyph-send{display:block}
/* Confirmation toast, mirroring .fb-msg colours. Floats just above the dial; auto-dismissed. */
.fb-idea-toast{position:absolute;right:0;bottom:calc(100% + .55rem);white-space:nowrap;background:var(--card,#fff);color:var(--text,#1f2328);border:1px solid var(--input-border,#d0d7de);border-radius:var(--r-pill,999px);font-size:.82rem;font-weight:600;padding:.4rem .8rem;box-shadow:0 3px 14px rgba(0,0,0,.4);opacity:0;pointer-events:none;transition:opacity .16s ease}
.fb-idea-toast.fb-idea-toast-show{opacity:1}
.fb-idea-toast.fb-ok{color:#3fb950}.fb-idea-toast.fb-err{color:#f85149}
/* Review-feedback overlay: message + screenshot picker (paste/attach), scoped .fb-rv-*.
   Mirrors the shared comment ImagePicker UX but self-contained so it works on ANY app the
   HUD is injected into (no dependency on the comments assets being on the page). */
.fb-rv-imgrow{display:flex;align-items:center;gap:var(--sp-2,.5rem);flex-wrap:nowrap;min-width:0;margin:.55rem 0 .2rem}
.fb-rv-imgbtn{display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-2,.5rem);box-sizing:border-box;flex:none;cursor:pointer;font-size:.85rem;color:var(--text,#1f2328);border:1px solid var(--input-border,#d0d7de);padding:.4rem .8rem;background:var(--panel2,#ffffff);white-space:nowrap}
.fb-rv-imgbtn:hover{border-color:var(--primary,#2ea043)}
.fb-rv-imgbtn input[type=file]{display:none}
.fb-rv-imgname{font-size:.82rem;color:var(--muted,#4a535e);flex:1 1 auto;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fb-rv-imgclear{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;flex:none;background:none;border:0;color:var(--muted,#4a535e);cursor:pointer;font-size:1rem;line-height:1;padding:.2rem .35rem}
.fb-rv-imgclear[hidden]{display:none}
.fb-rv-imgclear:hover{color:#f85149}
.fb-rv-preview{margin:.2rem 0}
.fb-rv-preview img{display:block;max-width:min(100%,320px);max-height:200px;border:1px solid var(--input-border,#d0d7de)}
.fb-rv-hint{margin:.35rem 0 0;font-size:.76rem}
/* ---- Review ANNOTATION (S3): draw over the LIVE dimmed viewport, then attach the
   annotated screen. #fb-annot is a full-viewport layer that sits UNDER the dock (so the
   dial stays tappable as the ✓ finish button) but OVER the page. .fb-annot-dim is the
   translucent scrim that dims the page so strokes read; .fb-annot-canvas is the pen
   surface. Scoped .fb-annot-* / .fb-an-*. touch-action:none so a finger draws instead of
   scrolling the page under it. */
.fb-annot{position:fixed;inset:0;z-index:2147482900;display:none}
.fb-annot.fb-annot-on{display:block}
.fb-annot-dim{position:absolute;inset:0;background:rgba(15,17,20,.34);pointer-events:none}
.fb-annot-canvas{position:absolute;inset:0;width:100%;height:100%;touch-action:none;cursor:crosshair;display:block}
/* Pen toolbar — a compact floating bar (top-centre, clear of the bottom-right dial) with
   colour swatches + undo/clear. Big tap targets for the phone. */
.fb-an-bar{position:absolute;top:calc(env(safe-area-inset-top,0px) + .6rem);left:50%;transform:translateX(-50%);
  z-index:2;display:flex;align-items:center;gap:var(--sp-2,.5rem);background:var(--card,#fff);border:1px solid var(--input-border,#d0d7de);
  border-radius:var(--r,5px);padding:.4rem .6rem;box-shadow:0 3px 16px rgba(0,0,0,.4);
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;max-width:calc(100vw - 1.2rem);flex-wrap:wrap;justify-content:center}
.fb-an-sw{width:1.7rem;height:1.7rem;min-height:1.7rem;border-radius:50%;border:2px solid rgba(255,255,255,.9);box-shadow:0 0 0 1px rgba(0,0,0,.25);cursor:pointer;padding:0;flex:none;transition:transform .1s ease}
.fb-an-sw.fb-an-active{transform:scale(1.22);box-shadow:0 0 0 2px var(--text,#111)}
.fb-an-tool{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:50%;border:1px solid var(--input-border,#d0d7de);background:var(--panel2,#f6f8fa);color:var(--text,#111);cursor:pointer;flex:none;padding:0;min-height:2rem}
.fb-an-tool:hover{border-color:var(--primary,#2ea043)}
.fb-an-tool:disabled{opacity:.4;cursor:default}
.fb-an-tool svg{width:1.1rem;height:1.1rem}
/* Tool selector (S5): Pen / Line / Text — mutually exclusive. The active tool gets the primary
   fill so the current mode is obvious. Its own :hover is declared (the ✓-hover lesson) so a host
   page's global button:hover can't repaint it illegibly. A thin divider separates the tool group
   from the colour swatches. Big 2.1rem targets for touch. */
.fb-an-tsep{width:1px;align-self:stretch;background:var(--input-border,#d0d7de);margin:.1rem .15rem;flex:none}
.fb-an-tbtn{display:inline-flex;align-items:center;justify-content:center;width:2.1rem;height:2.1rem;border:1px solid var(--input-border,#d0d7de);background:var(--panel2,#f6f8fa);color:var(--text,#111);cursor:pointer;flex:none;padding:0;position:relative;min-height:2.1rem}
.fb-an-tbtn:hover{border-color:var(--primary,#2ea043);background:var(--panel2,#f6f8fa);color:var(--text,#111)}
.fb-an-tbtn.fb-an-active,.fb-an-tbtn.fb-an-active:hover{background:var(--primary,#2ea043)!important;border-color:var(--primary,#2ea043)!important;color:#fff!important}
.fb-an-tbtn svg{width:1.25rem;height:1.25rem}
.fb-an-tbtn .fb-an-tlabel{font-weight:800;font-size:1.05rem;line-height:1}
/* Tool sub-toolbars (S5/S6): extrude BELOW the main bar when their owning tool is active — chart-
   drawing inspired (Line: free / horizontal / vertical / parallel-channel; Shape: rectangle /
   square / circle). Echoes the hot-dial expand feel with a short slide+fade; sits centred under the
   main bar. Each carries data-subfor="<tool>" and is shown independently (anSetTool toggles
   .fb-an-sub-visible on whichever one matches) — exactly one is ever open, since tools are mutually
   exclusive, but nothing here assumes there are only two. */
.fb-an-sub{position:absolute;top:calc(100% + .45rem);left:50%;transform:translateX(-50%) translateY(-6px);
  display:none;align-items:center;gap:var(--sp-2,.5rem);background:var(--card,#fff);border:1px solid var(--input-border,#d0d7de);
  border-radius:var(--r,5px);padding:.35rem .5rem;box-shadow:0 3px 16px rgba(0,0,0,.4);opacity:0;transition:opacity .15s ease,transform .15s ease;white-space:nowrap}
.fb-an-sub.fb-an-sub-visible{display:flex;opacity:1;transform:translateX(-50%) translateY(0)}
.fb-an-lmode,.fb-an-smode{display:inline-flex;align-items:center;justify-content:center;width:2.1rem;height:2.1rem;border:1px solid var(--input-border,#d0d7de);background:var(--panel2,#f6f8fa);color:var(--text,#111);cursor:pointer;flex:none;padding:0;min-height:2.1rem}
.fb-an-lmode:hover,.fb-an-smode:hover{border-color:var(--primary,#2ea043)}
.fb-an-lmode.fb-an-active,.fb-an-lmode.fb-an-active:hover,.fb-an-smode.fb-an-active,.fb-an-smode.fb-an-active:hover{background:var(--primary,#2ea043)!important;border-color:var(--primary,#2ea043)!important;color:#fff!important}
.fb-an-lmode svg,.fb-an-smode svg{width:1.3rem;height:1.3rem}
/* Text placement input (S5): a floating field that appears where the user taps in Text mode. It's
   absolutely positioned in viewport coords; Enter/blur commits the label into the annotation list.
   Sized/coloured to preview roughly how the baked text will look. */
.fb-an-textin{position:absolute;z-index:3;display:none;min-width:5rem;max-width:70vw;background:var(--card,#fff);
  border:1px solid var(--primary,#2ea043);padding:.25rem .4rem;font-size:1rem;font-weight:700;
  box-shadow:0 3px 14px rgba(0,0,0,.4);outline:none;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
.fb-an-textin.fb-an-textin-on{display:block}
.fb-an-hint{position:absolute;bottom:calc(env(safe-area-inset-bottom,0px) + 1.3rem);left:50%;transform:translateX(-50%);z-index:2;
  background:rgba(15,17,20,.82);color:#fff;font-size:.8rem;font-weight:600;padding:.4rem .8rem;border-radius:var(--r-pill,999px);
  pointer-events:none;white-space:nowrap;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;box-shadow:0 2px 10px rgba(0,0,0,.35)}
/* When annotating, the review spoke's own launch/dial reads as a TICK finish button. The dock
   is lifted to a z above the annot layer so it stays tappable. #fb-annot-tick is a dedicated
   round ✓ button injected into the dock while annotating (the normal spokes are hidden). */
.fb-dock.fb-annotating{z-index:2147482950}
.fb-dock.fb-annotating .fb-spoke,.fb-dock.fb-annotating .fb-morph,.fb-dock.fb-annotating .fb-prod{display:none!important}
.fb-annot-tick{pointer-events:auto;width:3.4rem;height:3.4rem;border-radius:50%;border:none;background:var(--primary,#1f883d);color:#fff;
  display:none;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.5);animation:fbTickIn .18s ease}
/* Hover MATCHES the shared spoke hover (.fb-spoke:hover) so the ✓ reads correctly in BOTH themes:
   a page's global button:hover{background/color} would otherwise repaint the tick light-on-light in
   light mode (the tick is a bare <button> on the host page). Lock the primary-strong background AND
   white fg with !important so no host button:hover rule can bleed in; the ✓ glyph is currentColor. */
.fb-annot-tick:hover,.fb-annot-tick:focus-visible{background:var(--primary-strong,#1a7f37)!important;color:#fff!important;box-shadow:0 4px 14px rgba(0,0,0,.5)}
.fb-dock.fb-annotating .fb-annot-tick{display:flex}
.fb-annot-tick svg{width:1.9rem;height:1.9rem}
@keyframes fbTickIn{from{opacity:0;transform:scale(.6)}to{opacity:1;transform:none}}