/* composed by console/style.py — edit app/orkis-app.css, not this */
/* ORKIS — product UI stylesheet.  Shared by workspace.html, pay.html, dashboard.html.
 *
 * ONE stylesheet, not three copies. The landing page is deliberately a single self-contained
 * file because it is one page that must paint instantly; these are three pages of the same
 * product, and three pasted copies of a design system is precisely the failure that put a
 * retired logo on the live site for a week — a copy has no link back to the thing it copies,
 * so nothing reports that it has drifted. Whatever is shared lives here exactly once.
 *
 * ─────────────────────────────────────────────────────────────────────────────────────────
 * REWRITTEN 2026-08-29. Oscar: "Orkis landing page and all the other orkis pages looks like
 * a different version of OKG i need it to be unique and Classic."
 *
 * He was right, and the diagnosis is worth writing down because it was not a colour problem.
 * These pages were dark-navy panels, cyan→violet gradient buttons, 14px corner radii, glow
 * washes and a monospace-uppercase register. That is the house style of every dev-tools SaaS
 * of the last decade — OKG included. Recolouring it would have produced a LIGHT dev-tools
 * SaaS, which is the same page wearing different paint.
 *
 * The answer was already in the tree: brand/out/letterhead-sample.png. The stationery Michael
 * and Oscar actually hand people is genuinely classic — ink on warm paper, a serif face, a
 * heavy masthead rule, a vermilion accent. So the product is now the same document set,
 * continued onto a screen: ruled rows instead of cards, small caps instead of uppercase mono,
 * hairlines instead of borders-and-shadows. Unique without trying to be, because nobody else
 * has this letterhead.
 *
 * The tokens below are LIFTED VERBATIM from index.html's :root. If the marketing site and the
 * product disagree about what "Orkis paper" is, the customer sees two companies.
 * ───────────────────────────────────────────────────────────────────────────────────────── */

/* The three faces are the ones brand/webfonts.py subsets. There is deliberately NO BOLD:
   EBGaramond12-Bold covers 127 of the 201 characters this product needs — no accented
   letters, no en/em dash, no curly quotes — and a missing glyph does not fail, it falls back
   PER CHARACTER, so one row would silently render in two typefaces. Classic setting
   emphasises with italic and small caps anyway, which is what .sc and <em> are for.

   ⚠️ Paths are ../brand/out/ so these pages render correctly opened straight off disk, which
   is how app/shots.py photographs them. app/deploy.py rewrites them to the site root and
   ASSERTS it rewrote exactly as many as there are @font-face rules. */
@font-face{font-family:'Orkis Serif';font-style:normal;font-weight:400;font-display:swap;
  src:url('/orkis-serif-regular.woff2') format('woff2')}
@font-face{font-family:'Orkis Serif';font-style:italic;font-weight:400;font-display:swap;
  src:url('/orkis-serif-italic.woff2') format('woff2')}
@font-face{font-family:'Orkis SC';font-style:normal;font-weight:400;font-display:swap;
  src:url('/orkis-serif-sc.woff2') format('woff2')}

:root{
  --paper:#fbfaf8; --card:#ffffff; --panel-2:#f4f2ee;
  --ink:#0f1420; --ink-2:#4a5165; --ink-3:#666f82;
  /* 🔴 --ink-3 WAS #697286 AND FAILED AA ON THIS FILE'S OWN PANEL. Measured 2026-08-29 across
     414 text nodes: it scores 4.63:1 on --paper, which is where it was chosen and where the
     marketing page still documents it as "the lightest text allowed" — and 4.32:1 on
     --panel-2, which is the background almost every .dim on the app screens actually sits on.
     Ten nodes on workspace.html plus four on dashboard.html were below the 4.5 threshold.
     A contrast ratio is a property of a PAIR, so a colour cannot be carried from one surface
     to another with its legibility attached; the token name travelled and the measurement did
     not. #666f82 is 3% darker — indistinguishable by eye — and clears 4.5 on all three
     backgrounds this token is used on (panel-2 4.51, paper 4.84, card 5.05), so the value is
     now true wherever it is used rather than true where it was born. */
  --rule:#d9dce3; --rule-2:#8f9195;
  --accent:#c33f1b;

  /* 🔴 STATUS COLOURS WERE RE-DERIVED, NOT RECOLOURED, AND THIS IS THE WHOLE REASON THE
     REWRITE COULD NOT BE A FIND-AND-REPLACE. The old values were checked at >=4.5:1 against
     a near-black panel and were correct there. Measured against paper they are:

         ok  #3fd88a -> 1.77:1     warn #ffc247 -> 1.54:1
         bad #ff6b6b -> 2.66:1     cyan #59e7ff -> 1.41:1

     All four fail, and three of them fail catastrophically — amber on warm paper is very
     nearly invisible. Nothing would have reported it: the page renders, the pill is there,
     the class name still says "ok". CONTRAST IS A PROPERTY OF A PAIR, so a colour cannot be
     carried from one background to another with its rating attached. Values below are
     measured against --paper: ok 5.11:1, warn 5.68:1, bad 6.27:1, info 6.56:1.

     Status colours are still NOT part of the brand palette and must not be. Vermilion means
     "Orkis"; green means "this is fine"; amber means "look at this". Reusing the accent for
     "healthy" would make every healthy row shout the brand colour and leave nothing to
     signal with. */
  --ok:#1d7a4d; --warn:#8a5a06; --bad:#b3261e; --info:#1f5f8b;

  --serif:'Orkis Serif',Georgia,'Times New Roman',Times,serif;
  --sc:'Orkis SC','Orkis Serif',Georgia,serif;
  /* Kept for one job only: machine strings — reference numbers, IDs, timestamps — where a
     human needs to compare two of them character by character. NOT a register for labels
     any more; that is what small caps are. */
  --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
  --rail:236px;
}

/* 🔴 `hidden` must actually hide. This looks redundant and it is not.
 *
 * The attribute is enforced by exactly ONE declaration, `[hidden]{display:none}`, and it
 * lives in the USER-AGENT stylesheet — which loses to every author rule, at any
 * specificity, in every browser. So any `.thing{display:grid}` we write silently disarms
 * `hidden` on that element: the attribute is still set, `el.hidden` still returns true,
 * JS that toggles it still "works", and the element is still on screen.
 *
 * That is not hypothetical here. `.pay{display:grid}` beat it on pay.html and the checkout
 * stayed painted underneath the receipt — while its sibling `#view-receipt` hid correctly,
 * purely because `.done` happens to declare no `display`. One of the two escaped by luck,
 * which is why fixing the one broken rule was the wrong repair: the next `display` anyone
 * adds re-opens it somewhere else.
 *
 * `!important` is load-bearing, not laziness. Without it this rule sits in the same origin
 * as the page's own `<style>` and loses on specificity to any class selector — i.e. it
 * would fail in exactly the cases it exists for. Anything that genuinely needs to override
 * it should not be using the attribute.
 *
 * Enforced by app/shots.py, which asserts computed display is `none` for every element
 * carrying the attribute, IN EVERY STATE it puts a page into — the load-time-only version
 * of that check returned clean on the live bug, because at load the broken element does
 * not carry the attribute yet. */
[hidden]{display:none !important}

*{box-sizing:border-box}
html,body{margin:0;padding:0}
/* No -webkit-font-smoothing:antialiased. It thins strokes, which is a correction for light
   type on a dark field and straightforwardly wrong for dark type on paper — it would make a
   serif at 15px look weak and slightly broken. */
body{background:var(--paper);color:var(--ink);font:16px/1.62 var(--serif);
     overflow-x:hidden;font-feature-settings:'onum' 1,'kern' 1,'liga' 1}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
input,select{font:inherit;color:inherit}

/* Small caps are REAL small caps from the SC face, not `text-transform:uppercase` at a small
   size. The difference is the whole register: synthetic caps are the same strokes scaled, so
   they sit heavier than the text around them; true small caps are drawn for the x-height and
   sit level with it.
   🔴 THE INPUT MUST BE LOWERCASE. This face maps a–z to small capitals — feed it "PRICING"
   and you get full-height capitals, silently, with no error anywhere. */
.sc{font-family:var(--sc);letter-spacing:.075em}

/* Focus is VISIBLE and it is the same everywhere. A product people work in all day is
   keyboard-driven whether or not we intended it to be; :focus-visible only paints for
   keyboard users, so mouse users never see a ring they would ask us to remove. */
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

/* The glow washes are GONE, not dimmed. They were the dark-UI depth trick — two radial
   gradients faking a light source. On paper there is nothing to glow: the page is the light
   source. Kept as an empty rule because three pages carry the element, and deleting the
   selector while leaving the markup is how a class becomes a mystery. */
.glow{display:none}

/* ------------------------------------------------------------------ preview banner
   Every one of these pages is a DESIGN, and each is full of numbers that look like
   business facts. A mockup that does not say it is a mockup is how a placeholder ends up
   quoted back at you in a meeting. This bar is not decoration and it does not scroll away. */
.preview{position:sticky;top:0;z-index:60;display:flex;flex-wrap:wrap;gap:6px 14px;
  align-items:center;justify-content:center;padding:10px 18px;
  background:var(--panel-2);border-bottom:1px solid var(--rule-2);
  font-family:var(--sc);font-size:13px;letter-spacing:.09em;color:var(--ink-2)}
.preview b{color:var(--accent)}
/* --rule-2, and it is meant to be fainter than the words on either side of it — that is the
   entire job of a separator. It measures 2.82:1, which the contrast sweep flags, and the fix
   is NOT to darken it: at 4.5:1 the dots would carry the same weight as the text they divide,
   which is the defect the low ratio was protecting against. It is decoration, so it is now
   marked aria-hidden in the markup — a screen reader should read "Design preview, what the
   client sees", not "Design preview middle-dot what the client sees" — and the sweep exempts
   aria-hidden nodes. The exemption is anchored to a real accessibility annotation rather than
   to a class name, so it cannot quietly grow to cover text somebody does need to read. */
.preview .sep{color:var(--rule-2)}

/* ------------------------------------------------------------------------ primitives */
.mark{display:block;height:16px;width:auto;flex:none}   /* wordmark: HEIGHT only — it carries its own 6.2:1 ratio */
.mark-o{display:block;height:22px;width:auto;flex:none}

/* A "card" is now a ruled block of a document, not a floating panel. No radius, no shadow,
   no fill: a hairline above and generous space below, which is how a printed statement
   separates sections. The class name is kept because three pages and app/shots.py's
   card-spill check all reference it — renaming it would be a cosmetic change that breaks a
   real control. */
.card{background:transparent;border:0;border-top:1px solid var(--rule);padding:22px 0 26px}
.card > h3{margin:0 0 4px;font-size:19px;font-weight:400;letter-spacing:-.005em}
.card > .sub{margin:0 0 18px;color:var(--ink-2);font-size:15px}

.kicker{font-family:var(--sc);font-size:13px;letter-spacing:.1em;
  color:var(--accent);margin:0 0 12px}

/* Buttons: set in small caps, squared off. `.pri` is solid vermilion, `.sec` is a ruled
   outline. No gradient, no transform on hover, no shadow — a lifting button is the single
   most recognisable SaaS gesture there is and it was a large part of why these read as OKG. */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  font-family:var(--sc);font-size:14.5px;letter-spacing:.07em;line-height:1.2;
  padding:11px 20px;border:1px solid transparent;transition:background .16s,border-color .16s}
.btn.pri{background:var(--accent);color:#fff;border-color:var(--accent)}
.btn.pri:hover{background:#a83415;border-color:#a83415}
.btn.sec{border-color:var(--rule-2);color:var(--ink)}
.btn.sec:hover{border-color:var(--ink);background:var(--panel-2)}
.btn.sm{padding:8px 14px;font-size:13px}
.btn.wide{width:100%;padding:15px 20px;font-size:16px}

/* Pills. The dot is a SECOND channel carrying the same state as the colour, because roughly
   1 in 12 men cannot separate the green from the amber and this product's users are, so far,
   entirely men in the trades. Colour alone is a single point of failure for status.
   That redundancy survives the redesign unchanged — it was never a dark-UI device.
   The tinted fill does NOT survive: on paper a tint light enough to look like stationery is
   too light to read as a state, and a tint dark enough to signal looks like a highlighter.
   A hairline rule does the same job in the document register. */
.pill{display:inline-flex;align-items:center;gap:7px;padding:3px 10px;
  font-family:var(--sc);font-size:12.5px;letter-spacing:.08em;
  border:1px solid var(--rule);color:var(--ink-2);white-space:nowrap}
.pill i{width:7px;height:7px;border-radius:50%;background:currentColor;flex:none}
.pill.ok{color:var(--ok);border-color:#b9d6c6}
.pill.warn{color:var(--warn);border-color:#dccdae}
.pill.bad{color:var(--bad);border-color:#e2bdba}
.pill.info{color:var(--info);border-color:#bacedd}

/* Owner chips. Orkis is two people and the whole pitch is "you will talk to both" — so who
   owns a thing is not metadata to be discovered on hover, it is on the face of every row.
   Squared, ruled, set in small caps: an initial in a box, the way a document is initialled. */
.who{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;
  font-family:var(--sc);font-size:12.5px;letter-spacing:.02em;flex:none;
  border:1px solid var(--rule-2);color:var(--ink-2);background:var(--card)}
.who.o{color:var(--ink);border-color:var(--ink)}
.who.m{color:var(--info);border-color:var(--info)}
/* The assistant is an owner too, and it gets its OWN colour rather than borrowing Oscar's or
   Michael's. On a screen whose entire promise is "you can tell who is handling this", a chip
   that reads as a person when it is not is the one mistake this component cannot make.
   Deliberately not green: green is reserved for status here (.pill.ok, "Live", "Answering"),
   and a green identity chip would read as a health indicator. */
.who.a{color:var(--warn);border-color:var(--warn);font-size:10.5px;letter-spacing:.04em}
.who.stack{margin-left:-8px}

/* ---------------------------------------------------------------- agent chips (internal)
 * Orkis is about to be FOUR principals, not two: Oscar, Michael, Oscar's agent and
 * Michael's agent, all writing to the same project on the same server. So "who did this"
 * stops being a two-way question and the chip has to answer two things at once —
 * WHOSE agent, and that it is an agent at all.
 *
 * Colour carries the first (an agent inherits its owner's hue, because an agent acting is
 * its owner acting and the accountability does not transfer). SHAPE carries the second: a
 * DASHED edge, never a solid one. That is deliberate redundancy of the same kind as the dot
 * inside .pill — if the only difference between "Michael did this" and "Michael's agent did
 * this" were a shade, then on a laptop at an angle, in sunlight, it would be no difference
 * at all. Shape survives all of that.
 *
 * ⚠️ THE SHAPE CHANNEL HAD TO CHANGE TO SURVIVE. It used to be "squircle among circles" —
 * but the redesign squares off .who, so a squircle would now differ from its own base class
 * by 8px of corner radius, which is not a channel, it is a rounding error. Dashed-vs-solid
 * is the same information carried by a difference that is still legible at a glance.
 * A redundancy channel is only redundant while the thing it contrasts against holds still.
 *
 * ⚠️ NOT the same component as .who.a. That one is the CUSTOMER-facing Orkis assistant on
 * the client dashboard — a different actor, on the other side of the relationship, and it
 * keeps its own amber. Merging them would be the tidier stylesheet and the wrong product:
 * "the thing our customer talks to" and "the thing Michael delegates to" must never render
 * alike on a screen whose job is attribution. */
.who.bot{border-style:dashed;font-size:11px;letter-spacing:.02em}
.who.bot.o{color:var(--ink-2);border-color:var(--ink-2)}
.who.bot.m{color:var(--info);border-color:var(--info)}

/* Legend. Present because four actors is past the number a reader silently infers — two
   chips teach themselves, four do not. It is a real part of the product, not a mockup
   annotation: on day one on the new server, neither founder has seen the other's agent. */
.legend{display:flex;flex-wrap:wrap;gap:6px 16px;align-items:center;
  margin:16px 0 0;padding-top:14px;border-top:1px solid var(--rule);
  font-size:14px;color:var(--ink-3)}
.legend span{display:inline-flex;align-items:center;gap:7px}

.mono{font-family:var(--mono);font-size:.92em;font-variant-numeric:tabular-nums}
/* 🔴 Figures need BOTH features named. The page default is `onum` — oldstyle, which is
   correct for prose and wrong for a column: oldstyle 3, 4, 5, 7 and 9 descend and the 1 is
   narrow, so a money column goes ragged down both edges. `lnum` restores common height and
   `tnum` gives every digit one width. `tabular-nums` alone would not undo the oldstyle
   inherited from body, which is the trap — it looks like it should. */
.num{font-feature-settings:'lnum' 1,'tnum' 1,'kern' 1}
.muted{color:var(--ink-2)}
.dim{color:var(--ink-3)}

hr.rule{border:0;border-top:1px solid var(--rule);margin:22px 0}

/* ------------------------------------------------------------------------- tables */
table.tbl{width:100%;border-collapse:collapse}
.tbl th{text-align:left;font-family:var(--sc);font-size:13px;letter-spacing:.09em;
  color:var(--ink-3);font-weight:400;padding:0 14px 10px 0;
  border-bottom:1px solid var(--ink);white-space:nowrap}
.tbl td{padding:13px 14px 13px 0;border-bottom:1px solid var(--rule);font-size:15.5px;
  vertical-align:middle}
.tbl tr:last-child td{border-bottom:0}
.tbl tbody tr{transition:background .14s}
.tbl tbody tr:hover{background:var(--panel-2)}
.tbl td:last-child,.tbl th:last-child{padding-right:0;text-align:right}

/* ------------------------------------------------------------------------ scrollbars
   Still styled, for the opposite reason to before: the default scrollbar track on a warm
   paper field reads as a cold grey stripe. The pipeline board scrolls sideways by design. */
.scroll-x{overflow-x:auto;scrollbar-width:thin;scrollbar-color:var(--rule-2) transparent}
.scroll-x::-webkit-scrollbar{height:8px}
.scroll-x::-webkit-scrollbar-thumb{background:var(--rule-2)}
.scroll-x::-webkit-scrollbar-track{background:transparent}

@media(prefers-reduced-motion:reduce){
  *{transition:none!important;animation:none!important}
}

/* ── from workspace.html ── */

  /* Page-specific layout only. Anything reusable is in orkis-app.css. */

  .shell{display:flex;min-height:100vh;position:relative;z-index:1}

  /* ------------------------------------------------------------------- rail */
  .rail{width:var(--rail);flex:none;border-right:1px solid var(--rule);
    background:var(--paper);
    display:flex;flex-direction:column;position:sticky;top:0;height:100vh}
  .rail-brand{display:flex;align-items:center;gap:11px;padding:20px 20px 18px;
    border-bottom:1px solid var(--rule)}
  .rail-brand-t{font:11px/1 var(--mono);letter-spacing:.2em;text-transform:uppercase;color:var(--ink-3)}
  .rail-nav{flex:1;overflow-y:auto;padding:16px 12px}
  .rail-h{font:10.5px/1 var(--mono);letter-spacing:.2em;text-transform:uppercase;
    color:var(--ink-3);margin:16px 0 7px;padding:0 10px}
  .rail-h:first-child{margin-top:0}
  .rail-nav a{display:flex;align-items:center;gap:11px;padding:9px 10px;
    color:var(--ink-2);font-size:14px;transition:background .14s,color .14s}
  .rail-nav a:hover{background:var(--panel-2);color:var(--ink)}
  /* The active item is marked THREE ways — tinted panel, brand-coloured left edge, and full
     ink text. On a dark UI a single subtle tint is invisible on a laptop screen in daylight,
     which is where half of these sales calls happen.
     ⚠️ Each item USED to carry a geometric glyph (◧ ◔ ◇ …) that turned accent-coloured when
     active. That was a FOURTH channel, not one of the three, so retiring the icons on
     2026-08-29 cost this state nothing — verified before deleting, because a channel that
     looks decorative is exactly the kind you remove without noticing it was load-bearing.
     They went because not one of those characters exists in the webfont: every one fell back
     to whatever the OS happened to have, so the sidebar was set in two typefaces at once. */
  .rail-nav a.on{background:var(--panel-2);color:var(--ink);font-weight:600;
    box-shadow:inset 2px 0 0 var(--accent)}
  /* Drawn from borders, not typed as ⌄. A character-based caret is one the font has to own,
     and this font does not — so it fell back per character to whatever the OS supplied. A
     shape made of borders renders identically everywhere and cannot go missing. */
  .caret{margin-left:auto;flex:none;width:6px;height:6px;border-right:1.5px solid var(--ink-3);
    border-bottom:1.5px solid var(--ink-3);transform:translateY(-2px) rotate(45deg)}
  .badge{margin-left:auto;font:10.5px/1 var(--mono);font-style:normal;padding:4px 7px;
    border-radius:999px;background:var(--panel-2);color:var(--ink-2)}
  .badge.bad{background:#fdf1f0;color:var(--bad)}
  .rail-me{display:flex;align-items:center;gap:10px;padding:14px 18px;
    border-top:1px solid var(--rule);cursor:pointer}
  .rail-me:hover{background:var(--panel-2)}
  .rail-me-t{display:flex;flex-direction:column;line-height:1.3;flex:1;min-width:0}
  .rail-me-t b{font-size:13.5px;font-weight:620}
  .rail-me-t small{font-size:11.5px;color:var(--ink-3)}

  /* ------------------------------------------------------------------- main */
  .main{flex:1;min-width:0}
  .top{position:sticky;top:37px;z-index:30;display:flex;align-items:center;
    justify-content:space-between;gap:20px;padding:17px 30px;
    border-bottom:1px solid var(--rule);background:var(--paper)}
  .top-l{display:flex;align-items:baseline;gap:14px;min-width:0}
  .top h1{font-size:20px;font-weight:660;letter-spacing:-.02em;margin:0}
  .top-date{font-size:13px;white-space:nowrap}
  .top-r{display:flex;align-items:center;gap:12px}
  .search{display:flex;align-items:center;gap:9px;background:var(--card);
    border:1px solid var(--rule);border-radius:10px;padding:8px 12px;width:290px}
  .search input{border:0;background:none;outline:none;flex:1;min-width:0;font-size:13.5px}
  .search input::placeholder{color:var(--ink-3)}
  .search kbd{font:10.5px/1 var(--mono);color:var(--ink-3);border:1px solid var(--rule-2);
    border-radius:5px;padding:3px 5px}

  .body{padding:24px 30px 60px;display:flex;flex-direction:column;gap:18px}

  /* ------------------------------------------------------------------- attention */
  .attn{display:flex;align-items:center;flex-wrap:wrap;gap:14px;padding:14px 18px;
    border:1px solid var(--bad);
    background:#fdf1f0}
  .attn-i{font-size:14px;color:var(--ink-2)}
  .attn-i b{color:var(--ink);font-weight:620}
  .attn-i:hover{color:var(--ink)}
  .attn-sep{width:1px;height:16px;background:var(--rule-2)}

  /* ------------------------------------------------------------------- stats */
  .stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
  .stat{background:var(--card);border:1px solid var(--rule);padding:18px 20px}
  .stat .kicker{margin-bottom:10px}
  .stat-v{margin:0;font-size:30px;font-weight:660;letter-spacing:-.03em;line-height:1;
    font-variant-numeric:tabular-nums}
  .stat-v small{font-size:13px;font-weight:400;color:var(--ink-3);letter-spacing:0}
  .stat-d{margin:10px 0 0;font-size:12.5px;color:var(--ink-2)}
  .stat-d.up{color:var(--ok)}
  .stat-d.warn{color:var(--warn)}

  /* ------------------------------------------------------------------- split
     Pipeline is wider than Today at a deliberate 1.55:1. They are not equals: the board is
     scannable-at-a-glance and the task list is read line by line, so giving them equal
     width would starve the board and pad the list. */
  /* Today is a FIXED 320px, not a fraction. It is a list of six one-line items: past about
     320px it gains nothing but slack, while every pixel given to it is taken from a 7-column
     table that was overflowing its own card by 36px at 1.55fr:1fr. A ratio splits the slack
     between two things with completely different appetites; naming the small one's width and
     giving the rest to the table matches what each is actually for. */
  .split{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;align-items:start}
  /* 🔴 min-width:0 is load-bearing, not tidying. A grid item's automatic minimum size is its
     MIN-CONTENT, and the pipeline board below sets min-width:max-content (~1018px) so its
     columns keep their width inside the scroller. Without this line that 1018px becomes the
     floor of the 1.55fr track, the track outgrows the viewport, and the whole PAGE scrolls
     sideways instead of the board — measured at 1590px wide in a 1440px viewport, and 1080
     in a 390px one. The scroll container cannot contain what it has already sized. */
  .split>*{min-width:0}
  /* Explicit placement, because source order and reading order differ here on purpose (see
     the comment above the section). Today is authored first and sits on the right.

     🔴 Named classes, NOT `:not(.today)`. The rule here used to be `.split>.card:not(.today)
     {grid-column:1;grid-row:1}` — correct while the section held exactly two cards, wrong the
     moment a third arrived: Recent activity matched it and was placed into the cell Clients
     already occupied, stacking one card on top of the other. **A rule written as "everything
     except X" silently recruits every future sibling**, and it does not fail when it does —
     it just puts two things in one place. Each slot is now claimed by name, so a card wearing
     neither class falls to auto-placement: it lands somewhere free and slightly wrong, which
     is a bug you can SEE, rather than in a cell that is already taken. */
  .split>.side-col{grid-column:2;grid-row:1;display:flex;flex-direction:column;gap:18px}
  .split>.wide    {grid-column:1;grid-row:1}
  .card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:18px}
  .card-head h3{margin:0 0 4px;font-size:15.5px;font-weight:640;letter-spacing:-.01em}
  .card-head .sub{margin:0;color:var(--ink-3);font-size:12.5px}
  .head-tools{display:flex;gap:8px}

  .cols{display:flex;gap:12px;min-width:max-content;padding-bottom:6px}
  .col{width:194px;flex:none;display:flex;flex-direction:column;gap:9px}
  .col-h{margin:0 0 3px;font:11px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;
    color:var(--ink-2);display:flex;justify-content:space-between;padding-bottom:9px;
    border-bottom:1px solid var(--rule)}
  .lead{display:flex;flex-direction:column;gap:7px;background:var(--panel-2);
    border:1px solid var(--rule);border-radius:11px;padding:12px;font-size:13.5px;
    transition:border-color .16s,transform .16s}
  .lead:hover{border-color:var(--rule-2);transform:translateY(-2px)}
  .lead b{font-weight:620;letter-spacing:-.005em}
  .lead .dim{font-size:12px;line-height:1.45}
  .lead-f{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:2px}
  .lead.ghost{align-items:center;justify-content:center;color:var(--ink-3);
    border-style:dashed;background:none;font-size:12.5px;padding:14px}

  .tasks{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
  .tasks li{display:flex;align-items:center;gap:12px;padding:13px 0;border-bottom:1px solid var(--rule)}
  .tasks li:last-child{border-bottom:0;padding-bottom:0}
  .tasks li:first-child{padding-top:0}
  .t-b{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}
  .t-b b{font-size:14px;font-weight:600}
  .t-b .dim{font-size:12.5px}

  /* Activity rows. align-items:flex-start, unlike .tasks: these wrap to two or three lines
     in a 320px column and a centred chip beside a three-line sentence stops pointing at the
     line it belongs to. Same reasoning as .f-check on the payment page. */
  .acts{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
  .acts li{display:flex;align-items:flex-start;gap:11px;padding:12px 0;
    border-bottom:1px solid var(--rule)}
  .acts li:last-child{border-bottom:0;padding-bottom:0}
  .acts li:first-child{padding-top:0}
  /* 🔴 display:block, overriding .t-b's flex-column. These rows are a SENTENCE — "<b>X</b>
     did Y" — and under flex-direction:column the bold actor name and the bare text node
     after it become two separate flex items, so the sentence breaks across two lines at the
     closing </b>. .t-b is shared with .tasks, where the bold line and the dim line really
     are two stacked blocks; here they are one prose line plus a timestamp. Same class, two
     different jobs, so the second one has to say so explicitly. */
  .acts .t-b{display:block;font-size:13.5px;line-height:1.5;min-width:0}
  .acts .t-b b{font-size:13.5px;font-weight:640}
  .acts .t-b .dim{display:block;font-size:12px;margin-top:3px}

  .foot-note{margin:16px 0 0;font-size:12.5px;color:var(--ink-3);
    border-left:2px solid var(--rule-2);padding-left:13px;max-width:78ch}

  /* ------------------------------------------------------------------- responsive
     The rail collapses to a top bar rather than a hamburger. A drawer hides the one thing a
     two-person team navigates by (which section am I in), and a hamburger on a tool you use
     forty times a day costs a tap every time. */
  @media(max-width:1180px){
    .split{grid-template-columns:1fr}
    /* One column ⇒ hand placement back to source order, which is why Today was authored
       first. Leaving the explicit grid-column:2 here would place it in a column that no
       longer exists and implicitly create one — the single-column collapse would silently
       become two again. */
    .split>.side-col,.split>.wide{grid-column:1;grid-row:auto}
    .stats{grid-template-columns:repeat(2,1fr)}
  }
  @media(max-width:900px){
    .shell{flex-direction:column}
    .rail{width:auto;height:auto;position:static;flex-direction:row;align-items:center;
      border-right:0;border-bottom:1px solid var(--rule);gap:12px;padding:0 14px;overflow-x:auto}
    .rail-brand{border-bottom:0;padding:14px 6px 14px 0}
    .rail-brand-t{display:none}
    .rail-nav{display:flex;align-items:center;gap:4px;padding:0;overflow:visible}
    .rail-h{display:none}
    .rail-nav a{padding:8px 11px;white-space:nowrap;font-size:13.5px}
    .rail-nav a.on{box-shadow:none}
    .rail-me{border-top:0;padding:10px 0 10px 8px;margin-left:auto}
    .rail-me-t,.rail-me .dim{display:none}
    .top{position:static}
    .top-r .search{display:none}
    .body{padding:18px 16px 48px}
  }
  @media(max-width:620px){
    .stats{grid-template-columns:1fr 1fr;gap:10px}
    .stat{padding:14px 15px}
    .stat-v{font-size:24px}
    .attn{flex-direction:column;align-items:flex-start;gap:10px}
    .attn-sep{display:none}
    /* 🔴 The board STACKS on a phone, for the identical reason the table below becomes cards —
       a decision this file had already made once and had not carried across. Measured at 390px
       before the fix: five columns, fixed 194px each, `min-width:max-content` on .cols pushing
       the last three to right edges of 622 / 828 / 1034 in a 390px viewport. Three of five
       columns were entirely off-screen, cut mid-card, with no fade, arrow or snap to say the
       region scrolled at all.
       ⚠️ The shots harness was GREEN on this and always would be: its overflow check has a
       bucket that deliberately EXCUSES legitimate horizontal scrollers, so the board fell into
       the exemption and the pass was a SKIP, not a finding. A screenshot cannot scroll either —
       so the phone shot Oscar and Michael actually receive photographed 1.9 of 5 columns and
       looked, on the page, like the whole board. */
    .cols{flex-direction:column;min-width:0;gap:20px}
    .col{width:auto}
    /* The table becomes CARDS, it does not scroll sideways. A 7-column table in a 390px
       viewport is a table you scroll and never read; each row is restated as a labelled
       block using the header text from data-l, so no information is dropped on a phone. */
    .tbl,.tbl tbody,.tbl tr,.tbl td{display:block;width:100%}
    .tbl thead{display:none}
    .tbl tr{border:1px solid var(--rule);border-radius:12px;padding:14px;margin-bottom:10px;
      background:var(--panel-2)}
    .tbl td{border:0;padding:5px 0;display:flex;align-items:center;justify-content:space-between;
      gap:14px;text-align:right}
    .tbl td:first-child{text-align:left;display:block;padding-bottom:10px;margin-bottom:6px;
      border-bottom:1px solid var(--rule)}
    .tbl td:last-child{text-align:right}
    .tbl td[data-l]:before{content:attr(data-l);font:10.5px/1 var(--mono);letter-spacing:.16em;
      text-transform:uppercase;color:var(--ink-3);text-align:left;flex:none}
  }

/* ── from dashboard.html ── */

  /* ------------------------------------------------------------------ bar */
  .bar{position:sticky;top:37px;z-index:30;display:flex;align-items:center;
    justify-content:space-between;gap:20px;padding:15px 30px;
    border-bottom:1px solid var(--rule);background:var(--paper)}
  .bar-l{display:flex;align-items:center;gap:14px;min-width:0}
  .bar-brand svg{display:block;height:15px;width:auto}
  .bar-div{width:1px;height:18px;background:var(--rule-2);flex:none}
  .bar-client{font-size:14.5px;font-weight:620;letter-spacing:-.01em;white-space:nowrap;
    overflow:hidden;text-overflow:ellipsis}
  .bar-nav{display:flex;align-items:center;gap:22px}
  .bar-nav a{font-size:13.5px;color:var(--ink-3);transition:color .15s}
  .bar-nav a:hover{color:var(--ink)}
  .bar-nav a.on{color:var(--ink);font-weight:600}

  .page{position:relative;z-index:1;max-width:1080px;margin:0 auto;
    padding:26px 30px 60px;display:flex;flex-direction:column;gap:16px}

  /* ------------------------------------------------------------------ status */
  .status{display:flex;align-items:flex-start;justify-content:space-between;gap:26px;
    background:var(--card);border:1px solid var(--rule);padding:26px 28px}
  .status-l{min-width:0}
  .pill.big{padding:5px 12px;font-size:11.5px}
  .status h1{font-size:clamp(21px,2.6vw,27px);font-weight:660;letter-spacing:-.025em;
    margin:14px 0 10px;line-height:1.2;max-width:22ch;text-wrap:balance}
  .status-s{margin:0;color:var(--ink-2);font-size:13.5px;line-height:1.7;max-width:62ch}
  .status-s b{color:var(--ink);font-weight:600}
  .status-r{display:flex;flex-direction:column;gap:9px;flex:none}

  /* ------------------------------------------------------------------ metrics */
  .card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:20px}
  .card-head h3{margin:0 0 4px;font-size:15.5px;font-weight:640;letter-spacing:-.01em}
  .card-head .sub{margin:0;color:var(--ink-3);font-size:12.5px;max-width:58ch}

  .mets{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
  .met{background:var(--panel-2);border:1px solid var(--rule);border-radius:12px;padding:18px 18px 16px}
  /* Exactly ONE metric is highlighted, and it is the one that is worth money. Highlighting
     three would highlight none. */
  .met.hi{border-color:transparent;
    background:linear-gradient(var(--panel-2),var(--panel-2)) padding-box,
               var(--accent) border-box;border:1px solid transparent}
  .met-v{margin:0 0 8px;font-size:32px;font-weight:660;letter-spacing:-.03em;line-height:1;
    font-variant-numeric:tabular-nums}
  .met-v small{font-size:14px;font-weight:400;color:var(--ink-3);letter-spacing:0;margin-left:2px}
  .met-l{margin:0 0 6px;font-size:13.5px;font-weight:600;line-height:1.35}
  .met-s{margin:0;font-size:12px;line-height:1.5}

  /* ------------------------------------------------------------------ feed */
  .feed,.reqs{list-style:none;margin:0;padding:0}
  .feed li{display:flex;align-items:flex-start;gap:14px;padding:14px 0;border-bottom:1px solid var(--rule)}
  .feed li:last-child{border-bottom:0;padding-bottom:0}
  .f-w{flex:none;width:96px}
  .f-b{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
  .f-b b{font-size:14px;font-weight:600}
  .f-b .dim{font-size:12.5px;line-height:1.55}
  .f-t{flex:none;font-size:12px;white-space:nowrap;padding-top:2px}

  /* ------------------------------------------------------------------ two-up */
  .two{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}
  .reqs li{display:flex;align-items:flex-start;gap:13px;padding:13px 0;border-bottom:1px solid var(--rule)}
  .reqs li:last-child{border-bottom:0;padding-bottom:0}
  .r-s{flex:none;width:104px}
  .r-b{display:flex;flex-direction:column;gap:3px;min-width:0}
  .r-b b{font-size:14px;font-weight:600}
  .r-b .dim{font-size:12.5px}
  .note{margin:16px 0 0;font-size:12.5px;color:var(--ink-3);line-height:1.6;
    border-left:2px solid var(--rule-2);padding-left:13px}
  .note b{color:var(--ink-2)}

  .plan{display:flex;align-items:baseline;gap:12px;padding-bottom:18px;border-bottom:1px solid var(--rule)}
  .plan-v{margin:0;font-size:32px;font-weight:660;letter-spacing:-.03em;line-height:1;
    font-variant-numeric:tabular-nums}
  .plan-v small{font-size:14px;font-weight:400;color:var(--ink-3);letter-spacing:0}
  .plan-l{margin:0;font-size:13.5px;color:var(--ink-2)}
  .bill{list-style:none;margin:0;padding:14px 0 0}
  .bill li{display:flex;justify-content:space-between;gap:16px;padding:8px 0;font-size:13.5px}
  .bill-cta{display:flex;gap:8px;margin-top:14px}

  /* ------------------------------------------------------------------ people */
  .ppl{display:grid;grid-template-columns:1fr 1fr;gap:14px}
  .person{display:flex;align-items:center;gap:14px;background:var(--panel-2);
    border:1px solid var(--rule);border-radius:12px;padding:16px}
  .who.lg{width:40px;height:40px;font-size:15px}
  .p-b{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}
  .p-b b{font-size:14.5px;font-weight:640}
  .p-r{font-size:12.5px;color:var(--ink-2)}
  .p-c{font-size:12px;color:var(--ink-3)}

  .foot{position:relative;z-index:1;max-width:1080px;margin:0 auto;padding:24px 30px 50px;
    display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between;
    border-top:1px solid var(--rule);font-size:12.5px;color:var(--ink-3)}
  .foot a{color:var(--ink-2)}
  .foot a:hover{color:var(--ink)}

  /* ------------------------------------------------------------------ responsive */
  @media(max-width:940px){
    .mets{grid-template-columns:1fr 1fr}
    .two,.ppl{grid-template-columns:1fr}
  }
  @media(max-width:720px){
    .bar{padding:12px 16px;gap:12px}
    .bar-nav{gap:14px}
    .bar-nav a{font-size:13px}
    .page{padding:16px 16px 46px}
    /* The status block stacks and the buttons go FULL WIDTH. "Ask for a change" is the
       reason a phone visitor opened this page at all; a right-aligned button that has
       shrunk to fit its text is the wrong target size on the surface where it matters
       most. */
    .status{flex-direction:column;padding:20px}
    .status-r{flex-direction:row;width:100%}
    .status-r .btn{flex:1}
    .feed li,.reqs li{flex-wrap:wrap;gap:8px 12px}
    .f-w,.r-s{width:auto}
    .f-t{width:100%;padding-top:0}
    .foot{padding:20px 16px 40px}
  }
  @media(max-width:460px){
    .mets{grid-template-columns:1fr}
    .bar-client{display:none}
    .bar-div{display:none}
  }

/* ── console-only ─────────────────────────────────────────────────────────────────────
   Written here rather than in orkis-app.css because none of it is design system: it is
   the plumbing of a real application (auth, command output, error states) that a static
   mockup never needed. Kept minimal on purpose. */

/* The sign-in and choose-a-password forms, and the pages that only say one sentence (403,
   404). Centred, narrow, no chrome — someone arriving
   here is doing exactly one thing and every additional element is a way to not do it. */
.auth{max-width:24em;margin:12vh auto;padding:0 24px}
.auth h1{font-size:26px;font-weight:660;letter-spacing:-.02em;margin:0 0 6px}
.auth .sub{color:var(--ink-2);margin:0 0 22px;font-size:15px}
.auth label{display:block;margin:18px 0 6px;font-family:var(--sc);font-size:13px;
  letter-spacing:.1em;color:var(--ink-2)}
/* 16px floor: iOS Safari zooms in on any smaller field and does not zoom back out, which
   strands someone on a half-visible page mid-login. */
.auth input{width:100%;font:16px/1.5 var(--serif);padding:12px 13px;
  border:1px solid var(--rule);border-radius:10px;background:var(--card);color:var(--ink)}
.auth input:focus{outline:2px solid var(--accent);outline-offset:1px}
.auth button{margin-top:24px;width:100%;font:16px/1 var(--serif);padding:14px;
  border:0;border-radius:10px;background:var(--accent);color:#fff;cursor:pointer}
.auth button:hover{background:#a83415}
/* The way out, UNDER the submit: "Forgotten your password?" on sign-in, "Back to sign in" on
   the recovery pages. `.auth .sub` is written for the line under the H1 — margin-top:0,
   bottom:22 — which is right there and wrong here: below the button it sits flush against it
   and reads as an overflow of the button rather than as the separate choice it is. It also
   inherits body colour with no underline, so the ONE control on the page for the person who
   is locked out did not look clickable at all. Both selectors, because an error box sits
   between the button and the link exactly when someone is most likely to need the link. */
.auth button+.sub,.auth .err+.sub{margin-top:16px;margin-bottom:0}
.auth .sub a{color:var(--accent);text-decoration:underline;text-underline-offset:3px}

.err{background:#fdf1f0;border:1px solid #e2bdba;color:var(--bad);
  padding:12px 14px;border-radius:10px;margin:18px 0 0;font-size:15px}

/* 🔴 THE STICKY HEADER'S OFFSET IS A FACT ABOUT AN ELEMENT THE CONSOLE DOES NOT RENDER.
   workspace.html says `.top{position:sticky;top:37px}`. 37px is not a design measurement —
   it is the height of `.preview`, the "Design preview · all figures are sample data" banner
   that sits `sticky;top:0` above it in every mockup. The mockups need that banner (a mockup
   that does not say it is a mockup is how placeholder numbers get quoted in a meeting); the
   live console has real data and correctly has no banner.

   So the console inherits a 37px reservation for something that is not there, and the first
   card scrolls up UNDER the header and is clipped along its top edge. Visible in
   console/shots/02-workspace-1440.png; invisible to every assertion in test_console.py,
   because the markup, the classes and the status code are all exactly right.

   This is a genuine console-only override and not a copied value: it does not restate the
   mockup's design, it cancels a coupling to a mockup-only element. If the console ever grows
   a real banner of its own, this is the line to revisit. */
.top{top:0}

/* An empty state is a first-class screen here, not a fallback. Most of these panels are
   empty on a client's first day and that is the correct answer, so it is styled to look
   deliberate rather than broken. */
.empty{border:1px dashed var(--rule-2);border-radius:12px;padding:34px 24px;
  text-align:center;color:var(--ink-2);font-size:15px}
.empty p{margin:0 0 8px}
.empty p:last-child{margin:0}

/* One row of the activity feed and of the People card: a `.who` ring, then two lines of
   text. The mockups' equivalents (.feed in dashboard.html, the .side card in workspace.html)
   are built around content the console does not have — a per-item status pill, a hand-written
   summary sentence — so this is a smaller row of the same shape rather than a copy of markup
   whose fields would be empty. `.who` itself is the design system's, unchanged. */
.ev{display:flex;gap:12px;align-items:flex-start;padding:12px 0;
  border-top:1px solid var(--rule)}
.ev:first-of-type{border-top:0}
.ev-t{min-width:0;font-size:14.5px;line-height:1.5}

.meta{color:var(--ink-3);font-size:13px;margin:0 0 10px}
.msg{white-space:pre-wrap;margin:10px 0 0;padding:13px 15px;background:var(--panel-2);
  border-radius:10px;border:1px solid var(--rule);font-size:15px}

/* Section rules between groups of cards. Small caps, matching .kicker's register. */
h2.sec{font-family:var(--sc);font-size:13px;letter-spacing:.1em;color:var(--ink-2);
  font-weight:400;margin:34px 0 12px;padding-bottom:9px;border-bottom:1px solid var(--rule)}
h2.sec .n{color:var(--ink-3);letter-spacing:0;font-family:var(--serif);font-size:13px}

/* The action buttons for a client. Each is its own form, so they need to sit in a row
   without the form elements adding layout of their own.

   🔴 THIS WAS CALLED `.acts` AND THE NAME WAS ALREADY TAKEN. dashboard.html defines `.acts`
   as a vertical <ul> of action items — `list-style:none;margin:0;padding:0;display:flex;
   flex-direction:column` — and that rule loads BEFORE this block. The console-only rule set
   `display:flex` and `flex-wrap:wrap` and did NOT set `flex-direction`, so the mockup's
   `column` was never overridden and every button sat on its own line. Nothing was in
   conflict as far as the cascade was concerned: two different components agreed to share a
   name, and the second one silently inherited the first one's axis.
   A collision like this cannot be fixed by adding `flex-direction:row`, because that only
   patches the ONE property that happened to be visible — `list-style`, `margin` and
   `padding` were leaking too. The fix is a name that is not already in use. */
.btn-row{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}
.btn-row form{margin:0;display:inline}
/* In a table cell the 16px is wrong: it is spacing BETWEEN a client's name and its buttons in
   the "Act on a client" block, and inside a <td> it just pushes the row's only content off its
   own baseline while the four cells beside it stay put. The row then reads as misaligned
   rather than as spaced. Same component, different container, so the margin is the one thing
   that has to be re-answered. */
td>.btn-row{margin-top:0}

/* One client's name above its row of buttons, in the "Act on a client" section. The buttons
   were briefly an eighth column of the clients table; four of them in a narrow cell wrapped
   to four lines and stood every row up at ~250px, and the mockup's `.tbl` is seven read-only
   columns at one line each. So they moved out to a block of their own, which is the shape
   `.acts` above was written for. Rule between rows, no rule above the first — the card's own
   border-top already draws that line, and two rules 1px apart read as a mistake. */
.act-row{padding:14px 0;border-top:1px solid var(--rule)}
.act-row:first-of-type{border-top:0}
.act-who{margin:0;font-size:15.5px}
.act-row .btn-row{margin-top:10px}

/* newclient's raw stdout. Monospace because it prints aligned gate output, and a
   proportional face would break the alignment it was written to have. */
pre.out{white-space:pre-wrap;word-break:break-word;font:13px/1.55 var(--mono);
  padding:14px 16px;border-radius:10px;overflow-x:auto;margin:14px 0 0}
pre.done{background:#f1f7f3;border:1px solid #b9d6c6}
pre.err{background:#fdf1f0;border:1px solid #e2bdba}

/* Enquiries: the delete controls. One form wraps every card (forms cannot nest, and the
   bulk "Delete selected" needs every checkbox in the same form as its button), so the
   pieces are laid out as flex rows rather than as forms of their own. The result notice
   is a div, not a <pre>, because it carries the Undo form; it borrows .done/.err colours. */
.enq-form{margin:0}
.enq-bar{display:flex;flex-wrap:wrap;align-items:center;gap:10px 14px;margin:0 0 16px}
.enq-bar .meta{margin:0}
.enq-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.enq-head h3{min-width:0}
.enq-pick{white-space:nowrap;font-size:13px;color:var(--ink-3);cursor:pointer;
  display:inline-flex;align-items:center;gap:6px}
.enq-pick input{margin:0;accent-color:var(--accent)}
.enq-foot{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;margin-bottom:0}
.enq-del{margin-left:auto}
.enq-notice{display:flex;flex-wrap:wrap;align-items:center;gap:10px 16px;
  padding:14px 16px;border-radius:10px;margin:0 0 16px;font-size:14.5px}
.enq-notice.done{background:#f1f7f3;border:1px solid #b9d6c6}
.enq-notice.err{background:#fdf1f0;border:1px solid #e2bdba}
.enq-undo{margin:0;display:inline}

/* Projects (2026-09-04): one article per app, grouped under a status heading. The facts
   list is a <dl> so "Price idea / Link / Folder" read as labelled facts, not prose. */
.prj-group{font-family:var(--serif);font-size:15px;margin:18px 0 8px;color:var(--ink-2)}
.prj-group .n{color:var(--ink-3);margin-left:8px;font-size:12px}
.prj{border:1px solid var(--rule);border-radius:12px;background:var(--card);padding:16px 18px;
  margin:0 0 12px}
.prj-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}
.prj-head h4{margin:0;font-family:var(--serif);font-size:18px;line-height:1.3}
.prj-head .sub{margin:2px 0 0}
.prj-pills{display:flex;gap:6px;flex-wrap:wrap}
.prj-facts{display:grid;grid-template-columns:max-content 1fr;gap:4px 14px;margin:12px 0 0;
  font-size:14px}
.prj-facts dt{color:var(--ink-3)}
.prj-facts dd{margin:0;min-width:0;overflow-wrap:anywhere}
.prj-facts code{font-size:12.5px}
.prj-readme{margin:12px 0 0;padding:10px 12px;border-radius:8px;background:var(--panel-2);
  font-size:12.5px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere}
.prj-notes{margin:12px 0 0;font-size:14px;white-space:pre-wrap}
.prj .newc{margin-top:12px}
@media(max-width:600px){.prj-facts{grid-template-columns:1fr}.prj-facts dt{margin-top:6px}}

/* Chat: pictures and the agent's status line (2026-09-04). `.ch-status` replaces the old
   static `.ch-wait` paragraph; it is one element the script updates in place, hidden when
   there is nothing to say. The dot pulses only while data-busy=1 — motion means "working",
   and a dot that pulses while nothing is happening teaches people to ignore it. */
.ch-status{display:flex;align-items:center;gap:8px;color:#8a6100;background:#fff6e0;
  border:1px solid #f0dcae;border-radius:8px;padding:8px 11px;font-size:12.5px;
  line-height:1.5;margin:0 0 10px}
.ch-status[hidden]{display:none}
.ch-dot{flex:0 0 auto;width:8px;height:8px;border-radius:50%;background:#c98a00}
.ch-status[data-busy="1"] .ch-dot{animation:ch-pulse 1.1s ease-in-out infinite}
@keyframes ch-pulse{0%,100%{opacity:.25;transform:scale(.8)}50%{opacity:1;transform:scale(1.15)}}
@media(prefers-reduced-motion:reduce){.ch-status[data-busy="1"] .ch-dot{animation:none}}
.ch-status-t{min-width:0}
.ch-files{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.ch-pic{display:block;max-width:100%}
.ch-img{display:block;max-width:min(320px,100%);max-height:320px;border-radius:8px;
  border:1px solid rgba(0,0,0,.12)}
.ch-msg.me .ch-img{border-color:rgba(255,255,255,.25)}
/* The file input hides inside its label so the label IS the button; keyboard users still
   reach it because the input stays in the tab order (clip, not display:none). */
.ch-attach{position:relative;overflow:hidden;cursor:pointer}
.ch-attach input{position:absolute;inset:0;opacity:0;width:100%;height:100%;cursor:pointer}
.ch-picked{color:var(--ink-3);font-size:12.5px}

/* Sign out is an action, not navigation, so it must be a POST (it carries a CSRF token) —
   but it should read as the quiet text link it is, not as a button. */
.logout{background:none;border:0;color:var(--ink-2);cursor:pointer;font:13.5px var(--serif);
  padding:0;text-decoration:underline;text-underline-offset:2px}
.logout:hover{color:var(--accent)}

/* 🔴 THERE IS DELIBERATELY NO @media BLOCK HERE, and it is worth saying why, because one was
   written and removed the same hour. It began "The mockups were photographed at desktop width
   and never had to answer this" — an assumption, asserted in a comment, never checked. The
   mockups answer it at THREE breakpoints (1180 / 900 / 620 in workspace.html, 940 / 720 / 460
   in dashboard.html), and better than mine did: the rail becomes a scrollable horizontal strip
   with the brand text hidden, not a wrapped block.

   It would not merely have been redundant. Mine was `max-width:820px`, which sits INSIDE the
   mockup's 900px block and loads after it, so at 820px my `.shell{display:block}` would have
   killed the `flex-direction:column` the mockup relies on — a phone-only layout break, on the
   pages nobody opens on a phone until a customer does. A duplicate rule at a DIFFERENT
   breakpoint is not a duplicate; it is an override that only fires in a range you did not test.

   If the console ever needs a responsive rule the mockups do not have, the rule belongs in the
   mockup, not here — same argument as the whole file. */

/* ── the founder ↔ agent conversation ────────────────────────────────────────────────────
   🔴 THIS BLOCK LIVED IN server.py AS AN INLINE <style> AND THAT WAS WRONG TWICE OVER. The
   reasoning written above it was "the design system does not have a conversation in it, and
   adding one to style.py would mean editing the shared stylesheet for a single screen" —
   which is the exact case CONSOLE_ONLY exists for, so the argument was for putting it here.

   What the mistake actually cost: `.rowlink` is used on the WORKSPACE page (the client name
   is the link into this chat), and it was defined in the chat page's inline block. The
   workspace never loads that block, so the one control Oscar asked for rendered as a default
   browser link in a serif design system. Caught by the class sweep, which reads the composed
   stylesheet — an inline block is invisible to it, so the page carrying the rules was also
   the page exempt from the check that would have found this.

   Every property is set explicitly rather than inherited: a scoped rule that only sets
   `color` loses its layout to whatever the global chrome sets for the same element. */
.ch-wrap{display:flex;gap:18px;align-items:flex-start}
.ch-col{flex:1 1 0;min-width:0;display:flex;flex-direction:column}
.ch-side{flex:0 0 42%;min-width:0}
@media(max-width:1000px){.ch-wrap{flex-direction:column}.ch-side{flex:1 1 auto;width:100%}}
.ch-tabs{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 16px}
.ch-tabs a{display:inline-block;padding:6px 12px;border:1px solid var(--rule);
  border-radius:999px;text-decoration:none;color:var(--ink-2);font-size:13px;line-height:1.4;
  background:var(--card)}
.ch-tabs a.on{background:var(--ink);border-color:var(--ink);color:var(--card)}
.ch-tabs a .n{color:var(--ink-3);margin-left:6px;font-size:12px}
.ch-tabs a.on .n{color:var(--rule-2)}
.ch-log{border:1px solid var(--rule);border-radius:12px;background:var(--card);padding:16px;
  height:52vh;min-height:320px;overflow-y:auto;display:flex;flex-direction:column;gap:14px}
.ch-msg{max-width:82%;padding:10px 13px;border-radius:12px;font-size:14.5px;line-height:1.55;
  white-space:pre-wrap;overflow-wrap:anywhere;margin:0}
.ch-msg.them{align-self:flex-start;background:var(--panel-2);color:var(--ink);
  border-bottom-left-radius:4px}
.ch-msg.me{align-self:flex-end;background:var(--ink);color:var(--card);
  border-bottom-right-radius:4px}
.ch-by{display:block;font-family:var(--sc);font-size:11px;letter-spacing:.08em;
  margin:0 0 4px;opacity:.62;font-variant-numeric:lining-nums}
.ch-none{color:var(--ink-2);font-size:14.5px;line-height:1.6;margin:auto;text-align:center;
  max-width:34em}
.ch-form{margin-top:14px;display:flex;flex-direction:column;gap:10px}
/* 16px floor, same reason as .auth input: iOS Safari zooms into anything smaller and does
   not zoom back out, which strands someone mid-sentence on a half-visible page. */
.ch-form textarea{width:100%;box-sizing:border-box;min-height:88px;resize:vertical;
  padding:11px 13px;border:1px solid var(--rule);border-radius:10px;
  font:16px/1.55 var(--serif);color:var(--ink);background:var(--card)}
.ch-form textarea:focus{outline:2px solid var(--accent);outline-offset:1px}
/* 🔴 There is deliberately no `.ch-form button` rule. There was one, and it was a private
   re-draw of `.btn.pri` — same accent, same hover, same radius, in a second place. The
   button sweep is class-based, so a bare element selector is invisible to it and the Send
   button read as unclassed: a browser-default control that happened to look right, until
   the design system moved and this copy did not. It wears `.btn pri` like every other
   primary action in the console. */
.ch-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.ch-hint{color:var(--ink-3);font-size:12.5px;line-height:1.5;margin:0}
.ch-wait{color:#8a6100;background:#fff6e0;border:1px solid #f0dcae;border-radius:8px;
  padding:8px 11px;font-size:12.5px;line-height:1.5;margin:0}
.ch-frame{width:100%;height:52vh;min-height:320px;border:1px solid var(--rule);
  border-radius:12px;background:var(--card);display:block}
.ch-cap{color:var(--ink-3);font-size:12.5px;line-height:1.5;margin:8px 0 0}

/* A whole table row's worth of hit area that does not look like a button. Used on the
   WORKSPACE page, not here — see the note at the top of this block. */
.rowlink{color:inherit;text-decoration:none;border-bottom:1px solid var(--rule-2)}
.rowlink:hover{border-bottom-color:var(--accent);color:var(--accent)}

/* ── the client list's group headings ───────────────────────────────────────────────────
   Live / In build / Closed, as rows of the clients table. `.tbl th` is styled for the header
   row at the top, so these need their own weight and rule or they read as a second set of
   column headers. `position:sticky` deliberately NOT set: it would need a top offset that
   depends on the page's sticky header, and a heading that floats over rows it does not
   belong to is worse than one that scrolls away. */
.tbl tr.grp th{text-align:left;font-family:var(--sc);font-size:12.5px;letter-spacing:.09em;
  font-weight:400;color:var(--ink-2);padding:20px 0 7px;border-bottom:1px solid var(--rule)}
.tbl tbody tr.grp:first-child th{padding-top:6px}
.tbl tr.grp .n{font-family:var(--serif);letter-spacing:0;color:var(--ink-3);font-size:13px}

/* ── the New client form ────────────────────────────────────────────────────────────────
   A <details>, because this origin sends `default-src 'none'` and has no JavaScript at all.
   `list-style:none` plus the ::-webkit- pseudo-element removes the disclosure triangle in
   both engines — the summary contains something already shaped like a button, and a triangle
   glued to its left edge reads as a rendering fault rather than as an affordance. */
.newc>summary{list-style:none;cursor:pointer;display:inline-block}
.newc>summary::-webkit-details-marker{display:none}
.newc>summary .btn{pointer-events:none}
.newc[open]>summary{margin-bottom:6px}

/* Two columns on a wide screen, one on a narrow. Not a grid of equal fractions: these are
   six fields of very different lengths and a `notes` box the width of `price` invites a
   four-word note. 620 is workspace.html's own narrow breakpoint, reused rather than chosen —
   a new number here would be a second opinion about what narrow means. */
.formg{display:grid;grid-template-columns:1fr 1fr;gap:2px 22px;margin-top:14px}
@media (max-width:620px){.formg{grid-template-columns:1fr}}
.formg>.meta,.formg>button{grid-column:1/-1}
.fld{margin:0 0 16px;min-width:0}
.fld label{display:block;margin:0 0 6px;font-family:var(--sc);font-size:12.5px;
  letter-spacing:.09em;color:var(--ink-2)}
/* 16px floor for the same reason `.auth input` has one: iOS Safari zooms into any smaller
   field and does not zoom back out, stranding someone mid-form on a half-visible page.
   `box-sizing` because the padding would otherwise push each field past its grid track. */
.fld input{width:100%;box-sizing:border-box;font:16px/1.5 var(--serif);padding:10px 12px;
  border:1px solid var(--rule);border-radius:10px;background:var(--paper);color:var(--ink)}
.fld input:focus{outline:0;border-color:var(--accent)}
.fld small{display:block;margin:5px 0 0;font-size:12.5px;line-height:1.45}



/* ── console theme: "Orkis Atelier" (2026-09-03) ────────────────────────────────────────
   Oscar, 2026-09-03: the paper workspace read "lab like, almost dull"; a dark version was
   tried and rejected the same hour — "have the workspace still match the landing page, i
   don't really like the dark tone". So this keeps EVERY token the landing page defines
   (paper, ink, vermilion, the serif) and adds only what the landing page has and the
   workspace lacked: surfaces with depth, real cards, a hairline of the brand colour where
   attention belongs, softer geometry. Appended last by style.py; the design system above is
   untouched, and deleting this file returns the console to the flat paper version. */
body{background:
  radial-gradient(1200px 520px at 20% -10%, rgba(195,63,27,.045), transparent 60%),
  radial-gradient(900px 600px at 110% 100%, rgba(15,20,32,.035), transparent 60%),
  var(--paper)}

/* rail */
.rail{background:linear-gradient(180deg,#f7f5f1,#f3f1ec);border-right:1px solid var(--rule)}
.rail-brand-t{color:var(--ink);letter-spacing:.16em}
.rail-h{color:var(--ink-3)}
.rail-nav a{color:var(--ink-2);border-radius:10px;transition:background .15s,color .15s}
.rail-nav a:hover{background:rgba(15,20,32,.05);color:var(--ink)}
.rail-nav a.on{background:#fff;color:var(--ink);box-shadow:inset 3px 0 0 var(--accent),0 1px 2px rgba(15,20,32,.06)}
.rail-me{border-top:1px solid var(--rule)}

/* header */
.top{background:rgba(251,250,248,.86);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
  border-bottom:1px solid var(--rule)}

/* panels */
.card{background:var(--card);border:1px solid var(--rule);border-top:1px solid var(--rule);
  border-radius:16px;padding:22px 24px 24px;margin-top:18px;
  box-shadow:0 1px 2px rgba(15,20,32,.04),0 14px 36px -24px rgba(15,20,32,.22)}
.card:first-child{margin-top:0}
#needs{border-color:#ead4cb;position:relative;overflow:hidden;
  background:linear-gradient(180deg,#fff8f5,#fff)}
#needs:before{content:"";position:absolute;left:0;right:0;top:0;height:3px;
  background:linear-gradient(90deg,var(--accent),rgba(195,63,27,.15))}
#needs .card-head h3{color:var(--accent)}
#client-head{background:linear-gradient(180deg,#fdfcfa,#fff)}

/* figures */
.stats{gap:16px}
.stat{background:var(--card);border:1px solid var(--rule);border-radius:16px;padding:20px 22px;
  position:relative;overflow:hidden;box-shadow:0 1px 2px rgba(15,20,32,.04),0 14px 36px -24px rgba(15,20,32,.22)}
.stat:before{content:"";position:absolute;left:0;right:0;top:0;height:3px;
  background:linear-gradient(90deg,var(--accent),rgba(195,63,27,.12))}

/* text + links */
.ev{border-top:1px solid var(--rule)}
.ev:first-of-type{border-top:0}
.ev-t a,.meta a,.ch-cap a{color:var(--accent);text-decoration:underline;text-underline-offset:3px;
  text-decoration-color:rgba(195,63,27,.35)}
.ev-t a:hover,.meta a:hover{text-decoration-color:var(--accent)}
.empty{border:1px dashed var(--rule-2);border-radius:14px;padding:18px 20px;background:var(--panel-2)}
.msg{background:var(--panel-2);border:1px solid var(--rule);border-radius:12px;padding:14px 16px}

/* pills — soft fills so a status reads at a glance */
.pill{border-radius:999px;background:var(--panel-2)}
.pill.ok{background:#e9f6ef;border-color:#b9d6c6}
.pill.warn{background:#fbf3e2;border-color:#dccdae}
.pill.bad{background:#fdecea;border-color:#e2bdba}
.pill.info{background:#e8f1f8;border-color:#bacedd}

/* needs-you markers: the avatar ring, coloured by urgency */
.who.nu{font-weight:700;font-size:13px}
.who.nu.bad{color:var(--bad);border-color:var(--bad);background:#fdecea}
.who.nu.warn{color:var(--warn);border-color:var(--warn);background:#fbf3e2}
.who.nu.note{color:var(--ink-3);border-color:var(--rule-2)}

/* buttons */
.btn{border-radius:11px;transition:background .15s,border-color .15s,box-shadow .15s,transform .05s}
.btn:active{transform:translateY(1px)}
.btn.sec{background:#fff}
.btn.sec:hover{border-color:var(--accent);background:#fff8f5}
.btn.pri{box-shadow:0 10px 22px -12px rgba(195,63,27,.8)}
.newc>summary .btn{background:#fff8f5;border:1px solid #ead4cb;color:var(--accent)}
.newc>summary .btn:hover{background:#fdece6}
.auth input,.fld input,.ch-form textarea,.search input{border-radius:11px;background:#fff}
.auth button{border-radius:11px}

/* table */
.tbl th{color:var(--ink-3)}
.tbl tr.grp th{background:var(--panel-2)}
.tbl tr.grp .n{color:var(--accent)}
.tbl tbody tr:hover{background:#fbfaf8}
.rowlink b{border-bottom:1px solid transparent}
.rowlink:hover b{border-bottom-color:var(--accent)}

/* actions + output */
.act-row{border-top:1px solid var(--rule)}
.act-row:first-of-type{border-top:0}
pre.out{border-radius:12px;border:1px solid var(--rule);background:var(--panel-2)}
pre.done{border-color:#b9d6c6}
pre.err,.err{border-color:#e2bdba;background:#fdecea}

/* recent activity: one sentence, the list behind a button */
.actv>summary{list-style:none;cursor:pointer;margin:6px 0 4px}
.actv>summary::-webkit-details-marker{display:none}
.actv[open]>summary{margin-bottom:12px}
.actv .ev:first-of-type{border-top:1px solid var(--rule)}

/* chat */
.ch-tabs a{background:#fff;border-radius:999px}
.ch-tabs a:hover{border-color:var(--accent)}
.ch-tabs a.on{background:var(--ink);color:#fff}
.ch-log{background:var(--card);border:1px solid var(--rule);border-radius:16px;
  box-shadow:0 1px 2px rgba(15,20,32,.04),0 14px 36px -24px rgba(15,20,32,.22)}
.ch-msg{border-radius:14px}
.ch-msg.them{background:var(--panel-2)}
.ch-side{background:transparent;border:0}
.ch-frame{background:#fff;border:1px solid var(--rule);border-radius:16px;
  box-shadow:0 1px 2px rgba(15,20,32,.04),0 14px 36px -24px rgba(15,20,32,.22)}
.ch-cap{padding:10px 4px 0}

/* client review: the preview at a usable size, the approve button under it */
#review .ch-frame{display:block;width:100%;height:520px;margin:14px 0 16px;border:1px solid var(--rule);border-radius:14px;background:#fff}
.formg textarea{width:100%;min-height:96px;font:16px/1.5 var(--serif);padding:12px 13px;border:1px solid var(--rule);border-radius:11px;background:#fff;color:var(--ink)}
.formg textarea:focus{outline:2px solid var(--accent);outline-offset:1px}

/* the intake page: two columns of fields, but the heading, intro, button and error span both */
.start{max-width:42em;margin:8vh auto}
.start>h1,.start>.sub,.start>button,.start>.err{grid-column:1/-1}
.start>h1{font-size:30px}
.start>button{margin-top:10px}
