/* shopfront.css — the shell every new portal page shares.
 *
 * 🚨 EXTRACTED 27/08/2026, at the moment the third page was created and the copy-the-shell habit
 * became a three-way fork. home.html, automations.html and map.html each carried their own copy
 * of this block; adding the map's styles to one of them left the other two behind within the
 * same minute. Three copies of a stylesheet is three answers to "what does a card look like".
 *
 * One file, linked by every page. A page keeps only what is genuinely its own, which so far is
 * nothing.
 */
*{box-sizing:border-box}
html,body{height:100%;margin:0}
body{font-family:var(--sf-font);background:var(--sf-ground);color:var(--text);font-size:var(--sf-fs-body-lg);-webkit-font-smoothing:antialiased}

/* 🚨 ONE TYPEFACE ON THE PAGE. THIS IS NOT TIDY-UP — IT WAS A LIVE DEFECT (Hamish, 02/09/2026:
   "the font's changing on the new portal", and he was right).

   FORM CONTROLS DO NOT INHERIT font-family. It is the one property browsers deliberately refuse
   to pass down, so a <button> falls back to the browser's own default — Arial/Helvetica on a
   Mac — no matter what `body` says. `.sf-btn` never set one. MEASURED on the automation page:
   140 elements rendered in the page's stack and THREE rendered in Arial, and those three were
   "Run it now", "Questions and to-dos" and its badge. The two most prominent controls on the
   page were in a different typeface from everything around them, and had been since .sf-btn was
   written. <pre> is the same trap with a louder result: it falls back to MONOSPACE, which is how
   a run's report was rendering.

   Set once, here, for every element that refuses to inherit — rather than remembered per rule,
   which is how three of them were missed. `scripts/test-design-tokens.js` now fails if this
   block goes away. */
button,input,select,textarea,optgroup,pre,code,kbd,samp{font-family:inherit;font-size:inherit}
/* A <pre> keeps its wrapping behaviour but loses the monospace: this is prose the engine wrote,
   not code, and it is read as a sentence. */
pre{white-space:pre-wrap}
.hidden{display:none!important}
a{text-decoration:none;color:inherit}

#loading{position:fixed;inset:0;background:var(--surface);z-index:90;display:flex;align-items:center;justify-content:center;color:var(--mut);font-size:var(--sf-fs-meta);padding:24px;text-align:center}
#localbanner{position:fixed;left:0;right:0;top:0;z-index:80;background:var(--sf-banner-bg);border-bottom:1px solid var(--sf-banner-line);color:var(--sf-banner-ink);text-align:center;font-size:var(--sf-fs-label);padding:7px;font-weight:600}
body.has-banner #app{padding-top:30px}

/* ---------- shell: fixed sidebar, scrolling main ---------- */
/* 🚨 THE APP IS A CARD ON A GROUND, NOT THE WHOLE WINDOW (10/09/2026, measured off home.png).
   ⚠️ NO `overflow:hidden` HERE. It would make #app a scroll container and .sf-side's
   `position:sticky` would resolve against it instead of the viewport, so the sidebar would
   scroll away with the page. The corners are cut on the sidebar itself instead. */
#app{min-height:calc(100vh - var(--sf-frame) * 2);display:flex;margin:var(--sf-frame);
     background:var(--surface);border-radius:var(--sf-frame-radius)}

/* ---------- sidebar ---------- */
.sf-side{width:var(--sf-side);flex:none;border-right:1px solid var(--border);background:var(--surface);
         padding:26px 20px 20px;display:flex;flex-direction:column;gap:26px;position:sticky;
         top:var(--sf-frame);height:calc(100vh - var(--sf-frame) * 2);
         border-radius:var(--sf-frame-radius) 0 0 var(--sf-frame-radius)}
.sf-brand{display:flex;align-items:center;padding:0 8px}
/* 46px, MEASURED 10/09/2026: the wordmark's ink band in home.png is 20px tall against 14px in
   the build, so the reference logo is ~1.43x this one. 34 x 1.43 = 48.6; 46 lands the ink band on
   the reference's without overshooting the sidebar's top padding. */
/* 🚨 width:auto IS LOAD-BEARING, 10/09/2026. The markup carries width="129" height="34", the
   real proportions of the file. Overriding only the HEIGHT to 46px left the width attribute
   standing, so every page in the portal drew the logo 129 x 46: stretched 35% taller than it is,
   on all fourteen pages, since the 46px change landed. An explicit width and height on an <img>
   are a promise about its shape, and changing one of them in CSS breaks that promise unless the
   other is released. */
.sf-brand img{height:46px;width:auto;display:block}
/* UI1, 04/09/2026: the skip link every page needs and none had. Off-screen until it receives
   keyboard focus (tab from a fresh page load lands here first), then pinned top-left so a
   keyboard or screen-reader user can jump straight to #sf-main instead of tabbing through the
   whole sidebar nav on every single page load. */
.sf-skip-link{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;
  z-index:1000}
.sf-skip-link:focus{left:12px;top:12px;width:auto;height:auto;padding:10px 16px;
  background:var(--sf-navy);color:var(--surface);border-radius:6px;font-weight:700;
  text-decoration:none;box-shadow:0 4px 14px rgba(0,0,0,.25)}
.sf-nav{flex:1;min-height:0;overflow-y:auto}
.sf-nav ul,.sf-nav-foot ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:3px}
/* padding 16px, not 11px, MEASURED 10/09/2026: nav rows in home.png are pitched 54px apart and
   the build's were 44px. The rows carry the sidebar's whole rhythm, and 10px per row across nine
   rows is what made the nav read as cramped against the reference. */
.sf-navitem{display:flex;align-items:center;gap:13px;padding:16px 14px;border-radius:12px;
            font-weight:600;font-size:var(--sf-fs-ui);color:var(--sf-ink-2);line-height:1}
a.sf-navitem:hover{background:var(--surface-2);color:var(--sf-navy)}
a.sf-navitem.is-active{background:var(--sf-blue-soft);color:var(--sf-blue)}
.sf-navitem.is-off{color:var(--sf-ink-off);cursor:default}
.sf-navitem span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sf-soon{font-style:normal;font-size:var(--sf-fs-micro);font-weight:800;letter-spacing:.06em;text-transform:uppercase;
         color:var(--sf-ink-off);background:var(--surface-2);border:1px solid var(--border);border-radius:5px;padding:2px 5px}
.sf-nav-foot{border-top:1px solid var(--sf-line-2);padding-top:14px}
.sf-ic{width:20px;height:20px;flex:none;display:block}
.sf-ic-blue{color:var(--sf-blue)}
.sf-ic-sm{width:14px;height:14px}

/* the workspace switcher: drawn, and inert while there is one workspace */
.sf-ws{display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--border);
       border-radius:12px;background:var(--surface);color:var(--sf-navy);font-weight:650;font-size:var(--sf-fs-body)}
.sf-ws-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sf-ws .sf-ic{width:16px;height:16px;color:var(--mut)}

/* ---------- main column ---------- */
.sf-col{flex:1;min-width:0;display:flex;flex-direction:column}
.sf-top{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:18px 30px 0}
.sf-icobtn{width:38px;height:38px;border-radius:11px;display:flex;align-items:center;justify-content:center;color:var(--sf-ink-2)}
.sf-icobtn[data-state="off"]{opacity:.55}
.sf-avatar{width:38px;height:38px;border-radius:50%;background:var(--sf-blue-soft);color:var(--sf-blue);
           display:flex;align-items:center;justify-content:center;font-weight:800;font-size:var(--sf-fs-meta-sm);flex:none;letter-spacing:.02em}
.sf-avatar-sm{width:28px;height:28px;font-size:var(--sf-fs-label-xs)}
/* 40px, measured: the mockups put the content's left edge at x=326 against a 285px sidebar. */
.sf-main{flex:1;min-width:0;padding:14px 40px 44px}

/* ---------- the ask block ---------- */
.sf-ask{max-width:1040px;margin:0 auto 34px;text-align:center}
/* The greeting is the first thing a client sees every morning; at 46px it sat level with a page
   heading. 🚨 THE SIZE IS A TOKEN NOW — see design-tokens.css. The top margin is 40px, not the
   26px it carried, because part 2 measured our heading starting 23px above the reference's.
   The ask box is ~176px tall, measured off 00-home.png. */
.sf-ask h1{margin:40px 0 10px;font-size:var(--sf-fs-hero);line-height:1.06;font-weight:800;letter-spacing:-.035em;color:var(--sf-navy)}
.sf-ask-sub{margin:0 0 30px;color:var(--sf-ink-4);font-size:var(--sf-fs-hero-sub);font-weight:500}
.sf-askbox{max-width:1040px;margin:0 auto;text-align:left;background:var(--surface);
           border:1.5px solid var(--sf-blue-edge);border-radius:18px;padding:26px 26px 20px;
           box-shadow:var(--sf-ask-ring)}
.sf-askbox-line{color:var(--sf-ink-4);font-size:var(--sf-fs-page-sub);padding-bottom:52px}
.sf-askbox-foot{display:flex;align-items:center;gap:12px}
.sf-sq{width:40px;height:40px;border-radius:11px;border:1px solid var(--border);display:flex;
       align-items:center;justify-content:center;color:var(--sf-ink-2);flex:none;opacity:.6}
.sf-sq-hammer{margin-left:auto}
.sf-sq-send{background:var(--sf-blue);border-color:var(--sf-blue);color:var(--sf-on-blue)}
.sf-off-note{flex:1;font-size:var(--sf-fs-label-sm);font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--sf-ink-off)}

/* ---------- the card row ---------- */
/* stretch, not start: 00-home.png has all four cards the same height, topped and tailed on the
   same lines. With `start` each card ended wherever its own content did and the row read as four
   unrelated boxes. The cards do NOT gain invented content to fill the height — D2 stands — they
   simply share it. */
.sf-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--sf-gap);align-items:stretch}
.sf-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
         box-shadow:var(--sf-card-shadow);padding:20px 20px 18px;display:flex;flex-direction:column;min-width:0}
/* A card with no source reads calm and deliberate, never as an error. Red or amber here would
   tell a client something is broken; nothing is broken, it is unbuilt. */
.sf-card-gap{background:var(--surface-2);box-shadow:none}
.sf-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:16px}
.sf-card-head h2{margin:0;font-size:var(--sf-fs-title-sm);font-weight:800;letter-spacing:-.015em;color:var(--sf-navy)}
.sf-viewall{flex:none;font-size:var(--sf-fs-meta-sm);font-weight:700;color:var(--sf-blue)}
.sf-viewall:hover{text-decoration:underline}
.sf-chip-off{flex:none;font-size:var(--sf-fs-label-xs);font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--sf-ink-off)}
.sf-card-body{flex:1;min-width:0}
.sf-empty{margin:0 0 12px;color:var(--sf-ink-2);font-size:var(--sf-fs-body);line-height:1.55}
.sf-sub{font-size:var(--sf-fs-label);font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--sf-ink-off);margin:0 0 8px}
/* Home's two live cards (17/09/2026): the person's own recent documents and today's meetings.
   Declared in the same pass as the markup - an undeclared class reads as bad taste rather than as
   a missing line (WS-PAGES-UNDECLARED-CLASSES, 11/09/2026). */
.sf-homelist{list-style:none;margin:0 0 12px;padding:0;display:flex;flex-direction:column;gap:8px}
.sf-homerow{display:flex;align-items:baseline;gap:10px;min-width:0;font-size:var(--sf-fs-body);color:var(--sf-ink-2)}
.sf-homerow-main{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--sf-ink)}
.sf-homerow-meta{flex:none;color:var(--sf-ink-4);font-size:var(--sf-fs-meta);white-space:nowrap}
.sf-sub + .sf-empty{margin-bottom:18px}

/* dashed panel: the design's shape, marked off, carrying no control */
.sf-dashed{display:flex;align-items:center;gap:12px;border:1.5px dashed var(--sf-line-dashed);border-radius:13px;
           padding:16px 14px;background:var(--surface);opacity:.85}
.sf-dashed-t{display:block;font-weight:700;font-size:var(--sf-fs-meta);color:var(--sf-navy)}
.sf-dashed-s{display:block;font-size:var(--sf-fs-label);color:var(--mut);margin-top:2px;line-height:1.4}
.sf-off{margin-left:auto;flex:none;font-size:var(--sf-fs-micro);font-weight:800;letter-spacing:.06em;text-transform:uppercase;
        color:var(--sf-ink-off);border:1px solid var(--border);border-radius:5px;padding:2px 5px}

/* tasks */
.sf-lane{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}
.sf-task{display:flex;align-items:center;gap:10px;padding:10px 2px;min-width:0;border-bottom:1px solid var(--sf-line-2)}
.sf-task:last-child{border-bottom:0}
.sf-task .sf-ic{color:var(--sf-ink-faint)}
.sf-task-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;color:var(--ink);font-size:var(--sf-fs-body)}
/* 🚨 THE BASE CARRIES A BACKGROUND, and it did not before. A status with no rule of its own —
   "Agreed", "Withdrawn", and anything a future register adds — fell through to a transparent
   background and rendered as bare grey text in a column of pills. The register's status list is
   open-ended and this stylesheet cannot know it, so the DEFAULT has to be a pill; the known
   statuses override the colour below. Found rendering 06-automations.png side by side. */
.sf-pill{flex:none;font-size:var(--sf-fs-meta-sm);font-weight:700;border-radius:999px;padding:6px 15px;
         line-height:1.45;background:var(--surface-2);color:var(--sf-ink-2)}
.sf-pill-high{color:var(--sf-danger-ink);background:var(--sf-danger-bg)}
.sf-pill-live{color:var(--sf-ok-ink);background:var(--sf-ok-bg)}
.sf-pill-building{color:var(--sf-warn-ink);background:var(--sf-warn-bg)}
.sf-pill-planned,.sf-pill-proposed{color:var(--sf-ink-2);background:var(--surface-2)}
.sf-pill-paused{color:var(--sf-danger-ink);background:var(--sf-danger-bg)}
.sf-more{margin:10px 0 0;font-size:var(--sf-fs-meta-sm);color:var(--mut)}
.sf-tally{display:flex;gap:18px;margin:14px 0 0;padding-top:13px;border-top:1px solid var(--sf-line-2)}
.sf-tally div{display:flex;flex-direction:column;gap:1px}
.sf-tally dt{font-size:var(--sf-fs-label-xs);color:var(--mut)}
.sf-tally dd{margin:0;font-size:var(--sf-fs-tally);font-weight:800;color:var(--sf-navy)}
.sf-tally-line{margin:12px 0 0;font-size:var(--sf-fs-meta-sm);font-weight:700;color:var(--sf-blue)}

/* automations (its own page card, not on home) */
.sf-autos{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}
.sf-auto{display:flex;align-items:center;gap:11px;padding:11px 12px;background:var(--surface);
         border:1px solid var(--border);border-radius:13px;min-width:0}
.sf-tile{width:34px;height:34px;flex:none;border-radius:10px;background:var(--sf-blue-soft);color:var(--sf-blue);
         display:flex;align-items:center;justify-content:center}
.sf-auto-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.sf-auto-name{font-weight:700;font-size:var(--sf-fs-body);color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sf-auto-run{font-size:var(--sf-fs-label-sm);color:var(--mut)}

/* the honesty line under a card: small, grey, always present when there is one to make —
   never a tooltip, because a caveat nobody sees is a caveat nobody made */
.sf-note{margin:16px 0 0;padding-top:13px;border-top:1px solid var(--sf-line-2);font-size:var(--sf-fs-label-sm);line-height:1.5;color:var(--sf-ink-3)}

/* ---------- page heads, stat tiles and the table (Automations page) ---------- */
/* The page heading was 34px and read as a card title rather than as the name of the page. 🚨 THE
   SIZE IS A TOKEN NOW — see design-tokens.css. The 33px top margin is part 2's: measured on both
   tasks and automations, our heading was starting 21px and 32px above the reference's. */
.sf-page-head{margin:33px 0 26px}
.sf-page-head h1{margin:0 0 6px;font-size:var(--sf-fs-page-title);font-weight:800;letter-spacing:-.03em;
                 line-height:1.08;color:var(--sf-navy)}
.sf-page-sub{margin:0;color:var(--sf-ink-4);font-size:var(--sf-fs-page-sub)}
.sf-tiles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sf-gap);margin-bottom:var(--sf-gap)}
/* Measured off 06-automations.png: the stat number is ~56px in a ~125px tall card. */
.sf-tile-stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
              box-shadow:var(--sf-card-shadow);padding:28px 24px;text-align:center}
.sf-tile-n{font-size:var(--sf-fs-stat);font-weight:800;letter-spacing:-.035em;color:var(--sf-navy);line-height:1.02}
.sf-tile-l{font-size:var(--sf-fs-ui-lg);color:var(--mut);margin-top:4px}
.sf-tablecard{padding:0;overflow:hidden}
/* The table scrolls inside its own card rather than pushing the page sideways. */
.sf-tablecard{overflow-x:auto}
.sf-table{width:100%;border-collapse:collapse;font-size:var(--sf-fs-ui);min-width:720px}
.sf-table th{text-align:left;font-size:var(--sf-fs-label);font-weight:700;color:var(--mut);padding:16px 20px;
             border-bottom:1px solid var(--border);white-space:nowrap}
.sf-table td{padding:18px 22px;border-bottom:1px solid var(--sf-line-2);vertical-align:middle;color:var(--sf-ink-2)}
.sf-table tr:last-child td{border-bottom:0}
.sf-table tr:hover td{background:var(--surface-2)}
/* The whole row leads to the automation's own page, so the name is the link and the row carries
   the pointer. The link is a REAL anchor, not a click handler on the row: it opens in a new tab
   with a middle-click, it is reachable by keyboard, and it works with script disabled. */
.sf-table-rows tbody tr{cursor:pointer}
.sf-rowlink{color:inherit;text-decoration:none;font-weight:600}
.sf-table-rows tbody tr:hover .sf-rowlink{color:var(--sf-blue)}
.sf-rowlink:focus-visible{outline:2px solid var(--sf-blue);outline-offset:2px;border-radius:4px}
.sf-td-name{display:flex;align-items:center;gap:11px;font-weight:700;color:var(--ink);white-space:nowrap}
.sf-td-what{max-width:420px}
.sf-td-go{text-align:right;white-space:nowrap}
.sf-unset{color:var(--sf-ink-off)}

/* ---------- the map page ---------- */
.sf-map-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
.sf-map-meta{display:flex;align-items:center;gap:16px;font-size:var(--sf-fs-body);color:var(--mut);
             white-space:nowrap;padding-top:10px}
.sf-mapcard{padding:14px 14px 14px;position:relative}
.sf-legend{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px}
.sf-dot{width:11px;height:11px;border-radius:50%;display:inline-block;flex:none}
/* The mockup's legend: dot + label pairs in one quiet strip, separated by hairlines, not the
   pill-per-row chips this had. */
.sf-legend-row{display:inline-flex;align-items:center;gap:8px;font-size:var(--sf-fs-meta);font-weight:600;
               color:var(--sf-ink-2);padding:4px 14px;border-right:1px solid var(--sf-line-2)}
.sf-legend-row:last-child{border-right:0}
/* The canvas scrolls inside its own card; the page itself never scrolls sideways. */
.sf-mapwrap{background:var(--sf-canvas);border:1px solid var(--sf-line-2);border-radius:12px;
            height:min(72vh,760px);overflow:hidden;position:relative}
.sf-map-svg{width:100%;height:100%;display:block}
/* The floating chrome, as drawn: minimap bottom-left, zoom cluster bottom-right. Both sit ON
   the canvas, so fullscreen carries them with it. */
.sf-mapmini{position:absolute;left:14px;bottom:14px;width:150px;height:104px;background:var(--surface);
            border:1px solid var(--border);border-radius:10px;box-shadow:var(--sf-card-shadow);
            padding:5px;cursor:pointer}
.sf-mapzoom{position:absolute;right:14px;bottom:14px;display:flex;gap:8px}
.sf-zoom{min-width:44px;height:40px;border:1px solid var(--border);border-radius:11px;background:var(--surface);
         display:inline-flex;align-items:center;justify-content:center;font-size:var(--sf-fs-ui-lg);font-weight:700;
         color:var(--sf-ink-2);cursor:pointer;padding:0 13px;box-shadow:var(--sf-card-shadow)}
.sf-zoom:hover{border-color:var(--sf-blue);color:var(--sf-blue)}
.sf-zoom:focus-visible{outline:2px solid var(--sf-blue);outline-offset:2px}

/* ---------- laptop down ---------- */
@media (max-width:1240px){ .sf-grid{grid-template-columns:repeat(2,minmax(0,1fr))} .sf-ask h1{font-size:var(--sf-fs-hero-md)} }

/* On the HOME card the task titles WRAP, as 00-home.png has them. The shared .sf-task-title
   ellipsises on one line, which is right in a wide table row and wrong in a quarter-width card:
   at that width every title truncated to "Send ..." and the card told you nothing. */
.sf-tasks{list-style:none;margin:0;padding:0}
.sf-tasks .sf-task{align-items:flex-start;gap:9px;padding:9px 2px}
.sf-tasks .sf-task-title{white-space:normal;overflow:visible;text-overflow:clip;line-height:1.4}
.sf-tasks .sf-ic{margin-top:1px}
.sf-tasks .sf-pill{align-self:flex-start}

/* ---------- the Tasks page ----------
   The tick is a real control — it writes — so it is a real button with a real focus ring.
   Everything drawn-but-off elsewhere in this UI is a span; nothing here is a disabled button. */
.sf-chips{display:flex;gap:10px;flex-wrap:wrap;margin:0 0 22px}
.sf-chip{display:inline-flex;align-items:center;gap:7px;font-size:var(--sf-fs-ui);font-weight:600;
         color:var(--sf-ink-2);background:var(--surface);border:1px solid var(--border);
         border-radius:999px;padding:10px 22px;cursor:pointer;user-select:none}
.sf-chip:hover{border-color:var(--sf-line-strong)}
/* The mockup's active chip is SOLID blue with white text, not the pale tint this had. */
.sf-chip.is-on{color:var(--sf-on-blue);background:var(--sf-blue);border-color:var(--sf-blue);font-weight:700}
.sf-chip.is-on:hover{border-color:var(--sf-blue)}

.sf-listcard{padding:4px 26px}
.sf-tasklist{list-style:none;margin:0;padding:0}
/* Our own tasks from the brain, own workspace only (REG-PORTAL-TASKS). Read only, so no tick
   column: title, then due date, then the one-liner under both. Tokens only. */
.sf-own{margin-top:var(--sf-gap)}
.sf-own-group{margin:18px 0 4px;font-size:var(--sf-fs-label);color:var(--sf-ink-3);
  text-transform:uppercase;letter-spacing:.04em}
.sf-own-count{color:var(--sf-ink-4);font-weight:600}
.sf-own-list{margin-bottom:6px}
/* 15/09/2026: each task is now a LONG CARD that opens in place, with a coloured urgency edge,
   over a search box and urgency chips. The row styles below stay for the summary line inside it. */
.sf-own-row{display:block;padding:0;border-top:0;margin:0 0 8px}
.sf-own-card{border:1px solid var(--sf-line);border-radius:14px;background:var(--surface);
  box-shadow:inset 4px 0 0 var(--sf-own-edge,var(--sf-line));transition:box-shadow var(--sf-t) var(--sf-ease),border-color var(--sf-t) var(--sf-ease)}
.sf-own-card:hover{border-color:var(--sf-line-strong)}
.sf-own-card[open]{border-color:var(--sf-blue-line);box-shadow:inset 4px 0 0 var(--sf-own-edge,var(--sf-line)),var(--sf-lift-sm)}
.sf-own-card>summary{list-style:none;cursor:pointer;display:grid;grid-template-columns:minmax(0,1fr) auto;
  gap:4px 16px;padding:14px 18px 14px 22px;border-radius:14px}
.sf-own-card>summary::-webkit-details-marker{display:none}
.sf-own-card>summary:focus-visible{outline:2px solid var(--sf-blue);outline-offset:-2px}
.sf-own-overdue{--sf-own-edge:var(--sf-danger-ink)}
.sf-own-week{--sf-own-edge:var(--sf-warn-ink)}
.sf-own-later{--sf-own-edge:var(--sf-blue)}
.sf-own-nodate{--sf-own-edge:var(--sf-line-strong)}
.sf-own-overdue .sf-own-due{color:var(--sf-danger-ink);font-weight:650}
.sf-own-more{display:grid;grid-template-columns:110px 1fr;gap:6px 14px;margin:0;padding:12px 22px 16px;
  border-top:1px solid var(--sf-line);font-size:var(--sf-fs-meta)}
.sf-own-more dt{color:var(--sf-ink-3);font-weight:600}
.sf-own-more dd{margin:0;color:var(--sf-ink-2);overflow-wrap:anywhere}
.sf-own-tools{display:flex;flex-direction:column;gap:10px;margin:8px 0 6px}
.sf-own-search input{width:100%;box-sizing:border-box;border:1px solid var(--sf-line);border-radius:12px;
  padding:11px 14px;font:inherit;font-size:var(--sf-fs-body);background:var(--surface);color:var(--sf-navy)}
.sf-own-search input:focus{outline:2px solid var(--sf-blue);outline-offset:1px;border-color:transparent}
.sf-own-chips{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.sf-own-chip{border:1px solid var(--sf-line);background:var(--surface);color:var(--sf-ink-2);border-radius:999px;
  padding:6px 12px;font:inherit;font-size:var(--sf-fs-meta);cursor:pointer;transition:background var(--sf-t-fast) var(--sf-ease),border-color var(--sf-t-fast) var(--sf-ease)}
.sf-own-chip:hover{border-color:var(--sf-blue-line);color:var(--sf-navy)}
.sf-own-chip.is-on{background:var(--sf-blue);border-color:var(--sf-blue);color:var(--sf-on-blue);font-weight:650}
.sf-own-chip.is-on .sf-own-count{color:inherit;opacity:.8}
.sf-own-clear{border:0;background:none;color:var(--sf-blue-ink);font:inherit;font-size:var(--sf-fs-meta);cursor:pointer;padding:6px 4px}
@media (max-width:640px){
  .sf-own-card>summary{grid-template-columns:1fr;padding-left:18px}
  .sf-own-more{grid-template-columns:1fr;gap:0}
  .sf-own-more dd{margin-bottom:8px}
}
.sf-own-title{font-size:var(--sf-fs-body);font-weight:650;color:var(--sf-navy);min-width:0;overflow-wrap:anywhere}
.sf-own-due{font-size:var(--sf-fs-meta);color:var(--sf-ink-3);white-space:nowrap}
.sf-own-waiting{font-style:normal;font-weight:650;color:var(--sf-warn-ink)}
.sf-own-line{grid-column:1 / -1;font-size:var(--sf-fs-meta);color:var(--sf-ink-2);overflow-wrap:anywhere}
/* Rows are ~78px tall in the mockup and were 55 here — the list read as dense and administrative
   rather than as the roomy list drawn. nowrap because a row that wraps loses the column
   alignment that is the whole point of the middle column. */
.sf-taskrow{display:flex;align-items:center;gap:12px;padding:24px 0;min-width:0;flex-wrap:nowrap;
            border-bottom:1px solid var(--sf-line-2)}
.sf-taskrow:last-child{border-bottom:0}
/* THE MOVEMENT IS THE RECEIPT. Ruled 27/08: the circle goes green, the row slides right and the
   rows below close the gap, so she never has to hunt for confirmation that the tick registered. */
.sf-taskrow[data-lane="done"]{transform:translateX(6px);opacity:.62;
                              transition:transform .18s ease,opacity .18s ease}
.sf-tick{flex:none;width:26px;height:26px;display:flex;align-items:center;justify-content:center;
         background:none;border:0;padding:0;cursor:pointer;color:var(--sf-ink-faint);border-radius:50%}
.sf-tick:hover{color:var(--sf-blue)}
.sf-tick:disabled{opacity:.45;cursor:default}
.sf-tick:focus-visible{outline:2px solid var(--sf-blue);outline-offset:2px}
/* THREE COLUMNS, as drawn: title (flexible), the agent column at a fixed share, then the pill
   hard right. The title and the agent each get their own track so the "Automated by" lines stack
   in a straight column down the page — they were previously a tail on the title and so began at
   a different x on every row. */
/* Fixed shares, not grow-to-fill: the agent column must start at the same x on every row, and a
   growing title column moves it wherever the longest title happens to end. 44%/34% puts the
   robot glyph where the mockup has it, with the pill column taking the remainder. */
.sf-taskrow-main{flex:0 0 44%;min-width:0;display:flex;align-items:center;gap:9px;
                 text-decoration:none;color:inherit}
.sf-taskrow-main:hover .sf-task-title{color:var(--sf-blue)}
.sf-task-by{flex:0 1 34%;min-width:0;display:flex;align-items:center;gap:8px;color:var(--sf-ink-3)}
.sf-task-by .sf-ic{width:19px;height:19px;flex:none;color:var(--sf-ink-off)}
.sf-task-from{min-width:0;font-size:var(--sf-fs-ui);color:var(--sf-ink-3);overflow:hidden;
              text-overflow:ellipsis;white-space:nowrap}
/* Row titles on a full list page are the mockup's ~17px, not the 13.5px of a card's summary. */
.sf-taskrow .sf-task-title{font-size:var(--sf-fs-title-sm)}
.sf-ic-warn{color:var(--sf-danger-ink);width:16px;height:16px}
.sf-warn{flex:none;display:inline-flex;cursor:help}
.sf-warn-lg .sf-ic-warn{width:26px;height:26px}
/* The pill column is fixed-width and right-aligned so every pill's right edge lines up, which is
   what the picture shows and what a ragged flex row cannot give. */
.sf-taskrow .sf-pill{flex:none;margin-left:auto;text-align:center;min-width:74px}
.sf-pill-checking{display:inline-flex;align-items:center;justify-content:center;gap:7px;
                  color:var(--sf-blue-ink);background:var(--sf-blue-tint)}
.sf-pill-done{color:var(--sf-ok-ink);background:var(--sf-ok-bg)}
/* To do: grey OUTLINE, the only pill in the set drawn as a border rather than a fill. */
.sf-pill-todo{color:var(--mut);background:var(--surface);border:1px solid var(--border)}
.sf-spin{width:9px;height:9px;border-radius:50%;border:1.6px solid var(--sf-blue-ring);
         border-top-color:var(--sf-blue-ink);animation:sf-spin .8s linear infinite}
@keyframes sf-spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){
  .sf-spin{animation:none;border-top-color:var(--sf-blue-ring)}
  .sf-taskrow[data-lane="done"]{transition:none}
}
/* 🚨 THE DOTS ARE NOT IN THE PICTURE, AND THE FEATURE BEHIND THEM IS RULED. Mockup 02 draws no
   three-dots control on any row; mockup 11 draws the delete-with-reason dialog the dots are the
   only way to reach. Deleting the control to match the picture would delete a ruled feature, so
   it is drawn at rest exactly as the picture has it — nothing there — and appears on hover or
   keyboard focus. The row keeps its width either way, so nothing shifts when it appears.
   Always visible on touch, where there is no hover and it would otherwise be unreachable. */
.sf-dots{flex:none;width:28px;height:28px;display:flex;align-items:center;justify-content:center;
         background:none;border:0;padding:0;cursor:pointer;color:var(--sf-ink-off);border-radius:8px;
         opacity:0;transition:opacity .12s ease}
.sf-taskrow:hover .sf-dots,.sf-dots:focus-visible{opacity:1}
.sf-dots:hover{background:var(--surface-2);color:var(--sf-ink-2)}
.sf-dots:focus-visible{outline:2px solid var(--sf-blue);outline-offset:1px}
@media (hover:none){ .sf-dots{opacity:1} }
.sf-rowerr{flex-basis:100%;font-size:var(--sf-fs-label);color:var(--sf-danger-ink);padding-top:6px}

/* A page header whose title and status pill share a line, as the mockups have them. The
   default .sf-page-head is a block; these two pages need the row. */
.sf-head-row{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
.sf-head-row>div{min-width:0}

/* ---------- one task, opened ---------- */
.sf-back{display:inline-block;margin:2px 0 14px;font-size:var(--sf-fs-meta);font-weight:600;
         color:var(--sf-ink-2);text-decoration:none}
.sf-back:hover{color:var(--sf-blue)}
.sf-detail{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:var(--sf-gap);align-items:start}
.sf-panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
          box-shadow:var(--sf-card-shadow);padding:20px 22px;margin-bottom:var(--sf-gap)}
.sf-panel-h{margin:0 0 10px;font-size:var(--sf-fs-label-xs);font-weight:800;letter-spacing:.06em;
            text-transform:uppercase;color:var(--sf-ink-3)}
.sf-panel p{margin:0 0 8px;font-size:var(--sf-fs-body-lg);line-height:1.6;color:var(--ink)}
.sf-why{color:var(--sf-ink-2);font-size:var(--sf-fs-meta)}
.sf-chipset{display:flex;gap:8px;flex-wrap:wrap}
.sf-toolchip{font-size:var(--sf-fs-meta-sm);font-weight:600;color:var(--sf-ink-2);background:var(--surface-2);
             border:1px solid var(--border);border-radius:9px;padding:6px 11px}
.sf-steps{margin:0;padding-left:20px}
.sf-steps li{font-size:var(--sf-fs-body-lg);line-height:1.65;color:var(--ink);margin-bottom:6px}
.sf-timeline{list-style:none;margin:0;padding:0}
.sf-timeline li{display:flex;align-items:baseline;gap:9px;padding:8px 0;font-size:var(--sf-fs-meta)}
.sf-dot{flex:none;width:7px;height:7px;border-radius:50%;background:var(--sf-ink-faint)}
.sf-tl-b{flex:1;min-width:0;color:var(--ink);line-height:1.5}
.sf-tl-t{flex:none;font-size:var(--sf-fs-label-sm);color:var(--sf-ink-3)}
.sf-detail-foot{display:flex;align-items:center;justify-content:flex-end;gap:14px;margin-top:4px}
.sf-btn{font-size:var(--sf-fs-body);font-weight:700;color:var(--sf-on-blue);background:var(--sf-blue);border:0;
        border-radius:11px;padding:11px 20px;cursor:pointer}
.sf-btn:hover{filter:brightness(.95)}
.sf-btn-danger{background:var(--sf-danger-ink)}
.sf-linkbtn{font-size:var(--sf-fs-body);font-weight:600;color:var(--sf-ink-2);background:none;border:0;
            padding:8px 4px;cursor:pointer}
.sf-linkbtn.sf-danger{color:var(--sf-danger-ink)}
/* Change / Withdraw on a person's own request (REG-L-13). Tokens only. */
.sf-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.sf-sugg-acts{display:inline-flex;gap:4px;flex:none}
.sf-taskrow:has(.sf-sugg-edit){flex-wrap:wrap}
.sf-sugg-edit{display:flex;flex-wrap:wrap;align-items:center;gap:8px;flex:1 1 100%;
  font-size:var(--sf-fs-body);color:var(--sf-ink-2)}
.sf-sugg-text{flex:1 1 100%;min-height:72px;padding:10px 12px;border:1px solid var(--sf-line);
  border-radius:var(--radius);font:inherit;font-size:var(--sf-fs-body);color:var(--sf-navy)}
.sf-btn:focus-visible,.sf-linkbtn:focus-visible{outline:2px solid var(--sf-blue);outline-offset:2px}

/* ---------- the delete dialog ---------- */
.sf-modal{position:fixed;inset:0;background:var(--sf-modal-scrim);display:flex;align-items:center;
          justify-content:center;padding:20px;z-index:50}
.sf-modal-card{background:var(--surface);border-radius:var(--radius);padding:26px 26px 20px;
               box-shadow:var(--sf-modal-shadow);max-width:430px;width:100%}
.sf-modal-card h2{margin:12px 0 6px;font-size:var(--sf-fs-modal-title);font-weight:800;color:var(--sf-navy)}
.sf-modal-sub{margin:0 0 16px;font-size:var(--sf-fs-body);line-height:1.55;color:var(--sf-ink-2)}
.sf-modal-task{margin:0 0 18px;font-size:var(--sf-fs-body);font-weight:600;color:var(--ink);
               background:var(--surface-2);border-radius:10px;padding:11px 13px}
.sf-label{display:block;font-size:var(--sf-fs-meta-sm);font-weight:700;color:var(--sf-ink-2);margin-bottom:7px}
.sf-textarea{width:100%;box-sizing:border-box;font:inherit;font-size:var(--sf-fs-body);color:var(--ink);
             border:1px solid var(--border);border-radius:11px;padding:10px 12px;resize:vertical}
.sf-textarea:focus{outline:2px solid var(--sf-blue);border-color:var(--sf-blue)}
.sf-modal-err{margin:9px 0 0;font-size:var(--sf-fs-meta-sm);color:var(--sf-danger-ink)}
.sf-modal-foot{display:flex;align-items:center;justify-content:space-between;margin-top:18px}

/* ---------- the Settings page ----------
   A secondary vertical tab list beside the panel, as the mockup has it. The tabs with no source
   carry the same "soon" treatment the sidebar uses, so a client meets one vocabulary for
   not-built-yet across the whole product rather than three. */
.sf-settings{display:grid;grid-template-columns:210px minmax(0,1fr);gap:26px;align-items:start}
.sf-subnav ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}
.sf-subnav-i{display:flex;align-items:center;gap:8px;padding:9px 13px;border-radius:10px;
             font-size:var(--sf-fs-body);font-weight:600;color:var(--sf-ink-2);cursor:pointer}
.sf-subnav-i:hover{background:var(--surface-2)}
.sf-subnav-i.is-on{color:var(--sf-blue);background:var(--sf-blue-tint)}
.sf-subnav-i.is-off{color:var(--sf-ink-off);cursor:default}
.sf-subnav-i.is-off:hover{background:none}
.sf-panel-sub{margin:0 0 16px;font-size:var(--sf-fs-body);line-height:1.6;color:var(--sf-ink-2)}
.sf-deflist{margin:0}
.sf-deflist>div{display:flex;gap:16px;padding:11px 0;border-bottom:1px solid var(--sf-line-2)}
.sf-deflist>div:last-child{border-bottom:0}
.sf-deflist dt{flex:none;width:150px;font-size:var(--sf-fs-meta);font-weight:700;color:var(--sf-ink-2)}
.sf-deflist dd{margin:0;font-size:var(--sf-fs-body);color:var(--ink);min-width:0;word-break:break-word}
/* A wide register scrolls inside its own box; the page body never scrolls sideways. */
.sf-tablewrap{overflow-x:auto;-webkit-overflow-scrolling:touch}

/* ---------- ROI, the automation page, and asking us for something ---------- */
.sf-tile-hero{grid-column:span 1}
.sf-roi-n{font-size:inherit;font-weight:inherit}
.sf-roi-u{font-size:var(--sf-fs-meta);font-weight:600;color:var(--sf-ink-2);margin-left:6px}
.sf-tiles-sm .sf-tile-stat{padding:14px 16px}
.sf-tiles-sm .sf-tile-n{font-size:var(--sf-fs-stat-sm)}
.sf-section-h{margin:26px 0 12px;font-size:var(--sf-fs-ui-lg);font-weight:800;letter-spacing:-.01em;color:var(--sf-navy)}
/* 🚨 THIS RULE USED TO BE `.sf-td-what{display:block}` AND IT BROKE THE AUTOMATIONS TABLE.
   One class name was doing two unrelated jobs: a real <td> in the automations and run-report
   tables, and a stacked sub-line <span> on the ROI page. The ROI rule won by document order and
   forced `display:block` onto a table cell, which takes the cell OUT of its row — so the
   automations page rendered every description dropped onto its own line under the name, with the
   "What it does" column standing empty beside it. It is a CSS collision, not a markup fault, and
   it was live on the shipped page. The stacked sub-line now has its own name. */
.sf-subline{display:block;font-size:var(--sf-fs-label);color:var(--sf-ink-3);margin-top:2px}
.sf-kv{display:inline-block;margin:0 10px 2px 0;font-size:var(--sf-fs-label);color:var(--sf-ink-2)}
.sf-kv b{font-weight:700;color:var(--sf-ink-3);text-transform:capitalize}
.sf-table tr.is-high td{background:var(--sf-danger-row)}
.sf-runbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.sf-runhint{font-size:var(--sf-fs-label);color:var(--sf-ink-3)}
.sf-btn:disabled{opacity:.5;cursor:default}
.sf-btn-confirm{background:var(--sf-ok-ink)}
.sf-runstate{margin:0 0 12px;font-size:var(--sf-fs-body);color:var(--ink)}
.sf-selection{margin:0 0 14px;font-size:var(--sf-fs-meta);color:var(--sf-ink-2);display:flex;
              align-items:center;gap:7px;flex-wrap:wrap}
.sf-vocab{display:flex;flex-direction:column;gap:12px;margin-top:14px}
.sf-pick{cursor:pointer;user-select:none}
.sf-pick:hover{border-color:var(--sf-line-strong)}
.sf-pick.is-on{color:var(--sf-blue);background:var(--sf-blue-tint);border-color:var(--sf-blue-line)}
.sf-td-go a{color:var(--sf-blue);font-weight:600;text-decoration:none;white-space:nowrap}
.sf-td-go a:hover{text-decoration:underline}

/* ---------- phone ----------
   The sidebar becomes a horizontal strip above the content and the grid drops to one column.
   Checked at 375px, the width the old header collision was found at. */
@media (max-width:820px){
  .sf-detail{grid-template-columns:minmax(0,1fr)}
  /* The agent column is the first thing to go on a phone: the title and the pill are what she
     needs, and three columns at 375px turns every row into three lines. */
  .sf-task-by{display:none}
  .sf-settings{grid-template-columns:minmax(0,1fr);gap:14px}
  .sf-subnav ul{flex-direction:row;flex-wrap:wrap;gap:6px}
  .sf-deflist>div{flex-direction:column;gap:3px}
  .sf-deflist dt{width:auto}
  /* 🚨 TWO ROWS, NOT ONE (Hamish, 11/09/2026, an iPhone screenshot). One row held the logo and the
     whole nav, so only Home and Sessions fitted and every other section sat off the right edge with
     nothing saying it was there; the top bar then spent a whole second row on three inert icons.
     Now: logo left with the avatar and Sign out beside it, the sections one swipeable strip below,
     faded at the right edge so it reads as more to come. Rows that go nowhere ("soon") are not
     drawn on a phone: they cost a swipe and do nothing when tapped. */
  #app{flex-direction:column;position:relative}
  .sf-side{width:auto;height:auto;position:static;flex-direction:column;align-items:stretch;gap:10px;
           padding:12px 14px 8px;border-right:0;border-bottom:1px solid var(--border);overflow:hidden;
           border-radius:var(--sf-frame-radius) var(--sf-frame-radius) 0 0}
  .sf-brand{padding:0;min-height:36px} .sf-brand img{height:26px;width:auto}
  .sf-nav{flex:none;min-height:0;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;
          scrollbar-width:none;margin:0 -14px;padding:0 14px;
          -webkit-mask-image:linear-gradient(to right,var(--sf-navy) calc(100% - 36px),transparent);
                  mask-image:linear-gradient(to right,var(--sf-navy) calc(100% - 36px),transparent)}
  .sf-nav::-webkit-scrollbar{display:none}
  .sf-nav ul{flex-direction:row;gap:4px;padding-right:28px}
  .sf-navitem{padding:9px 12px;gap:8px;font-size:var(--sf-fs-nav-sm);white-space:nowrap}
  .sf-navitem .sf-ic{width:18px;height:18px}
  .sf-navitem .sf-soon{display:none}
  .sf-nav li:has(> .sf-navitem.is-off){display:none}
  .sf-nav-foot,.sf-ws,#sf-ws-slot{display:none}
  .sf-top{position:absolute;top:12px;right:14px;padding:0;gap:8px}
  .has-banner .sf-top{top:42px}
  .sf-top .sf-icobtn{display:none}
  .sf-top .sf-avatar{width:34px;height:34px}
  .sf-top .sf-signout{margin-left:0;padding:7px 12px}
  /* 16px or larger in every box a person types into. iOS Safari zooms the whole page in when a box
     under 16px takes focus, and leaves it zoomed, which reads as the page breaking. */
  #app input:not([type=checkbox]):not([type=radio]):not([type=range]),#app textarea,#app select{font-size:var(--sf-fs-gate-input-sm)}
  .sf-main{padding:8px 16px 32px}
  .sf-ask h1{font-size:var(--sf-fs-hero-sm);margin-top:14px}
  .sf-ask-sub{font-size:var(--sf-fs-hero-sub-sm);margin-bottom:18px}
  .sf-askbox{padding:16px 16px 12px}
  .sf-grid{grid-template-columns:minmax(0,1fr)}
  .sf-tiles{grid-template-columns:minmax(0,1fr)}
  .sf-page-head h1{font-size:var(--sf-fs-page-title-sm)}
}

/* ---------- the run history and its search (01/09/2026) ----------
   Hamish's ask: every previous test in the new UI, searchable, with the mode on every row. */
.sf-runsearch{display:flex;align-items:center;gap:12px;margin:0 0 var(--sf-gap);flex-wrap:wrap}
.sf-search{flex:1;min-width:240px;max-width:520px;font:inherit;font-size:var(--sf-fs-ui);
           color:var(--ink);background:var(--surface);border:1px solid var(--border);
           border-radius:999px;padding:11px 18px}
.sf-search::placeholder{color:var(--sf-ink-off)}
.sf-search:focus{outline:2px solid var(--sf-blue);outline-offset:1px;border-color:var(--sf-blue)}
.sf-runcount{flex:none;font-size:var(--sf-fs-meta-sm);font-weight:700;color:var(--sf-ink-3)}

/* The board button, drawn like "Run it now" as ruled, with the count as a red circle on it.
   The badge is only ever rendered when the number is non-zero — see renderAutomationPage. */
.sf-btn-count{display:inline-flex;align-items:center;gap:10px;text-decoration:none;flex:none}
.sf-badge{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:22px;
          padding:0 6px;border-radius:999px;background:var(--sf-danger-ink);color:var(--sf-on-blue);
          font-size:var(--sf-fs-label-xs);font-weight:800;line-height:1}

/* ---- The sign-in page (signin.html) --------------------------------------------------------
   Built to the reference Hamish sent 01/09/2026: the console's own gate with the word changed.
   The proportions are the console's, copied rather than re-eyeballed, so the two front doors of
   this business look like the same business. What differs is the METHODS: three here, one there,
   because a client is not a WeTalk admin and the live client's tenant is Microsoft.

   🚨 THE CARD IS THE PAGE. It replaces the body rather than covering it, so there is nothing
   underneath to reveal by deleting a node in dev tools. Same reasoning as the console's gate. */
.sf-gate {
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
  padding: 24px; background: var(--sf-canvas, #f7f8fa);
  font-family: var(--sf-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
}
.sf-gate-shell { width: 100%; max-width: 642px; }
.sf-gate-logo { height: 63px; width: auto; display: block; margin: 0 auto 50px; }
.sf-gate-card {
  background: var(--surface); border: 1px solid var(--sf-line, #e6e8ec); border-radius: 16px;
  padding: 46px 44px 62px; width: 100%; text-align: center;
  box-shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 12px 32px rgba(16, 24, 40, .06);
}
.sf-gate-icon { color: var(--sf-blue, #1a73e8); margin: 0 auto 32px; width: 110px; }
.sf-gate-icon svg { width: 100%; height: auto; display: block; }
.sf-gate-title {
  margin: 0; font-size: var(--sf-fs-gate-title); line-height: 1.1; font-weight: 700;
  letter-spacing: -.025em; color: var(--sf-navy, #0a203f);
}
.sf-gate-lede {
  margin: 20px 0 0; font-size: var(--sf-fs-gate-lede); line-height: 1.45; color: var(--sf-ink-2, #5a6472);
}
/* The message reads as body copy rather than as a red panel: a panel around a two-line sentence
   pushes the buttons below the fold on a phone. It keeps the red ink so it can never be mistaken
   for the ordinary prompt, which is exactly the misreading behind the console's 20/08 loop. */
.sf-gate-msg {
  margin: 20px 0 0; font-size: var(--sf-fs-gate-msg); line-height: 1.5;
  color: var(--sf-ink-2, #5a6472); text-align: center;
}
.sf-gate-msg.is-bad { color: var(--sf-red, #c0392b); }

.sf-gate-btn {
  margin-top: 22px; width: 100%; display: flex; align-items: center; justify-content: center;
  gap: 26px; min-height: 72px; padding: 0 20px; border-radius: 11px;
  background: var(--surface); color: var(--sf-navy, #0a203f); border: 1px solid var(--sf-line, #e6e8ec);
  font-size: var(--sf-fs-gate-btn); font-weight: 700; letter-spacing: -.01em; cursor: pointer;
  font-family: inherit;
}
/* The first button carries the reference's 46px gap under the lede; the second sits closer, so
   the two read as one pair of equal choices rather than as a primary and an afterthought. */
.sf-gate-msg + .sf-gate-btn, .sf-gate-lede + .sf-gate-btn { margin-top: 46px; }
.sf-gate-btn:hover:not(:disabled) { border-color: var(--sf-blue, #1a73e8); }
.sf-gate-btn:disabled { opacity: .6; cursor: default; }
.sf-gate-bicon { width: 36px; height: 36px; flex: 0 0 36px; display: block; }
.sf-gate-bicon svg { width: 100%; height: 100%; display: block; }

.sf-gate-or {
  display: flex; align-items: center; gap: 16px; margin: 34px 0 6px;
  color: var(--sf-ink-3, #8a93a1); font-size: var(--sf-fs-gate-label);
}
.sf-gate-or::before, .sf-gate-or::after {
  content: ""; flex: 1; height: 1px; background: var(--sf-line, #e6e8ec);
}
.sf-gate-label {
  display: block; text-align: left; font-size: var(--sf-fs-gate-label); font-weight: 600;
  color: var(--sf-ink-2, #5a6472); margin: 14px 0 8px;
}
.sf-gate-input {
  width: 100%; box-sizing: border-box; min-height: 60px; padding: 0 18px;
  border: 1px solid var(--sf-line, #e6e8ec); border-radius: 11px;
  font-size: var(--sf-fs-gate-input); color: var(--sf-navy, #0a203f); font-family: inherit; background: var(--surface);
}
.sf-gate-input:focus { outline: none; border-color: var(--sf-blue, #1a73e8); }
.sf-gate-link {
  margin-top: 12px; width: 100%; min-height: 56px; border-radius: 11px;
  background: var(--surface); color: var(--sf-blue, #1a73e8);
  border: 1px solid var(--sf-line, #e6e8ec);
  font-size: var(--sf-fs-gate-input); font-weight: 700; cursor: pointer; font-family: inherit;
}
.sf-gate-link:hover:not(:disabled) { border-color: var(--sf-blue, #1a73e8); }
.sf-gate-link:disabled { opacity: .6; cursor: default; }

.sf-gate-foot {
  display: flex; align-items: center; gap: 19px; margin: 66px 0 0; text-align: left;
  font-size: var(--sf-fs-gate-msg); line-height: 1.55; color: var(--sf-ink-2, #5a6472);
}
.sf-gate-foot-icon {
  width: 52px; height: 52px; flex: 0 0 52px; border-radius: 50%;
  background: var(--sf-blue-soft, #e8f0fe); color: var(--sf-blue, #1a73e8);
  display: flex; align-items: center; justify-content: center;
}
.sf-gate-foot-icon svg { width: 26px; height: 26px; }

/* The phone. The card is the whole screen at 390px, so the desktop type scale has to come down
   or the two buttons and the email box cannot all be reached without scrolling past the fold. */
@media (max-width: 620px) {
  .sf-gate { padding: 18px; }
  .sf-gate-logo { height: 40px; margin-bottom: 30px; }
  .sf-gate-card { padding: 32px 22px 36px; }
  .sf-gate-icon { width: 78px; margin-bottom: 24px; }
  .sf-gate-title { font-size: var(--sf-fs-gate-title-sm); }
  .sf-gate-lede { font-size: var(--sf-fs-gate-lede-sm); margin-top: 14px; }
  .sf-gate-msg { font-size: var(--sf-fs-gate-msg-sm); }
  .sf-gate-btn { min-height: 56px; font-size: var(--sf-fs-gate-lede-sm); gap: 16px; margin-top: 14px; }
  .sf-gate-msg + .sf-gate-btn, .sf-gate-lede + .sf-gate-btn { margin-top: 28px; }
  .sf-gate-bicon { width: 26px; height: 26px; flex: 0 0 26px; }
  .sf-gate-or { margin: 24px 0 4px; font-size: var(--sf-fs-gate-msg-sm); }
  .sf-gate-label { font-size: var(--sf-fs-gate-msg-sm); }
  .sf-gate-input { min-height: 50px; font-size: var(--sf-fs-gate-input-sm); }
  .sf-gate-link { min-height: 48px; font-size: var(--sf-fs-gate-input-sm); }
  .sf-gate-foot { margin-top: 34px; font-size: var(--sf-fs-gate-msg-sm); gap: 14px; }
  .sf-gate-foot-icon { width: 40px; height: 40px; flex: 0 0 40px; }
  .sf-gate-foot-icon svg { width: 20px; height: 20px; }
}

/* Sign out. The only live control in the top bar, so it is deliberately not another grey icon:
   the three beside it are inert spans, and a person who has learned nothing up here does
   anything needs this one to look unlike them. */
.sf-signout {
  margin-left: 10px; padding: 8px 14px; border-radius: 9px;
  background: var(--surface); color: var(--sf-ink-2, #5a6472);
  border: 1px solid var(--sf-line, #e6e8ec);
  font-size: var(--sf-fs-meta); font-weight: 600; cursor: pointer; font-family: inherit; white-space: nowrap;
}
.sf-signout:hover:not(:disabled) { border-color: var(--sf-blue, #1a73e8); color: var(--sf-blue, #1a73e8); }
.sf-signout:disabled { opacity: .6; cursor: default; }

/* ============================================================================
   THE AUTOMATION PAGE: one control bar, a run LIST, and two popups.
   Hamish, 02/09/2026 — "every function and vision that was on the old portal in
   automations should be in the new portal but in the new UI... not all spaced
   out, grouped like it was."

   Every colour and size below is a declared token. shopfront.css already carries
   a count of raw literals that test-design-tokens is red about; this block does
   not add to it.
   ============================================================================ */

/* The control bar. Run and Questions sit side by side, as the old page had them,
   instead of owning a full-width card each. */
.sf-controlbar .sf-runbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.sf-btn-count{position:relative;display:inline-flex;align-items:center;gap:9px}

/* The runs list. A row per run, the report one press away. */
.sf-runlist{padding:0;overflow:hidden}
.sf-runrow{display:flex;align-items:center;gap:14px;width:100%;padding:14px 22px;
  background:none;border:0;border-bottom:1px solid var(--sf-line-2);
  font-family:inherit;font-size:var(--sf-fs-body);color:var(--sf-ink-2);
  text-align:left;cursor:pointer}
.sf-runlist .sf-runrow:last-child{border-bottom:0}
.sf-runrow:hover{background:var(--sf-blue-tint)}
.sf-runrow:focus-visible{outline:2px solid var(--sf-blue);outline-offset:-2px}
.sf-runrow-when{font-weight:700;color:var(--sf-navy);min-width:96px}
.sf-runrow-counts{color:var(--sf-ink-3);font-size:var(--sf-fs-meta)}
/* Pushed to the right so every row's "read the report" lands in one column. */
.sf-runrow-go{margin-left:auto;color:var(--sf-blue);font-weight:700;
  font-size:var(--sf-fs-label-sm);white-space:nowrap}

/* The orders-inbox confirmation. Green, positive-only, never a cross. */
.sf-filed,.sf-filedchip{display:inline-block;padding:3px 9px;border-radius:999px;
  background:var(--sf-ok-bg);border:1px solid var(--sf-ok-line);color:var(--sf-ok-ink);
  font-size:var(--sf-fs-micro);font-weight:800;white-space:nowrap}
.sf-filedchip{margin-left:6px}

/* A source the run skipped or could not reach is not a reading. */
.sf-src-off td{color:var(--sf-ink-off)}

.sf-section-sub{margin:22px 0 10px;font-size:var(--sf-fs-title-sm);font-weight:800;
  color:var(--sf-navy)}
.sf-digest{margin:0;padding:14px 16px;background:var(--sf-canvas);
  border:1px solid var(--sf-line-2);border-radius:10px;
  font-size:var(--sf-fs-meta);line-height:1.6;color:var(--sf-ink-2);
  white-space:pre-wrap;word-break:break-word;overflow-x:auto}

/* ---- the per-order timeline and its report card ----
   lanes/engine/order-timeline-contract-2026-09-03.md is the spec; the exact classes and states
   are written up as the exact-match reference for this block in
   lanes/engine/portal-order-timeline-design-2026-09-03.md, because no image mockup exists for
   it. Every colour and size below is a declared token — none of this adds a raw hex or a raw
   font-size, same discipline as the block above. */
.sf-tl-list{display:flex;flex-direction:column;gap:var(--sf-gap)}
.sf-tl-order{padding:16px 20px;background:var(--surface);border:1px solid var(--sf-line-2);
  border-radius:var(--radius)}
.sf-tl-order+.sf-tl-order{margin-top:0}
.sf-tl-head{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:14px}
.sf-tl-ref{font-size:var(--sf-fs-title-sm);font-weight:800;color:var(--sf-navy)}
.sf-tl-customer{font-size:var(--sf-fs-body);color:var(--sf-ink-2)}
.sf-tl-total{margin-left:auto;font-size:var(--sf-fs-title-sm);font-weight:800;color:var(--sf-navy)}
/* The "not done, next run" pill pushes the total left of it, matching how a flagged order
   still needs its total visible — the pill is the newest fact, not a replacement for it. */
.sf-tl-head .sf-pill-high[data-nextrun]{margin-left:0}
.sf-tl-head .sf-tl-total+.sf-pill-high[data-nextrun]{margin-left:10px}

/* The stops, as a horizontal row. Fixed-width items in a scrolling track rather than flexing
   to fit: eight stops (the WCD case, with the inbox stop) crushed to illegibility under flex,
   and a client reading "Waiting for Xero" needs the whole phrase, not a wrapped fragment. */
.sf-tl-stops{display:flex;align-items:flex-start;list-style:none;margin:0 0 18px;padding:2px 2px 4px;
  overflow-x:auto}
.sf-tl-stop{position:relative;flex:0 0 118px;min-width:0;display:flex;flex-direction:column;
  align-items:center;text-align:center;padding-top:9px}
/* The thermometer fill: a line BEFORE this dot, absolutely positioned so it neither shifts the
   dot nor depends on the previous stop's own width. Grey at rest; green the moment this stop
   is anything but pending, because reaching a stop — done, working, held or failed — is what
   "filled up to it" means. The transition is the one animation this block owns beyond the
   spinner it already reuses. */
.sf-tl-seg{position:absolute;top:18px;right:50%;width:100%;height:2px;background:var(--sf-line-2);
  z-index:0;transition:background-color .25s ease}
.sf-tl-seg.is-filled{background:var(--sf-ok-ink)}
.sf-tl-stop.is-held .sf-tl-seg.is-filled,.sf-tl-stop.is-failed .sf-tl-seg.is-filled{
  background:var(--sf-warn-ink)}
.sf-tl-stop.is-failed .sf-tl-seg.is-filled{background:var(--sf-danger-ink)}
.sf-tl-dot{position:relative;z-index:1;width:18px;height:18px;border-radius:50%;flex:none;
  display:flex;align-items:center;justify-content:center;margin-bottom:8px;
  background:var(--surface);border:2px solid var(--sf-ink-faint)}
.sf-tl-stop.is-done .sf-tl-dot{border-color:var(--sf-ok-ink);background:var(--sf-ok-bg)}
.sf-tl-stop.is-working .sf-tl-dot{border-color:transparent;background:transparent}
.sf-tl-stop.is-held .sf-tl-dot{border-color:var(--sf-warn-ink);background:var(--sf-warn-bg)}
.sf-tl-stop.is-failed .sf-tl-dot{border-color:var(--sf-danger-ink);background:var(--sf-danger-bg)}
/* The spinner IS sf-spin (blue by default, prefers-reduced-motion handled where that class is
   already declared above) — green from the moment the work is Unleashed's or Xero's rather
   than ours, per stopSpinColor in shopfront.js. Higher specificity than .sf-spin's own colour
   declaration, so it wins without duplicating the rule. */
.sf-tl-dot[data-color="green"] .sf-spin{border-color:var(--sf-ok-line);border-top-color:var(--sf-ok-ink)}
.sf-tl-tick{color:var(--sf-ok-ink);font-size:var(--sf-fs-meta);font-weight:800;line-height:1}
.sf-tl-label{font-size:var(--sf-fs-label-sm);color:var(--sf-ink-3);max-width:112px}
.sf-tl-stop.is-done .sf-tl-label,.sf-tl-stop.is-working .sf-tl-label{color:var(--sf-navy);font-weight:600}
.sf-tl-stop.is-held .sf-tl-label{color:var(--sf-warn-ink);font-weight:600}
.sf-tl-stop.is-failed .sf-tl-label{color:var(--sf-danger-ink);font-weight:600}
.sf-tl-reason{margin-top:4px;font-size:var(--sf-fs-label-xs);color:var(--sf-warn-ink);max-width:118px}
.sf-tl-stop.is-failed .sf-tl-reason{color:var(--sf-danger-ink)}

/* The report card: two columns, labelled, never merged — Hamish's own words in the site-visit
   notes, "make clear which information relates to Unleashed and which to Xero." */
.sf-rc{display:grid;grid-template-columns:1fr 1fr;gap:var(--sf-gap);padding-top:14px;
  border-top:1px solid var(--sf-line-2)}
.sf-rc-col{min-width:0}
.sf-rc-h{margin:0 0 8px;font-size:var(--sf-fs-label);font-weight:800;letter-spacing:.02em;
  text-transform:uppercase;color:var(--sf-ink-off)}
.sf-rc-fields{display:flex;flex-direction:column;gap:5px;margin:0 0 10px}
.sf-rc-field{display:flex;justify-content:space-between;gap:10px;font-size:var(--sf-fs-body)}
.sf-rc-field dt{color:var(--sf-ink-3)}
.sf-rc-field dd{margin:0;font-weight:600;color:var(--sf-navy);text-align:right}
/* 🚨 min-width:0 IS LOAD-BEARING (found 04/09/2026 on the first render of a real run, REG-N-10
   preview): .sf-table carries min-width:720px for the full-width run tables, and a report-card
   column is about 435px wide, so without this the Unleashed table spilled sideways under the
   Xero column and the Xero table ran off the popup's right edge. Cell padding tightened for the
   same reason: these are small per-order tables, not the page's row lists. */
.sf-rc-lines{width:100%;min-width:0;table-layout:auto;font-size:var(--sf-fs-meta)}
.sf-rc-lines th,.sf-rc-lines td{padding:8px 10px}
.sf-rc-lines th:not(:first-child),.sf-rc-lines td:not(:first-child){text-align:right}
.sf-rc-match{grid-column:1/-1;margin:10px 0 0;padding:8px 12px;border-radius:10px;
  font-size:var(--sf-fs-body);font-weight:700}
.sf-rc-match[data-match="yes"]{color:var(--sf-ok-ink);background:var(--sf-ok-bg)}
.sf-rc-match[data-match="no"]{color:var(--sf-danger-ink);background:var(--sf-danger-bg)}
@media (max-width:640px){ .sf-rc{grid-template-columns:1fr} }

/* ---- the order tracker, version 2 (order-timeline-contract-v2-2026-09-04.md) ----
   A SEPARATE class family (.sf-trk-*) from the v1 block above (.sf-tl-*) on purpose: a legacy or
   v1 run keeps rendering through the old classes untouched, and the two families can never bleed
   into each other through a shared selector. Every colour and size below is a token already
   declared above — none of this adds a raw hex or a raw font-size. */
.sf-trk-list{display:flex;flex-direction:column;gap:var(--sf-gap)}
.sf-trk-order{padding:16px 20px;background:var(--surface);border:1px solid var(--sf-line-2);
  border-radius:var(--radius);position:relative}
.sf-trk-head{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:14px}
/* Dates: started top-left (first in the row), complete top-right (pushed there by the auto
   margin — nothing between it and the edge needs its own positioning rule). */
.sf-trk-started{font-size:var(--sf-fs-label-sm);color:var(--sf-ink-3)}
.sf-trk-ref{font-size:var(--sf-fs-title-sm);font-weight:800;color:var(--sf-navy)}
.sf-trk-customer{font-size:var(--sf-fs-body);color:var(--sf-ink-2)}
.sf-trk-total{font-size:var(--sf-fs-title-sm);font-weight:800;color:var(--sf-navy)}
.sf-trk-completed{margin-left:auto;font-size:var(--sf-fs-label-sm);font-weight:700;color:var(--sf-ink-3)}

.sf-trk-stops{display:flex;align-items:flex-start;list-style:none;margin:0 0 10px;padding:2px 2px 4px;
  overflow-x:auto}
/* Eight stages share the card's width (flex:1 1 0), never a fixed 118px each: at 8 x 118 the
   line ran off the popup's right edge and the last stages needed a sideways scroll (found on the
   first browser render, 04/09/2026). Labels wrap inside their share instead. */
.sf-trk-stop{position:relative;flex:1 1 0;min-width:0;display:flex;flex-direction:column;
  align-items:center;text-align:center;padding-top:20px}
.sf-trk-when{position:absolute;top:0;font-size:var(--sf-fs-label-xs);color:var(--sf-ink-3);white-space:nowrap}
.sf-trk-seg{position:absolute;top:29px;right:50%;width:100%;height:2px;background:var(--sf-line-2);
  z-index:0;transition:background-color .25s ease}
.sf-trk-seg.is-filled{background:var(--sf-ok-ink)}
.sf-trk-stop.is-held .sf-trk-seg.is-filled,.sf-trk-stop.is-failed .sf-trk-seg.is-filled{
  background:var(--sf-warn-ink)}
.sf-trk-stop.is-failed .sf-trk-seg.is-filled{background:var(--sf-danger-ink)}
/* The frozen card's orange line and X reuse the warn tokens (amber) already declared for held —
   the closest existing colour to "orange" in the token set, same reuse-not-invent rule the v1
   design doc used for held/failed ("no new pill colours"). */
.sf-trk-seg.is-frozen-line{background:var(--sf-warn-ink)}
.sf-trk-dot{position:relative;z-index:1;width:18px;height:18px;border-radius:50%;flex:none;
  display:flex;align-items:center;justify-content:center;margin-bottom:8px;
  background:var(--surface);border:2px solid var(--sf-ink-faint)}
.sf-trk-stop.is-done .sf-trk-dot{border-color:var(--sf-ok-ink);background:var(--sf-ok-bg)}
.sf-trk-stop.is-working .sf-trk-dot{border-color:transparent;background:transparent}
.sf-trk-stop.is-held .sf-trk-dot{border-color:var(--sf-warn-ink);background:var(--sf-warn-bg)}
.sf-trk-stop.is-failed .sf-trk-dot{border-color:var(--sf-danger-ink);background:var(--sf-danger-bg)}
/* "Only the NEXT stage spins: a grey ring inside the circle, no tick." (§1) — v2 has no
   early/late colour split, so this is the whole spinner rule, unlike v1's data-color pair. */
.sf-trk-spin{border-color:var(--sf-ink-faint);border-top-color:var(--sf-ink-3)}
.sf-trk-tick{color:var(--sf-ok-ink);font-size:var(--sf-fs-meta);font-weight:800;line-height:1}
.sf-trk-dot.is-frozen-x{color:var(--sf-warn-ink);font-size:var(--sf-fs-meta);font-weight:800;
  line-height:1;border-color:var(--sf-warn-ink);background:var(--sf-warn-bg)}
.sf-trk-label{font-size:var(--sf-fs-label-sm);color:var(--sf-ink-3);max-width:100%;overflow-wrap:anywhere}
.sf-trk-stop.is-done .sf-trk-label,.sf-trk-stop.is-working .sf-trk-label{color:var(--sf-navy);font-weight:600}
.sf-trk-stop.is-held .sf-trk-label{color:var(--sf-warn-ink);font-weight:600}
.sf-trk-stop.is-failed .sf-trk-label{color:var(--sf-danger-ink);font-weight:600}
.sf-trk-actor{margin-top:3px;font-size:var(--sf-fs-label-xs);color:var(--sf-ink-off)}
.sf-trk-reason{margin-top:4px;font-size:var(--sf-fs-label-xs);color:var(--sf-warn-ink);max-width:118px}
.sf-trk-stop.is-failed .sf-trk-reason{color:var(--sf-danger-ink)}

/* The second, blue timeline a shadow order carries alongside the real one (§2). */
.sf-trk-shadow-label{margin:0 0 6px;font-size:var(--sf-fs-label);font-weight:800;letter-spacing:.02em;
  text-transform:uppercase;color:var(--sf-blue-ink)}
.sf-trk-stops.is-shadow .sf-trk-seg.is-filled{background:var(--sf-blue-ink)}
.sf-trk-stops.is-shadow .sf-trk-stop.is-done .sf-trk-dot{border-color:var(--sf-blue-ink);background:var(--sf-blue-tint)}
.sf-trk-stops.is-shadow .sf-trk-tick{color:var(--sf-blue-ink)}
.sf-trk-stops.is-shadow .sf-trk-spin{border-color:var(--sf-blue-ring);border-top-color:var(--sf-blue-ink)}
.sf-trk-stops.is-shadow .sf-trk-stop.is-done .sf-trk-label,
.sf-trk-stops.is-shadow .sf-trk-stop.is-working .sf-trk-label{color:var(--sf-blue-ink)}

/* The frozen-card overlay (§4): every line struck through, an orange tag at the top, and — once
   order_tracker later reports it resolved — a green or grey line naming when. */
.sf-trk-order.is-frozen .sf-trk-head *,.sf-trk-order.is-frozen .sf-trk-label{text-decoration:line-through}
.sf-pill-frozen{color:var(--sf-warn-ink);background:var(--sf-warn-bg)}
.sf-trk-frozen-done{margin:8px 0 0;font-size:var(--sf-fs-body);font-weight:700;color:var(--sf-ok-ink)}
.sf-trk-frozen-cancelled{margin:8px 0 0;font-size:var(--sf-fs-body);font-weight:700;color:var(--sf-ink-3)}

/* ---- Awaiting Orders, the pen (§4) ---- */
.sf-awaiting-tabs{display:flex;gap:16px;margin:14px 0 18px;border-bottom:1px solid var(--sf-line-2);
  padding-bottom:10px}
.sf-awaiting-tabs .sf-linkbtn.is-active{color:var(--sf-blue);font-weight:800}

/* ---- the popups ----
   The wide variant of the modal that already exists here, so the scrim, the
   shadow and the radius stay one definition. A report and a three-column board
   both need far more width than the delete dialog's 430px. */
.sf-modal-wide{padding:24px;overflow:auto}
/* Most of the screen (Hamish, 07/09/2026: "make the pop up most of the screen"). A COLUMN,
   so the document takes whatever height the header and the decision bar do not, instead of
   being a fixed fraction of the viewport that has to be re-tuned every time the footer changes
   — which is what pushed the buttons off the bottom earlier the same evening. */
/* 🚨 THE CARD SCROLLS BY DEFAULT (09/09/2026). It was `overflow:hidden`, which is right for a
   popup that manages its own inner scrolling (the document, the process card) and silently wrong
   for every other one: a fixed 94vh that clips is a popup you can read to the fold and no
   further. Six popups were built on this card and at least three of them — the questions board,
   Awaiting Orders, a long conversation — were unreadable past the first screenful. The default
   belongs HERE rather than on each popup, because the next popup will not remember. A card whose
   body scrolls on its own never triggers this one; `auto` only scrolls what actually overflows.
   test-modal-scroll.js holds the line. */
.sf-modal-card-wide{max-width:1400px;width:100%;height:94vh;max-height:94vh;
  display:flex;flex-direction:column;overflow-x:hidden;overflow-y:auto;
  -webkit-overflow-scrolling:touch;padding:0 26px 16px}
.sf-modal-card-wide > [data-doc-body]{flex:1;min-height:0;display:flex;flex-direction:column}
.sf-modal-card-wide > [data-doc-body] > .sf-doc-pdf{flex:1;min-height:0;height:auto}
/* Sticky so the way out stays on screen on a long report. */
.sf-modal-head{position:sticky;top:0;z-index:1;display:flex;align-items:center;
  justify-content:space-between;gap:16px;padding:22px 0 12px;background:var(--surface);
  border-bottom:1px solid var(--sf-line-2)}
.sf-modal-head h2{margin:0;font-size:var(--sf-fs-modal-title);font-weight:800;color:var(--sf-navy)}
.sf-modal-x{flex:none;width:34px;height:34px;border-radius:50%;border:1px solid var(--border);
  background:var(--surface);color:var(--sf-ink-3);font-size:var(--sf-fs-ui-lg);line-height:1;
  cursor:pointer;font-family:inherit}
.sf-modal-x:hover{border-color:var(--sf-line-strong);color:var(--sf-navy)}
.sf-modal-x:focus-visible{outline:2px solid var(--sf-blue);outline-offset:2px}
/* The report reuses the run card wholesale; inside the dialog it is the dialog,
   so it drops the card chrome rather than drawing a card inside a card. */
.sf-modal-card-wide .sf-card{border:0;box-shadow:none;padding:0;margin:0}

/* The kanban board. Three columns on a desktop, stacked on a narrow screen —
   a horizontally scrolling board on a phone hides the columns nobody scrolled to. */
.sf-board{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-top:18px}
.sf-board-col{background:var(--sf-canvas);border:1px solid var(--sf-line-2);
  border-radius:12px;padding:14px;min-width:0}
.sf-board-h{display:flex;align-items:center;justify-content:space-between;gap:8px;
  margin-bottom:12px;font-size:var(--sf-fs-label-sm);font-weight:800;color:var(--sf-navy);
  text-transform:uppercase;letter-spacing:.04em}
.sf-board-n{flex:none;min-width:22px;padding:2px 7px;border-radius:999px;
  background:var(--sf-blue-tint);color:var(--sf-blue-ink);
  font-size:var(--sf-fs-micro);text-align:center}
.sf-board-card{display:block;margin-bottom:9px;padding:12px 13px;background:var(--surface);
  border:1px solid var(--border);border-radius:10px;
  font-size:var(--sf-fs-meta);line-height:1.5;color:var(--sf-ink-2);text-decoration:none}
a.sf-board-card:hover{border-color:var(--sf-blue);color:var(--sf-navy)}
.sf-board-title{display:block;font-weight:600;color:var(--ink)}
.sf-board-card .sf-pill{margin-top:7px}

/* 🔑 THE QUESTION, SITTING ON THE ORDER IT IS HOLDING UP (ORDLINK-2, 09/09/2026). Same card as
   the questions board draws, because it IS that card. It sits above the timeline and is marked
   off with the blue edge the rest of the portal already uses for "this one is asking you
   something", so on a card full of order detail the eye lands on the part that needs her. */
.sf-trk-question{margin:0 0 14px;padding-left:12px;border-left:3px solid var(--sf-blue)}
.sf-trk-question .sf-qcard{margin-bottom:0}

/* 🚨 THE BOARD SCROLLS, COLUMN BY COLUMN (09/09/2026). The card above is a fixed 94vh with
   overflow:hidden — right for a document, wrong for a board, because a column of 62 items was
   drawn in full, counted in full, and reachable only as far as the fold. Each column takes the
   height the header and the footer leave and scrolls inside that, so the heading and the count
   stay put and "Needs you" never leaves the screen while somebody reads down "Sorted". */
.sf-modal-card-board .sf-board{flex:1;min-height:0}
.sf-modal-card-board .sf-board-col{display:flex;flex-direction:column;min-height:0}
/* The side padding is given back inside the scroller so the cards' shadows are not sheared off
   at the scrolling edge. */
.sf-modal-card-board .sf-board-body{flex:1;min-height:0;overflow-y:auto;
  -webkit-overflow-scrolling:touch;margin:0 -4px;padding:0 4px 2px}

@media (max-width:860px){
  .sf-board{grid-template-columns:1fr}
  /* Stacked, three scrollers each a third of the popup would be worse than one page. So on a
     narrow screen the popup itself scrolls and the columns simply run their full length. */
  .sf-modal-card-board{overflow-y:auto;-webkit-overflow-scrolling:touch}
  .sf-modal-card-board .sf-board{flex:none}
  .sf-modal-card-board .sf-board-body{flex:none;overflow:visible;margin:0;padding:0}
  .sf-runrow{flex-wrap:wrap;gap:8px}
  .sf-runrow-go{margin-left:0}
}

/* ============================================================================
   THE BOARD'S CARDS, THE CONVERSATION, AND THE RUN PROBLEM FORM.
   Hamish, 02/09/2026: "make the cards fucking like the old ones but just with
   the new UI." The old portal's card CONTENT and rules are ported exactly; this
   is the new UI's clothing on them. Declared tokens only.
   ============================================================================ */

/* The columns get wider now they hold real cards rather than one-line rows. */
.sf-board-col{background:var(--sf-canvas);border:1px solid var(--sf-line-2);
  border-radius:12px;padding:14px;min-width:0}

/* ---- NEEDS YOU: the full card, with the box she types into ---- */
.sf-qcard{background:var(--surface);border:1px solid var(--border);border-radius:11px;
  padding:14px;margin-bottom:11px;box-shadow:var(--sf-card-shadow)}
.sf-qtop{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
.sf-qtitle{font-size:var(--sf-fs-body);font-weight:700;color:var(--ink);cursor:pointer;min-width:0}
.sf-qtitle:hover{color:var(--sf-blue)}
.sf-qwhen{flex:none;font-size:var(--sf-fs-micro);color:var(--sf-ink-4)}
.sf-qbody{margin-top:9px;font-size:var(--sf-fs-meta);line-height:1.55;color:var(--sf-ink-2)}
/* When our follow-up has taken the lead, the original question becomes context and
   steps back so the live ask is what the eye lands on. */
.sf-qbody.is-ctx{padding-left:11px;border-left:2px solid var(--sf-line-2);color:var(--sf-ink-3)}
.sf-qbody.is-ctx i,.sf-qask i,.sf-qback i,.sf-qmine i,.sf-qreso i{display:block;
  font-style:normal;font-size:var(--sf-fs-micro);font-weight:800;letter-spacing:.03em;
  text-transform:uppercase;color:var(--sf-ink-4);margin-bottom:4px}
.sf-qwhy{margin-top:9px;font-size:var(--sf-fs-meta);line-height:1.5;color:var(--sf-ink-3)}
.sf-qwhy b{color:var(--sf-ink-2)}
/* The live ask. Blue, because it is the thing being asked right now. */
.sf-qask{margin-top:10px;padding:10px 12px;border-radius:9px;background:var(--sf-blue-tint);
  border:1px solid var(--sf-blue-line);font-size:var(--sf-fs-meta);line-height:1.55;
  color:var(--sf-ink-2)}
.sf-qback,.sf-qmine{margin-top:10px;padding:10px 12px;border-radius:9px;
  background:var(--sf-canvas);border:1px solid var(--sf-line-2);
  font-size:var(--sf-fs-meta);line-height:1.55;color:var(--sf-ink-2)}
.sf-qmeta{margin-top:9px;font-size:var(--sf-fs-micro);color:var(--sf-ink-4)}
.sf-qans{margin-top:12px;padding-top:12px;border-top:1px solid var(--sf-line-2)}
.sf-qprompt{margin-bottom:7px;font-size:var(--sf-fs-meta);color:var(--sf-ink-3)}
.sf-qans textarea,.sf-field textarea,.sf-field select{width:100%;padding:9px 11px;
  border:1px solid var(--border);border-radius:9px;background:var(--surface);
  color:var(--ink);font-size:var(--sf-fs-meta);line-height:1.5;resize:vertical}
.sf-qans textarea:focus,.sf-field textarea:focus,.sf-field select:focus{outline:none;
  border-color:var(--sf-blue);box-shadow:0 0 0 3px var(--sf-blue-ring)}
/* 04/09/2026: three prompts, not one. The why and the where sit under their own prompt line
   in shorter boxes, so the card still reads as one answer rather than a form. */
.sf-qprompt.sf-qpart{margin-top:10px}
.sf-qans textarea.sf-qsmall{min-height:44px}
/* 08/09/2026: what the run could not do, shown when it finishes. Each line leads with the order
   so she can see WHICH one at a glance, then what stopped it in the words the card already uses.
   Scrolls rather than growing, because a run that held six orders must not push the buttons off
   the bottom of a laptop screen. */
.sf-held-list{list-style:none;margin:12px 0 0;padding:0;max-height:46vh;overflow-y:auto}
.sf-held-list li{display:flex;flex-direction:column;gap:3px;padding:11px 0;
  border-top:1px solid var(--border)}
.sf-held-list li:first-child{border-top:none}
.sf-held-ref{font-size:var(--sf-fs-micro);font-weight:600;color:var(--sf-blue);
  letter-spacing:.02em}
.sf-held-what{font-size:var(--sf-fs-meta);color:var(--ink);font-weight:600}
.sf-held-why{font-size:var(--sf-fs-micro);color:var(--sf-ink-3,var(--ink));line-height:1.5}

/* 08/09/2026: one-off, or a new rule. Deliberately quiet — it sits below the three prompts as a
   line she reads rather than a control competing with the Send button, because the safe answer
   is the one it already has ticked and she only has to touch it to change the rules. The whole
   row is the label, so the words are the hit target as well as the box. */
.sf-qexc{display:flex;align-items:flex-start;gap:9px;margin-top:11px;padding:9px 11px;
  border:1px solid var(--border);border-radius:9px;background:var(--sf-tint,var(--surface));
  font-size:var(--sf-fs-micro);line-height:1.5;color:var(--sf-ink-2,var(--ink));cursor:pointer}
.sf-qexc input{margin:2px 0 0;flex:0 0 auto;width:15px;height:15px;cursor:pointer;
  accent-color:var(--sf-blue)}
.sf-qexc b{color:var(--ink)}
.sf-qexc:focus-within{border-color:var(--sf-blue);box-shadow:0 0 0 3px var(--sf-blue-ring)}
/* 🔑 THE WATCH-OUT (ORDLINK-3, 09/09/2026). Shown only from the SECOND time she has handled the
   same condition the same way, so it is rare by construction and can afford to be noticed. It
   marks the box rather than adding a control: the offer is "untick this", which already exists,
   and a second button would be a second thing to understand. */
.sf-qexc.is-repeat{border-color:var(--sf-blue-line);background:var(--sf-blue-tint)}
.sf-qexc-seen{display:block;margin-top:6px;font-size:var(--sf-fs-micro);line-height:1.5;
  color:var(--sf-blue-ink)}
.sf-qmine{white-space:pre-line}
.sf-qfoot{display:flex;align-items:center;gap:10px;margin-top:9px;flex-wrap:wrap}
.sf-btn-sm{padding:7px 13px;font-size:var(--sf-fs-label-sm)}
.sf-qmsg{font-size:var(--sf-fs-micro);color:var(--sf-ink-3)}

/* ---- WITH US, and SORTED ---- */
.sf-qprog,.sf-qres{position:relative;background:var(--surface);border:1px solid var(--border);
  border-radius:11px;padding:13px;margin-bottom:11px;cursor:pointer}
.sf-qprog:hover,.sf-qres:hover{border-color:var(--sf-blue)}
.sf-qrest{font-size:var(--sf-fs-meta);font-weight:700;color:var(--ink);line-height:1.45}
.sf-qresw{margin-top:7px;font-size:var(--sf-fs-micro);color:var(--sf-ink-4)}
.sf-qreso{margin-top:9px;padding-top:9px;border-top:1px solid var(--sf-line-2);
  font-size:var(--sf-fs-meta);line-height:1.5;color:var(--sf-ink-2)}
/* An unanswered message on a finished-looking card is the one case she must never
   have to infer, so it is coloured as the warning it is. */
.sf-qresowe{margin-top:6px;font-size:var(--sf-fs-micro);font-weight:700;color:var(--sf-warn-ink)}
/* Removed, but still visible while "show the ones you removed" is on. */
.sf-qres.is-hid{opacity:.55}
.sf-qresx{position:absolute;top:9px;right:9px;width:24px;height:24px;border-radius:50%;
  border:1px solid var(--border);background:var(--surface);color:var(--sf-ink-4);
  font-size:var(--sf-fs-micro);line-height:1;cursor:pointer}
.sf-qresx:hover{border-color:var(--sf-line-strong);color:var(--sf-navy)}
/* Something SHE reported. A red edge and a flag, never a fourth status colour —
   blue/orange/green already mean unread/following up/resolved, and a colour
   competing with those would stop a resolved problem reading as resolved. */
.sf-qcard.is-prob,.sf-qprog.is-prob,.sf-qres.is-prob{border-left:3px solid var(--sf-danger-ink)}
.sf-probtag{display:inline-block;margin-bottom:7px;font-size:var(--sf-fs-micro);
  font-weight:800;color:var(--sf-danger-ink)}
.sf-board-foot{margin-top:14px;text-align:center}

/* ---- the conversation ---- */
.sf-thread{list-style:none;margin:18px 0 0;padding:0}
.sf-thread-item{display:block;margin-bottom:12px;padding:12px 14px;border-radius:11px;
  background:var(--sf-canvas);border:1px solid var(--sf-line-2)}
.sf-thread-item.is-mine{background:var(--sf-blue-tint);border-color:var(--sf-blue-line)}
/* A bookkeeping line ("Ticked off.") is not a message and must not look like one. */
.sf-thread-item.is-quiet{background:none;border:0;padding:2px 0;color:var(--sf-ink-4)}
.sf-thread-who{display:block;font-size:var(--sf-fs-micro);font-weight:800;
  letter-spacing:.03em;text-transform:uppercase;color:var(--sf-ink-4);margin-bottom:5px}
.sf-thread-at{font-weight:600;text-transform:none;letter-spacing:0}
.sf-thread-text{display:block;font-size:var(--sf-fs-meta);line-height:1.6;color:var(--sf-ink-2);
  white-space:pre-wrap}
.sf-thread-foot{display:flex;align-items:center;gap:14px;margin-top:18px;padding-top:14px;
  border-top:1px solid var(--sf-line-2);flex-wrap:wrap}

/* ---- reporting a problem with a run ---- */
.sf-probwrap{margin-top:18px}
.sf-btn-quiet{background:var(--surface);color:var(--sf-danger-ink);
  border:1px solid var(--border)}
.sf-btn-quiet:hover{border-color:var(--sf-danger-ink);filter:none}
.sf-field{display:block;margin-top:12px}
.sf-field>span{display:block;margin-bottom:5px;font-size:var(--sf-fs-label-sm);
  font-weight:700;color:var(--sf-ink-2)}
/* "Can't confirm" is about OUR record, not her work. Grey and quiet — never a cross,
   never red. */
.sf-unconfirmed{display:inline-block;margin-left:6px;padding:3px 9px;border-radius:999px;
  background:var(--sf-canvas);border:1px solid var(--sf-line-2);color:var(--sf-ink-4);
  font-size:var(--sf-fs-micro);font-weight:700;white-space:nowrap}
/* A part-confirmed run gets a neutral chip, not a green one: a green badge on a run
   with an unconfirmed order in it is the reassurance we must not give. */
.sf-filed.is-part{background:var(--sf-canvas);border-color:var(--sf-line-2);color:var(--sf-ink-3)}
/* ---------- the client sign-off card and the documents (05/09/2026) ----------
   Approve is the house blue (.sf-btn), deny the house red (.sf-btn-danger): the spec's own
   words. The document is rendered on the page in a scrolling article so nobody leaves the
   portal to read what they are agreeing to. Deny is never styled as an error. */
.sf-signoff .sf-signoff-q{margin:10px 0;font-size:var(--sf-fs-body);color:var(--ink)}
.sf-doc{max-height:60vh;overflow:auto;padding:16px 18px;margin:12px 0;border:1px solid var(--border);
  border-radius:var(--radius);background:var(--surface-2);font-size:var(--sf-fs-body);line-height:1.5}
.sf-doc h3{margin:18px 0 6px;font-size:var(--sf-fs-title-sm);font-weight:800;color:var(--sf-navy)}
.sf-doc table{border-collapse:collapse;width:100%;margin:8px 0}
.sf-doc th,.sf-doc td{border:1px solid var(--border);padding:6px 8px;text-align:left;vertical-align:top}
.sf-doc th{background:var(--surface);font-weight:700}
.sf-signoff-actions{display:flex;gap:12px;flex-wrap:wrap;margin:14px 0 6px}
.sf-btn-approve{background:var(--sf-blue)}
.sf-btn[disabled]{opacity:.45;cursor:not-allowed}
.sf-signoff-history{margin-top:14px;padding-top:12px;border-top:1px solid var(--border)}
.sf-signoff-history h3{margin:0 0 6px;font-size:var(--sf-fs-label-sm);font-weight:700;color:var(--sf-ink-2)}
.sf-signoff-history ul{margin:0;padding-left:18px}
.sf-signoff-reason{display:block;color:var(--sf-ink-2);margin-top:2px}
.sf-signoff-reasonbox{margin:10px 0}
.sf-doclist ul{margin:0;padding-left:18px}
.sf-doclist li{margin:6px 0}
/* .sf-doclist-ul — found undeclared 15/09/2026 (CLASS-CHECK-PREFIX-BLIND, the escaped-quote
   widening). Used once, renderAdminPage()'s "Sign-offs waiting on you" list, which is NOT
   nested inside a .sf-doclist section (it sits under a plain .sf-card), so the descendant rule
   above never reached it. Trivially the same intended list look as .sf-doclist ul just above,
   restated as its own class-selector rather than the class off the element. */
.sf-doclist-ul{margin:0;padding-left:18px}
/* 🚨 THIS RULE USED TO SAY `color:var(--sf-blue); padding:2px 0`, left over from when the
   document row was a text link. The moment the row became a real .sf-btn (07/09/2026) that
   turned into BLUE TEXT ON A BLUE BUTTON — a solid blue pill with its label invisible, and
   squashed to 17px tall by the padding. Nothing errored and the HTML was correct the whole
   time; it was only ever visible by looking at the page. Kept as a named no-op rather than
   deleted, because the class is still the hook the click handler uses. */
.sf-doc-open{white-space:nowrap}

/* ---------- an automation's three tabs (07/09/2026) ----------
   Automation | Specifications | Reports. A tab carries a red badge only when something is
   actually waiting on this person; a tab with nothing owed carries no badge at all, because a
   permanent nought is a thing people stop seeing. */
.sf-tabs{display:flex;gap:4px;border-bottom:1px solid var(--sf-line);margin:20px 0 0}
.sf-tab{appearance:none;background:none;border:0;border-bottom:2px solid transparent;
  padding:10px 14px;font:inherit;font-weight:600;color:var(--sf-ink-soft);cursor:pointer;
  display:inline-flex;align-items:center;gap:8px;border-radius:6px 6px 0 0}
.sf-tab:hover{color:var(--sf-ink);background:var(--sf-tint)}
.sf-tab.is-active{color:var(--sf-blue);border-bottom-color:var(--sf-blue)}
.sf-tab:focus-visible{outline:2px solid var(--sf-blue);outline-offset:2px}

/* Every panel is in the DOM; only the active one is shown, so switching keeps scroll position
   and needs no re-fetch. */
.sf-tabpanel{display:none;padding-top:18px}
.sf-tabpanel.is-active{display:block}

/* The badge on a tab, and on a section heading. Red because it is a thing owed, not news. */
/* The base .sf-badge is already the danger colour in this stylesheet, so an
   'owed' badge needs no new colour and must not invent a variable that does not
   exist here (a CSS variable that is not defined kills the rule silently). This
   class is kept as the hook the tests and any future styling use. */
.sf-badge-owed{}

/* ---------- the document pop-up: the real PDF, read inside the portal ----------
   Tall enough to actually read a specification in, and it scrolls inside its own frame rather
   than the page scrolling behind the modal. */
/* A document row: the blue button that opens it, and the date sitting beside it on the same
   line (07/09/2026). Wraps rather than squashes on a narrow screen. */
.sf-doclist li.sf-docrow{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:10px}
.sf-doclist li.sf-docrow:last-child{margin-bottom:0}
/* 🚨 NOT 70vh. The pop-up is capped at 88vh and the decision sits UNDER the document, so a
   viewer that tall pushes Approve, Deny and the name box off the bottom of the screen — which on
   07/09/2026 read as "there are no buttons" through two screenshots, after the buttons had been
   fixed. The document is still the biggest thing in the pop-up; it just no longer takes the whole
   of it. Anything that grows this number has to account for the footer's height as well. */
.sf-doc-pdf{display:block;width:100%;height:52vh;min-height:260px;border:1px solid var(--sf-line);
  border-radius:8px;background:var(--sf-tint)}
/* The footer stacks now (07/09/2026): the signature line, then the buttons under it. It was a
   single flex row when Approve was the whole of it; a name field belongs above the press it
   authorises, not beside it. */
/* Stuck to the bottom of the pop-up so the decision is always reachable without hunting for it,
   the same reasoning as the sticky header above: the way out, and the way to act, stay on screen
   however long the document is. */
/* ONE ROW, and as short as it goes while still reading cleanly (Hamish, 07/09/2026: "as
   narrow in height as possible"). The label, the name box and both buttons sit on a single
   line; it wraps rather than squashes on a narrow screen. It stays flex-none inside the column
   above, so the document gets every pixel this does not need. */
.sf-doc-foot{flex:none;display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  padding:12px 0 0;border-top:1px solid var(--sf-line);margin-top:10px;background:var(--surface)}
.sf-doc-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-left:auto}
.sf-doc-download{margin-left:auto;text-decoration:none}
/* The typed signature (Hamish ruling 07/09/2026: "you have to write your name"). Deliberately
   narrow rather than full width — it takes a name, and a field the width of the pop-up invites a
   sentence. Same border, radius and focus ring as the textarea so it reads as the same family. */
.sf-input{box-sizing:border-box;font:inherit;font-size:var(--sf-fs-body);color:var(--ink);
  border:1px solid var(--border);border-radius:11px;padding:10px 12px;background:var(--surface)}
.sf-input:focus{outline:2px solid var(--sf-blue);border-color:var(--sf-blue)}
.sf-signname{width:240px;flex:none}
.sf-doc-foot .sf-label{margin-bottom:0;white-space:nowrap}
.sf-doc-foot .sf-input{padding:8px 12px}
.sf-doc-foot .sf-btn{padding:9px 18px}
/* the note under the document is a single quiet line, not a paragraph eating the view */
[data-doc-body] > [data-state="pdf-note"]{flex:none;margin:6px 0 0;font-size:var(--sf-fs-meta-sm)}
/* the error and the deny box drop onto their own full-width line under the row */
.sf-doc-foot [data-err="name"], .sf-doc-foot [data-doc-err],
.sf-doc-foot .sf-signoff-reasonbox{flex-basis:100%}
/* The deny questions, stacked, inside the pop-up as well as on the card. */
.sf-signoff-reasonbox .sf-label{display:block;margin:10px 0 4px;font-weight:600}
.sf-signoff-reasonbox .sf-textarea{width:100%}

/* ---------- "Learn the automation process": the thirteen steps, as a pop-up ----------
   Styled from the PDF it replaces (DOME Intelligence - Automation Start to Finish, 04/09/2026):
   a pale intro panel, then numbered steps with a hairline between each, a soft blue disc for the
   number, and a filled navy disc plus a SIGNATURE pill on the three that need one. Scrolls inside
   the pop-up rather than the page scrolling behind it. */
.sf-proc-card{height:auto;max-height:92vh;display:flex;flex-direction:column}
.sf-proc-body{flex:1;min-height:0;overflow:auto;padding-bottom:8px}
.sf-proc-intro{background:var(--surface-2);border-radius:12px;padding:16px 18px;margin:0 0 22px;
  color:var(--mut);line-height:1.55}
.sf-proc-steps{list-style:none;margin:0;padding:0}
.sf-proc-step{display:flex;gap:16px;align-items:flex-start;padding:16px 0;
  border-top:1px solid var(--border)}
.sf-proc-step:first-child{border-top:0;padding-top:0}
.sf-proc-n{flex:none;width:32px;height:32px;border-radius:50%;display:flex;align-items:center;
  justify-content:center;font-weight:700;font-size:var(--sf-fs-meta-sm);
  background:var(--sf-blue-soft);color:var(--navy)}
/* the three that need a signature read as heavier, exactly as they do in the document */
.sf-proc-step.is-sig .sf-proc-n{background:var(--navy);color:var(--sf-on-blue)}
.sf-proc-text{min-width:0}
.sf-proc-text h3{margin:4px 0 6px;font-size:var(--sf-fs-body);color:var(--navy);
  display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.sf-proc-text p{margin:0;color:var(--mut);line-height:1.55}
.sf-proc-sig{font-size:var(--sf-fs-meta-sm);font-weight:700;letter-spacing:.06em;
  background:var(--sf-blue-soft);color:var(--navy);border-radius:6px;padding:3px 8px}
.sf-proc-open{white-space:nowrap}

/* 🚨 THE LIVE RUN BUTTON IS RED, AND IT IS THE ONLY RED CONTROL ON THIS PAGE.
   Hamish, 08/09/2026: "make it red please, so that it's obvious." Every other button in the
   actions row prepares, asks or reads; this is the one that writes into the client's own systems
   for real. The colour is the warning, so it does NOT reuse .primary: a live run must not look
   like the four things beside it that cost nothing to press.
   Its own tokens rather than a shared danger class, because there is no other destructive control
   on this surface to share one with, and inventing a system for one button is how a system stops
   meaning anything. The five values are declared in design-tokens.css (09/09/2026): they were
   written inline here first, which put seven raw colours into this file and turned the token
   check red, and one of them was --sf-danger-ink typed out a second time. */
.liverunbtn{
  background:var(--sf-danger-fill); border:1px solid var(--sf-danger-edge);
  color:var(--sf-on-danger); font-weight:700;
}
.liverunbtn:hover:not(:disabled){ background:var(--sf-danger-hover); }
.liverunbtn:disabled{
  background:var(--sf-danger-off); border-color:var(--sf-danger-off); color:var(--sf-on-danger);
}

/* ---- "How this works" (ORDLINK-4, 09/09/2026) ----
   A quiet round ? beside the two controls it explains. Quiet on purpose: it is there for the
   first week and for the day something looks wrong, and a loud one would compete with the
   controls that actually do something. */
.sf-btn-icon{width:34px;padding:0;flex:none;font-weight:800;line-height:1}
.sf-howto{padding:4px 0 8px;max-width:70ch}
.sf-howto h3{margin:20px 0 6px;font-size:var(--sf-fs-body);font-weight:800;color:var(--sf-navy)}
.sf-howto h3:first-child{margin-top:6px}
.sf-howto p{margin:0;font-size:var(--sf-fs-body);line-height:1.6;color:var(--sf-ink-2)}
.sf-howto ul{margin:8px 0 0;padding-left:20px}
.sf-howto li{margin-bottom:7px;font-size:var(--sf-fs-body);line-height:1.6;color:var(--sf-ink-2)}
.sf-howto b{color:var(--ink)}

/* ============================================================================================
   PORTAL-FEEL-1 — the look-and-feel pass, 11/09/2026.

   Hamish, on the automations list and the automation page: "make it not look like a fucking
   excel sheet... it should be interactive and fun... everything moves really cleanly", and on
   the control bar: "all the boxes are the same so I was like what the fuck is that question
   mark doing".

   🚨 THE SECOND COMPLAINT WAS A BUG, NOT A TASTE. `.sf-btn-ghost` was used in five places in
   shopfront.js and declared in NO stylesheet, so every control meant to read as quiet rendered
   as a solid blue primary: Refresh looked exactly like Run, "Not now" looked exactly like the
   button that acts, and the round quiet "?" the comment above .sf-btn-icon describes was a blue
   square in a row of blue rectangles. A class nothing declares does nothing, silently — the
   same failure mode as a var() pointing at an undeclared token, one level up.
   scripts/test-css-classes-declared.js is the check; it was shown red on this tree first.

   NOTHING IS REMOVED AND NO FUNCTION CHANGES. Every button, column, count and control that was
   on these pages is still there. What changes is which of them SHOUTS.
   ============================================================================================ */

/* ---- 1. THE BUTTON LADDER --------------------------------------------------------------
   Four weights, and a bar may hold at most one of the top two. Before this there was one
   weight, so the eye had nowhere to land and every control claimed to be the main one.

     .sf-btn            solid blue     the one thing this screen is for      (Run)
     .sf-btn-danger     solid red      the irreversible one                  (Live run)
     .sf-btn-soft       pale blue      opens something to look at            (Awaiting Orders)
     .sf-btn-ghost      white outline  costs nothing, changes nothing        (Refresh) */
.sf-btn{transition:background var(--sf-t) var(--sf-ease),box-shadow var(--sf-t) var(--sf-ease),
        transform var(--sf-t-fast) var(--sf-ease),color var(--sf-t) var(--sf-ease),
        border-color var(--sf-t) var(--sf-ease);
        box-shadow:0 1px 2px rgba(10,32,63,.10)}
.sf-btn:hover:not(:disabled){box-shadow:var(--sf-lift-sm)}
/* The press. 1px is enough to feel and small enough that nothing reflows around it. */
.sf-btn:active:not(:disabled){transform:translateY(1px);box-shadow:0 1px 1px rgba(10,32,63,.12)}
.sf-btn:disabled{box-shadow:none}

/* .sf-btn-primary — an alias, and declared rather than left to evaporate. It is used on the
   held-orders control, which IS the primary thing in its own dialog; saying so in the markup is
   worth keeping, so the class asserts the base rather than doing nothing. */
.sf-btn-primary{background:var(--sf-blue);color:var(--sf-on-blue)}
/* .sf-btn-deny — likewise. The deny control is already .sf-btn-danger; this names it as the
   refusing half of an approve/deny pair and gives it the darker edge the Live run button has,
   so the two destructive shapes in this product look like each other. */
.sf-btn-deny{border:1px solid var(--sf-danger-edge)}
.sf-btn-danger:hover:not(:disabled){background:var(--sf-danger-hover);filter:none}

/* THE FIX. A quiet control: white, hairline edge, ordinary ink. It reads as pressable and it
   does not compete. */
.sf-btn-ghost{background:var(--surface);color:var(--sf-ink-2);border:1px solid var(--border);
              box-shadow:none}
.sf-btn-ghost:hover:not(:disabled){background:var(--surface-2);border-color:var(--sf-line-strong);
              color:var(--ink);filter:none;box-shadow:var(--sf-lift-sm)}

/* The middle weight: "there is something behind this, go and look". Pale blue rather than white
   because these carry counts and a count wants a little more presence than Refresh. */
.sf-btn-soft{background:var(--sf-blue-soft);color:var(--sf-blue-ink);
             border:1px solid var(--sf-blue-line);box-shadow:none}
.sf-btn-soft:hover:not(:disabled){background:var(--sf-blue-tint);border-color:var(--sf-blue-ring);
             filter:none;box-shadow:var(--sf-lift-sm)}

/* ---- 2. THE "?" ------------------------------------------------------------------------
   The comment beside .sf-btn-icon already said what this was meant to be — "a quiet round ?...
   Quiet on purpose" — and the rule under it set a width and nothing else, so it inherited the
   solid blue and sat in the bar looking like a sixth action. Round, quiet, and it says what it
   is on hover, because a lone glyph in a row of worded controls is a riddle. */
.sf-btn-help{width:34px;height:34px;padding:0;flex:none;border-radius:999px;
             background:var(--surface);color:var(--sf-ink-3);border:1px solid var(--border);
             box-shadow:none;position:relative;font-weight:800;line-height:1}
.sf-btn-help:hover{background:var(--sf-blue-soft);color:var(--sf-blue);
             border-color:var(--sf-blue-line);filter:none;box-shadow:none}
/* The label. CSS only — the portal allows no inline script and this needs none. */
.sf-btn-help::after{content:attr(data-label);position:absolute;bottom:calc(100% + 8px);left:50%;
             transform:translateX(-50%) translateY(3px);white-space:nowrap;pointer-events:none;
             background:var(--sf-navy);color:var(--sf-on-blue);font-size:var(--sf-fs-label-sm);
             font-weight:700;padding:5px 9px;border-radius:8px;opacity:0;
             transition:opacity var(--sf-t) var(--sf-ease),transform var(--sf-t) var(--sf-ease)}
.sf-btn-help:hover::after,.sf-btn-help:focus-visible::after{opacity:1;
             transform:translateX(-50%) translateY(0)}

/* ---- 3. THE CONTROL BAR, IN GROUPS -----------------------------------------------------
   Six controls in one undifferentiated row is a list, not a bar. Three groups — what RUNS it,
   what OPENS on it, what it COSTS NOTHING to press — separated by a hairline, so the eye reads
   three decisions instead of six equal ones. The separator is decorative and is hidden from
   screen readers at the markup. */
.sf-runbar{row-gap:10px}
.sf-runbar-sep{flex:none;width:1px;align-self:stretch;min-height:24px;background:var(--border);
               margin:0 2px}
.sf-controlbar{transition:box-shadow var(--sf-t) var(--sf-ease)}
/* The count badge no longer sits on a solid blue field, so it needs its own edge to stay a
   circle rather than a smudge on pale blue. */
.sf-btn-soft .sf-badge{box-shadow:0 0 0 2px var(--surface)}
/* A badge appearing is a thing happening. One pulse on arrival, never a loop: a control that
   throbs forever is an alarm nobody can switch off. */
.sf-badge{animation:sf-pop var(--sf-t-slow) var(--sf-ease) both}

/* ---- 4. THE STAT TILES ------------------------------------------------------------------
   Same three numbers, same three words, same order. What is added is a colour that agrees with
   the pills in the table below (green live, amber in build, blue runs), so the tile and the
   column it summarises are visibly the same fact — and a lift, so the row does not read as
   three printed boxes. */
.sf-tile-stat{position:relative;overflow:hidden;
              transition:transform var(--sf-t) var(--sf-ease),box-shadow var(--sf-t) var(--sf-ease),
                         border-color var(--sf-t) var(--sf-ease)}
.sf-tile-stat:hover{transform:translateY(-2px);box-shadow:var(--sf-lift);
                    border-color:var(--sf-line-strong)}
/* The accent: a 3px bar across the top, in the colour of what is being counted. */
.sf-tile-stat::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;
                      background:var(--sf-ink-faint)}
.sf-tile-stat[data-accent="live"]::before{background:var(--sf-ok-ink)}
.sf-tile-stat[data-accent="building"]::before{background:var(--sf-warn-ink)}
.sf-tile-stat[data-accent="week"]::before{background:var(--sf-blue)}
.sf-tile-l{font-weight:600;letter-spacing:-.005em}

/* ---- 5. THE TABLE, WHICH WAS THE SPREADSHEET -------------------------------------------
   The five columns stay, the five headings stay, the row still leads to the automation's own
   page. Three things change:

     · the icon tile takes the row's OWN status colour. Every row carried the identical blue
       square, which is what made a table of five different things look like one thing repeated.
     · the status pill carries a dot, so the column is scannable at a glance and legible to
       someone who cannot separate the amber from the green.
     · the row answers the pointer — a blue rail on the left, a chevron that slides in on the
       right — so it reads as a door rather than as a cell.  */
.sf-table-rows tbody tr{transition:background var(--sf-t) var(--sf-ease)}
.sf-table-rows tbody td{transition:background var(--sf-t) var(--sf-ease)}
.sf-table-rows tbody tr:hover td{background:var(--sf-blue-soft)}
/* The rail. box-shadow rather than a border, so nothing in the row shifts by 3px on hover. */
.sf-table-rows tbody tr td:first-child{box-shadow:inset 0 0 0 0 var(--sf-blue);
                                       transition:box-shadow var(--sf-t) var(--sf-ease)}
.sf-table-rows tbody tr:hover td:first-child{box-shadow:inset 3px 0 0 0 var(--sf-blue)}
.sf-table-rows tbody tr:focus-within td{background:var(--sf-blue-soft)}
.sf-table-rows tbody tr:focus-within td:first-child{box-shadow:inset 3px 0 0 0 var(--sf-blue)}

/* the row's icon tile, coloured by status. --sf-ink-off is the vocabulary for not-built-yet and
   an identified automation is exactly that, so it stays grey rather than being given a colour
   that would claim progress it has not made. */
.sf-tile{transition:background var(--sf-t) var(--sf-ease),color var(--sf-t) var(--sf-ease),
         transform var(--sf-t) var(--sf-ease)}
.sf-tile-live{background:var(--sf-ok-bg);color:var(--sf-ok-ink)}
.sf-tile-building{background:var(--sf-warn-bg);color:var(--sf-warn-ink)}
.sf-tile-paused{background:var(--sf-danger-bg);color:var(--sf-danger-ink)}
.sf-tile-off{background:var(--surface-2);color:var(--sf-ink-off)}
.sf-table-rows tbody tr:hover .sf-tile{transform:scale(1.06)}

/* the pill's dot */
.sf-pill{display:inline-flex;align-items:center;gap:7px}
.sf-pill::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;
                 flex:none;opacity:.85}
/* Pills that are not a status do not take a dot: the frozen note, the checking spinner, the
   "high" priority flag on a task and the tally pills all carry their own vocabulary. */
.sf-pill-checking::before,.sf-pill-frozen::before,.sf-pill-high::before,.sf-pill-todo::before{
                 display:none}
/* Live is the only one that earns motion, and only while it is actually live. */
.sf-pill-live::before{animation:sf-breathe 2.6s var(--sf-ease) infinite}

/* the chevron that slides in on hover. The glyph is the shared down-chevron turned a quarter, so
   there is one chevron in this product and not two that drift apart. */
.sf-row-go{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;
           margin-left:10px;color:var(--sf-blue);opacity:0;transform:rotate(-90deg) translateY(6px);
           transition:opacity var(--sf-t) var(--sf-ease),transform var(--sf-t) var(--sf-ease);
           vertical-align:middle}
.sf-row-go svg{width:18px;height:18px}
.sf-table-rows tbody tr:hover .sf-row-go,
.sf-table-rows tbody tr:focus-within .sf-row-go{opacity:1;transform:rotate(-90deg) translateY(0)}

/* ---- 6. ENTRANCES ------------------------------------------------------------------------
   The page arriving, once, at speed. Rows come in on a stagger so a list of five reads as a
   list being dealt rather than as a screen being painted — the stagger is capped at eight
   because past that it stops being motion and starts being a wait.

   🚨 THESE ARE CSS ANIMATIONS AND NOT SCRIPT ON PURPOSE. The portal allows no inline script and
   an entrance driven from shopfront.js would be one more thing that can fail between the data
   arriving and the row existing. A row with the animation blocked still renders — `both` fills
   from the last frame, and the last frame is the ordinary appearance. */
@keyframes sf-rise{from{opacity:0;transform:translateY(9px)}to{opacity:1;transform:none}}
@keyframes sf-fade{from{opacity:0}to{opacity:1}}
@keyframes sf-pop{0%{transform:scale(.6);opacity:0}60%{transform:scale(1.12)}100%{transform:scale(1);opacity:1}}
@keyframes sf-breathe{0%,100%{opacity:.85}50%{opacity:.35}}

.sf-tiles .sf-tile-stat{animation:sf-rise var(--sf-t-slow) var(--sf-ease) both}
.sf-tiles .sf-tile-stat:nth-child(2){animation-delay:.05s}
.sf-tiles .sf-tile-stat:nth-child(3){animation-delay:.1s}
.sf-tablecard{animation:sf-rise var(--sf-t-slow) var(--sf-ease) .08s both}
.sf-table-rows tbody tr{animation:sf-fade var(--sf-t-slow) var(--sf-ease) both}
.sf-table-rows tbody tr:nth-child(1){animation-delay:.14s}
.sf-table-rows tbody tr:nth-child(2){animation-delay:.18s}
.sf-table-rows tbody tr:nth-child(3){animation-delay:.22s}
.sf-table-rows tbody tr:nth-child(4){animation-delay:.26s}
.sf-table-rows tbody tr:nth-child(5){animation-delay:.30s}
.sf-table-rows tbody tr:nth-child(6){animation-delay:.34s}
.sf-table-rows tbody tr:nth-child(7){animation-delay:.38s}
.sf-table-rows tbody tr:nth-child(n+8){animation-delay:.42s}

/* ---- 7. THE OFF SWITCH -------------------------------------------------------------------
   🚨 NOT OPTIONAL AND NOT A NICETY. A client who has asked their operating system for less
   motion has usually asked for a medical reason, and every rule above is switched off here
   rather than merely shortened. The page keeps every colour, every group and every hover state;
   what it loses is movement. Nothing is hidden by an animation that never runs, because every
   entrance uses `both` and its end state is the ordinary one. */
@media (prefers-reduced-motion: reduce){
  .sf-tiles .sf-tile-stat,.sf-tablecard,.sf-table-rows tbody tr,.sf-badge{animation:none}
  .sf-tile-stat:hover{transform:none}
  .sf-table-rows tbody tr:hover .sf-tile{transform:none}
  .sf-btn:active:not(:disabled){transform:none}
  .sf-pill-live::before{animation:none}
  .sf-row-go{transition:none}
  .sf-btn,.sf-tile-stat,.sf-tile,.sf-table-rows tbody tr,.sf-table-rows tbody td,
  .sf-btn-help::after{transition:none}
}

/* Two hint spans sat side by side in the bar with nothing between them, so "Not checked yet"
   and "We will confirm before anything starts" read as one broken sentence. A middot, drawn by
   the stylesheet rather than typed into either string, because neither sentence owns it and the
   pages that draw only one of them must not grow a stray dot. */
.sf-runbar .sf-runhint+.sf-runhint::before{content:"·";margin-right:8px;color:var(--sf-ink-faint)}
