/* Direction D: "דיו" ("Ink"), approved 2026-08-26 (docs/design.md). Supersedes C ("דבש",
   2026-08-25, shipped 0.55.0–0.58.1), which design.md keeps exactly as C kept B and B kept A.
   C's machinery is kept — the measured token table, the sampled spring curves, the elevation
   model, --deep as a non-inverting proof material. What changed is everything the merchant
   touches after logging in, because C changed none of it: three directions in a row were a
   single stylesheet edit, and a single stylesheet edit is a repaint.
   The one measured difference that drives the rest: C's accent carried white at 1.84:1 and
   could therefore only ever be a FILL, which is why every bubble, bar and button was a
   saturated block. D's indigo clears AA both ways, so --tint and --tint-t are the same value
   in light and the accent stops shouting. */
:root{color-scheme:light;
/* MEASURED, never picked by eye — scripts/measure-palette.mjs prints every pair below and
   test/html.test.ts asserts them. The binding surface is the one where text has LEAST
   contrast: in light the darkest surface (--fill), in dark the lightest (--fill again — its
   ROLE survives the scheme even though its lightness order inverts). */
--bg:#EFF2F7;--surf:#FFFFFF;--fill:#E7EBF2;--ink:#0D1220;--ink2:#59627A;--hair:#DCE1EA;
/* --deep is the one surface that does NOT invert, kept from C because it was the part of C
   that worked: it is not "the dark version of a card", it is a material — the surface a
   claim gets PROVED on (hero, featured plan, pull-quote). Because it ignores the scheme,
   text on it never resolves through --ink; it uses --on-deep, --on-deep2 and --tint-d,
   each measured against this exact value and nothing else. */
--deep:#0B0E17;--on-deep:#F2F5FA;--on-deep2:#98A3B8;--hair-deep:#242B3A;
--grain:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='.055'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
/* THE ACCENT IS THREE TOKENS, AND THE COUNT WENT DOWN FOR A REASON. C's honey carried white
   at 1.84:1, so it could never be a foreground: it needed --on-tint near-black in both
   schemes plus a separate --tint-t walked down its own hue to clear AA on white, and the
   only thing it could do on screen was FILL. That is why every Lucy bubble was a saturated
   block that read like a warning. This indigo carries white at 6.46:1 AND clears 5.41:1 as
   text on --fill, so --tint and --tint-t are THE SAME VALUE in light: one accent that can be
   a button, a link, an active row and a focus ring without changing identity. --tint-t
   diverges only in dark, where the saturated indigo drops to 2.26:1 on --fill. --tint-d is
   the same pale indigo, and exists because --deep does not invert: one surface that ignores
   the scheme needs one partner that ignores it too. */
--tint:#4F39F6;--on-tint:#FFFFFF;--tint-t:#4F39F6;--tint-d:#A5B4FC;
--ok:#0F7A3D;--warn:#9A4B00;--stop:#C1123A;
/* Type as a scale, in rem — so a merchant who raises their browser's text size scales the
   LAYOUT with it instead of breaking it. Assistant replaces Heebo: Heebo is Roboto's Hebrew
   derivative and reads like a system default, which is most of why the console looked cheap
   next to its own landing page. Assistant was already one of the four faces the widget
   offered merchants (widgetConfig.ts), so this is a default moving, not a face arriving. */
--t-display:clamp(2.4rem,6vw,4rem);--t-title:1.375rem;--t-head:1.0625rem;--t-body:.9375rem;--t-cap:.8125rem;--t-micro:.6875rem;
/* SPACE IS A SCALE NOW. Colour, type, radius, elevation and motion were all tokenized and
   space was not — every margin and padding in this sheet was a hand-typed rem, which is why
   no two cards ever agreed on their own padding and why changing the rhythm meant a
   find-and-replace across 550 lines. Six steps on a 4px grid, named by role rather than by
   size so a component asks for the gap it means. */
--s1:.25rem;--s2:.5rem;--s3:.75rem;--s4:1rem;--s5:1.5rem;--s6:2rem;--s7:3rem;--s8:4.5rem;
/* Geometry. The pill stays a real member of the scale rather than a value the launcher is
   allowed to use: a pill says "press me" without a border, a 10px rectangle says "type in
   me", and keeping them distinguishable is the whole job of having both. Tighter than C
   across the board — 18px radii on a dense console read as toys, not as software. */
--r:14px;--rs:10px;--rl:22px;
/* A shadow depicts DEPTH, not ink — mixing var(--ink) turns a drop shadow into a halo the
   moment --ink goes near-white. Fixed dark literal in both schemes, on purpose; this is the
   one place the "every colour is a token" rule points the wrong way, and widget.ts carries
   the same exception for the same reason. Two layers each: a tight contact shadow plus a
   wide ambient one, because a single blur reads as a smudge rather than as height. */
--e1:0 1px 1px color-mix(in srgb,#070A12 4%,transparent),0 1px 3px color-mix(in srgb,#070A12 6%,transparent);
--e2:0 1px 2px color-mix(in srgb,#070A12 6%,transparent),0 8px 20px color-mix(in srgb,#070A12 10%,transparent);
--e3:0 2px 6px color-mix(in srgb,#070A12 8%,transparent),0 20px 48px color-mix(in srgb,#070A12 16%,transparent);
/* Springs, not guesses at springs. Each linear() below is the real second-order response
   sampled at 25 points, so a CSS transition and the widget's own rAF integrator describe the
   same physical motion rather than two different ideas of "smooth". */
--spring:linear(0,0.043,0.14,0.258,0.376,0.487,0.584,0.667,0.736,0.793,0.838,0.874,0.903,0.925,0.943,0.956,0.967,0.975,0.981,0.986,0.989,0.992,0.994,0.995,1);
--springb:linear(0,0.062,0.202,0.366,0.526,0.664,0.776,0.861,0.922,0.964,0.99,1.005,1.012,1.015,1.015,1.013,1.01,1.008,1.006,1.004,1.002,1.001,1.001,1,1);
--springq:linear(0,0.045,0.146,0.266,0.387,0.499,0.597,0.68,0.748,0.803,0.848,0.883,0.91,0.931,0.948,0.96,0.97,0.978,0.983,0.987,0.991,0.993,0.995,0.996,1);
--mono:ui-monospace,SFMono-Regular,Menlo,monospace}
/* Thirteen values and nothing else — the payoff of the rule that no colour in this sheet
   escapes the token table. Every surface, button, tab, tint and hairline below inverts on
   its own because each one names a token rather than a hex.
   CHOSEN, NOT DETECTED. This is an attribute, not a prefers-color-scheme query: a
   merchant's OS setting is a statement about their device, not about which palette they
   want to work in, and the same person on a shop iPad and a laptop can want different
   answers. Light is what everyone gets first; dark is opt-in, per browser.
   ELEVATION INVERTS WITH THE SCHEME, and this is the part a "just darken everything" pass
   gets wrong. Light mode separates surfaces with SHADOW, so --fill is DARKER than --surf.
   Dark mode separates them with LIGHTNESS, so --fill is LIGHTER than --surf and --bg is
   pure black underneath both. The roles are stable — --fill is always the recessed
   control, --surf always the card — but their lightness order is not, and the previous
   palette's rule ("--sunk must be the darkest surface") was true of A and is false here.
   Separations: surf/fill 1.19 (light: 1.23), surf/bg 1.12 (1.12).
   --ok/--warn/--stop are RE-PICKED, not inverted: the light values were chosen to hold
   white text and measure 1.4–2.1 against the dark --fill. color-scheme travels with the
   tokens so native controls (input[type=color], scrollbars, autofill) follow. */
:root[data-theme="dark"]{color-scheme:dark;
--bg:#080A10;--surf:#141821;--fill:#232935;--ink:#F3F5F9;--ink2:#9BA5BA;--hair:#2E3542;
/* --deep and its three partners do NOT appear here. That is the point of them. */
/* --tint keeps its light value: white still clears 6.46:1 on it, so a filled button is the
   same button in both schemes. Only --tint-t moves, because the saturated indigo falls to
   2.26:1 as TEXT on the dark --fill. */
--tint:#4F39F6;--on-tint:#FFFFFF;--tint-t:#A5B4FC;
--ok:#34D399;--warn:#FBBF24;--stop:#FB7185}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{font:400 var(--t-body)/1.5 Assistant,system-ui,sans-serif;font-optical-sizing:auto;font-variant-numeric:tabular-nums;margin:0;background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased}
/* Hierarchy is weight + size + LEADING as a set, never size alone (Apple's typography
   rule). Tracking is size-specific and NEVER POSITIVE: Apple tightens large text and
   loosens small text, and the second half of that does not survive contact with Hebrew,
   whose word shapes letter-spacing damages and which has no capitals to space out. So
   large text tightens and everything else sits at 0. Same reason a th carries no
   uppercase and no tracking (a rule test/html.test.ts pins). */
h1{font-size:var(--t-title);line-height:1.2;font-weight:700;letter-spacing:-.012em;margin:0 0 1rem}
h2{font-size:var(--t-head);line-height:1.3;font-weight:600;margin:0 0 .5rem}
h3{font-size:var(--t-body);line-height:1.35;font-weight:600;margin:0 0 .25rem}
.display{font-size:var(--t-display);line-height:1.06;font-weight:700;letter-spacing:-.025em}
/* Translucent chrome, with content scrolling UNDER it rather than an opaque strip that
   consumes a fixed band of every page. color-mix over a token, never an 8-digit hex — an
   #RRGGBBAA does not follow a remapped token, and following the tokens is the whole point
   of having them. */
header{position:sticky;top:0;z-index:20;padding:.5rem 1rem;display:flex;gap:.125rem;align-items:center;flex-wrap:wrap;background:color-mix(in srgb,var(--surf) 72%,transparent);-webkit-backdrop-filter:saturate(180%) blur(20px);backdrop-filter:saturate(180%) blur(20px)}
/* A scroll EDGE, not a hairline: the fade only exists where floating chrome actually
   overlaps content, which is what a 1px border can never say. */
header::after{content:"";position:absolute;inset-inline:0;top:100%;block-size:14px;pointer-events:none;background:linear-gradient(color-mix(in srgb,var(--bg) 88%,transparent),transparent)}
header a{color:var(--ink2);text-decoration:none;font-weight:500;font-size:var(--t-cap);display:inline-flex;gap:.4rem;align-items:center;padding:.44rem .6rem;border-radius:var(--rs)}
header a:hover{background:var(--fill);color:var(--ink)}
header .brand{font-size:1.0625rem;font-weight:700;letter-spacing:-.022em;color:var(--ink);margin-inline-end:.5rem}
header .brand:hover{background:none}
header form{margin:0}
header button{padding:.4rem .7rem;font-size:var(--t-cap)}
.i{inline-size:18px;block-size:18px;flex:none;fill:currentColor}
main{max-width:56rem;margin:0 auto;padding:1.5rem 1.25rem 4rem}
/* The three-pane desk needs the whole window; the reading surfaces want a measure. :has
   lets the shell say so without a second page() parameter every caller has to pass. */
main:has(.desk){max-width:92rem}
main:has(.land){max-width:64rem}
/* UNDERLINED BY DEFAULT, and the tint is the second signal rather than the only one. Colour
   alone does not distinguish a link inside a paragraph (WCAG 1.4.1) — found by an accesslint
   run over the rendered landing page, on the support mailto in the pricing card, which is
   exactly the kind of link that only ever appears mid-sentence. The components that are
   already buttons or rows (.cta, .tabs, .rail, a.btn, .steps) each turn it back off, which
   is the right direction for the default to point. */
main a{color:var(--tint-t);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px}
main a:hover{text-decoration-thickness:2px}
/* ── surfaces ─────────────────────────────────────────────────────────────── */
.card,.pane,.rail,.ctx,.tile,.tablewrap{background:var(--surf);border-radius:var(--r);box-shadow:var(--e1)}
.card{padding:1rem 1.1rem;margin:.9rem 0}
.card h3,.card h2{margin:0 0 .25rem;font-size:var(--t-head);font-weight:600;display:flex;gap:.5rem;align-items:center}
.card p{margin:0 0 .75rem;color:var(--ink2);font-size:var(--t-cap)}
.pane{padding:var(--s4) var(--s5);min-inline-size:0}
/* Inside the desk the pane is a column: a heading that stays put, a transcript that scrolls,
   and a composer pinned to the bottom edge where a composer belongs. It used to be one
   scrolling block, so on a long thread the reply box was somewhere below the fold. */
.desk .pane{display:flex;flex-direction:column;padding:0;overflow:hidden}
.desk .pane>h1{flex:none;margin:0;padding:var(--s3) var(--s5);border-block-end:1px solid var(--hair);font-size:var(--t-head);display:flex;gap:var(--s2);align-items:center}
.desk .pane .thread{flex:1;min-block-size:0;overflow-y:auto;overscroll-behavior:contain;margin:0;padding:var(--s4) var(--s5)}
.desk .pane form.reply{flex:none;margin:0;padding:var(--s3) var(--s5);border-block-start:1px solid var(--hair);background:var(--surf)}
.desk .pane .empty{margin:auto;padding:var(--s6)}
.tablewrap{overflow-x:auto}
table{border-collapse:collapse;inline-size:100%}
th,td{text-align:right;padding:.6rem .75rem;border-bottom:1px solid var(--hair);white-space:nowrap}
/* No uppercase, no tracking: Hebrew has no capitals and letter-spacing damages its word
   shapes. Hierarchy comes from weight and colour. */
th{font-size:var(--t-cap);font-weight:600;color:var(--ink2)}
tbody tr:last-child td{border-bottom:0}
tbody tr:hover{background:var(--fill)}
/* Every tint below is color-mix over a token, never a baked 8-digit hex. */
.tag{display:inline-flex;align-items:center;gap:.3rem;padding:.15rem .55rem;border-radius:999px;font-size:var(--t-micro);font-weight:600;background:var(--fill);color:var(--ink2)}
.tag.escalated{background:color-mix(in srgb,var(--stop) 10%,transparent);color:var(--stop)}
.tag.active{background:color-mix(in srgb,var(--ok) 10%,transparent);color:var(--ok)}
/* A closed thread is quieter, not unreadable: same text colour, hollow chip. opacity on
   text is an unwritten contrast decision and took this chip to 2.2:1 once already. */
.tag.closed{background:none;box-shadow:inset 0 0 0 1px var(--hair)}
/* Status as colour ALONE would be inaccessible, so every dot ships beside a .vh label. */
.dot{inline-size:.5rem;block-size:.5rem;border-radius:999px;background:var(--ink2);flex:none}
.dot.active{background:var(--ok)}.dot.escalated{background:var(--stop)}.dot.closed{background:var(--hair)}
/* ── the overview (0.59.0) ───────────────────────────────────────────────── */
/* Home for a merchant whose assistant answers the messages: what happened, what still needs
   a human, and what it cost not to know something. Three questions, in that order. */
main:has(.ov){max-inline-size:64rem}
.ov{padding-block-end:var(--s8)}
.ovhead{display:flex;gap:var(--s4);align-items:flex-end;justify-content:space-between;margin-block-end:var(--s5)}
.ovhead h1{margin:0}
/* A sparkline is a SHAPE. The axis, the values and the window selector live on the analytics
   page; putting them here too would make the overview a second analytics page instead of an
   answer to "is anything happening". */
.spark{display:flex;align-items:flex-end;gap:2px;block-size:2.5rem;inline-size:12rem;flex:none}
/* Mixed over --fill, NOT over transparent. A colour mixed with transparent is painted over
   whatever is behind it, and behind it in dark is a near-black page — so a 28% tint that read
   as a pale indigo bar in light rendered as an all-but-invisible smudge in dark. Mixing over a
   token means the bar follows the scheme instead of dissolving into it. --tint-t rather than
   --tint for the same reason: it is the one that moves when the scheme does. */
.spark i{flex:1;background:color-mix(in srgb,var(--tint-t) 45%,var(--fill));border-radius:2px 2px 0 0;min-block-size:6%}
.spark i:last-child{background:var(--tint-t)}
.ovsec{margin-block-start:var(--s6)}
.ovsec>h2{font-size:var(--t-head);margin:0 0 var(--s3)}
.needs{list-style:none;margin:0;padding:0;background:var(--surf);border:1px solid var(--hair);border-radius:var(--r);overflow:hidden}
.needs li+li{border-block-start:1px solid var(--hair)}
.needs a{display:flex;gap:var(--s3);align-items:flex-start;padding:var(--s3) var(--s4);text-decoration:none;color:inherit}
.needs a:hover{background:var(--fill);text-decoration:none}
.needs .bd{min-inline-size:0;flex:1;display:flex;flex-direction:column;gap:.1rem}
.needs .top{display:flex;gap:var(--s2);align-items:baseline;justify-content:space-between}
.needs .nm{font-weight:600}
/* An empty to-do list is good news and should look like it, not like a missing table. */
.allclear{display:flex;gap:var(--s3);align-items:center;padding:var(--s5);background:var(--surf);border:1px solid var(--hair);border-radius:var(--r)}
.allclear .i{inline-size:26px;block-size:26px;color:var(--ok);flex:none}
.allclear b{display:block}
.allclear p{margin:.15rem 0 0}
a.tile{text-decoration:none;color:inherit;display:flex;flex-direction:column}
a.tile:hover{border-color:var(--tint);text-decoration:none}
/* ── the account menu ────────────────────────────────────────────────────── */
.acct{position:relative}
.acct>summary{list-style:none;cursor:pointer;display:grid;place-items:center;inline-size:2rem;block-size:2rem;border-radius:999px;background:var(--fill);color:var(--ink2)}
.acct>summary::-webkit-details-marker{display:none}
.acct>summary:hover{color:var(--ink)}
.acct[open]>summary{background:var(--tint);color:var(--on-tint)}
.acct-p{position:absolute;inset-block-start:calc(100% + .4rem);inset-inline-end:0;z-index:30;min-inline-size:12rem;display:flex;flex-direction:column;gap:2px;padding:var(--s1);background:var(--surf);border:1px solid var(--hair);border-radius:var(--rs);box-shadow:var(--e2)}
.acct-p form{margin:0}
.acct-p button{inline-size:100%;justify-content:flex-start;background:none;border:0;box-shadow:none;color:var(--ink);font-weight:500}
.acct-p button:hover{background:var(--fill)}
header .push{margin-inline-start:auto;display:flex;align-items:center}
/* ── the desk: rail | thread | context ────────────────────────────────────── */
/* Three panes since 0.54.0. Everything that used to stack BELOW the transcript — status
   actions, tags, export, verification — is "who is this", not "what did they say", and
   belongs beside the thread rather than under it. The context column folds away under
   1200px and the rail under 880px. */
/* THE CONSOLE FILLS THE WINDOW. Until 0.59.0 the desk was three content-height cards laid on
   the page ground with align-items:start, which meant a two-message thread left roughly
   sixty percent of a 1800px screen empty and the merchant scrolled a page instead of a list.
   An inbox is an application, not a document: the shell owns the viewport, and each of the
   three tracks scrolls inside itself. Scoped with :has so it only claims the window on the
   routes that render a .desk — every other page stays a normal scrolling document. */
/* CROSS-DOCUMENT VIEW TRANSITIONS. Every desk interaction is a full navigation — reply,
   status change, filter, opening a thread — and each one used to be a white blink. This is
   the whole animation layer for a server-rendered console: two declarations, no JavaScript,
   no framework, no client router. Browsers without it navigate exactly as before.
   The named tracks are what make it read as an application rather than a page: the rail and
   the context column are THE SAME ELEMENT across the navigation, so they hold still while
   only the thread cross-fades. Without the names the whole viewport would fade, which looks
   more expensive and communicates less. */
@view-transition{navigation:auto}
@media(prefers-reduced-motion:no-preference){
::view-transition-group(rail),::view-transition-group(ctx){animation-duration:.001s}
::view-transition-old(pane),::view-transition-new(pane){animation-duration:.22s;animation-timing-function:var(--spring)}
}
/* A person who asked for less motion gets none of it, not a faster version of it. */
@media(prefers-reduced-motion:reduce){::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none!important}}
html[data-a11y-motion] ::view-transition-group(*),html[data-a11y-motion] ::view-transition-old(*),html[data-a11y-motion] ::view-transition-new(*){animation:none!important}
.desk .rail{view-transition-name:rail}
.desk .pane{view-transition-name:pane}
.desk .ctx{view-transition-name:ctx}
body:has(.desk){block-size:100dvh;display:flex;flex-direction:column;overflow:hidden}
body:has(.desk) header{flex:none;position:static}
/* margin:0 is load-bearing, not tidying: main carries margin:0 auto for the reading pages,
   and an auto inline margin inside a flex column absorbs the free space instead of
   stretching — so the console rendered at its content width in the middle of the screen
   with the viewport claimed and unused. */
body:has(.desk) main{flex:1;min-block-size:0;max-inline-size:none;margin:0;padding:0;display:flex;flex-direction:column}
/* align-self, because a stretched inline-flex segmented control is a full-width grey bar. */
body:has(.desk) .tabs{flex:none;align-self:flex-start;margin:var(--s3) var(--s4)}
.desk{flex:1;min-block-size:0;display:grid;grid-template-columns:20rem minmax(0,1fr);align-items:stretch;border-block-start:1px solid var(--hair)}
/* Hairlines, not gaps and shadows. Three floating cards read as three unrelated documents;
   three tracks divided by a rule read as one surface with structure. */
.desk>*{min-block-size:0;overflow-y:auto;overscroll-behavior:contain;border-radius:0;box-shadow:none}
.desk .rail{border-inline-start:1px solid var(--hair)}
.desk .ctx{border-inline-end:1px solid var(--hair)}
/* The context column only exists on a thread route, so the track only exists when the
   markup brought one. :has means renderDesk does not need a second layout to switch to. */
.desk:has(.ctx){grid-template-columns:20rem minmax(0,1fr) 19rem}
.rail{padding:var(--s1)}
.rail a{display:flex;gap:var(--s3);align-items:flex-start;padding:var(--s2) var(--s3);text-decoration:none;color:inherit;border-radius:var(--rs)}
.rail a:hover{background:var(--fill);text-decoration:none}
/* The selected row is marked by a filled AND a shape change, never by colour alone: the
   inline-start bar survives high-contrast mode and greyscale, where a 12% wash does not. */
.rail a[aria-current]{background:color-mix(in srgb,var(--tint) 10%,transparent);box-shadow:inset 3px 0 0 var(--tint)}
/* A person-shaped anchor for the eye to land on. Not a photo we do not have — two digits of
   their own number, which differs per row and needs no network. */
.av{inline-size:2rem;block-size:2rem;border-radius:999px;flex:none;display:grid;place-items:center;background:var(--fill);color:var(--ink2);font-size:var(--t-cap);font-weight:600;font-variant-numeric:tabular-nums}
.av-web{color:var(--ink2)}
.av .i{inline-size:16px;block-size:16px}
/* One line of the last thing said. Clamped rather than truncated by width so a Hebrew line
   breaks at a word; overflow-wrap because a pasted url has no words to break at. */
.pv{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;font-size:var(--t-cap);color:var(--ink2);line-height:1.4;overflow-wrap:anywhere}
.pv b{font-weight:600;color:var(--ink2)}
.rail .bd{min-width:0;flex:1;display:block}
.rail .dot{margin-top:.45rem}
.rail .top{display:flex;gap:var(--s2);align-items:baseline;justify-content:space-between}
.rail .nm{font-weight:600;font-size:var(--t-body);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rail .bd{display:flex;flex-direction:column;gap:.1rem}
.rail .meta{display:flex;gap:.35rem;align-items:center;font-size:var(--t-micro)}
/* Sticky, because the rail is the scroll container: a search box that scrolls out of
   reach on a 200-row list is the thing it was added to fix. */
.railsearch{position:sticky;top:0;z-index:1;background:var(--surf);padding:.4rem;border-bottom:1px solid var(--hair);margin:-.25rem -.25rem .25rem}
.railsearch input{inline-size:100%}
.rail a.railmore{justify-content:center;color:var(--tint-t);font-size:var(--t-cap);font-weight:600}
/* The matched message under a search hit. A thread found by its BODY has a phone and tags
   that say nothing about the term, so without this line the row looks like a wrong result. */
.rail .hit{margin-top:.25rem;font-size:var(--t-micro);color:var(--ink2);background:var(--fill);border-radius:var(--rs);padding:.25rem .4rem;overflow-wrap:anywhere}
.ctx{padding:var(--s4);display:flex;flex-direction:column;gap:var(--s4)}
.ctx h2{font-size:var(--t-cap);color:var(--ink2);margin:0}
.ctx .bar,.ctx form.tags{margin:0}
.ctx .stack{max-width:none}
/* A full-height grid has no "below", so the old fallback — drop the context column to a
   full-width row under the panes — cannot work here. Under 1200px it simply goes away; every
   action in it also exists on the thread itself. Under 880px the rail goes too and the pane
   is the whole window, which is the right shape for a phone. */
@media(max-width:1200px){.desk:has(.ctx){grid-template-columns:20rem minmax(0,1fr)}.desk .ctx{display:none}}
@media(max-width:880px){.desk,.desk:has(.ctx){grid-template-columns:minmax(0,1fr)}.desk .rail{display:none}.desk:not(:has(.pane)) .rail,.desk:has(.rail:only-child) .rail{display:block}}
/* Visually hidden but read aloud — for labels the layout has no room to show. */
.vh{position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
/* iOS segmented control: a raised pill riding a recessed track, not two links that happen
   to sit beside each other. */
/* The inset hairline is not decoration. Everywhere else --fill sits on --surf and separates
   at 1.21; the tab strip is the one place it sits on --bg, where the same pair measures
   1.08 and the track all but vanishes. A defined edge is what an iOS segmented control has
   over a grouped background, and it costs one property rather than a fourth grey. */
.tabs{display:inline-flex;gap:2px;padding:2px;background:var(--fill);border-radius:var(--rs);margin-bottom:.9rem;box-shadow:inset 0 0 0 1px var(--hair)}
.tabs a{display:inline-flex;gap:.4rem;align-items:center;padding:.4rem .8rem;border-radius:calc(var(--rs) - 2px);text-decoration:none;color:var(--ink2);font-weight:600;font-size:var(--t-cap)}
.tabs a:hover{color:var(--ink);text-decoration:none}
.tabs a[aria-current]{background:var(--surf);color:var(--ink);box-shadow:var(--e1)}
/* The analytics date range reuses the segmented shape but sits under an h1 and must not
   read as a second level of navigation. */
.ranges{margin:-.25rem 0 .5rem}
.ranges a{padding:.3rem .7rem;font-size:var(--t-micro)}
.empty{text-align:center;color:var(--ink2);padding:2.5rem 1.25rem}
/* h1 because an empty pane IS the page's main content (desk.ts emptyPane), sized like the
   h2 it replaced — the level is the document outline, not the type scale. */
.empty h1{color:var(--ink);margin:0 0 .35rem;font-size:var(--t-head)}
/* ── the transcript ───────────────────────────────────────────────────────── */
/* .msg's auto inline margins are what put a bubble on its own side, and they need a flex
   column to resolve against — .thread is layout, not spacing. */
.thread{display:flex;flex-direction:column;gap:.15rem;margin-top:1rem}
.msg{max-width:70%;padding:.5rem .8rem;border-radius:var(--rl);margin:.15rem 0;border:1px solid transparent;font-size:var(--t-cap);line-height:1.45}
.inbound{background:var(--fill);margin-inline-end:auto;border-end-start-radius:var(--rs)}
/* Lucy gets a WASH of the accent, not the accent itself. A bubble filled with a saturated
   brand colour is the single loudest object on the screen, and in a transcript the loudest
   object should be what the CUSTOMER said, not what the bot replied. The wash keeps --ink as
   the text colour, so a long answer stays as readable as the rest of the page. */
.outbound{background:color-mix(in srgb,var(--tint) 8%,var(--surf));color:var(--ink);border-color:color-mix(in srgb,var(--tint) 22%,transparent);margin-inline-start:auto;border-end-end-radius:var(--rs)}
/* Sender treatments: who sent it is a different question from which way it went — a
   campaign and an order update are both outbound, and neither is Lucy answering. */
/* ONE RULE for everything that is outbound but is NOT Lucy answering: a plain surface with a
   coloured rail on the inline-start edge naming who sent it. Three different washes was the
   old answer, and it meant the palette had to own a readable tint for every sender — which
   is how a warm beige campaign bubble survived into a cool scheme and stopped belonging to
   anything. An edge rail carries the same information in one channel that never fights the
   text, and it costs the palette nothing. */
.outbound.human{background:var(--surf);color:var(--ink);border-color:var(--hair);border-inline-start:3px solid var(--ok)}
.msg.broadcast{background:var(--surf);color:var(--ink);border-color:var(--hair);border-inline-start:3px solid var(--warn)}
.msg.notification{background:var(--surf);color:var(--ink);border-color:var(--hair);border-inline-start:3px solid var(--ink2)}
.msg.flow{background:var(--surf);color:var(--ink);border-color:var(--hair);border-inline-start:3px solid var(--tint)}
.msg .who{font-size:var(--t-micro);font-weight:700;margin-bottom:.15rem;color:var(--ink2)}
/* Every bubble now carries --ink text on a light surface, so one rule covers all of them. */
.msg .who,.msg .meta{color:var(--ink2)}
.meta{font-size:var(--t-micro);color:var(--ink2);margin-top:.15rem}
/* ── analytics ────────────────────────────────────────────────────────────── */
/* 9rem, not 8.5: a tile carries a delta and a sparkline since 0.59.0, and five of them
   still have to sit on one row of an 850px pane — a fifth tile alone on a second line reads
   as a different kind of number. */
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(9rem,1fr));gap:.6rem;margin:0 0 1rem}
.tile{padding:.8rem .9rem}
.tile .k{display:block;font-size:var(--t-micro);color:var(--ink2);font-weight:600}
.tile .v{display:block;font-size:var(--t-title);font-weight:700;line-height:1.15;letter-spacing:-.02em;margin-top:.15rem}
.tile .s{display:block;font-size:var(--t-micro);color:var(--ink2);margin-top:.1rem}
/* THE CHART HAS A SCALE (0.59.0). Every bar carried its own count floating above it: at
   thirty buckets that is thirty small numbers competing with the shape they are drawn on, and
   none of them said what a HEIGHT meant. Three gridlines answer that once, the count answers
   "which bar is that" on hover, and the peak is in the chart's own aria-label so a screen
   reader is never told to point at something.
   The plot area is .bars' CONTENT box and the gridlines are inset to exactly that box, which
   is why the labels and the date row are absolutely positioned into the padding rather than
   given grid rows of their own: a row that grows with its text moves the axis off the bars.
   visibility, not opacity: opacity on text is an unwritten contrast decision, and this sheet
   bans it everywhere else for the same reason. */
.chart{padding-inline-start:2.6rem}
.bars{position:relative;display:flex;gap:.4rem;block-size:9rem;margin:1rem 0 .5rem;padding-block:1rem 1.4rem}
.bars>div:not(.grid){position:relative;flex:1;display:flex;align-items:flex-end;justify-content:center}
/* Capped, not just flexible. A 7-day window has seven columns and a 90-day window has
   thirteen, but a range with two buckets in it turned each bar into a half-page slab —
   flex:1 says "share the width", which is the wrong instruction when there is almost
   nothing to share. */
.bars .b{inline-size:100%;max-inline-size:3.25rem;background:var(--tint);border-radius:6px 6px 2px 2px;min-block-size:2px}
.bars .n,.bars .d{position:absolute;inset-inline:0;text-align:center;font-size:var(--t-micro);color:var(--ink2);white-space:nowrap}
.bars .n{inset-block-end:100%;margin-block-end:.15rem;visibility:hidden;font-weight:600;color:var(--ink)}
.bars .d{inset-block-start:100%;margin-block-start:.25rem;overflow:hidden;text-overflow:ellipsis}
.bars>div:hover .n,.bars>div:focus-within .n{visibility:visible}
.bars>div:hover .b{background:var(--tint-t);box-shadow:0 0 0 3px color-mix(in srgb,var(--tint) 14%,transparent)}
.bars .grid{position:absolute;inset-block:1rem 1.4rem;inset-inline:0;display:flex;flex-direction:column;justify-content:space-between;pointer-events:none}
.bars .gl{position:relative;border-block-end:1px solid var(--hair)}
.bars .gl span{position:absolute;inset-inline-end:100%;margin-inline-end:.5rem;inset-block-end:0;transform:translateY(50%);font-size:var(--t-micro);color:var(--ink2);font-variant-numeric:tabular-nums}
/* The delta on a stat tile. Colour ONLY where the direction has one meaning — see the
   "good" argument in renderAnalytics: more escalations is a busy week, not a worse one. */
.tile .dl{display:inline-flex;gap:.2rem;align-items:center;font-size:var(--t-micro);font-weight:600;color:var(--ink2)}
.tile .dl .i{inline-size:14px;block-size:14px}
.tile .dl.ok{color:var(--ok)}
.tile .dl.bad{color:var(--stop)}
.tile .spark{inline-size:auto;block-size:1.5rem;margin-block-start:var(--s2)}
td .spark{inline-size:6rem;block-size:1.25rem}
.railcount{margin:0;padding:var(--s2) var(--s3);font-size:var(--fs-1);color:var(--ink-2)}
/* ── controls ─────────────────────────────────────────────────────────────── */
form.reply{display:flex;gap:var(--s2);margin-top:var(--s4);align-items:flex-end}
/* THE COMPOSER IS ONE FIELD, not a textarea beside a button. Inside the desk it reads as a
   single bordered surface the button sits within, which is what every messaging app has
   converged on and what stops the send action from competing with the transcript for weight.
   The focus ring moves to the wrapper via :focus-within so the whole control lights up. */
.desk form.reply{display:flex;gap:var(--s2);align-items:flex-end;background:var(--surf);border:1px solid var(--hair);border-radius:var(--r);padding:var(--s2);margin:var(--s3) var(--s5) var(--s4)}
.desk form.reply:focus-within{border-color:var(--tint);box-shadow:0 0 0 3px color-mix(in srgb,var(--tint) 16%,transparent)}
.desk form.reply textarea{flex:1;border:0;background:none;padding:var(--s2);resize:none;min-block-size:2.4rem;field-sizing:content;max-block-size:9rem}
.desk form.reply textarea:focus{outline:0}
.desk form.reply button{flex:none;align-self:flex-end}
/* THE TRANSCRIPT HANGS FROM THE BOTTOM. A chat that grows downward from the top leaves the
   newest message stranded in the middle of the pane with the composer far below it — which
   is what the desk did. auto margin on the first child rather than justify-content:flex-end,
   because that one makes the top of an overflowing list unreachable in a scroll container. */
.desk .pane .thread>:first-child{margin-block-start:auto}
/* The control is WRAPPED by its <label>, so the association needs no id/for pair — and a
   field can't ship without one by accident. */
label{display:block;font-size:var(--t-cap);font-weight:600;color:var(--ink2)}
label>input,label>select,label>textarea{margin-top:.25rem}
input,select,textarea{inline-size:100%;padding:.55rem .7rem;font:inherit;font-size:var(--t-body);color:var(--ink);background:var(--fill);border:1px solid transparent;border-radius:var(--rs)}
/* A radio is a control, not a field: inline-size:100% above stretched the dot across the
   line and the label's own margin-top pushed it off its text. A colour swatch sizes itself. */
input[type=radio],input[type=checkbox]{inline-size:auto;width:auto;margin:0;padding:0;accent-color:var(--tint)}
label:has(input[type=radio]),label:has(input[type=checkbox]){display:flex;gap:.45rem;align-items:center;color:var(--ink);font-weight:500}
input[type=color]{inline-size:3.5rem;block-size:2.4rem;padding:3px}
textarea{flex:1;resize:vertical}
input:focus,select:focus,textarea:focus{outline:2px solid var(--tint-t);outline-offset:-1px;background:var(--surf);border-color:var(--tint-t)}
:focus-visible{outline:2px solid var(--tint-t);outline-offset:2px}
/* a.btn rides the same rules: a link sitting in a .bar beside real buttons has to look
   like one, and a download is a GET — a form with a button would be the wrong verb. */
button,a.btn{display:inline-flex;gap:.4rem;align-items:center;justify-content:center;padding:.55rem 1rem;border:1px solid transparent;border-radius:var(--rs);background:var(--tint);color:var(--on-tint);font:inherit;font-weight:600;font-size:var(--t-cap);cursor:pointer;text-decoration:none}
button:hover,a.btn:hover{background:var(--tint-t);text-decoration:none}
button.ghost,a.btn.ghost{background:var(--surf);border-color:var(--hair);color:var(--ink)}
button.ghost:hover,a.btn.ghost:hover{background:var(--fill)}
.bar{display:flex;gap:.5rem;margin-top:.75rem;flex-wrap:wrap;align-items:center}
.stack{display:flex;flex-direction:column;gap:.6rem;max-width:26rem}
/* A fieldset per group, because eighteen controls in one flat list is a form nobody
   finishes — and <legend> is the grouping a screen reader gets for free. */
fieldset{border:0;border-top:1px solid var(--hair);margin:.9rem 0 0;padding:.75rem 0 0;min-inline-size:0}
legend{padding:0;font-size:var(--t-cap);font-weight:600;color:var(--ink2)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:.6rem .75rem}
@media(max-width:620px){.grid2{grid-template-columns:minmax(0,1fr)}}
/* The REAL widget, in an iframe, on a mock storefront — not a second widget drawn in this
   stylesheet, which is the only way the console and the storefront cannot drift apart.
   A DESKTOP-sized viewport (920px) scaled down to fit the card: at the card's own width
   the widget would cross its own 640px breakpoint and the merchant would be styling a
   phone while thinking they were styling a shop. transform, NOT zoom — zoom shrinks the
   iframe's inner viewport with it, which is the one thing this must not do. --z is the
   scale in one place, because the wrapper's height has to follow it. */
/* Controls on one side, the real widget on the other — the point of cutting eighteen knobs to
   five is that five fit beside a preview, so a merchant sees the thing they are changing while
   they change it instead of saving and going to look. */
/* A save that says nothing looks like a save that failed. Sits at the top of the section
   list, same slot the error uses, so the two can never both shout at once. */
.okmsg{display:flex;gap:var(--s2);align-items:center;padding:var(--s3) var(--s4);margin:var(--s3) 0;border-radius:var(--rs);background:color-mix(in srgb,var(--ok) 10%,var(--surf));border:1px solid color-mix(in srgb,var(--ok) 28%,transparent);color:var(--ink);font-size:var(--t-cap);font-weight:500}
.okmsg .i{color:var(--ok);flex:none}
.wdesign{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,20rem);gap:var(--s5);align-items:start}
.wdform{display:flex;flex-direction:column;gap:var(--s3)}
.wdform .bar{margin-block-start:var(--s2)}
@media(max-width:880px){.wdesign{grid-template-columns:minmax(0,1fr)}}
.wprev{--z:.66;overflow:hidden;block-size:calc(640px * var(--z));border:1px solid var(--hair);border-radius:var(--r);background:var(--fill)}
.wpreview{display:block;inline-size:920px;block-size:640px;border:0;transform:scale(var(--z));transform-origin:top right}
@media(max-width:1100px){.wprev{--z:.58}}
@media(max-width:620px){.wprev{--z:.42}}
summary{cursor:pointer;font-weight:600;font-size:var(--t-cap);color:var(--tint-t);list-style:none}
summary::-webkit-details-marker{display:none}
details{margin-top:.5rem}
.narrow{max-width:24rem;margin:3rem auto;background:var(--surf);border-radius:var(--rl);box-shadow:var(--e2);padding:1.75rem 1.6rem}
/* A login screen has one job on it, so its heading gets the room a page of cards cannot
   spare — size and leading as a pair, not size alone. */
.narrow h1{font-size:1.625rem;line-height:1.15;letter-spacing:-.02em;margin:0 0 .35rem}
.narrow .sub{margin:0 0 1.25rem;font-size:var(--t-cap)}
.narrow button{inline-size:100%;padding:.66rem 1rem;font-size:var(--t-body)}
.narrow .foot{text-align:center;color:var(--ink2)}
.err{background:color-mix(in srgb,var(--stop) 10%,transparent);color:var(--stop);padding:.6rem .75rem;border-radius:var(--rs);margin:.75rem 0;font-size:var(--t-cap);font-weight:500}
.notice{background:color-mix(in srgb,var(--warn) 10%,transparent);color:var(--warn);padding:.6rem .75rem;border-radius:var(--rs);margin:.75rem 0;font-size:var(--t-cap);font-weight:500}
.err a,.notice a{color:inherit;text-decoration:underline}
/* 10% of a dark tint over a light page reads as a band; 10% of a LIGHT tint over near-black
   reads as noise. Same specificity as the two rules above, so it has to sit after them.
   currentColor is already --stop / --warn. */
:root[data-theme="dark"] .err,:root[data-theme="dark"] .notice{background:color-mix(in srgb,currentColor 18%,transparent)}
/* Usage meter. The fill uses logical inline-size, not width, so it grows from the inline
   start under RTL like every other bar in this stylesheet. */
.meter{background:var(--fill);border-radius:999px;block-size:.5rem;overflow:hidden;margin:0 0 .6rem}
.meter span{display:block;block-size:100%;background:var(--ok);border-radius:999px}
.meter.near span{background:var(--warn)}
.meter.over span{background:var(--stop)}
.done{color:var(--ok);font-weight:600;display:inline-flex}
/* ── the settings page: rows, not cards ───────────────────────────────────── */
/* 0.59.0 made the console an application and left this page a document: ten cards in one
   column, each with a heading, a paragraph and a form, all at the same weight — so nothing
   could be scanned, nothing could be found twice, and the kill switch sat in the same visual
   register as a usage meter. It is the desk shell now (.desk), which is where the full
   height, the hairline tracks and the named view transitions already live: a section rail
   that navigates, and a pane of ROWS. A row is what a setting is: a name and one line of
   help on one side, its control on the other, a hairline between. */
.set{grid-template-columns:16rem minmax(0,1fr)}
.setrail{display:flex;flex-direction:column;gap:2px;padding:var(--s2)}
.sethead{padding:var(--s2) var(--s3) var(--s3)}
.sethead b{display:block;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sethead span{display:block;font-size:var(--t-micro);color:var(--ink2)}
.setrail a{align-items:center;gap:var(--s2)}
.setrail .nm{flex:1;font-weight:500;font-size:var(--t-cap)}
.setrail a[aria-current] .nm{font-weight:600}
.setrail .i{color:var(--ink2)}
.setrail a[aria-current] .i{color:var(--tint-t)}
.setrail .mk{display:inline-flex;color:var(--ink2)}
/* Lucy's own state is not a section and not a step, so it is not in the list — it is what
   the page is describing, pinned under it on every section. */
/* The accessibility launcher is fixed to the bottom inline-start corner, which under RTL is
   the same corner this rail ends in — so the pause button gets the room to clear it rather
   than sitting under it. */
.setfoot{margin-block-start:auto;padding:var(--s3) var(--s3) 3.6rem;border-block-start:1px solid var(--hair);display:flex;flex-direction:column;gap:var(--s2);align-items:flex-start}
.setfoot b{display:flex;gap:var(--s2);align-items:center;font-size:var(--t-cap)}
.setfoot>span{font-size:var(--t-micro);color:var(--ink2)}
.setfoot form{margin:0}
.setfoot button{inline-size:100%}
/* The pane's own scroller, the same shape the transcript uses: the header stays, the rows
   move. A measure cap, because a settings row read edge-to-edge on a 1800px screen puts its
   control a hand's width from its label. */
.setbody{flex:1;min-block-size:0;overflow-y:auto;overscroll-behavior:contain;padding:var(--s5);max-inline-size:60rem}
.set .pane>h1 .sub{font-weight:400;font-size:var(--t-cap);margin:0}
.rows{background:var(--surf);border:1px solid var(--hair);border-radius:var(--r);overflow:hidden;margin:0 0 var(--s5)}
.rows>*+*{border-block-start:1px solid var(--hair)}
.row{display:flex;gap:var(--s4);align-items:center;justify-content:space-between;padding:var(--s3) var(--s4);margin:0}
.rl{min-inline-size:0;flex:1;color:var(--ink);font-weight:400;font-size:var(--t-body)}
.rl b{display:block;font-weight:600}
.rl>span{display:block;margin-block-start:.1rem;color:var(--ink2);font-size:var(--t-cap)}
.rc{flex:none;display:flex;gap:var(--s2);align-items:center;max-inline-size:22rem}
.rc form{margin:0}
.rc input,.rc select,.rc textarea{min-inline-size:12rem}
/* A swatch and a tick size themselves; the rule above is for fields that hold text. */
.rc input[type=color],.rc input[type=checkbox],.rc input[type=radio]{min-inline-size:0}
.rc .skel{inline-size:7rem;margin:.2rem 0}
.rc .meter{inline-size:9rem;margin:0}
/* A row that opens. Closed it is a row; open, its form drops under the hairline. The one
   thing that must never happen is a <details> inside a <label> — the summary would toggle
   the label's control — which is why fieldRow and rowOpen are two primitives, not one. */
details.row{display:block;padding:0}
details.row>summary{display:flex;gap:var(--s4);align-items:center;justify-content:space-between;padding:var(--s3) var(--s4);color:inherit;font-weight:400;font-size:var(--t-body)}
details.row>summary:hover{background:var(--fill)}
details.row .lnk{color:var(--tint-t);font-weight:600;font-size:var(--t-cap)}
details.row>summary>.rc>.i{color:var(--ink2)}
details.row[open]>summary>.rc>.i{transform:rotate(180deg)}
.rbody{padding:var(--s4);border-block-start:1px solid var(--hair);background:var(--fill)}
.rbody>:first-child{margin-block-start:0}
.rbody .stack,.rbody form{max-width:30rem}
.rbody code{background:var(--surf)}
/* The paragraph walls, folded. Nine of the ten cards carried three to five lines of Hebrew
   that a merchant scrolled past on every visit. */
details.why{margin:var(--s2) 0 0}
details.why>summary{font-size:var(--t-cap);display:inline-flex;gap:.3rem;align-items:center}
details.why>summary::before{content:"?";display:grid;place-items:center;inline-size:1.05rem;block-size:1.05rem;border-radius:999px;background:var(--fill);color:var(--ink2);font-size:var(--t-micro);font-weight:700}
details.why>div{margin-block-start:.35rem;color:var(--ink2);font-size:var(--t-cap);max-inline-size:44rem}
.setbody>details.why{margin-block-end:var(--s5)}
/* A note about the group it follows sits against it, not a row's gap below it. */
.setbody>.rows+details.why{margin-block-start:calc(-1 * var(--s4))}
.grouphead{font-size:var(--t-cap);font-weight:600;color:var(--ink2);margin:0 0 var(--s2)}
/* ── the flow canvas (0.61.0) ── The page stays RTL; only the canvas is LTR so node coordinates,
   the SVG and the port maths are plain left-handed numbers. Text inside a node is RTL again.
   Every colour is a token or a color-mix over one, so dark mode needs nothing here. */
.flow:has(.ctx){grid-template-columns:15rem minmax(0,1fr) 22rem}
.flow .pane{display:flex;flex-direction:column;overflow:hidden;position:relative;padding-block-end:var(--s3)}
.flow .pane>h1{display:flex;flex-wrap:wrap;gap:var(--s2);align-items:baseline;margin:0 0 var(--s3)}
.flow .pane>h1 .sub{font-weight:400;font-size:var(--t-cap);color:var(--ink2);margin:0}
.flowrail{display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3);overflow-y:auto}
.flowrail h2{font-size:var(--t-cap);color:var(--ink2);margin:var(--s2) 0 0}
.flowrail form,.flowrail .stack{margin:0;max-width:none}
.flowrail .btn{justify-content:flex-start}
.palette{display:grid;grid-template-columns:1fr 1fr;gap:var(--s1)}
.palette button{justify-content:flex-start;font-weight:500;padding-inline:var(--s2)}
.canvas{flex:1;min-block-size:16rem;position:relative;overflow:hidden;direction:ltr;touch-action:none;cursor:grab;border:1px solid var(--hair);border-radius:var(--r);background:var(--bg);background-image:radial-gradient(var(--hair) 1px,transparent 1px);background-size:20px 20px}
.canvas.panning{cursor:grabbing}
.canvas>svg{position:absolute;inset:0;inline-size:100%;block-size:100%;overflow:visible;pointer-events:none}
.edge{fill:none;stroke:var(--ink2);stroke-width:2}
.edge.on{stroke:var(--tint);stroke-width:2.5}
.edge.temp{display:none;stroke:var(--tint);stroke-dasharray:6 4}
.edge.temp.live{display:block}
.stage{position:absolute;left:0;top:0;transform-origin:0 0}
.node{position:absolute;inline-size:14rem;direction:rtl;background:var(--surf);border:1px solid var(--hair);border-radius:var(--r);box-shadow:var(--e1);user-select:none}
.node.sel{border-color:var(--tint);box-shadow:0 0 0 3px color-mix(in srgb,var(--tint) 18%,transparent),var(--e2)}
.node.bad{border-color:var(--stop);box-shadow:0 0 0 3px color-mix(in srgb,var(--stop) 18%,transparent)}
.node.hot{border-color:var(--ok);box-shadow:0 0 0 3px color-mix(in srgb,var(--ok) 22%,transparent)}
.node.drag{box-shadow:var(--e3)}
.nhead{inline-size:100%;display:flex;gap:var(--s2);align-items:center;justify-content:flex-start;padding:var(--s2) var(--s3);margin:0;border:0;border-radius:var(--r) var(--r) 0 0;background:color-mix(in srgb,var(--tint) 9%,var(--surf));color:var(--ink);font-size:var(--t-cap);font-weight:600;cursor:grab}
.nhead:hover{background:color-mix(in srgb,var(--tint) 15%,var(--surf))}
.nhead .i{color:var(--tint-t)}
.nhead:focus-visible{outline:2px solid var(--tint-t);outline-offset:-2px}
.nbody{padding:var(--s2) var(--s3);font-size:var(--t-cap);color:var(--ink2);overflow-wrap:anywhere;min-block-size:1.4em;white-space:pre-line}
.nbadge{padding:.15rem var(--s3) .3rem;font-size:var(--t-micro);color:var(--tint-t);font-weight:600}
.flsum{display:flex;flex-direction:column;gap:.15rem;margin:0}
.flsum>div{display:flex;justify-content:space-between;gap:var(--s2);font-size:var(--t-cap)}
.flsum dt{color:var(--ink2)}
.flsum dd{margin:0;font-weight:600;font-variant-numeric:tabular-nums}
.nout{position:relative;display:flex;justify-content:space-between;align-items:center;padding:.2rem var(--s3);font-size:var(--t-micro);color:var(--ink);border-block-start:1px solid var(--hair)}
.port{position:absolute;inline-size:.8rem;block-size:.8rem;border-radius:999px;background:var(--surf);border:2px solid var(--tint);cursor:crosshair}
.port.out{right:-.45rem;top:50%;translate:0 -50%}
.port.in{left:-.45rem;top:1rem}
.port:hover{background:var(--tint)}
.flowbar{position:absolute;bottom:var(--s4);left:var(--s5);display:flex;gap:var(--s1);direction:ltr}
.flowbar button{margin:0;min-inline-size:2.4rem}
.insp{display:flex;flex-direction:column;gap:var(--s3)}
.insp h2{color:var(--ink);font-size:var(--t-body);display:flex;gap:var(--s2);align-items:center}
.insp label{display:block;font-size:var(--t-cap);font-weight:500}
.insp label:has(input[type=checkbox]){display:flex;gap:var(--s2);align-items:center;font-weight:400}
.insp input:not([type=checkbox]),.insp select,.insp textarea{inline-size:100%;margin-top:.2rem}
.insp .opts{display:flex;flex-direction:column;gap:var(--s1)}
.insp .opts>div{display:flex;gap:var(--s1);align-items:flex-end}
.insp .opts>div>label{flex:1}
.insp .opts button{margin:0;min-inline-size:2.4rem}
.insp button{margin:0}
.conns{display:flex;flex-direction:column;gap:var(--s2)}
.conns:not(:empty){padding-block-start:var(--s2);border-block-start:1px solid var(--hair)}
.flerrs{display:flex;flex-direction:column;gap:var(--s1)}
.flerrs button.err{inline-size:100%;justify-content:flex-start;text-align:start;margin:0;font-size:var(--t-cap)}
.flstat{font-size:var(--t-cap);color:var(--ink2)}
.flstat.ok{color:var(--ok)}
.flstat.bad{color:var(--stop)}
.flowsim summary{display:flex;gap:var(--s2);align-items:center;cursor:pointer;font-weight:600;font-size:var(--t-cap)}
.flowsim .thread{display:flex;flex-direction:column;max-block-size:18rem;overflow-y:auto;margin:var(--s2) 0 0;gap:.15rem}
.flowsim .thread .msg{max-width:88%}
.flowsim form.reply{margin:var(--s2) 0 0;display:flex;gap:var(--s1);align-items:flex-end}
.flowsim form.reply textarea{flex:1;min-inline-size:0}
.flowsim form.reply button{margin:0}
.flowsim .note{align-self:center;font-size:var(--t-micro);color:var(--ink2);text-align:center;padding:.15rem .5rem}
@media(max-width:1200px){.flow:has(.ctx){grid-template-columns:12rem minmax(0,1fr) 18rem}.flow .ctx{display:flex}}
/* ponytail: desktop-first. Under 880px the three tracks stack and the canvas keeps a minimum
   height; a phone can read and tweak a flow through the inspector, not really draw one. */
@media(max-width:880px){.flow,.flow:has(.ctx){grid-template-columns:minmax(0,1fr)}.flow .canvas{min-block-size:22rem}}
.setbody .err,.setbody .notice,.setbody .okmsg{margin-block-start:0}
.setbody .wdesign{margin-block-end:var(--s5)}
.wdform .rows{margin-block-end:var(--s3)}
/* The rail is the only way to reach another section, so it may not be the thing that folds
   away: under 880px the desk hides its rail, and here it becomes a scrolling strip instead. */
@media(max-width:880px){
.set,.set:has(.ctx){grid-template-columns:minmax(0,1fr)}
.set .rail{display:flex;flex-direction:row;align-items:center;gap:var(--s1);overflow-x:auto;overflow-y:hidden;border-inline-start:0;border-block-end:1px solid var(--hair)}
.set .rail a{flex:none}
.sethead,.setfoot>span{display:none}
.setfoot{margin:0;margin-inline-start:auto;padding:var(--s1);border:0;flex-direction:row;align-items:center;gap:var(--s2)}
.setfoot button{inline-size:auto}
.setbody{padding:var(--s4)}
.set .pane>h1{flex-wrap:wrap}.set .pane>h1 .sub{flex-basis:100%}
.row,details.row>summary{flex-direction:column;align-items:stretch;gap:var(--s2)}
.rc{max-inline-size:none}
}
/* The inactive platform's fields collapse with no script: the radio is inside its label,
   so :has asks the question the sibling combinator can't. Hidden inputs still submit —
   connectStore reads only the fields belonging to the platform that was chosen. */
.fields{display:flex;flex-direction:column;gap:.6rem;margin:.5rem 0 .25rem}
label:has(input[type=radio]:not(:checked))+.fields{display:none}
/* Skeleton for the connecting state. A moving background-position, NOT an opacity pulse
   and NOT an alpha hex — both are banned in this sheet because they escape the token
   table. Two tokens and a gradient say "working" without either. */
.skel{block-size:.75rem;border-radius:var(--rs);background:var(--fill);margin:.5rem 0}
.skel.w2{max-inline-size:60%}
/* Quieter, not dimmer. opacity on text is an unwritten contrast decision — the same
   mistake .tag.closed made once, and the one .sub exists to stop callers repeating. */
.step,.step h3,.step h2{color:var(--ink2)}
.step h3,.step h2{font-weight:500}
.sub{color:var(--ink2)}
.foot{margin-top:1rem;font-size:var(--t-cap)}
/* A section heading inside a page that already has an h1: weight and colour, no size jump. */
h2.sec{font-size:var(--t-cap);font-weight:600;color:var(--ink2);margin-top:1.75rem}
form.tags{margin-top:.75rem}
/* Machine strings (tokens, ids, phone numbers) never reorder inside an RTL line. */
.mono,code{font-family:var(--mono);direction:ltr;unicode-bidi:isolate}
code{display:inline-block;background:var(--fill);padding:.15rem .4rem;border-radius:6px;font-size:var(--t-cap);word-break:break-all}
/* == direction C element vocabulary ======================================== */
/* Everything below is shared by the marketing surface AND the console, which is the point:
   B had six elements and every page invented the rest inline. A named element here is one
   a caller cannot get subtly wrong, and one that changes everywhere at once. */
/* The espresso proof surface. It does NOT invert, so nothing inside it may resolve through
   --ink/--ink2/--surf; it carries its own three partners instead. Any rule that paints text
   in here names --on-deep, --on-deep2 or --tint-d, never a scheme token. */
.deep{background-color:var(--deep);background-image:var(--grain);color:var(--on-deep);border-radius:var(--rl);padding:2.75rem 2.25rem;position:relative;overflow:hidden}
.deep h1,.deep h2,.deep h3{color:var(--on-deep)}
.deep .sub,.deep p{color:var(--on-deep2)}
.deep .accent{color:var(--tint-d)}
/* Prose links inside the espresso surface, and ONLY those. The .pill exclusions are not
   tidiness: without them this rule beat .pill's own colour and painted --tint-d text on a
   --tint fill at 1.15:1, which made the hero's primary call to action unreadable. Caught by
   an accesslint run, NOT by looking at the screenshot — the same lesson bugs.md #69 carries,
   from the opposite direction: the eye filled it in and the engine did not. */
.deep a:not(.btn):not(.pill){color:var(--tint-d)}
.deep hr{border:0;border-top:1px solid var(--hair-deep);margin:1.5rem 0}
@media(max-width:640px){.deep{padding:1.75rem 1.25rem}}
/* A pill says press me without needing a border; the 12px rectangle says type in me. That
   distinction is why --rs went up in C but the CTA left the scale entirely. */
.pill{display:inline-flex;align-items:center;justify-content:center;gap:.45rem;padding:.72rem 1.5rem;border:1px solid transparent;border-radius:999px;background:var(--tint);color:var(--on-tint);font:inherit;font-weight:600;font-size:var(--t-body);text-decoration:none;cursor:pointer}
.pill:hover{background:var(--tint-d);text-decoration:none}
.pill.quiet{background:var(--surf);border-color:var(--hair);color:var(--ink);box-shadow:var(--e1)}
.pill.quiet:hover{background:var(--fill)}
.deep .pill.quiet{background:none;border-color:var(--hair-deep);color:var(--on-deep);box-shadow:none}
.deep .pill.quiet:hover{background:var(--hair-deep)}
/* The eyebrow chip: a section label, a plan badge, a "new" flag. Never a link. */
.chip{display:inline-flex;align-items:center;gap:.4rem;padding:.3rem .8rem;border-radius:999px;background:var(--fill);color:var(--ink2);font-size:var(--t-micro);font-weight:600}
.chip.on{background:var(--tint);color:var(--on-tint)}
.deep .chip{background:none;box-shadow:inset 0 0 0 1px var(--hair-deep);color:var(--tint-d)}
/* A claim, then immediately the thing that proves it. The number is decoration and is
   hidden from the reader that would have to hear it out loud. */
.args{display:flex;flex-direction:column;gap:3.5rem;margin:2rem 0}
.arg{display:grid;grid-template-columns:1fr 1fr;gap:2.5rem;align-items:center}
.arg:nth-child(even) .arg-proof{order:-1}
.arg-n{display:inline-flex;align-items:center;justify-content:center;inline-size:2.4rem;block-size:2.4rem;border-radius:999px;background:var(--fill);color:var(--tint-t);font-weight:700;font-size:var(--t-cap);margin-bottom:.9rem}
.arg h3{font-size:var(--t-title);font-weight:700;letter-spacing:-.022em;line-height:1.2;margin:0 0 .5rem}
.arg p{color:var(--ink2);margin:0 0 .9rem;line-height:1.6}
.arg .pull{background:color-mix(in srgb,var(--tint) 22%,transparent);border-radius:var(--r);padding:.9rem 1.1rem;font-weight:600;color:var(--ink)}
@media(max-width:820px){.arg{grid-template-columns:minmax(0,1fr);gap:1.25rem}.arg:nth-child(even) .arg-proof{order:0}}
/* Customer logos. Greyed by filter rather than by opacity, because opacity would dim the
   page under them too and this sheet does not allow that. */
.logos{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:1.25rem 2.75rem;margin:1rem 0}
.logos span{font-size:var(--t-head);font-weight:700;letter-spacing:-.02em;color:var(--ink2);filter:grayscale(1)}
/* Testimonials. A rail that scrolls rather than a carousel that needs script. */
.quotes{display:flex;gap:1rem;overflow-x:auto;scroll-snap-type:x proximity;padding-bottom:.5rem;margin:1rem 0}
.quote{flex:0 0 22rem;scroll-snap-align:start;background:var(--surf);border-radius:var(--r);box-shadow:var(--e1);padding:1.5rem 1.4rem;display:flex;flex-direction:column;gap:1rem}
.quote blockquote{margin:0;font-size:var(--t-head);line-height:1.55}
.quote .who{margin-top:auto;border-top:1px solid var(--hair);padding-top:.9rem}
.quote .who b{display:block;font-size:var(--t-cap)}
.quote .who span{display:block;font-size:var(--t-micro);color:var(--ink2)}
@media(max-width:640px){.quote{flex-basis:17rem}}
/* Integrations and trust both read as a card grid; one grid rule serves both. */
.cards3{display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:.9rem;margin:1rem 0}
.cards3>*{background:var(--surf);border-radius:var(--r);box-shadow:var(--e1);padding:1.25rem 1.3rem}
/* h2 rather than h3: these titles sit directly under the page h1 on /pricing and
   /security, with no group heading between, and h3 there is a skipped level. They are
   sized as card titles regardless of their level — the level is the OUTLINE, the size is
   the design, and conflating the two is what produces headings chosen by how big they
   should look. */
.cards3 h2{font-size:var(--t-head);font-weight:600;margin:0 0 .3rem}
.cards3 p{color:var(--ink2);font-size:var(--t-cap);margin:0;line-height:1.55}
.cards3 .kind{display:block;font-size:var(--t-micro);color:var(--tint-t);font-weight:600;margin-bottom:.5rem}
/* A card that is entirely a link must not be entirely underlined. The affordance moves
   to the heading, where a reader looks for it. */
.cards3 a{text-decoration:none;color:inherit;display:block}
.cards3 a:hover{box-shadow:var(--e2)}
.cards3 a:hover h2{text-decoration:underline}
/* Plans. The featured one is --deep rather than a coloured border: the surface itself is
   the emphasis, so the card does not need a second signal fighting the badge. */
.plans{display:grid;grid-template-columns:repeat(auto-fit,minmax(16rem,1fr));gap:1rem;margin:1.5rem 0;align-items:start}
.plan{background:var(--surf);border-radius:var(--rl);box-shadow:var(--e2);padding:1.75rem 1.6rem;display:flex;flex-direction:column;gap:.9rem}
.plan.hero-plan{background-color:var(--deep);background-image:var(--grain);color:var(--on-deep);box-shadow:var(--e3)}
.plan.hero-plan h2,.plan.hero-plan .price{color:var(--on-deep)}
.plan.hero-plan li{color:var(--on-deep2)}
/* The unit beside the price ("הודעות בחודש"). On a white card --ink2 is right; on the
   espresso featured card it measured 2.72:1, because --ink2 is a LIGHT-scheme grey and
   --deep does not follow the scheme. Same class of miss as the widget portrait in the hero:
   a token that is correct everywhere except on the one surface that ignores the scheme. */
.plan.hero-plan .price small{color:var(--on-deep2)}
.plan.hero-plan li::before{color:var(--tint-d)}
.plan h2{font-size:var(--t-title);font-weight:700;letter-spacing:-.02em;margin:0}
.plan .price{font-size:var(--t-display);font-size:2rem;font-weight:700;letter-spacing:-.03em;line-height:1}
.plan .price small{font-size:var(--t-cap);font-weight:500;color:var(--ink2)}
.plan ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.55rem;font-size:var(--t-cap);color:var(--ink2)}
.plan li{display:flex;gap:.5rem;align-items:flex-start}
.plan li::before{content:"\2713";color:var(--tint-t);font-weight:700}
/* FAQ. details/summary, so it opens with no script and prints open. */
.faqs{border-top:1px solid var(--hair)}
.faqs details{border-bottom:1px solid var(--hair)}
.faqs summary{cursor:pointer;list-style:none;padding:1.15rem .25rem;font-weight:600;font-size:var(--t-head);color:var(--ink);display:flex;justify-content:space-between;gap:1rem;align-items:center}
.faqs summary::-webkit-details-marker{display:none}
.faqs summary::after{content:"+";color:var(--tint-t);font-weight:700;font-size:var(--t-title);line-height:1}
.faqs details[open] summary::after{content:"\2212"}
.faqs p{margin:0 .25rem 1.15rem;color:var(--ink2);line-height:1.6;max-width:46rem}
/* A WhatsApp thread, drawn rather than screenshotted, so it stays legible at every width
   and needs no asset pipeline. Its greens are the PRODUCT's colours, not ours — same
   deliberate exception the widget preview already carries in landing.ts. */
.wa{background:var(--deep);border-radius:var(--r);padding:1rem;display:flex;flex-direction:column;gap:.5rem;font-size:var(--t-cap)}
.wa b{color:var(--on-deep);font-size:var(--t-micro);letter-spacing:.02em}
.wa .m{max-inline-size:82%;padding:.5rem .75rem;border-radius:var(--rs);line-height:1.45;background:var(--surf);color:var(--ink);align-self:flex-start}
.wa .m.out{align-self:flex-end;background:var(--tint);color:var(--on-tint)}
.wa .t{font-size:var(--t-micro);color:var(--on-deep2);text-align:center}
/* The daily insight card: findings above, actions below, and the estimate labelled as one. */
.insight{background:var(--surf);border-radius:var(--r);box-shadow:var(--e1);padding:1.25rem 1.3rem}
.insight h2{margin:0 0 .15rem;font-size:var(--t-cap);font-weight:600}
.insight .when{font-size:var(--t-micro);color:var(--ink2);margin:0 0 .9rem}
.insight ul{list-style:none;margin:0 0 1rem;padding:0;display:flex;flex-direction:column;gap:.5rem;font-size:var(--t-cap)}
.insight li{display:flex;justify-content:space-between;gap:.75rem;align-items:baseline;border-bottom:1px solid var(--hair);padding-bottom:.5rem}
.insight li:last-child{border-bottom:0}
.insight li b{color:var(--tint-t);white-space:nowrap;font-size:var(--t-micro)}
.insight .act{background:var(--fill);border-radius:var(--rs);padding:.75rem .85rem}
.insight .act ul{margin:0}
.insight .act li{border-bottom:0;padding-bottom:0}
.insight .est{font-size:var(--t-micro);color:var(--ink2);margin:.6rem 0 0}
/* The band that closes every marketing page. */
.ctaband{margin:4rem 0 0;text-align:center}
.ctaband h2{font-size:var(--t-display);line-height:1.06;letter-spacing:-.035em;font-weight:700;margin:0 0 .6rem;color:var(--on-deep)}
.ctaband .cta{display:flex;gap:.6rem;flex-wrap:wrap;justify-content:center;margin-top:1.5rem}
/* Announcement strip. Above the header, scrolls away, never sticky — a permanent second
   bar costs a band of every page for a message read once. */
.ann{background:var(--deep);color:var(--on-deep);font-size:var(--t-cap);padding:.6rem 1.25rem;text-align:center;border-radius:999px;margin:0 0 1.5rem}
/* Pushes whatever follows it to the far end of the header. Was an inline style until
   0.55.0, which test/deskRender.test.ts rightly objects to. */
header a.push{margin-inline-start:auto}
.ann a{color:var(--tint-d);font-weight:600}
/* Marketing pages breathe wider than the console and set their sections apart. */
main:has(.mk){max-width:64rem}
.mk section{margin:5rem 0}
.mk section:first-child{margin-top:1.25rem}
.mk h1,.mk h2{font-size:clamp(1.75rem,3.4vw,2.5rem);line-height:1.1;letter-spacing:-.03em;font-weight:700;margin:0 0 .4rem}
.mk .eyebrow{display:block;font-size:var(--t-micro);font-weight:600;color:var(--tint-t);margin-bottom:.6rem}
.mk .lead{font-size:var(--t-head);color:var(--ink2);margin:0 0 1.75rem;max-width:38rem;line-height:1.55}
.mk .prose{max-width:44rem}
.mk .prose h2{font-size:var(--t-head);line-height:1.35;letter-spacing:0;margin:2rem 0 .4rem}
.mk .prose p,.mk .prose li{color:var(--ink2);line-height:1.7}
@media(max-width:640px){.mk section{margin:3.25rem 0}}
/* The marketing footer. Grid of link columns; the brand block spans the full row on
   narrow screens so the columns can collapse under it rather than beside it. */
.sf{display:grid;grid-template-columns:2fr repeat(4,1fr);gap:2rem 1.5rem;margin:5rem 0 0;padding:2.5rem 0 0;border-top:1px solid var(--hair)}
.sf-b b{display:block;font-size:1.0625rem;font-weight:700;letter-spacing:-.022em}
.sf-b span{display:block;font-size:var(--t-cap);color:var(--ink2);margin-top:.35rem;max-width:16rem;line-height:1.5}
.sf h3{font-size:var(--t-micro);font-weight:600;color:var(--ink2);margin:0 0 .6rem}
.sf a{display:block;font-size:var(--t-cap);color:var(--ink);text-decoration:none;padding:.2rem 0}
.sf a:hover{text-decoration:underline}
@media(max-width:760px){.sf{grid-template-columns:1fr 1fr}.sf-b{grid-column:1/-1}}
/* == accessibility control (a11y.ts) ======================================= */
/* Bottom-INLINE-START, which under RTL is the left: the widget launcher defaults to the
   left too, but it is a guest on a MERCHANT's page and this control only ever appears on
   ours, so they never share a corner. */
.a11y{position:fixed;inset-block-end:1rem;inset-inline-start:1rem;z-index:40}
.a11y>summary{list-style:none;cursor:pointer;background:var(--deep);color:var(--on-deep);border-radius:999px;padding:.6rem 1.1rem;font-size:var(--t-cap);font-weight:600;box-shadow:var(--e2)}
.a11y>summary::-webkit-details-marker{display:none}
.a11y-p{position:absolute;inset-block-end:3rem;inset-inline-start:0;inline-size:15rem;background:var(--surf);border:1px solid var(--hair);border-radius:var(--r);box-shadow:var(--e3);padding:.9rem;display:flex;flex-direction:column;gap:.4rem}
.a11y-p h2{font-size:var(--t-cap);margin:0 0 .25rem}
.a11y-p button{justify-content:space-between;background:var(--fill);color:var(--ink);border-radius:var(--rs);font-weight:500;inline-size:100%}
.a11y-p button:hover{background:var(--hair)}
.a11y-p button[aria-pressed=true]{background:var(--tint);color:var(--on-tint);font-weight:600}
.a11y-p .lv{font-size:var(--t-micro);color:var(--ink2)}
.a11y-p button[aria-pressed=true] .lv{color:var(--on-tint)}
.a11y-p .rst{background:none;color:var(--ink2);font-size:var(--t-micro)}
.a11y-p p{font-size:var(--t-micro);color:var(--ink2);margin:.35rem 0 0;line-height:1.5}
/* The five adjustments. Each one re-states the rule its matching @media query already
   carries — a preference query answers "what did your OS say", these answer "what did you
   just press", and a person on a borrowed machine only ever has the second. */
html[data-a11y-text="1"]{font-size:112.5%}
html[data-a11y-text="2"]{font-size:131.25%}
[data-a11y-links] main a{text-decoration:underline}
[data-a11y-focus] :focus-visible{outline:3px solid var(--tint-t);outline-offset:3px}
[data-a11y-contrast] .card,[data-a11y-contrast] .pane,[data-a11y-contrast] .rail,[data-a11y-contrast] .ctx,[data-a11y-contrast] .tile,[data-a11y-contrast] .tablewrap,[data-a11y-contrast] .narrow,[data-a11y-contrast] .quote,[data-a11y-contrast] .plan,[data-a11y-contrast] .cards3>*{border:1px solid var(--hair)}
[data-a11y-contrast] .tag{box-shadow:inset 0 0 0 1px currentColor}
[data-a11y-contrast] main a{text-decoration:underline}
[data-a11y-contrast] header{background:var(--surf);-webkit-backdrop-filter:none;backdrop-filter:none;border-bottom:1px solid var(--hair)}
/* Stopping animation means stopping it, including the one infinite loop in the sheet. */
[data-a11y-motion] *{animation:none!important;transition:none!important}
/* ── motion ───────────────────────────────────────────────────────────────── */
/* Feedback lives on the PRESS, not on the release: :active fires on pointer-down, which is
   the whole difference between an interface that answers and one that waits to be sure.
   Everything that moves rides a spring curve and only compositor-friendly properties. */
@media(prefers-reduced-motion:no-preference){
a,button,summary,tr,.tag,.rail a,.tabs a,.steps li{transition:background-color .18s var(--springq),border-color .18s var(--springq),color .18s var(--springq),box-shadow .18s var(--springq)}
button:active,a.btn:active,.rail a:active,.tabs a:active,summary:active{transform:scale(.97);transition:transform .1s ease-out}
details[open]>*:not(summary){animation:reveal .32s var(--spring)}
.card,.tile{animation:rise .4s var(--spring)}
@keyframes reveal{from{opacity:0;transform:translateY(-.35rem)}to{opacity:1;transform:none}}
@keyframes rise{from{opacity:0;transform:translateY(.4rem) scale(.995)}to{opacity:1;transform:none}}
.skel{background:linear-gradient(90deg,var(--fill),var(--hair),var(--fill));background-size:200% 100%;animation:shim 1.4s linear infinite}
@keyframes shim{from{background-position:200% 0}to{background-position:-200% 0}}}
/* Reduced motion is not NO feedback — it is a gentler, non-vestibular equivalent. The
   colour and press responses stay; the travel goes. */
@media(prefers-reduced-motion:reduce){
a,button,summary,tr,.tag,.rail a,.tabs a,.steps li{transition:background-color .12s linear,color .12s linear}}
/* Translucency is a preference, not a given: when it is turned off the material becomes
   opaque rather than merely less blurred, because a half-frosted bar is the worst of both. */
@media(prefers-reduced-transparency:reduce){
header{background:var(--surf);-webkit-backdrop-filter:none;backdrop-filter:none;border-bottom:1px solid var(--hair)}
header::after{background:none}
.rail a[aria-current],.steps li[aria-current]{background:var(--fill)}}
/* More contrast means a defined edge, not a darker grey: every surface gains a real border
   and the tints that carry meaning go solid. */
@media(prefers-contrast:more){
.card,.pane,.rail,.ctx,.tile,.tablewrap,.narrow{border:1px solid var(--hair)}
.msg{border-color:var(--hair)}
button.ghost,a.btn.ghost{border-color:var(--ink2)}
.tag{box-shadow:inset 0 0 0 1px currentColor}
main a{text-decoration:underline}}
