
@font-face{font-family:"Big Shoulders Inline Text";font-style:normal;font-weight:900;font-display:swap;
  src:url(/assets/fonts/big-shoulders-inline-text-900.woff2) format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Onest";font-style:normal;font-weight:100 900;font-display:swap;
  src:url(/assets/fonts/onest-variable.woff2) format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Geist Mono";font-style:normal;font-weight:400 700;font-display:swap;
  src:url(/assets/fonts/geist-mono-variable.woff2) format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
:root{color-scheme:dark;
  --bg:#06090F;--card:#0F1424;--elev:#161F33;--line:#1F2A40;--line2:#2A3852;
  --ink:#EDEBE3;--mute:#8A93A7;--faint:#5C677E;
  --accent:#06B6D4;--accent2:#38BDF8;--on-accent:#04141A;--sky:#38BDF8;
  --agn-mark:#5FBFB4;
  --warn:#F59E0B;--on-warn:#1a1206;--ok:#3fbf9f;--danger-ink:#FF8B7D;--danger-line:#5a3130;
  /* Round 127 (ADR-101): the brand faces are back, and self-hosted this time,
     so they actually load instead of being named at a CDN nobody calls. Onest
     for body, Geist Mono for code, Big Shoulders Inline Text for display
     headings. Segoe UI stays as the FIRST fallback, so a surface that has not
     got the face yet still reads in the round 126 face rather than in a
     browser default. Values mirror shared/theme.css in slobal.com. */
  --sans:"Onest","Segoe UI","Segoe UI Variable Text",system-ui,-apple-system,sans-serif;
  --mono:"Geist Mono",ui-monospace,"Cascadia Code",Consolas,monospace;
  --display:"Big Shoulders Inline Text","Big Shoulders Display",sans-serif;}
@media (prefers-color-scheme:light){:root{color-scheme:light;
  --bg:#F5F4EF;--card:#FFFFFF;--elev:#EFEEE7;--line:#E0DED6;--line2:#CDC9BC;
  --ink:#0A0E1A;--mute:#374151;--faint:#6B7385;--accent:#0E7490;--accent2:#0891B2;--on-accent:#FFFFFF;--sky:#0369A1;
  --agn-mark:#03625A;
  /* ROUND 140. --ok and --danger-ink carried the DARK theme's values in this
     LIGHT block, and both failed WCAG AA here: #3fbf9f is about 2.0:1 on white
     and #FF8B7D about 2.5:1, which is why qa/a11y/axe-gate.mjs shipped in round
     139 with two named contrast exceptions and a note saying the fix was two
     token values. These are those two values, and they are not new colours: the
     design system already declares --ds-ok-ink #067647 and --ds-bad-ink #B42318
     for the same two roles in the same theme. The dark block above is untouched,
     where those values are the readable ones. The exceptions are gone from the
     gate, which is strict from this round on. */
  --warn:#F59E0B;--on-warn:#1a1206;--ok:#067647;--danger-ink:#B42318;--danger-line:#5a3130;}}
html[data-theme="dark"]{color-scheme:dark;
  --bg:#06090F;--card:#0F1424;--elev:#161F33;--line:#1F2A40;--line2:#2A3852;
  --ink:#EDEBE3;--mute:#8A93A7;--faint:#5C677E;
  --accent:#06B6D4;--accent2:#38BDF8;--on-accent:#04141A;--sky:#38BDF8;
  --agn-mark:#5FBFB4;
  --warn:#F59E0B;--on-warn:#1a1206;--ok:#3fbf9f;--danger-ink:#FF8B7D;--danger-line:#5a3130;}
html[data-theme="light"]{color-scheme:light;
  --bg:#F5F4EF;--card:#FFFFFF;--elev:#EFEEE7;--line:#E0DED6;--line2:#CDC9BC;
  --ink:#0A0E1A;--mute:#374151;--faint:#6B7385;--accent:#0E7490;--accent2:#0891B2;--on-accent:#FFFFFF;--sky:#0369A1;
  --agn-mark:#03625A;
  /* ROUND 140. --ok and --danger-ink carried the DARK theme's values in this
     LIGHT block, and both failed WCAG AA here: #3fbf9f is about 2.0:1 on white
     and #FF8B7D about 2.5:1, which is why qa/a11y/axe-gate.mjs shipped in round
     139 with two named contrast exceptions and a note saying the fix was two
     token values. These are those two values, and they are not new colours: the
     design system already declares --ds-ok-ink #067647 and --ds-bad-ink #B42318
     for the same two roles in the same theme. The dark block above is untouched,
     where those values are the readable ones. The exceptions are gone from the
     gate, which is strict from this round on. */
  --warn:#F59E0B;--on-warn:#1a1206;--ok:#067647;--danger-ink:#B42318;--danger-line:#5a3130;}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html{-webkit-text-size-adjust:100%}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
body{margin:0;font:16px/1.55 var(--sans);background:var(--bg);color:var(--ink);min-height:100vh;
  padding:0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
  font-feature-settings:"kern" 1,"liga" 1;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;position:relative}

.topbar{display:flex;align-items:center;gap:12px;min-height:50px;padding:env(safe-area-inset-top) calc(16px + env(safe-area-inset-right)) 0 calc(16px + env(safe-area-inset-left));background:var(--card);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:10}
.topbar::after{content:'';position:absolute;left:0;right:0;bottom:-1px;height:1px;background:linear-gradient(90deg,transparent,rgba(6,182,212,.5) 40%,rgba(56,189,248,.4) 62%,transparent)}
/* Round 101 (TagFix sweep): the top-left of every surface is the FULL Slobal
   wordmark - the logotype, not the bare torus - and the whole brand slot is a
   LINK to the apex home. The letters are drawn in currentColor so the ◐ toggle
   recolours them with the rest of the bar; the O keeps its gradient in both
   themes. A product name (a workspace, or "Helmsly" on the offline page)
   follows behind a hairline divider, Entra style, and IT is the part that
   ellipsises - the logotype itself never squeezes or wraps. .brand shrinks
   (flex:0 1 auto + min-width:0) so a long workspace name can never widen the
   page; relay-fit.mjs holds that at an iPhone-12 viewport. */
.brand{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:-.015em;font-size:15px;flex:0 1 auto;min-width:0;color:var(--ink);text-decoration:none}
a.brand:hover{color:var(--ink);text-decoration:none}
.brand .slobal-wordmark,.brand img,.brand .bsep{flex:none}
.brand .bsep{width:1px;height:19px;background:var(--line2)}
.brand .pname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The identity cluster must SHRINK, never widen the page: long emails
   ellipsise on narrow screens (QA-1..5 - every signed-in surface scrolled
   horizontally on a phone because this row had no min-width:0 path). */
.who{margin-left:auto;display:flex;align-items:center;gap:9px;font-size:13px;color:var(--mute);min-width:0;flex:0 1 auto}
.who>span,.who>a{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.who>a.act,.who .themetoggle,.who .slbadge{flex:none;overflow:visible}
/* The avatar keeps the flex:none and NOT the overflow:visible above.
   Round 128: that one declaration is what made the corner picture a hard
   square on every relay surface while the hub and AgnCred drew a circle.
   border-radius does not clip a REPLACED element's content on its own - the
   UA sheet's img{overflow:clip} is what does, and overflow:visible here
   overrode it, so the photo painted right out to its own corners with the
   round drawn behind it. (display:flex on the same element is harmless, and
   display:block alone does NOT bring the clip back - both measured.)
   The reset exists to undo the ellipsis rule one line up, whose selector is
   .who>span,.who>a - DIRECT children. Neither avatar is one: both sit inside
   a.whoami. So this element never needed the reset in the first place. */
.who .avatar{flex:none}
/* The identity IS the way to My Account (panel parity): the picture and the
   name, one link, no separate button beside them. 13.5px/600 in a 260px
   maximum - the panel's .acct, value for value. */
.who a.whoami{display:inline-flex;align-items:center;gap:8px;color:var(--mute);font-size:13.5px;font-weight:600;max-width:260px;text-decoration:none}
.who a.whoami:hover{color:var(--ink);text-decoration:none}
.who a.whoami .em{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@media(max-width:600px){.topbar{gap:8px}.who{gap:7px}.who .slbadge{display:none}.brand{gap:8px}.brand .slobal-wordmark{height:18px}.brand .bsep{height:16px}.brand .pname{font-size:14px}}
/* Identical to the Helmsly panel's theme button: the constant ◐ glyph in the
   same rounded-rect chrome as every other header button; current mode lives
   in the tooltip, not the glyph. Panel size: a 34px square, 8px radius. */
.themetoggle{position:relative;background:var(--elev);color:var(--ink);border:1px solid var(--line2);border-radius:8px;width:34px;height:34px;padding:0;font:15px/1 var(--sans);cursor:pointer;transition:border-color .16s,color .16s;display:inline-flex;align-items:center;justify-content:center;flex:none}
.themetoggle:hover{border-color:var(--accent)}
/* Round 135 (goal 5.1, ADR-103): THE STATUS INDICATOR IS THE TOGGLE SQUARE.
   The corner used to carry a theme square and, in the Helmsly panel, a second
   square whose whole job was one dot - two 34px controls side by side saying
   two unrelated things. The square keeps its toggle role and the surface's own
   state rides INSIDE its frame, bottom right, as the ADR-103 dot: 8px, the
   same diameter the .ds-status pill draws, so the platform has one dot size
   and not a corner size beside a pill size.
   The state WORD lives on the button as data-state (ADR-103 decision 6), so a
   gate reads the attribute and never the colour, and the four selectors below
   are the only place a state is coloured. Every colour is a shell theme token,
   because this rule is in TOPBAR_CSS - outside the [data-ds] layer - and has
   to paint on a surface that never opted in.
   Specificity note: the ds layer is appended AFTER this sheet and carries a
   [data-ds] .ds-status-dot rule, background currentColor, at (0,2,0). Every
   rule here is (0,3,0) because the button always carries data-state, so the
   corner dot keeps its own size and colour on a [data-ds] surface too.
   The ring is the top bar's own ground, so the dot stays separable where it
   crosses the square's border. */
.themetoggle[data-state] .ds-status-dot{position:absolute;right:3px;bottom:3px;width:8px;height:8px;border-radius:50%;background:var(--mute);box-shadow:0 0 0 2px var(--card)}
.themetoggle[data-state="live"] .ds-status-dot{background:var(--ok)}
.themetoggle[data-state="waking"] .ds-status-dot{background:var(--warn)}
.themetoggle[data-state="last_seen"] .ds-status-dot{background:var(--mute)}
.themetoggle[data-state="unknown"] .ds-status-dot{background:var(--danger-ink)}
/* The glyph is a child now rather than the button's own text node, so the dot
   is a sibling of it rather than a thing the theme script can overwrite. */
.themetoggle .tgglyph{display:block;line-height:1}
/* The PORTAL pill, verbatim from the Helmsly panel chrome (34px tall, 13px
   sides): the way home from every SIGNED-IN surface, beside the theme button,
   exactly as the panel's own #slhome is. Round 101 took it off anonymous
   pages - there is no session home to go to from a sign-in door, and the
   brand link in the top-left is the way to the front of the platform.
   Round 102 hides it on the portal itself, and the hidden state has to be
   spelled out here: the UA's own [hidden]{display:none} loses to any author
   display value, so without this rule a hidden pill would still paint. */
.slbadge{display:inline-flex;align-items:center;height:34px;padding:0 13px;border:1px solid var(--line2);border-radius:999px;background:var(--elev);font:800 12px/1 var(--sans);letter-spacing:.08em;color:var(--mute);flex:none;text-decoration:none;transition:border-color .16s,color .16s}
.slbadge[hidden]{display:none}
a.slbadge:hover{color:var(--ink);border-color:var(--accent);text-decoration:none}
/* One Sign Out control for the whole platform: a quiet bordered button, the
   right-most element of the cluster, same class and shape on all surfaces -
   the panel's small ghost button (28px tall, 10px radius, 13px). */
.who a.signout{text-decoration:none;white-space:nowrap;height:28px;padding:0 11px;border-radius:10px;font-size:13px;background:transparent;margin:0}
/* ONE class, TWO elements, and until round 128 one rule for both: the stored
   PHOTO (an img.avatar) and the coloured INITIAL that stands in when there is
   none (a span.avatar). This rule is the geometry they genuinely share - the
   size, the round, and the letter's own colouring and centring, which an image
   simply ignores. Every size is still written here and only here; the rule
   that follows adds no geometry of its own. */
.avatar{width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--accent2));color:var(--on-accent);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px}
/* The PICTURE's own rule, built the way the two surfaces the operator calls
   correct build theirs - the hub's .acctpic (app/ui/v3/tokens.css) and
   AgnCred's img.admin-avatar (apps/web/src/styles.css). Both put the round on
   the image itself with display:block and object-fit:cover, and neither lets
   overflow off the UA's clip. That last part is the whole defect: a replaced
   element paints its content through a border-radius unless the overflow keeps
   it in, so this rule STATES the clip rather than trusting that nobody
   overrides it again. display:block retires the inline-baseline gap; the two
   member grids override it with their own inline-block, because there the
   picture really does sit on a line of text (global-admin.mjs, tenant-admin.mjs
   - the round now comes from HERE and only the flow does not). object-fit:cover
   lives here too, so no caller writes it inline any more. */
img.avatar{display:block;overflow:hidden;object-fit:cover}
/* The identity picture in the TOP BAR is chrome, not content: it sits inside
   the 34px cluster at the panel's own .acctpic size (18px round), so it is
   sized here once rather than by whatever number each surface passed. The
   28px .avatar above stays the size the account page's own forms use.
   The 8px initial is the account page's own ratio (size / 2.3) at 18.
   Round 105 (tags 02-04): these two numbers are now the ONLY place the
   corner picture is sized. The account page used to write them inline as
   well, from its own arithmetic, where the cascade let them win - so WHOAMI
   below emits the picture with no width, height or font-size of its own and
   a test in server.test.mjs holds every surface to that. */
.who .avatar{width:18px;height:18px;font-size:8px}
body::before{content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
  background:radial-gradient(1100px 520px at 50% -10%,rgba(6,182,212,.06),transparent 60%)}
body::after{content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;opacity:.022;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
.wrap{max-width:440px;margin:0 auto;padding:56px 22px}
.wide{max-width:960px}
.logo{font-size:38px;text-align:center;margin-bottom:8px;filter:drop-shadow(0 2px 10px rgba(6,182,212,.28))}
/* A hero that is an outline GLYPH rather than the Slobal mark: no brand glow
   behind it, and no inline line box from the mark-sized font. */
.logo.glyph{filter:none;font-size:0;line-height:1}
/* The page title is the one place the brand DISPLAY face speaks (round 127,
   ADR-101). It is an inline face and already condensed, so it takes positive
   tracking: the round 126 -.025em was drawn for Segoe and closes the inline
   strokes up until the counters fill in. */
h1{font-size:23px;margin:0 0 4px;text-align:center;letter-spacing:.02em;font-family:var(--display);font-weight:900;text-transform:uppercase}
.sub{color:var(--mute);text-align:center;margin:0 0 26px;font-size:14px;line-height:1.5}
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:20px}
.btn{display:flex;align-items:center;justify-content:center;gap:9px;width:100%;border:0;border-radius:8px;padding:12px;font:600 15px var(--sans);cursor:pointer;text-decoration:none;margin:0 0 10px;transition:border-color .16s,background .16s,filter .16s}
.btn.sso{background:var(--elev);color:var(--ink);border:1px solid var(--line2)}
.btn.sso:hover{border-color:var(--accent)}
.btn.primary{background:var(--accent);color:var(--on-accent)}
.btn.primary:hover{filter:brightness(1.08)}
/* The app-icon slot, same declaration the admin shell carries (round 86, tag
   03): the tenant front door names an app on a button, so its mark comes from
   APP_ICON like every other app mark on the platform rather than from a raw
   FLAG at a hand-picked size. */
.appic{flex:none;display:inline-flex;align-items:center;justify-content:center}
.appic>svg{display:block}
/* ROUND 140 (qa/a11y/axe-gate.mjs, now strict). This line was --faint, which is
   #6B7385 in the light theme: 4.31:1 on the page ground, and this is 12.5px
   text, so AA wants 4.5:1. --mute is #374151, 10.3:1, and is what every other
   secondary line on these pages already uses (.muted2 below, .sub above). No
   new colour, one token swapped for the one beside it.
   The anchors inside it are UNDERLINED for the other half of the same failure:
   axe's link-in-text-block rule wants a link distinguishable from its
   surrounding text by something other than hue, and --sky against --mute is
   1.24:1 of hue and nothing else. */
.muted{color:var(--mute);font-size:12.5px;text-align:center;margin-top:20px;line-height:1.5}
.muted a,.muted2 a{text-decoration:underline}
.muted2{color:var(--mute)}
input{width:100%;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:12px 14px;font:inherit;margin:0 0 10px;transition:border-color .16s,box-shadow .16s}
input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(6,182,212,.14)}
table{width:100%;border-collapse:collapse;font-size:14px;font-variant-numeric:tabular-nums lining-nums}
th,td{text-align:left;padding:9px 10px;border-bottom:1px solid var(--line);vertical-align:top}
th{font-size:10.5px;text-transform:uppercase;letter-spacing:.07em;color:var(--faint);font-weight:650}
.chip{display:inline-block;font:600 10.5px/1 var(--mono);letter-spacing:.03em;padding:3px 8px;border-radius:4px;border:1px solid var(--line2);margin:1px 2px;color:var(--mute)}
.chip.on{color:var(--accent);border-color:var(--accent)}.chip.off{color:var(--warn);border-color:var(--warn)}
.act{display:inline-flex;align-items:center;gap:6px;background:var(--elev);color:var(--ink);border:1px solid var(--line2);border-radius:6px;padding:5px 10px;font:500 12px var(--sans);cursor:pointer;margin:1px;transition:border-color .14s,background .14s}
.act:hover{border-color:var(--accent)}
.act.danger{color:var(--danger-ink);border-color:var(--danger-line)}
.act.go{background:var(--accent);color:var(--on-accent);border:1px solid var(--accent);font-weight:600}
.row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:8px 0}
.pill{font-size:12px;color:var(--mute)}
a{color:var(--sky);text-decoration:none}a:hover{text-decoration:underline;text-underline-offset:2px}
.tier{border:1px solid var(--line);border-radius:8px;padding:16px;margin:0 0 12px;background:var(--bg)}
.tier h3{margin:0 0 2px;font-size:12px;text-transform:uppercase;letter-spacing:.07em;color:var(--accent)}
.tier .p{font-size:21px;font-weight:700;margin:4px 0;letter-spacing:-.02em}
@media(pointer:coarse){.btn{min-height:48px}.act{min-height:44px;padding:10px 14px}input,select{min-height:46px;font-size:16px}}
@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
/* ROUND 129 (ADR-102): design-system layer, scoped to html[data-ds]. */
/* Theme-dependent ds values. Base = DARK. Origins in the ds spec: BADGES
   ("tinted background with dark text"), TOKENS (--ok #067647, --ok-bg #ECFDF3,
   --warn #B54708, --warn-bg #FFFAEB, --neutral-bg #EFEEE8) and the Slobal
   context's --primary-tint #E3F1F5. The shell has a token for none of them.
   On a dark ground a tint glows, so a dark badge is a transparent ground with
   a coloured border and coloured text, and a dark card takes no shadow at all
   (a shadow on a dark ground is mud). */
html[data-ds]{
  --ds-ok-ink:var(--ok);--ds-ok-bg:transparent;--ds-ok-line:var(--ok);
  --ds-warn-ink:var(--warn);--ds-warn-bg:transparent;--ds-warn-line:var(--warn);
  --ds-neutral-ink:var(--mute);--ds-neutral-bg:transparent;--ds-neutral-line:var(--line2);
  --ds-tile-hover:var(--elev);
  --ds-shadow:none}
@media (prefers-color-scheme:light){html[data-ds]:not([data-theme="dark"]){
  --ds-ok-ink:#067647;--ds-ok-bg:#ECFDF3;--ds-ok-line:#ECFDF3;
  --ds-warn-ink:#B54708;--ds-warn-bg:#FFFAEB;--ds-warn-line:#FFFAEB;
  --ds-neutral-ink:var(--mute);--ds-neutral-bg:#EFEEE8;--ds-neutral-line:#EFEEE8;
  --ds-tile-hover:#E3F1F5;
  --ds-shadow:0 1px 2px rgba(10,14,26,.05)}}
html[data-ds][data-theme="light"]{
  --ds-ok-ink:#067647;--ds-ok-bg:#ECFDF3;--ds-ok-line:#ECFDF3;
  --ds-warn-ink:#B54708;--ds-warn-bg:#FFFAEB;--ds-warn-line:#FFFAEB;
  --ds-neutral-ink:var(--mute);--ds-neutral-bg:#EFEEE8;--ds-neutral-line:#EFEEE8;
  --ds-tile-hover:#E3F1F5;
  --ds-shadow:0 1px 2px rgba(10,14,26,.05)}
/* TYPE ROLES (ds spec TYPE SCALE). The display face speaks once per page, on
   the page title, at the spec's own 46/34 and 1.05. It is NOT uppercased here:
   the casing law puts uppercase in eyebrows and table headers only, and the
   display role is not one of them. .subhead keeps its own voice, so the two
   heading roles step around it rather than overriding a component. */
html[data-ds] body{font-size:15px;line-height:1.6}
html[data-ds] .pagetitle,html[data-ds] h2.pagetitle{font-family:var(--display);font-weight:900;font-size:46px;line-height:1.05;letter-spacing:.02em;text-transform:none;color:var(--ink);margin:0 0 12px}
html[data-ds] .ds-h2,html[data-ds] h2:not(.pagetitle):not(.subhead){font-family:var(--sans);font-weight:700;font-size:26px;line-height:1.25;letter-spacing:-.01em;color:var(--ink);margin:0 0 12px}
html[data-ds] .ds-h3,html[data-ds] h3:not(.subhead){font-family:var(--sans);font-weight:600;font-size:18px;line-height:1.35;letter-spacing:0;color:var(--ink);margin:0 0 8px}
html[data-ds] .ds-eyebrow{display:block;font:600 11px/1.4 var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--accent);margin:0 0 6px}
html[data-ds] .ds-small{font-size:13px;line-height:1.5;color:var(--mute)}
html[data-ds] .ds-data{font:400 13px/1.5 var(--mono);font-variant-numeric:tabular-nums lining-nums;color:var(--ink)}
/* SPEC LIST (ds spec COMPONENTS: label-value pairs, 11px uppercase tracked
   labels, values in body or mono, 1px separators). Both themes get the same
   shape because every colour in it is a theme token; on a phone the label
   stacks over its value rather than squeezing the value to nothing. */
html[data-ds] .ds-spec-list{display:block;margin:0;padding:0;border-top:1px solid var(--line)}
html[data-ds] .ds-spec-row{display:flex;gap:16px;align-items:baseline;flex-wrap:wrap;padding:10px 0;border-bottom:1px solid var(--line)}
html[data-ds] .ds-spec-label{flex:none;width:180px;font:600 11px/1.5 var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--faint)}
html[data-ds] .ds-spec-value{flex:1;min-width:0;font-size:15px;line-height:1.6;color:var(--ink)}
html[data-ds] .ds-spec-value.ds-data{font:400 13px/1.6 var(--mono);font-variant-numeric:tabular-nums lining-nums}
@media(max-width:720px){html[data-ds] .ds-spec-row{gap:2px}html[data-ds] .ds-spec-label{width:100%}}
/* CARD (ds spec: surface, 1px line border, radius 8, padding 24). Borders over
   shadows: the shadow ceiling rides --ds-shadow, which is the spec's
   0 1px 2px rgba(10,14,26,.05) on light and nothing at all on dark. */
html[data-ds] .ds-card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:24px;box-shadow:var(--ds-shadow)}
/* BADGE (ds spec: pill, 24px, dot indicator). The dot is currentColor, so a
   variant states its colour once and the dot follows. */
html[data-ds] .ds-badge{display:inline-flex;align-items:center;gap:7px;height:24px;padding:0 11px;border-radius:999px;font:600 12px/1 var(--sans);white-space:nowrap;background:var(--ds-neutral-bg);border:1px solid var(--ds-neutral-line);color:var(--ds-neutral-ink)}
html[data-ds] .ds-badge::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:currentColor}
html[data-ds] .ds-badge.ok{background:var(--ds-ok-bg);border-color:var(--ds-ok-line);color:var(--ds-ok-ink)}
html[data-ds] .ds-badge.warn{background:var(--ds-warn-bg);border-color:var(--ds-warn-line);color:var(--ds-warn-ink)}
html[data-ds] .ds-badge.neutral{background:var(--ds-neutral-bg);border-color:var(--ds-neutral-line);color:var(--ds-neutral-ink)}
/* APP TILE, converged on the ds card metrics. Ground, border, radius and focus
   only: the inner layout (the app mark and its .nm label) and the tile's size
   are untouched, so no icon and no click target moves. */
html[data-ds] .tile.app{background:var(--card);border:1px solid var(--line);border-radius:8px}
html[data-ds] .tile.app:hover{border-color:var(--accent);background:var(--ds-tile-hover)}
html[data-ds] .tile.app:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* DENSITY (ds spec SPACING: section 64 desktop, 48 mobile; body measure 640). */
html[data-ds] .ds-section{margin:0 0 64px}
html[data-ds] .ds-section:last-child{margin-bottom:0}
html[data-ds] .pagedesc,html[data-ds] .ds-measure{max-width:640px}
@media(max-width:720px){html[data-ds] .ds-section{margin-bottom:48px}}
/* This layer introduces no animation. The sheets above already kill every
   transition under reduced motion with !important; the tile is named again
   here so the one hover this layer touches states its own answer. */
@media (prefers-reduced-motion:reduce){html[data-ds] .tile.app{transition:none}}
/* ROUND 130: the component vocabulary the account surface needs - buttons,
   form fields, data tables and the badge's bad state. APPENDED, never edited
   into the block above, so the round 129 sheet stays a byte-prefix of this one
   and a reader can still see round 129 whole. Everything below is scoped under
   html[data-ds] exactly as everything above it is, so a surface that has not
   opted in still paints what it painted before.
   One correction to the note four lines up: this round DOES introduce hover
   transitions, on the button and the input. The reduced-motion guard at the
   foot of this block names them, and the sheets above already kill every
   transition under reduced motion with !important, so the guard is a statement
   rather than a fix. */
/* New theme-dependent ds values, in the same three-block pattern the head of
   this layer uses: base carries DARK (this shell's default), the
   prefers-color-scheme:light block that excludes a forced dark theme carries
   LIGHT, and html[data-ds][data-theme="light"] carries it again so the toggle
   wins in both directions. Origins in the ds spec: TOKENS (--bad #B42318,
   --bad-bg #FEF3F2), the Slobal context's --primary-hover #0B5C73, and the
   DATA TABLE line ("hover row tint --paper", which is this shell's --bg).
   The shell carries a token for none of them. It does carry --danger-ink and
   --danger-line, but they are ONE salmon-on-maroon pair used in both themes:
   drawn for the dark ground, and #FF8B7D on white is nowhere near readable. So
   dark keeps the shell's pair and light takes the spec's red, which is what
   the ok and warn trios above already do. */
html[data-ds]{
  --ds-bad-ink:var(--danger-ink);--ds-bad-bg:transparent;--ds-bad-line:var(--danger-line);
  --ds-bad-edge:var(--danger-line);
  --ds-row-hover:var(--elev);
  --ds-input-bg:var(--elev);
  --ds-accent-hover:var(--accent2)}
@media (prefers-color-scheme:light){html[data-ds]:not([data-theme="dark"]){
  --ds-bad-ink:#B42318;--ds-bad-bg:#FEF3F2;--ds-bad-line:#FEF3F2;
  --ds-bad-edge:#B42318;
  --ds-row-hover:var(--bg);
  --ds-input-bg:var(--card);
  --ds-accent-hover:#0B5C73}}
html[data-ds][data-theme="light"]{
  --ds-bad-ink:#B42318;--ds-bad-bg:#FEF3F2;--ds-bad-line:#FEF3F2;
  --ds-bad-edge:#B42318;
  --ds-row-hover:var(--bg);
  --ds-input-bg:var(--card);
  --ds-accent-hover:#0B5C73}
/* BADGE, bad state (ds spec BADGES: "States: ok, warn, bad, neutral, brand").
   Same three properties the ok and warn variants take, so the family reads as
   one set: a tinted ground with dark ink in light, a transparent ground with a
   coloured border and coloured text in dark. */
html[data-ds] .ds-badge.bad{background:var(--ds-bad-bg);border-color:var(--ds-bad-line);color:var(--ds-bad-ink)}
/* BUTTONS (ds spec COMPONENTS: "primary (solid --primary, white text),
   secondary (surface, --primary text, line border, tint on hover), ghost.
   Heights 40 and 32", radius 6 from SPACING AND LAYOUT). Every variant keeps
   the same 1px box, so a row of mixed variants lines up on one baseline and
   nothing shifts when a button changes variant. The solid ground is
   var(--accent) over var(--on-accent), which resolves to teal with white text
   in light and cyan with near-black text in dark: the spec's ban on white on
   cyan is answered by the token pair itself, not by a second rule here. The
   ds law of one primary per view is the page's to keep, not the sheet's.
   Labels are the page's too: sentence case, stating the outcome. */
html[data-ds] .ds-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:40px;padding:0 16px;border:1px solid var(--accent);border-radius:6px;background:var(--accent);color:var(--on-accent);font:600 14px/1 var(--sans);text-align:center;text-decoration:none;white-space:nowrap;cursor:pointer;transition:background .16s,border-color .16s,color .16s}
html[data-ds] .ds-btn:hover{background:var(--ds-accent-hover);border-color:var(--ds-accent-hover);text-decoration:none}
html[data-ds] .ds-btn-sm{height:32px;padding:0 12px;font-size:13px}
html[data-ds] .ds-btn-secondary{background:var(--card);border-color:var(--line);color:var(--accent)}
html[data-ds] .ds-btn-secondary:hover{background:var(--ds-tile-hover);border-color:var(--accent);color:var(--accent)}
html[data-ds] .ds-btn-ghost{background:transparent;border-color:transparent;color:var(--accent)}
html[data-ds] .ds-btn-ghost:hover{background:var(--ds-tile-hover);border-color:transparent;color:var(--accent)}
/* The destructive variant. Quiet at rest (a hairline and coloured text, no
   filled red shouting from the page) and solid only under the pointer, which
   is the last thing a person crosses on the way to a confirm. The account
   page's own arm-then-confirm mechanics are untouched: this is the skin, the
   two-step stays where it is written. */
html[data-ds] .ds-btn-danger{background:transparent;border-color:var(--ds-bad-edge);color:var(--ds-bad-ink)}
html[data-ds] .ds-btn-danger:hover{background:var(--ds-bad-ink);border-color:var(--ds-bad-ink);color:var(--bg)}
html[data-ds] .ds-btn:disabled,html[data-ds] .ds-btn[aria-disabled="true"]{opacity:.55;pointer-events:none}
/* FORM FIELD (ds spec COMPONENTS: "label above (13px, 600, --heading), input
   40px height, radius 6, line border, focus outline 2px --primary offset 2px,
   help text 13px --muted below. Error state: --bad border, help text states
   what happened and how to fix it."). The label sits above rather than beside,
   which is what the spec asks for and what a phone needs. The ground is --card
   in light and --elev in dark: on light the field sits ON a card and a --card
   ground would need the border to carry the whole shape, on dark a recessed
   ground is the thing that reads as an input at all.
   The shells above give every input outline:none and a 3px cyan ring on focus.
   These rules are more specific and later, so a ds input takes the spec's
   outline instead; the ring is cleared explicitly rather than left to fight. */
html[data-ds] .ds-field{display:flex;flex-direction:column;gap:6px;margin:0 0 16px}
html[data-ds] .ds-label{font:600 13px/1.4 var(--sans);color:var(--ink)}
html[data-ds] .ds-input{width:100%;height:40px;padding:0 12px;border:1px solid var(--line);border-radius:6px;background:var(--ds-input-bg);color:var(--ink);font:400 14px/1.4 var(--sans);transition:border-color .16s}
html[data-ds] select.ds-input{padding-right:8px}
html[data-ds] textarea.ds-input{height:auto;min-height:96px;padding:10px 12px;line-height:1.5;resize:vertical}
html[data-ds] .ds-input:focus{outline:2px solid var(--accent);outline-offset:2px;border-color:var(--accent);box-shadow:none}
html[data-ds] .ds-input::placeholder{color:var(--faint)}
html[data-ds] .ds-help{font:400 13px/1.5 var(--sans);color:var(--mute)}
html[data-ds] .ds-field-error>.ds-input{border-color:var(--ds-bad-ink)}
html[data-ds] .ds-field-error>.ds-help{color:var(--ds-bad-ink)}
/* DATA TABLE (ds spec COMPONENTS: "uppercase tracked 11px headers in --muted,
   1px row borders, hover row tint --paper, mono for identifiers, numbers right
   aligned in mono"). Two hooks for one treatment: .ds-table on a table the
   page writes fresh, and .ds-tablecard on a card that ALREADY holds a table,
   so the workspaces and orders tables on the account page adopt the ds skin by
   gaining one class on their wrapper rather than by having every row rewritten.
   The tracked headers and the mono cells are the two places the casing law
   allows uppercase and the type scale asks for the mono face. */
html[data-ds] .ds-table,html[data-ds] .ds-tablecard>table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums lining-nums}
html[data-ds] .ds-table th,html[data-ds] .ds-tablecard>table th{font:600 11px/1.4 var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--faint);text-align:left;padding:8px 10px;border-bottom:1px solid var(--line)}
html[data-ds] .ds-table td,html[data-ds] .ds-tablecard>table td{font-size:13px;line-height:1.5;color:var(--ink);text-align:left;padding:10px;border-bottom:1px solid var(--line);vertical-align:top}
html[data-ds] .ds-table tbody tr:hover,html[data-ds] .ds-tablecard>table tbody tr:hover{background:var(--ds-row-hover)}
html[data-ds] .ds-mono{font:400 13px/1.5 var(--mono);font-variant-numeric:tabular-nums lining-nums}
html[data-ds] .ds-num{text-align:right;font:400 13px/1.5 var(--mono);font-variant-numeric:tabular-nums lining-nums}
/* A cell rule beats a class rule on specificity, so the two cell roles are
   named again AS cells; without this a td.ds-mono would keep the body face. */
html[data-ds] .ds-table td.ds-mono,html[data-ds] .ds-tablecard>table td.ds-mono{font:400 13px/1.5 var(--mono)}
html[data-ds] .ds-table td.ds-num,html[data-ds] .ds-tablecard>table td.ds-num{text-align:right;font:400 13px/1.5 var(--mono)}
/* Touch. The shells above already raise their own controls on a coarse
   pointer; the ds controls answer the same law, and the 16px input keeps a
   phone browser from zooming the page on focus. */
@media(pointer:coarse){
  html[data-ds] .ds-btn{min-height:44px}
  html[data-ds] .ds-btn-sm{min-height:40px}
  html[data-ds] .ds-input{min-height:46px;font-size:16px}
  html[data-ds] textarea.ds-input{min-height:96px}}
/* The two hovers this round adds, named for the same reason the tile above is
   named: each states its own answer under reduced motion. */
@media (prefers-reduced-motion:reduce){html[data-ds] .ds-btn,html[data-ds] .ds-input{transition:none}}
/* ROUND 131 fit fix: the display face is wide, and a long one-word pagetitle
   (Customisation) at the 34px phone size is wider than a 390px viewport,
   which fails the relay-fit gate in hub CI (v3-boot-smoke renders every
   section view at 390). Below 720px the title scales with the viewport,
   clamped so it is 34px wherever 34px fits (anything wider than about 440px)
   and shrinks only on narrow phones; overflow-wrap is the safety net so no
   future title can widen the page whatever its length. */
@media (max-width:720px){
  html[data-ds] .pagetitle,html[data-ds] h2.pagetitle{font-size:clamp(26px,7.7vw,34px);overflow-wrap:anywhere}
}
/* ROUND 133 (estate sweep, tag 01): on a phone the topbar cannot hold the
   wordmark, a workspace name, the theme square, an avatar, a person's name
   and Sign Out; both names ellipsized to two characters ("No...", "Sa..."),
   which reads as noise. Below 480px the pagetitle already names the
   workspace and the avatar already names the person, so the topbar drops
   the two text copies and keeps the marks. */
@media (max-width:480px){
  html[data-ds] .brand .pname,html[data-ds] .brand .bsep{display:none}
  html[data-ds] .who .whoami .em{display:none}
}
/* ROUND 141 (finding TEN-001, decision G5): the PRODUCT CARD ROW on the three
   portal variants. Two cards side by side, each stating one product's state and
   the acts available on it, collapsing to one column when there is no room for
   two. APPENDED, so every sheet above stays a byte-prefix of this one.
   Layout only: not one colour, border or radius is declared here - the cards
   themselves are .ds-card and take all of that from the layer above. */
html[data-ds] .ds-cardrow{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin:0 0 20px;align-items:stretch}
html[data-ds] .ds-cardrow .ds-card{display:flex;flex-direction:column;gap:12px}
/* The acts on a card sit on one line and wrap rather than squeeze: a button
   narrower than its label is a button whose label is a guess. */
html[data-ds] .ds-cardacts{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
/* The card's heading row carries the product name and its state pill together,
   because a state that is not beside the thing it describes describes nothing. */
html[data-ds] .ds-cardhead{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:space-between}
html[data-ds] .ds-cardhead .ds-h3{margin:0}
/* ROUND 141 (finding SLB-005): a COMMAND, on the install page. A command is
   data - it is typed character for character or it does not work - so it takes
   the mono role the design system reserves for data, and it scrolls inside its
   own box rather than widening the page. Every value here is a token. */
html[data-ds] .ds-pre{margin:0;padding:12px 14px;background:var(--elev);border:1px solid var(--line);border-radius:6px;overflow-x:auto;-webkit-overflow-scrolling:touch}
html[data-ds] .ds-pre code{font:400 13px/1.5 var(--mono);white-space:pre;color:var(--ink)}
/* ROUND 141, TWO CONTRAST REPAIRS the strict axe gate found the moment a spec
   list and a linked sentence were put in front of it. Both are pre-existing and
   neither is new to this round; what is new is a page that renders them where
   the gate could see them. Round 140's precedent is followed exactly: a light
   theme role that fails AA takes an existing readable token, no new colour is
   declared, and the committed baselines move with it.

   1. THE SPEC LABEL. --faint is #6B7385, which is 4.83:1 on the card ground and
      only 4.31:1 on the page ground (--bg #F5F4EF) - and these labels are 11px,
      so AA asks 4.5:1 of them. Every spec list on a portal sits on the page
      ground rather than in a card, so every one of them has been failing since
      round 129 with nothing looking at it. --mute (#374151) is the token the
      design system already carries for readable secondary ink in this theme;
      the label keeps its size, its tracking and its uppercase role.
   2. LINKS IN A SENTENCE. A link inside body copy distinguished only by colour
      fails 1.4.1, and the accent against --mute is 1.73:1. Underlining a link
      that sits inside a sentence is the fix, and it is scoped to running text:
      a .ds-btn is not a link in a text block and keeps its own shape. */
html[data-ds] .ds-spec-label{color:var(--mute)}
/* ROUND 141: a left-aligned reading column for a full public page. SHELL's own
   .wrap centres its text, which is right for a sign-in door (one mark, one
   sentence, two buttons) and wrong for a page carrying a stat row, a step rail
   and three sections: half the page would be centred and half left, which reads
   as a layout that broke rather than one that was chosen. The measure is the
   design system's 840 content column. Layout only, no colour. */
html[data-ds] .ds-doc{max-width:840px;margin:0 auto;padding:32px 20px 64px;text-align:left;display:flex;flex-direction:column;gap:24px}
/* SHELL_CSS centres its h1, because its h1 is a sign-in door's one word under a
   mark. A document's title sits at the head of its own column. */
html[data-ds] .ds-doc > h1{margin:0;text-align:left}
html[data-ds] .ds-doc > .ds-eyebrow{margin:0}
html[data-ds] .ds-small a:not(.ds-btn),html[data-ds] .ds-measure a:not(.ds-btn){text-decoration:underline}
/* packages/ui/ui-components.css - ROUND 134 (ADR-103): the shared component
   vocabulary the Q4 platform draws from. CANONICAL FILE. Byte-copies live at
   relay/lib/ui-components.css and app/ui/v3/ui-components.css and are written
   by packages/ui/publish.mjs; never edit a copy.

   Every rule is scoped under the [data-ds] ATTRIBUTE with no html tag in
   front, so both the relay shell (html[data-ds], ADR-102) and a panel
   container stamped data-ds match the same selector. The relay appends this
   file at the END of DS_CSS, which keeps the previous sheets a byte-prefix of
   the new ones; nothing above it is edited.

   Colour comes from the ADR-103 token contract and nothing else: the shell's
   --bg --card --elev --line --line2 --ink --mute --faint --accent --on-accent
   --ok --warn --danger-ink --danger-line --sans --mono --display, plus the
   [data-ds] layer's --ds-ok-* --ds-warn-* --ds-neutral-* --ds-bad-*
   --ds-shadow --ds-row-hover --ds-tile-hover. The four values this file needs
   that no token carries are declared once, immediately below, in the same
   three-block pattern DS_CSS uses: base carries DARK (the shell's default),
   a prefers-color-scheme:light block that excludes a forced dark theme
   carries LIGHT, and [data-ds][data-theme="light"] carries it again so the
   toggle wins in both directions. They are the only raw colour values in the
   file. */
[data-ds]{
  --ds-col-bg:var(--elev);
  --ds-mask-ink:var(--faint);
  --ds-stale-bg:transparent;
  --ds-stale-line:var(--line2)}
@media (prefers-color-scheme:light){[data-ds]:not([data-theme="dark"]){
  --ds-col-bg:#EFEEE8;
  --ds-mask-ink:var(--faint);
  --ds-stale-bg:#FAFAF7;
  --ds-stale-line:var(--line)}}
[data-ds][data-theme="light"]{
  --ds-col-bg:#EFEEE8;
  --ds-mask-ink:var(--faint);
  --ds-stale-bg:#FAFAF7;
  --ds-stale-line:var(--line)}

/* STATUS PILL (.ds-status, ADR-103 decision 6). Four states and no fifth. The
   dot is 8px and takes currentColor, so a state names its colour once. The
   state word rides data-state, so a gate reads the attribute rather than the
   paint. live takes the ok trio, waking the warn trio (it is progress, not a
   fault), last_seen the neutral trio (the value is remembered, not wrong) and
   unknown the bad trio (nothing is known at all). */
[data-ds] .ds-status{display:inline-flex;align-items:center;gap:7px;height:24px;padding:0 11px;border-radius:999px;font:600 12px/1 var(--sans);white-space:nowrap;vertical-align:middle;background:var(--ds-neutral-bg);border:1px solid var(--ds-neutral-line);color:var(--ds-neutral-ink)}
[data-ds] .ds-status-dot{flex:none;width:8px;height:8px;border-radius:50%;background:currentColor}
[data-ds] .ds-status-text{min-width:0}
[data-ds] .ds-status-text time{font:inherit;color:inherit;text-decoration:none}
[data-ds] .ds-status-live{background:var(--ds-ok-bg);border-color:var(--ds-ok-line);color:var(--ds-ok-ink)}
[data-ds] .ds-status-waking{background:var(--ds-warn-bg);border-color:var(--ds-warn-line);color:var(--ds-warn-ink)}
[data-ds] .ds-status-last_seen{background:var(--ds-neutral-bg);border-color:var(--ds-neutral-line);color:var(--ds-neutral-ink)}
[data-ds] .ds-status-unknown{background:var(--ds-bad-bg);border-color:var(--ds-bad-line);color:var(--ds-bad-ink)}
/* The one animation in this file: a waking dot breathes, because starting is
   the only state that is going to change on its own. */
@keyframes ds-waking-pulse{0%,100%{opacity:1}50%{opacity:.35}}
[data-ds] .ds-status-waking .ds-status-dot{animation:ds-waking-pulse 1.6s ease-in-out infinite}
@media (prefers-reduced-motion:reduce){
  [data-ds] .ds-status-waking .ds-status-dot{animation:none}}

/* STAT ROW and KPI (.ds-stat-row, .ds-kpi, round 127A styleguide). The classes
   and the markup are the styleguide's; this file adds the state line. */
[data-ds] .ds-stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px}
[data-ds] .ds-kpi{display:flex;flex-direction:column;gap:8px;min-width:0}
[data-ds] .ds-kpi-label{font:600 11px/1.4 var(--sans);text-transform:uppercase;letter-spacing:.09em;color:var(--faint)}
[data-ds] .ds-kpi-value{font:500 28px/1.1 var(--mono);font-variant-numeric:tabular-nums lining-nums;color:var(--ink);overflow-wrap:anywhere}
[data-ds] .ds-kpi-value.ds-kpi-words{font:600 22px/1.2 var(--sans);letter-spacing:-.01em}
[data-ds] .ds-kpi-unit{font-size:.55em;color:var(--mute);margin-left:.35em}
[data-ds] .ds-kpi-note{font:400 13px/1.5 var(--sans);color:var(--mute)}
[data-ds] .ds-kpi-state{display:block}

/* SPLIT SECTION (.ds-split, round 127A styleguide). Words on one side,
   evidence on the other; .ds-split-flip puts the evidence first. */
[data-ds] .ds-split{display:grid;grid-template-columns:1fr 1fr;gap:48px;align-items:center}
[data-ds] .ds-split-copy{min-width:0}
[data-ds] .ds-split-copy>:last-child{margin-bottom:0}
[data-ds] .ds-split-data{min-width:0}
[data-ds] .ds-split-flip .ds-split-data{order:-1}
[data-ds] .ds-split-body{margin:0 0 16px;font-size:15px;line-height:1.6;color:var(--mute);max-width:640px}
[data-ds] .ds-split-link{color:var(--accent);font:600 14px/1.5 var(--sans)}

/* STEP RAIL (.ds-steps, round 127A styleguide). Three to five steps, numbered
   markers joined by a line. data-done and data-current are the hooks a surface
   reads; the marker states its own progress so nothing depends on colour. */
[data-ds] .ds-steps{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:16px;margin:0;padding:0;list-style:none;counter-reset:ds-step}
[data-ds] .ds-step{position:relative;padding-top:40px;font-size:15px;line-height:1.5;color:var(--mute);counter-increment:ds-step;min-width:0;overflow-wrap:anywhere}
[data-ds] .ds-step::before{content:counter(ds-step);position:absolute;top:0;left:0;display:grid;place-items:center;width:28px;height:28px;border:1px solid var(--accent);border-radius:50%;background:var(--card);color:var(--accent);font:400 13px/1 var(--mono);z-index:1}
[data-ds] .ds-step::after{content:"";position:absolute;top:14px;left:28px;right:-16px;height:1px;background:var(--line)}
[data-ds] .ds-step:last-child::after{display:none}
[data-ds] .ds-step[data-done="true"]{color:var(--ink)}
[data-ds] .ds-step[data-done="true"]::before{background:var(--accent);color:var(--on-accent)}
[data-ds] .ds-step[data-current="true"]{color:var(--ink);font-weight:600}
[data-ds] .ds-step[data-current="true"]::before{box-shadow:0 0 0 3px var(--ds-tile-hover)}

/* SPEC LIST (.ds-spec-list). Round 129 drew this in DS_CSS, under the selector
   `html[data-ds]`, and round 134 left it there - "the list itself is drawn by
   DS_CSS and is not touched here" - because at that point the only host that
   stamped data-ds was the relay's own <html>.

   Round 135 (WS18) is where that stopped being true. The panel stamps data-ds
   on its STAGE, not on <html>, so `html[data-ds]` matches nothing there and a
   spec list on a panel surface arrived with no layout at all: label and value
   ran together as one string ("Enforced atThe next agent build"). Found by
   looking at the first Rules baseline.

   So the layout is stated here, in the file both estates share, under the
   file's own bare-attribute rule. The declarations are DS_CSS's, value for
   value. On the relay `html[data-ds] .ds-spec-row` is the more specific
   selector and still wins, with the same numbers, so nothing already on a
   relay surface moves by a pixel; on the panel these are the only rules there
   are. DS_CSS keeps its copy: this file is appended to it, and editing what is
   above would break the byte-prefix ADR-102 gates. */
[data-ds] .ds-spec-list{display:block;margin:0;padding:0;border-top:1px solid var(--line)}
[data-ds] .ds-spec-row{display:flex;gap:16px;align-items:baseline;flex-wrap:wrap;padding:10px 0;border-bottom:1px solid var(--line)}
[data-ds] .ds-spec-label{flex:none;width:180px;font:600 11px/1.5 var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--faint)}
[data-ds] .ds-spec-value{flex:1;min-width:0;font-size:15px;line-height:1.6;color:var(--ink)}
[data-ds] .ds-spec-value.ds-data{font:400 13px/1.6 var(--mono);font-variant-numeric:tabular-nums lining-nums}
@media(max-width:720px){[data-ds] .ds-spec-row{gap:2px}[data-ds] .ds-spec-label{width:100%}}
/* The state line, which is this file's own from round 134. */
[data-ds] .ds-spec-state{display:inline-flex;margin-left:8px;vertical-align:middle}
/* A 64-character digest in a mono value has no break opportunity in it, and
   at 390 it carries the whole page 137px past the viewport. The rule is
   scoped to a row this LIBRARY drew (dsSpecList is the only thing that stamps
   data-state on a spec row), so every spec list already on a relay surface
   keeps the wrapping it has. */
[data-ds] .ds-spec-row[data-state] .ds-spec-value{min-width:0;overflow-wrap:anywhere}

/* KANBAN BOARD (.ds-kanban). Markup and hooks only: data-column and data-card
   are what phase 4 hangs drag on. No drag behaviour lives here. */
[data-ds] .ds-kanban{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(240px,1fr);gap:16px;align-items:start}
[data-ds] .ds-kanban-col{display:flex;flex-direction:column;gap:10px;min-width:0;padding:12px;border:1px solid var(--line);border-radius:8px;background:var(--ds-col-bg)}
[data-ds] .ds-kanban-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
[data-ds] .ds-kanban-title{font:600 11px/1.4 var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--faint);min-width:0;overflow-wrap:anywhere}
[data-ds] .ds-kanban-count{flex:none;font:400 12px/1.4 var(--mono);font-variant-numeric:tabular-nums lining-nums;color:var(--mute)}
[data-ds] .ds-kanban-card{display:flex;flex-direction:column;gap:8px;min-width:0;padding:12px;border:1px solid var(--line);border-radius:8px;background:var(--card);box-shadow:var(--ds-shadow)}
[data-ds] .ds-kanban-card:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
[data-ds] .ds-kanban-cardtitle{font:600 14px/1.35 var(--sans);color:var(--ink);overflow-wrap:anywhere}
[data-ds] .ds-kanban-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font:400 12px/1.4 var(--sans);color:var(--mute);min-width:0}
[data-ds] .ds-kanban-metatext{min-width:0;overflow-wrap:anywhere}
[data-ds] .ds-avatar{flex:none;display:grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--elev);color:var(--ink);font:600 10px/1 var(--sans);letter-spacing:.02em}

/* DASHBOARD PANEL (.ds-panel, ADR-103 decision 7). A panel in a state other
   than live never draws its live body: the state sentence replaces it, and a
   remembered body follows the sentence inside .ds-panel-stale so a reader can
   see at a glance that what is under the sentence is memory. */
[data-ds] .ds-panel{display:flex;flex-direction:column;gap:16px;padding:24px;border:1px solid var(--line);border-radius:8px;background:var(--card);box-shadow:var(--ds-shadow);min-width:0}
[data-ds] .ds-panel-head{display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;min-width:0}
[data-ds] .ds-panel-heading{flex:1;min-width:0}
[data-ds] .ds-panel-title{margin:0;font:600 18px/1.35 var(--sans);color:var(--ink);overflow-wrap:anywhere}
[data-ds] .ds-panel-actions{flex:none;display:flex;align-items:center;gap:8px;margin-left:auto;flex-wrap:wrap}
[data-ds] .ds-panel-body{min-width:0}
[data-ds] .ds-panel-state{margin:0;font-size:15px;line-height:1.6;color:var(--mute)}
[data-ds] .ds-panel-stale{margin-top:12px;padding-top:12px;border-top:1px solid var(--ds-stale-line);background:var(--ds-stale-bg);opacity:.72;min-width:0}

/* FILE BROWSER (.ds-files). A breadcrumb over a table of entries; the kind
   glyph is one outline path in currentColor with no fill, the family every
   icon in this estate belongs to. */
[data-ds] .ds-files-path{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:0 0 12px;font:400 13px/1.5 var(--sans);color:var(--mute);min-width:0}
[data-ds] .ds-files-crumb{min-width:0;overflow-wrap:anywhere}
[data-ds] .ds-files-crumb:last-child{color:var(--ink);font-weight:600}
[data-ds] .ds-files-sep{flex:none;color:var(--faint)}
[data-ds] .ds-files{width:100%;table-layout:fixed;border-collapse:collapse;font-variant-numeric:tabular-nums lining-nums}
[data-ds] .ds-files th{font:600 11px/1.4 var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--faint);text-align:left;padding:8px 10px;border-bottom:1px solid var(--line)}
[data-ds] .ds-files td{font-size:13px;line-height:1.5;color:var(--ink);padding:10px;border-bottom:1px solid var(--line);vertical-align:top;overflow-wrap:anywhere}
[data-ds] .ds-files tbody tr:hover{background:var(--ds-row-hover)}
[data-ds] .ds-files-name{display:flex;align-items:center;gap:8px;min-width:0}
[data-ds] .ds-files-name a{color:var(--ink)}
[data-ds] .ds-files-glyph{flex:none;color:var(--faint)}
/* A cell rule beats a class rule on specificity, so the two data columns are
   named AS cells; without this the td rule above would keep the body face on a
   size that is meant to be mono. The same correction DS_CSS makes for
   td.ds-mono. The header keeps its own tracked face, so only the width is
   shared between th and td. */
[data-ds] .ds-files th.ds-files-size,[data-ds] .ds-files td.ds-files-size{width:96px;text-align:right}
[data-ds] .ds-files th.ds-files-when,[data-ds] .ds-files td.ds-files-when{width:140px}
[data-ds] .ds-files td.ds-files-size{font:400 13px/1.5 var(--mono);font-variant-numeric:tabular-nums lining-nums}
[data-ds] .ds-files td.ds-files-when{font:400 13px/1.5 var(--sans);color:var(--mute)}

/* FLOW CANVAS NODE (.ds-node). The n8n grammar design.md records: a glyph in a
   framed square with the title BELOW it, and a port marker on each side. Hooks
   only: data-node, data-kind, data-state, data-port. */
/* The box is 132px wide at rest (a 44px glyph over a title that wraps at
   112px), and grows only to hold a state pill: a corner pill that is out of
   flow sizes nothing, so "Last seen 4 Sep 14:02" hung 55px off the left edge
   of the frame and would sit over the next node on a canvas. The pill is a
   flowed item ordered to the top right instead, so the node states its own
   width and the instant stays whole. */
[data-ds] .ds-node{position:relative;display:flex;flex-direction:column;align-items:center;gap:8px;width:max-content;min-width:132px;max-width:100%;padding:14px 10px;border:1px solid var(--line);border-radius:8px;background:var(--card);box-shadow:var(--ds-shadow);text-align:center}
[data-ds] .ds-node:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
[data-ds] .ds-node[data-selected="true"]{border-color:var(--accent);box-shadow:0 0 0 2px var(--ds-tile-hover)}
[data-ds] .ds-node-glyph{display:grid;place-items:center;width:44px;height:44px;border:1px solid var(--line2);border-radius:8px;background:var(--ds-col-bg);color:var(--accent)}
[data-ds] .ds-node-title{max-width:112px;font:600 13px/1.35 var(--sans);color:var(--ink);overflow-wrap:anywhere}
[data-ds] .ds-node-state{order:-1;align-self:flex-end;margin:-6px -4px -4px 0;max-width:100%}
[data-ds] .ds-node-state .ds-status{height:18px;padding:0 7px;font-size:10px;gap:5px}
[data-ds] .ds-node-state .ds-status-dot{width:6px;height:6px}
[data-ds] .ds-port{position:absolute;top:50%;width:9px;height:9px;margin-top:-4.5px;border:1px solid var(--line2);border-radius:50%;background:var(--card)}
[data-ds] .ds-port-in{left:-5px}
[data-ds] .ds-port-out{right:-5px}

/* VAULT ROW (.ds-vault-row). The masked value is CSS content and is never
   data: the renderer refuses a value or a secret property outright, so there
   is no path by which a secret reaches this markup. */
[data-ds] .ds-vault-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap;min-width:0;padding:12px 0;border-bottom:1px solid var(--line)}
[data-ds] .ds-vault-name{font:400 13px/1.5 var(--mono);color:var(--ink);min-width:0;overflow-wrap:anywhere}
[data-ds] .ds-vault-mask{flex:none;font:400 13px/1.5 var(--mono);letter-spacing:.18em;color:var(--ds-mask-ink)}
[data-ds] .ds-vault-mask::before{content:"\2022\2022\2022\2022\2022\2022\2022\2022"}
[data-ds] .ds-vault-rotated{font:400 13px/1.5 var(--sans);color:var(--mute)}
[data-ds] .ds-vault-consumers{flex-basis:100%;font:400 12px/1.5 var(--sans);color:var(--faint);min-width:0;overflow-wrap:anywhere}

/* QUEUE ROW (.ds-queue-row). L3 of the goal: a queued thing states its
   position AND the reason it is waiting, in full, on the row itself. */
[data-ds] .ds-queue-row{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;min-width:0;padding:12px 0;border-bottom:1px solid var(--line)}
[data-ds] .ds-queue-pos{flex:none;font:400 13px/1.5 var(--mono);font-variant-numeric:tabular-nums lining-nums;color:var(--mute)}
[data-ds] .ds-queue-main{flex:1;min-width:0}
[data-ds] .ds-queue-title{display:block;font:600 14px/1.4 var(--sans);color:var(--ink);overflow-wrap:anywhere}
[data-ds] .ds-queue-reason{display:block;font:400 13px/1.5 var(--sans);color:var(--mute);overflow-wrap:anywhere}
[data-ds] .ds-queue-since{flex:none;font:400 13px/1.5 var(--sans);color:var(--faint)}

/* PHONE (max-width 720px). Everything that is a row of columns on a desktop
   becomes one column: a board that scrolls sideways on a phone hides work, and
   a gate that measures overflow would fail it anyway. */
@media (max-width:720px){
  [data-ds] .ds-stat-row{grid-template-columns:1fr}
  [data-ds] .ds-split{grid-template-columns:1fr;gap:24px}
  [data-ds] .ds-split-flip .ds-split-data{order:0}
  [data-ds] .ds-steps{grid-auto-flow:row;gap:12px}
  [data-ds] .ds-step{padding-top:0;padding-left:40px;min-height:28px}
  [data-ds] .ds-step::after{top:28px;bottom:-12px;left:13px;right:auto;width:1px;height:auto}
  [data-ds] .ds-kanban{grid-auto-flow:row;grid-auto-columns:auto}
  [data-ds] .ds-files-size{width:72px}
  [data-ds] .ds-files-when{width:88px;font-size:12px}
  [data-ds] .ds-panel{padding:16px}
  /* flex:none on a desktop keeps the cluster on one line beside the title; on
     a phone that same rule refuses to shrink, so three nowrap controls push
     the page 137px past a 390 viewport. Below 720 the cluster takes its own
     full-width line and wraps inside itself. */
  [data-ds] .ds-panel-actions{flex:1 1 100%;min-width:0;margin-left:0}
  [data-ds] .ds-node{width:100%}
}

/* ===========================================================================
   THE ADR-103 BASE SHAPES (round 135, phase 5 integration).
   ===========================================================================
   The renderers above lean on a layer of base shapes - the type roles, the
   card, the badge, the button and the ds form field - that round 129 drew in
   the RELAY's DS_CSS (relay/lib/shell.mjs) under html[data-ds]. The panel
   never loads that sheet and never stamps the attribute on <html>, so four of
   the five phase 5 surface groups each arrived carrying their own copy of the
   block: dashboards.css, browsers.css, flows.css and WS19's ds-base.css.
   dashboards.css recorded the debt in the file itself ("fold this block into
   packages/ui/ui-components.css so one file draws them for both estates") and
   left it because five groups were in flight at once. They have landed, so it
   is taken here: the rules live ONCE, in the library both estates publish.

   DS_CSS STAYS THE ORIGIN. Every declaration below is that block value for
   value, with the two transforms ADR-103 decision 4 already requires:
   `html[data-ds] X` becomes `[data-ds] X`, and a compound selector that also
   names a bare element (`h2:not(.pagetitle):not(.subhead)`) keeps only its
   `.ds-*` half, because restyling every h2 in a panel view would repaint
   surfaces phase 5 has not migrated. Nothing is retyped and no colour is a raw
   hex. On the relay this file is appended INSIDE DS_CSS's layer, so DS_CSS's
   own `html[data-ds]` rules are both later and more specific and keep winning:
   no relay surface can move because of this section.

   The spec list is NOT repeated here - the library has drawn it since round
   134 (see the SPEC LIST section above), and the four lane copies of it were
   duplicates of this file, not of DS_CSS. */

/* TYPE ROLES. The class forms only. */
[data-ds] .ds-h2{font-family:var(--sans);font-weight:700;font-size:26px;line-height:1.25;letter-spacing:-.01em;color:var(--ink);margin:0 0 12px}
[data-ds] .ds-h3{font-family:var(--sans);font-weight:600;font-size:18px;line-height:1.35;letter-spacing:0;color:var(--ink);margin:0 0 8px}
[data-ds] .ds-eyebrow{display:block;font:600 11px/1.4 var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--accent);margin:0 0 6px}
[data-ds] .ds-small{font-size:13px;line-height:1.5;color:var(--mute)}
[data-ds] .ds-data{font:400 13px/1.5 var(--mono);font-variant-numeric:tabular-nums lining-nums;color:var(--ink)}
[data-ds] .ds-mono{font:400 13px/1.5 var(--mono);font-variant-numeric:tabular-nums lining-nums}

/* CARD. Surface, 1px line border, radius 8, padding 24. dsStatRow's KPI cells
   are `.ds-card .ds-kpi`, so without this rule a stat row has no ground. */
[data-ds] .ds-card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:24px;box-shadow:var(--ds-shadow)}

/* BADGE. Pill, 24px, dot indicator in currentColor. */
[data-ds] .ds-badge{display:inline-flex;align-items:center;gap:7px;height:24px;padding:0 11px;border-radius:999px;font:600 12px/1 var(--sans);white-space:nowrap;background:var(--ds-neutral-bg);border:1px solid var(--ds-neutral-line);color:var(--ds-neutral-ink)}
[data-ds] .ds-badge::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:currentColor}
[data-ds] .ds-badge.ok{background:var(--ds-ok-bg);border-color:var(--ds-ok-line);color:var(--ds-ok-ink)}
[data-ds] .ds-badge.warn{background:var(--ds-warn-bg);border-color:var(--ds-warn-line);color:var(--ds-warn-ink)}
[data-ds] .ds-badge.neutral{background:var(--ds-neutral-bg);border-color:var(--ds-neutral-line);color:var(--ds-neutral-ink)}
[data-ds] .ds-badge.bad{background:var(--ds-bad-bg);border-color:var(--ds-bad-line);color:var(--ds-bad-ink)}

/* BUTTONS. Heights 40 and 32, radius 6, one 1px box on every variant so a
   mixed row lines up. The law of one primary per view is the page's to keep. */
[data-ds] .ds-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:40px;padding:0 16px;border:1px solid var(--accent);border-radius:6px;background:var(--accent);color:var(--on-accent);font:600 14px/1 var(--sans);text-align:center;text-decoration:none;white-space:nowrap;cursor:pointer;transition:background .16s,border-color .16s,color .16s}
[data-ds] .ds-btn:hover{background:var(--ds-accent-hover);border-color:var(--ds-accent-hover);text-decoration:none}
[data-ds] .ds-btn-sm{height:32px;padding:0 12px;font-size:13px}
[data-ds] .ds-btn-secondary{background:var(--card);border-color:var(--line);color:var(--accent)}
[data-ds] .ds-btn-secondary:hover{background:var(--ds-tile-hover);border-color:var(--accent);color:var(--accent)}
[data-ds] .ds-btn-ghost{background:transparent;border-color:transparent;color:var(--accent)}
[data-ds] .ds-btn-ghost:hover{background:var(--ds-tile-hover);border-color:transparent;color:var(--accent)}
[data-ds] .ds-btn-danger{background:transparent;border-color:var(--ds-bad-edge);color:var(--ds-bad-ink)}
[data-ds] .ds-btn-danger:hover{background:var(--ds-bad-ink);border-color:var(--ds-bad-ink);color:var(--bg)}
[data-ds] .ds-btn:disabled,[data-ds] .ds-btn[aria-disabled="true"]{opacity:.55;pointer-events:none}

/* FORM FIELD. Label above, 40px control, radius 6, help text under. */
[data-ds] .ds-field{display:flex;flex-direction:column;gap:6px;margin:0 0 16px}
[data-ds] .ds-label{font:600 13px/1.4 var(--sans);color:var(--ink)}
[data-ds] .ds-input{width:100%;height:40px;padding:0 12px;border:1px solid var(--line);border-radius:6px;background:var(--ds-input-bg);color:var(--ink);font:400 14px/1.4 var(--sans);transition:border-color .16s}
[data-ds] select.ds-input{padding-right:8px}
[data-ds] textarea.ds-input{height:auto;min-height:96px;padding:10px 12px;line-height:1.5;resize:vertical}
[data-ds] .ds-input:focus{outline:2px solid var(--accent);outline-offset:2px;border-color:var(--accent);box-shadow:none}
[data-ds] .ds-input::placeholder{color:var(--faint)}
[data-ds] .ds-help{font:400 13px/1.5 var(--sans);color:var(--mute)}
[data-ds] .ds-field-error>.ds-input{border-color:var(--ds-bad-ink)}
[data-ds] .ds-field-error>.ds-help{color:var(--ds-bad-ink)}

/* THE PANEL BODY STACK. This one is the library's own, not a DS_CSS lift:
   dashboards.css wrote it because a run of evidence shapes inside one panel
   body (a stat row over a list, a spec list over a control cluster) carries no
   gap of its own, and the shape it spaces is `.ds-panel-body`, which this file
   draws. A surface that wants a different rhythm says so on its own root with
   one more class in the selector, the way market.css does. */
[data-ds] .ds-panel-body>*+*{margin-top:16px}

@media (max-width:720px){
  /* A 16px step between panels reads as a hairline of air on a 390px screen,
     where the panels are already edge to edge. */
  [data-ds] .ds-panel-body>*+*{margin-top:12px}
}
@media(pointer:coarse){
  [data-ds] .ds-btn{min-height:44px}
  [data-ds] .ds-btn-sm{min-height:40px}
  [data-ds] .ds-input{min-height:46px;font-size:16px}
  [data-ds] textarea.ds-input{min-height:96px}}
@media (prefers-reduced-motion:reduce){[data-ds] .ds-btn,[data-ds] .ds-input{transition:none}}
