/* LAYER: page — HQ's own layout. Layered like everything else rather than left out: an
   unlayered sheet beats every layer regardless of specificity, which would make
   a one-class rule here silently outrank a three-class rule in brand.css. */
@layer page {

/* Drift HQ — the app's own layout. The BRAND is /shared/brand.css, loaded
 * before this file (see index.html); the spec is ../brand/BRAND.html, which HQ
 * serves at Company > Brand. The tokens, the reset, the base type, the buttons,
 * the cards and the sidebar all come from there. What is HQ's is below: where
 * the chrome sits, and then boards, modals, the presentation stage, the observe
 * frame.
 *
 * HQ DEVIATES FROM THE SYSTEM IN THREE PLACES, and all three are stated at the
 * rule that makes them, so "what does HQ do differently" is a read rather than a
 * grep: the body leading immediately below, .app-content's horizontal padding,
 * and .btn's white-space. Each one is a density decision that follows from HQ
 * being a working surface with a 284px column already spent.
 *
 * brand.css's --announce-h, --nav-h and --header-h are NOT overridden here, and
 * their absence is deliberate: HQ's chrome is a column, so it has no
 * announcement strip, no navbar and no band to clear. Nothing on this page reads
 * those tokens, and a zeroed override for a band that does not exist is a number
 * the next reader has to disprove. */

/* 1.5 against the system's 1.75. HQ is a dense operational UI — boards, tables,
 * modals — where the marketing site's airier leading costs a visible row of
 * content per screen. */
body { line-height: 1.5; }

.hidden { display: none !important; }
.muted { color: var(--muted); }

/* ---- App shell: an ink sidebar, and the content beside it ------------------
 * HQ's whole chrome is one column. The COMPONENT is brand.css's `.sidenav` —
 * the ink field, the wordmark, the group headings, the links and their states.
 * What is below is only what a host owes it: where the column sits, how the
 * content clears it, and the two pieces of furniture in its foot.
 *
 * That split is the navbar's rule too (BRAND.html, "the header"): a component
 * that inherits its position from whoever loads it is not a component, and one
 * that dictates its position cannot be used twice.
 *
 * Scoped by class, NOT left as a bare `body` rule: login.html shares this
 * stylesheet and its <body class="login-body"> is a CENTRED flex row, which a
 * bare rule here would silently flip to a column and then rely on specificity
 * to undo — a trap rather than a layout. */
.app-body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* FIXED and full height, scrolling independently of the page. The account
   cluster sits at the foot of this column, so a sidebar that scrolled away with
   a long backlog would take the way out of HQ with it. */
.app-side {
  position: fixed;
  inset: 0 auto 0 0;
  z-index: 998;
  /* The column's own right edge, one step up from ink. It is nearly silent
     against the grey desk, and it is load-bearing on ONE view: Company > Brand
     frames BRAND.html, which carries an ink sidebar of its own, and without a
     rule between them the two abut into a single 535px slab holding two
     wordmarks. Ink cannot separate ink. */
  border-right: 2px solid var(--ink-raised);
}

.app-shell {
  flex: 1 1 auto;
  display: flex;
  min-height: 0;
  /* Room for the sidebar. It is out of flow, so nothing reserves this space on
     its own and the boards would start underneath it. --sidenav-w is declared
     on :root by brand.css, which owns the column's width — reading it here is
     what stops the offset and the width being two numbers that can disagree. */
  margin-left: var(--sidenav-w);
}

/* The account cluster, on ink. --muted is a dark-on-light value and measures
   1.4:1 against this field — brand.css names an on-ink rank for exactly this.
   A flex row rather than inline text so the three parts keep one gap between
   them and the whole thing wraps inside a 284px column instead of running out
   of it. */
.hq-greeting {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 12px;
  font-weight: 300;
  color: var(--on-ink-muted);
}

/* The way out. HQ's own button at HQ's own density — what changes on a dark
   field is the EDGE: an ink border and an ink drop shadow are both invisible
   there, so both go white. That is the same inversion the wordmark's last two
   underlines make two rows above it. */
.sidenav-foot .btn {
  width: 100%;
  border-color: #fff;
}
.sidenav-foot .btn:hover { box-shadow: 0 3px 0 #fff; }
.sidenav-foot .btn:active { box-shadow: 0 0 0 #fff; }

.app-content {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  max-width: var(--max-width);
  margin: 0 auto;
  /* NOT the site's --padding, and the deviation is the sidebar. That token is
     sized for a page that owns the whole viewport; HQ's content starts 284px in,
     and a second gutter of up to 150px on top of the column costs the backlog's
     toolbar a row of buttons at laptop width. The column IS the left gutter —
     this is the air between it and the work, not the margin of a page.
     The top number is larger than the sides for a different reason: nothing
     sits above the first heading, so this is the only air it gets. */
  padding: 40px clamp(20px, 3vw, 64px);
}

/* Observe and Brand hand the entire canvas to a framed document: no padding, no
   max-width, no centring gutters. Set on <body> rather than on the section,
   because all of those live on .app-content, a parent.

   The class names the LAYOUT rather than any one page, so Observe, Brand and
   Docs share it — a second identical block keyed on a second class is how they
   would drift apart. app.js toggles it for all three. */
body.full-bleed .app-content {
  padding: 0;
  max-width: none;
}

/* A full-bleed view hands the canvas to a document that scrolls its own parts,
   so the shell has to be EXACTLY the viewport rather than at least it. Under
   min-height alone the body grows to its content — measured at 2586px against a
   1050px viewport — and every inner overflow:auto below it is decorative.

   The selector sits on <body> itself, not on a descendant: `.app-body` IS the
   body element, so `body.full-bleed .app-body` matches nothing at all. */
body.app-body.full-bleed { height: 100vh; min-height: 0; overflow: hidden; }

/* ---- Narrow screens ----
 * A 284px column is most of a phone, so the sidebar comes out of its pinning
 * and sits in normal flow at the top of the document. The SHAPE it takes there
 * — a wrapping row of links with the marker on the bottom edge — is brand.css's,
 * at this same breakpoint. Only the pinning is HQ's to undo, because only the
 * pinning is HQ's to state.
 *
 * .app-side is the first element in <body> and .app-body is a flex column, so
 * dropping the fixed positioning is the whole of the move: the content follows
 * the column instead of sitting beside it, and nothing is re-ordered. */
@media (max-width: 900px) {
  /* The right edge goes with the pinning. In flow it is a 2px line down the
     viewport's own edge, with nothing matching it on the left. */
  .app-side { position: static; border-right: none; }
  .app-shell { margin-left: 0; }
  /* The foot is a row again rather than a stack: at full width the greeting and
     the button fit side by side, and a full-width logout button across a phone
     is a target the size of a banner. */
  .sidenav-foot { justify-content: space-between; }
  .sidenav-foot .btn { width: auto; }
}

/* A toolbar with no title: the actions stay right-aligned rather than
   collapsing to the left where the heading used to be. */
.toolbar.toolbar-headless { justify-content: flex-end; }

/* ---- Toolbar ---- */
.toolbar {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 20px;
}
.toolbar-title h1 { font-size: 22px; font-weight: 600; }
.toolbar-title p { font-size: 13px; }
.toolbar-actions { display: flex; align-items: center; gap: 10px; }

/* The backlog's controls row: search on the left, the sort/closed/new cluster on
   the right. .toolbar aligns to flex-end for the title-and-actions shape, which
   is wrong here — every child is a control of the same height, so they centre. */
.backlog-toolbar { align-items: center; }
.backlog-search {
  display: flex;
  align-items: center;
  gap: 10px;
  /* Takes the room the actions do not, and min-width:0 lets it actually shrink
     inside the flex row instead of forcing the toolbar wider than the list. */
  flex: 1 1 auto;
  min-width: 0;
}
.search-input {
  font-family: inherit;
  font-size: 13px;
  font-weight: 300;
  color: var(--ink);
  background: #fff;
  border: 2px solid var(--ink);
  border-radius: 0;
  /* The same padding as .sort-select and .ms-trigger, so the row is one band
     of equal-height controls rather than three sizes. */
  padding: 0.55rem 0.6rem;
  width: 100%;
  /* Fills the room it was given. The 340px cap was drawn when the toolbar was a
     right-aligned cluster and the search box was a guest in it; now that search
     owns the left of the row, capping it just left a gap between the field and
     the space allocated to it. */
  max-width: none;
  /* Matches the row. Every other control here settles at 38px (the New task
     button sets that floor) and the input came out 36 — two pixels is not much
     until three controls sit in a line and one of them is visibly short. */
  min-height: 38px;
  box-sizing: border-box;
}
.search-input::placeholder { color: var(--muted); }
/* The same focus ring the modal fields use. */
.search-input:focus { outline: none; box-shadow: 0 0 0 2px var(--brand-2); }
/* Only present while a query is — a permanent "47 of 47" is noise. */
.search-count {
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
}
.sort-select {
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  border: 2px solid var(--ink);
  padding: 0.55rem 1.7rem 0.55rem 0.6rem;
  /* A native <select> draws itself with the platform's rounded corners — very
     visibly so on iOS, where it arrives as a pill. The brand has no rounded
     corners anywhere, so the native appearance is dropped outright and the
     caret redrawn: appearance:none takes the corners AND the arrow, so leaving
     the arrow out would leave a control that does not look like one. */
  -webkit-appearance: none;
  appearance: none;
  border-radius: 0;
  /* Same 38px floor as the search box and the button beside it. */
  min-height: 38px;
  box-sizing: border-box;
  background-color: #fff;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0h10L5 6z' fill='%231b2844'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.6rem center;
}

/* Observe — the Grafana embed, full bleed.
   The frame IS the page: no toolbar above it, no padding around it, and it
   owns its own scrolling so the page never grows a second scrollbar. Height is
   the viewport minus the shell's own chrome. */
/* The bar Grafana's kiosk mode makes necessary. Kiosk hides Grafana's own
   toolbar, so the two controls it owned — the time range and the refresh
   interval — have to exist somewhere or they are simply unreachable. WHICH
   board is not here: that is the sidebar's question, and its children answer it.
   Same border weight and flat fills as the rest of HQ. */
.observe-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 10px 12px;
  background: var(--bg);
  border-bottom: 2px solid var(--ink);
}

.observe-group { display: flex; gap: 6px; flex-wrap: wrap; }

/* Both controls answer one question — how the board is windowed — so they sit
   together as one cluster against the bar's right edge, opposite the sidebar
   that says which board it is. Only the FIRST takes the auto margin; a rule on
   both would push them to opposite ends. */
#observe-ranges { margin-left: auto; }
#observe-refresh { padding-left: 12px; border-left: 2px solid var(--ink); }

/* A non-selected button: same rectangle, inverted fill, so a row reads as one
   control rather than several competing primaries. */
.btn-ghost {
  background: #fff;
  color: var(--ink);
  border: 2px solid var(--ink);
}

/* The frame claims whatever height is left, rather than 100vh minus a number
   someone wrote down. The flex chain runs body → .app-shell → .app-content →
   the framed section, and EVERY link needs min-height:0 or the frame refuses to
   shrink below its content and pushes a scrollbar onto the page. */
/* A full-bleed view hands the canvas to a document that scrolls its own parts,
   so the shell has to be EXACTLY the viewport rather than at least it. With
   min-height alone the body grows to its content — measured at 2586px against a
   1050px viewport — and every inner overflow:auto below it is decorative.

   On <body> itself, not on a descendant: `.app-body` IS the body element. */
body.full-bleed .app-content {
  display: flex;
  flex-direction: column;
  /* Without this the flex children cannot shrink below their content, so a
     column taller than the viewport pushes the PAGE into scrolling instead of
     scrolling itself. Every ancestor in a scrolling chain needs it. */
  min-height: 0;
}
#view-observe,
#view-brand,
#view-economics,
#view-docs {
  flex: 1 1 auto;
  min-height: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.observe-frame,
.brand-frame,
.economics-frame {
  width: 100%;
  flex: 1 1 auto;
  min-height: 0;
  border: 0;
  display: block;
  background: #fff;
}

/* Floats over the frame rather than displacing it — otherwise showing an error
   would resize Grafana and reload every panel. */
.observe-status {
  position: absolute;
  z-index: 5;
  top: 12px;
  left: 12px;
  right: 12px;
  border: 2px solid var(--ink);
  border-left: 5px solid var(--brand-1);
  background: #fff;
  padding: 0.7rem 0.9rem;
  margin: 0 0 12px;
  font-size: 13px;
  font-weight: 600;
}
/* The probe reports three states, not two, and the third is the one that used
   to be invisible: "still asking". A panel that shows nothing while the request
   is in flight looks exactly like a panel that has decided everything is fine,
   so a slow Grafana and a healthy Grafana were indistinguishable for as long as
   the fetch took. Ink border while uncertain; the resolved states keep their
   own colours below. */
.observe-status.checking {
  border-left-color: var(--ink);
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 300;
}

/* ---- The spinner ----
 * Spec: brand/BRAND.html, "The spinner". A static square whose four edges light
 * in sequence, then snap to one colour on completion. No rotation — nothing
 * else in this brand rotates, and progress is carried by colour.
 *
 * There was a `.drift-spinner` in shared/styles/brand.css for a long time. It
 * could never have worked: it set a border and `display: inline-block` and no
 * width or height, so it rendered at 0x0. That it was never noticed is the
 * clearest evidence the stylesheet was dead, and it is why this is written
 * fresh here rather than moved.
 */
.drift-spinner {
  --unlit: rgba(27, 40, 68, 0.15);
  display: inline-block;
  flex: none;
  width: 14px;
  height: 14px;
  border: 3px solid var(--unlit);
  animation: drift-edge-fill 2.4s steps(1, end) infinite;
}
/* Every frame names all four edges. An animation replaces the base
   `border-color` outright rather than layering over it, so an unnamed edge is
   not "left faint" — `transparent` there would punch a hole in the square and
   leave the mark reading as a broken bracket instead of a box. */
@keyframes drift-edge-fill {
  0%   { border-color: var(--brand-1) var(--unlit)  var(--unlit)  var(--unlit); }
  25%  { border-color: var(--unlit)   var(--brand-2) var(--unlit) var(--unlit); }
  50%  { border-color: var(--unlit)   var(--unlit)  var(--brand-3) var(--unlit); }
  75%  { border-color: var(--unlit)   var(--unlit)  var(--unlit)  var(--ink); }
  100% { border-color: var(--brand-1) var(--unlit)  var(--unlit)  var(--unlit); }
}
@media (prefers-reduced-motion: reduce) {
  .drift-spinner { animation: none; border-color: var(--ink); }
  /* Wet Ink is entirely decorative — the content is identical with or without
     it, so it simply does not run. */
  .epic-body.arrives .todo-item,
  .epic-body.arrives .epic-description > *,
  .todo-detail.arrives { animation: none; }
  /* A finished card still comes back to full on hover — it just arrives there
     rather than fading. The state change is information, the fade is not. */
  .epic-task-group.done > .todo-item { transition: none; }
}
/* Completion snaps to a single colour and stops. Both are set by JS the moment
   the probe answers, so the square is never left mid-sequence. */
.drift-spinner.is-ok    { animation: none; border-color: var(--brand-3); }
.drift-spinner.is-error { animation: none; border-color: var(--error); }

/* ---- Buttons (toy affordance) ---- */
.btn {
  font-family: inherit;
  font-weight: 600;
  font-size: 14px;
  color: #fff;
  background: var(--brand-1);
  border: 2px solid var(--ink);
  border-radius: 0;
  padding: 0.6rem 1rem;
  cursor: pointer;
  /* A label is one thing to press, so it is one line. "+ New epic" breaking
     after "New" is not a narrow-screen adaptation, it is a two-line button. Let
     the toolbar ROW wrap instead — that is a layout decision, and this is not. */
  white-space: nowrap;
  transition: transform 0.1s ease, box-shadow 0.1s ease;
}
.btn:hover { transform: translateY(-3px); box-shadow: 0 3px 0 var(--ink); }
.btn:active { transform: translateY(0); box-shadow: 0 0 0 var(--ink); }
.btn-primary   { background: var(--brand-1); }
.btn-secondary { background: var(--brand-2); }
.btn-tertiary  { background: var(--brand-3); }
.btn-dark      { background: var(--ink); }
/* Leaving, not arriving — and the only filled red control in HQ. `danger-outline`
   below is the other half of the pair: red on white, for a delete that sits
   among ordinary buttons and must not read as the obvious thing to press. */
.btn-danger    { background: var(--error); }
.btn-mini {
  font-size: 12px;
  padding: 2px 7px;
  font-weight: 600;
}

/* ---- Board ---- */
.board {
  display: flex;
  gap: 14px;
  overflow-x: auto;
  padding-bottom: 12px;
  align-items: flex-start;
}
.col {
  flex: 0 0 240px;
  background: transparent;
}
.col-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 6px 2px;
  border-bottom: 2px solid var(--ink);
  margin-bottom: 10px;
}
.col-count {
  font-weight: 700;
  border: 2px solid var(--ink);
  background: var(--panel);
  padding: 0 7px;
  font-size: 11px;
}
.col-body { display: flex; flex-direction: column; gap: 10px; min-height: 24px; }

/* ── Everything else, on a phone ────────────────────────────────────────────
 *
 * The Cloud backlog was reworked first; this is the rest of HQ. The recurring
 * mistake is the same one each time: a layout that spends horizontal space it
 * does not have, because it was drawn on a screen that had it.
 */
@media (max-width: 640px) {
  /* THE KANBAN STOPS BEING A KANBAN.
     Six 240px stage columns in a 342px viewport means a sideways scroll inside
     the page, and what you actually saw was one empty column and half a card
     reading "Meridian C…". A board's value is comparing columns at a glance;
     at this width you cannot see two at once, so the column IS the cost with
     none of the benefit. Stacked, each stage is a full-width section with its
     cards under it — you scroll one axis instead of two, and the stage names
     become headings rather than a row you have to drag through. */
  .board {
    flex-direction: column;
    overflow-x: visible;
    gap: 20px;
  }
  .col { flex: 1 1 auto; width: 100%; }
  /* An empty stage still says it exists and that it holds nothing — that is a
     real fact about the pipeline — but it stops reserving a screen of blank. */
  .col-body { min-height: 0; }

  /* The toolbar: title over action, action full width. Shared by Studio,
     Company and every board, so this one rule straightens all of them. */
  .toolbar {
    flex-direction: column;
    align-items: stretch;
  }
  .toolbar-title h1 { font-size: 19px; }
  .toolbar .btn { width: 100%; min-height: 48px; }
  .toolbar-actions .btn { width: auto; }

  /* One card per row — 320px minimum in a 342px column was a single column
     already, but only by luck, and it clipped on a 360px phone. */
  .presentation-grid { grid-template-columns: 1fr; }

  /* Observe is a Grafana iframe. Dashboards are dense desktop artifacts and no
     amount of CSS makes one readable at 390px, so the honest treatment is to
     let it keep a usable width and scroll, rather than squeeze it into an
     illegible sliver and call that responsive. */
  /* Observe's control bar was 203px — 24% of the screen — before the dashboard
     began, because 15 buttons across 3 groups WRAPPED. They cannot simply be
     dropped: kiosk mode hides Grafana's own chrome, so anything the chrome owned
     is otherwise unreachable from here. So each group stops wrapping and scrolls
     on its own line instead — three ~44px rows rather than an unpredictable pile
     whose height depends on how many dashboards exist.

     No scrollbar: a control sliced by the edge already says "there is more this
     way", and says it better than a 3px bar does. */
  .observe-bar {
    flex-direction: column;
    align-items: stretch;
    flex-wrap: nowrap;
    gap: 8px;
    padding: 10px;
  }
  .observe-group {
    display: flex;
    flex-wrap: nowrap;
    gap: 6px;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  .observe-group::-webkit-scrollbar { display: none; }
  .observe-group > * { flex: 0 0 auto; min-height: 40px; }
  /* The frame takes what the bar gave back. A Grafana dashboard at 390px is
     never going to be comfortable, but the honest goal is "as much of it as
     possible", not "squeezed evenly with its own toolbar". */
  .observe-frame { min-height: 78vh; }
  /* Brand has no control bar to give height back, so it gets more of the
     viewport. BRAND.html is responsive in its own right — it is the document
     that defines the rules — so the frame only has to be tall enough not to
     nest one scrollbar inside another. */
  .brand-frame { min-height: 88vh; }

  /* TASK TITLES WRAP INSTEAD OF TRUNCATING.
     A title gets 268px of a 342px row, which at 13px is about 35 characters —
     and these titles are SENTENCES ("Router's shared 30s WriteTimeout silently
     caps its own 300s runtime budget…"). Every row ended in an ellipsis at
     roughly the point where it started being informative, so the list read as a
     column of prefixes and you had to expand a card to learn what it was.

     One line + ellipsis is correct on a wide screen, where the whole title fits
     and alignment is worth more than the tail. On a phone the trade inverts:
     horizontal space is the scarce axis and vertical is the cheap one. Two lines
     costs ~18px per row and buys the difference between identifying a task and
     guessing at it. Clamped at 2 rather than unbounded so a long title cannot
     turn one row into a paragraph and break the scannability of the list. */
  /* The `.todo-item` qualifier is load-bearing, not decoration. The base
     `.todo-title` rule that sets white-space:nowrap lives LATER in this file,
     and at equal specificity the later rule wins no matter what media query
     wraps this one — so the first version of this override silently did
     nothing. Raising specificity makes it independent of source order. */
  .todo-item .todo-title {
    white-space: normal;
    overflow: hidden;
    line-height: 1.3;
    /* The cap is max-height, NOT -webkit-line-clamp. The title is a flex item,
       so `display: -webkit-box` is blockified to flow-root and the clamp never
       engages — measured, not assumed. Two line-heights does the same job with
       no vendor prefix and no dependence on a display value the layout is
       allowed to overrule. */
    max-height: 2.6em;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
  }
  /* The row's controls stay put while the title grows downward — centring them
     against a two-line title floats the checkbox and badges into the middle of
     nothing. */
  .todo-row { align-items: flex-start; }
  .todo-row .todo-priority-badge,
  .todo-row .todo-state-badge { margin-top: 2px; }

  /* CONVERSATIONS: clamp each takeaway, do not hide the block.
     Measured at 390px, one conversation card was 880px — taller than the 844px
     viewport, so the list could not be scanned at all: you got one card per
     screen and had to scroll past a whole meeting to reach the next name.

     The takeaways are deliberately always-visible (see conversationCard: "the
     distilled 'so what' — a repository people should be able to scan without a
     click"), so hiding them would trade one failure for another. The problem is
     not that they are shown, it is that each one wraps to five lines here. Every
     takeaway therefore stays on screen, clamped to two lines: five of them cost
     ~220px instead of ~800px, and the card goes back to being scannable, which
     is what the always-visible decision was FOR. The full text is a tap away
     with the notes. */
  .conversation-takeaways li {
    display: block;
    max-height: 2.8em;
    overflow: hidden;
    line-height: 1.4;
  }
  .conversation-card { padding: 14px; }
  .conversation-chips { flex-wrap: wrap; }
}

/* ---- Engagement card ---- */
.eng {
  background: var(--panel);
  border: 2px solid var(--ink);
  border-radius: 0;
  padding: 12px 14px;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.eng:hover { transform: translateY(-4px); box-shadow: 0 4px 0 var(--ink); }
.eng-client { font-weight: 600; font-size: 14px; }
.eng-title { font-size: 13px; margin: 2px 0 8px; }
.eng-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
.badge {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: 2px solid var(--ink);
  color: #fff;
  padding: 1px 6px;
}
.badge-site      { background: var(--brand-3); }
.badge-app       { background: var(--brand-2); }
.badge-migration { background: var(--brand-1); }
.badge-reseller  { background: var(--ink); }
.eng-value { font-size: 12px; font-weight: 600; color: var(--ink); }
.eng-notes { font-size: 12px; color: var(--muted); margin-bottom: 10px; }
.eng-partner-stats { font-size: 12px; font-weight: 600; color: var(--brand-3); margin-bottom: 6px; }
.eng-foot { display: flex; align-items: center; gap: 6px; }
.eng-foot .spacer { flex: 1; }
.move-btn {
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  background: var(--panel);
  color: var(--ink);
  border: 2px solid var(--ink);
  width: 24px; height: 24px;
  cursor: pointer;
  line-height: 1;
}
.move-btn:hover { background: var(--bg); }
.move-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.link-btn {
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  background: none;
  border: none;
  color: var(--brand-2);
  cursor: pointer;
  padding: 2px 4px;
}
.link-btn.danger { color: var(--error); }
.link-btn:hover { text-decoration: underline; }

/* ---- Coming soon ---- */
.coming-soon {
  border: 2px dashed var(--muted);
  background: var(--panel);
  padding: 40px;
  text-align: center;
  color: var(--muted);
  font-size: 15px;
}

/* ---- Cloud tab: TODO board (replaces TODO.md) ---- */
/* Every TODO is its own detached card too now, same as components/
   sub-components/systems — a sibling in #todo-tree, not nested inside its
   parent's card, indented via the same .todo-card-nested-N margin classes
   (see renderTodoTree). The detail block (description/badges/actions) only
   takes space once a row is expanded via .todo-item.expanded. */
.todo-item { background: var(--panel); border: 2px solid var(--ink); }
.todo-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  cursor: pointer;
}
.todo-item input[type="checkbox"] {
  width: 16px;
  height: 16px;
  cursor: pointer;
  flex-shrink: 0;
}
/* A closed card's title is styled like any other. Being finished is carried by
   the section it sits in — the "Finished" label above it and the 0.5 its group
   applies — so a strikethrough said the same thing a third time, and its own
   0.5 multiplied with the group's into 0.25, which read as damaged rather than
   quiet. */

/* ---- The type IS the card ----
 * The row is filled with its type's own colour and the [B]/[F]/[E] letter is
 * gone. Text colour per type is not a preference: it is whichever of white or
 * ink actually passes against that fill, measured, at 13px/600 (needs 4.5:1).
 *
 *   bug          #d23838   white 4.81  ink 3.04   -> white
 *   feature      #10b981   white 2.54  ink 5.77   -> ink
 *   chore        #5f6b85   white 5.35  ink 2.74   -> white
 *   feedback     #f1a006   white 2.15  ink 6.82   -> ink
 *   enhancement  #8269eb   white 4.06  ink 3.61   -> white, AND IT FAILS AA
 *
 * Backbone purple clears neither. White is the better of the two and lands at
 * 4.06 against a 4.5 bar. It is not fixable in CSS: darkening it here would
 * invent a one-off tint, which the palette forbids, and the palette has no
 * darker Backbone. Recorded rather than quietly shipped — the fix is a brand
 * decision about a second purple, like the missing on-ink secondary already in
 * Known divergences.
 *
 * Only the ROW is filled. .todo-detail stays white: a description, a plan and a
 * link list on a saturated field is not readable, and the expanded card is the
 * one you are there to read. */
.todo-item.todo-is-bug         .todo-row { background: var(--error);   color: #fff; }
.todo-item.todo-is-chore       .todo-row { background: var(--muted);   color: #fff; }
.todo-item.todo-is-enhancement .todo-row { background: var(--brand-2); color: #fff; }
.todo-item.todo-is-feature     .todo-row { background: var(--brand-3); color: var(--ink); }
.todo-item.todo-is-feedback    .todo-row { background: var(--brand-1); color: var(--ink); }
.todo-item.todo-is-task        .todo-row { background: var(--panel);   color: var(--ink); }
/* The title inherits the row's decision rather than restating it per type. */
.todo-row .todo-title { color: inherit; }
/* The key button is an outline control sitting on a filled field. */
.todo-row .todo-key { color: inherit; }

/* Colour is not an accessible encoding on its own (WCAG 1.4.1), so the type's
   name is still in the DOM for assistive tech — and on hover via the row's
   title. This does NOT solve it for a sighted colour-blind reader; that is the
   open question about this treatment. */
.todo-title {
  font-size: 13px;
  font-weight: 600;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.todo-detail { display: none; padding: 0 14px 12px 34px; }
.todo-item.expanded .todo-detail { display: block; }
/* #SITESHQCLOUD1: type + priority badges live in the always-visible row now,
   just left of the boxed key — not hidden inside the expand-only detail. */
/* The id is a copy BUTTON — a clipboard, no key printed. It sits with the badges
 * rather than at the row's end: all three are per-task controls of the same
 * size, and a lone button across a variable-length title was a fourth column
 * that moved with every title.
 *
 * Same 18px box as the badges so the three line up, muted until hovered because
 * it must not compete with the title beside it. */
.todo-key {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  margin-right: 2px;
  color: var(--muted);
  background: none;
  border: 2px solid transparent;
  padding: 0;
  cursor: pointer;
  flex-shrink: 0;
}
.todo-key:hover {
  color: var(--ink);
  border-color: var(--ink);
  background: var(--panel);
}
.todo-key svg { display: block; }
/* The confirmation is a glyph in a box sized for an icon, so it needs a size of
   its own — the icon's 12px would leave the tick looking shrunken. */
.todo-key.todo-key-copied { font-size: 13px; font-weight: 700; color: var(--brand-3); }


/* The id is a click target now — say so, and use the same slam-flat affordance
   the rest of the UI uses so it reads as a control rather than as decoration. */
.todo-key-copy {
  cursor: pointer;
  transition: transform 0.06s, box-shadow 0.06s;
}

.todo-key-copy:hover {
  transform: translateY(-2px);
  box-shadow: 0 2px 0 var(--ink);
}

.todo-key-copy:active {
  transform: translateY(0);
  box-shadow: none;
}

.todo-key-copied {
  background: var(--ink);
  color: #fff;
}

/* Single-letter badges: a real square with the letter centred on both axes.
 *
 * text-align + padding was not enough. It centres horizontally but leaves the
 * height to the line box, so the glyph sat off-centre vertically and the box was
 * taller than it was wide — a column of near-squares, which reads as sloppier
 * than either a square or a rectangle. Fixed width AND height with flex centring
 * makes the box square and the letter's optical centre its centre, whatever the
 * letter is. */
.todo-type-badge,
.todo-priority-badge {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  line-height: 1;
  flex-shrink: 0;
}

.todo-type-badge {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border: 2px solid var(--ink);
  color: #fff;
  padding: 1px 6px;
  flex-shrink: 0;
  white-space: nowrap;
}
.todo-type-feature     { background: var(--brand-3); }
.todo-type-enhancement { background: var(--brand-2); }
/* Feedback takes Atomic orange — the one triad colour no live type was using.
   (.todo-type-research below is the class it was free from: no card on the board
   carries that type, so the colour was spoken for on paper only.) */
.todo-type-feedback    { background: var(--brand-1); }
.todo-type-research { background: var(--brand-1); }
.todo-type-bug         { background: var(--error); }
/* chore had NO colour, so its badge rendered as an empty white box on 9 cards —
   a badge that says nothing is worse than no badge, because the reader assumes
   it failed to load. Muted on purpose: a chore is the least urgent thing here. */
.todo-type-chore       { background: var(--muted); }

/* ── Filter multiselects ──────────────────────────────────────────────────
 * A trigger that looks like the selects beside it, and a panel of checkable
 * options under it. Both filters use this, so the two answer their questions
 * the same way.
 *
 * The trigger fills with ink when something is selected. A filter that is ON
 * while the board looks empty is the one state a reader must never have to
 * deduce, so the resting and active states cannot be a subtle difference. */
.multiselect { position: relative; }
.ms-trigger {
  position: relative;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  background: #fff;
  border: 2px solid var(--ink);
  border-radius: 0;
  /* Room on the right for the caret drawn by ::after. */
  padding: 0.55rem 1.7rem 0.55rem 0.6rem;
  /* The same 38px floor as .search-input and .sort-select, so the controls row
     is one band of equal-height controls rather than three sizes. */
  min-height: 38px;
  box-sizing: border-box;
  cursor: pointer;
  white-space: nowrap;
}
.ms-trigger.on { background: var(--ink); color: #fff; }
.ms-trigger::after {
  content: "\25BE";
  position: absolute;
  right: 0.6rem;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
}
.multiselect.open .ms-trigger::after { content: "\25B4"; }

.ms-panel {
  display: none;
  position: absolute;
  top: calc(100% + 4px);
  /* Right-aligned: these sit near the right edge of the toolbar, and a panel
     hanging off the left would leave the viewport on a narrow window. */
  right: 0;
  z-index: 40;
  min-width: 220px;
  /* Taller than this and the panel is a page; the registry is three levels and
     scrolls inside itself rather than pushing the board down. */
  max-height: 60vh;
  overflow-y: auto;
  background: var(--panel);
  border: 2px solid var(--ink);
  box-shadow: 4px 4px 0 var(--ink);
  padding: 4px 0;
}
.multiselect.open .ms-panel { display: block; }

.ms-opt {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  text-align: left;
  color: var(--ink);
  background: transparent;
  border: none;
  padding: 7px 12px;
  cursor: pointer;
}
.ms-opt:hover { background: var(--bg); }
.ms-opt.depth-1 { padding-left: 28px; font-weight: 400; }
.ms-opt.depth-2 { padding-left: 44px; font-weight: 400; font-size: 12px; color: var(--muted); }

/* A real box, not a tick alone: an empty square states "this can be selected
   and is not", which a missing tick does not. */
.ms-box {
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  border: 2px solid var(--ink);
  background: #fff;
  box-sizing: border-box;
}
.ms-opt.on .ms-box { background: var(--ink); }
.ms-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ms-count {
  flex: 0 0 auto;
  font-weight: 700;
  font-size: 10px;
  border: 2px solid var(--ink);
  background: #fff;
  padding: 0 6px;
}
.ms-clear {
  width: 100%;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  text-align: left;
  color: var(--ink);
  background: transparent;
  border: none;
  border-top: 2px solid var(--ink);
  margin-top: 4px;
  padding: 8px 12px;
  cursor: pointer;
}
.ms-clear:hover { background: var(--bg); }
.ms-empty { font-size: 12px; color: var(--muted); padding: 8px 12px; }

@media (max-width: 900px) {
  /* Thumb targets. */
  .ms-opt { min-height: 40px; }
  .ms-trigger { min-height: 44px; }
}

.todo-priority-badge {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border: 2px solid var(--ink);
  color: var(--ink);
  padding: 1px 6px;
  background: #fff;
  flex-shrink: 0;
  white-space: nowrap;
}
.todo-priority-urgent { background: var(--error); color: #fff; border-color: var(--error); }
.todo-priority-high   { background: var(--brand-1); color: #fff; border-color: var(--brand-1); }
.todo-priority-medium { background: #fff; color: var(--ink); }
.todo-priority-low    { background: #fff; color: var(--muted); border-color: var(--muted); }

/* LIFECYCLE STATE — where the work has got to.
   A word, not a letter: DEVELOPED and DEPLOYED both start with D, and so do
   both TESTED states, so the single-letter treatment type and priority use
   cannot work here. Fixed width so the badges form a column and the titles
   beside them stay aligned.
   Brand rules as everywhere else: sharp rectangles, flat fills, 2px borders,
   no gradients, no shadows. The progression is INTENSITY, so a glance down the
   column reads as distance travelled: outlined while it is only an intention,
   filled once it is real, and the brand triad marks the three real milestones
   — purple (built) -> orange (shipped) -> green (finished). */
.todo-state-badge {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 46px;
  height: 18px;
  padding: 1px 5px;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1;
  text-transform: uppercase;
  border: 2px solid var(--ink);
  background: #fff;
  color: var(--ink);
  flex-shrink: 0;
}
/* Intention — nothing exists yet. */
.todo-state-new          { background: #fff; color: var(--muted); border-color: var(--muted); }
.todo-state-planned      { background: #fff; color: var(--ink); border-color: var(--ink); }
/* Built. */
.todo-state-developed    { background: #ece7fb; color: var(--ink); border-color: var(--brand-2); }
.todo-state-tested-local { background: var(--brand-2); color: #fff; border-color: var(--brand-2); }
/* Shipped. */
.todo-state-deployed     { background: #fdeccd; color: var(--ink); border-color: var(--brand-1); }
.todo-state-tested-alpha { background: var(--brand-1); color: #fff; border-color: var(--brand-1); }
/* Finished. */
.todo-state-complete     { background: var(--brand-3); color: #fff; border-color: var(--brand-3); }
/* Verdicts, not stages. Deliberately outside the progression: they say the work
   stopped or went wrong, and must not read as "further along" than anything. */
.todo-state-incomplete   { background: #fff; color: var(--muted); border-color: var(--ink); }
.todo-state-buggy        { background: var(--error); color: #fff; border-color: var(--error); }
/* Description fields (SITES-HQ-CLOUD-0YM9C4). Brand rules: sharp rectangles,
   flat fills, no rounded corners, no shadows. The label is the affordance —
   a reader should be able to find "Done when" without reading anything else. */
.todo-meta-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 12px;
}

.todo-meta-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  border: 2px solid var(--ink);
  background: #fff;
  padding: 3px 7px;
  font-size: 11px;
  line-height: 1.4;
  max-width: 100%;
}

.todo-meta-label {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  white-space: nowrap;
}

.todo-meta-value { overflow-wrap: anywhere; }

.todo-lead { margin: 0 0 12px; }

.todo-field {
  border-left: 3px solid var(--ink);
  padding: 0 0 0 10px;
  margin: 0 0 12px;
}

.todo-field:last-child { margin-bottom: 0; }

.todo-field-label {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 0 0 4px;
}

.todo-field-value { font-size: 13px; line-height: 1.5; }
.todo-field-value > :last-child { margin-bottom: 0; }

.todo-text { font-size: 13px; line-height: 1.5; }
.todo-text p { margin: 0 0 8px; }
.todo-text > :last-child { margin-bottom: 0; }
.todo-text ul, .todo-text ol { margin: 4px 0 8px; padding-left: 20px; }
.todo-text li { margin: 3px 0; }
.todo-text h4 { font-size: 13px; font-weight: 700; margin: 12px 0 4px; color: var(--ink); }
.todo-text code { font-family: 'Courier New', monospace; font-size: 12px; background: rgba(130, 105, 235, 0.1); padding: 1px 4px; }
.todo-text strong { font-weight: 700; color: var(--ink); }
.todo-text a { color: var(--brand-2); }
.todo-card-unfiled .todo-unfiled-row { cursor: pointer; padding: 8px 12px; font-size: 13px; font-weight: 600; color: #a23a3a; background: rgba(162, 58, 58, 0.07); border-left: 3px solid #a23a3a; }
/* Implementation plan / research results — a distinct callout under the
   description (Claude-authored). Sharp corners + a purple accent, on-brand. */
.todo-plan { margin-top: 10px; }
.todo-plan-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--brand-2);
  margin-bottom: 4px;
}
.todo-plan-body {
  font-size: 13px;
  white-space: pre-wrap;
  background: #f6f4fd;
  border-left: 3px solid var(--brand-2);
  padding: 8px 10px;
}
.todo-links { margin-top: 6px; }
.todo-link-row { font-size: 12px; margin-bottom: 2px; }
.todo-link-label { font-weight: 600; color: var(--muted); }
.todo-link-key { font-family: 'Courier New', monospace; padding: 0 2px; }
.todo-actions { display: flex; gap: 2px; flex-shrink: 0; }
.todo-empty {
  border: 2px dashed var(--muted);
  color: var(--muted);
  text-align: center;
  padding: 30px;
  font-size: 13px;
}

/* ---- Demos / Stories (#SITESHQ20) — simple title+description+link cards ---- */
.simple-list { display: flex; flex-direction: column; gap: 8px; }
.simple-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  background: var(--panel);
  border: 2px solid var(--ink);
  padding: 10px 14px;
}
.simple-item-body { flex: 1 1 auto; min-width: 0; }
.simple-item-title { font-size: 14px; font-weight: 600; margin-bottom: 3px; }
.simple-item-description { font-size: 13px; white-space: pre-wrap; color: var(--muted); }

/* ---- Conversations (#SITESHQ22) ----------------------------------------
   A repository, not a scratchpad: each entry is its own record card with
   real topic separation (who/when/where -> takeaways -> full notes), not a
   collapsible log row. Takeaways are always visible (the distilled "so
   what"); notes are the raw material and collapse behind their own toggle,
   independent of the takeaways. Purple accents throughout — Studio's own
   color (--brand-2), same triad the sidebar section dot already uses. ---- */
.conversation-list { display: flex; flex-direction: column; gap: 20px; }

.conversation-card {
  background: var(--panel);
  border: 2px solid var(--ink);
  padding: 20px 24px;
}

.conversation-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.conversation-who { min-width: 0; }
.conversation-contact { font-size: 19px; font-weight: 700; color: var(--ink); line-height: 1.25; }
.conversation-company { font-size: 13px; color: var(--muted); margin-top: 2px; }
.conversation-chips { display: flex; flex-wrap: wrap; gap: 6px; flex-shrink: 0; }
.conversation-chip {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  border: 2px solid var(--ink);
  color: var(--ink);
  background: #fff;
  padding: 3px 8px;
  white-space: nowrap;
}

/* Section labels — same small-caps convention as .field-group-title, so a
   card's internal sections read with the same authority as a form's. */
.conversation-section-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 6px;
}

.conversation-takeaways-block {
  margin-top: 16px;
  padding: 12px 16px;
  background: var(--bg);
  border-left: 4px solid var(--brand-2);
}
.conversation-takeaways { list-style: none; margin: 0; padding: 0; font-size: 13px; }
.conversation-takeaways li {
  position: relative;
  padding-left: 16px;
  margin-bottom: 6px;
  line-height: 1.5;
}
.conversation-takeaways li:last-child { margin-bottom: 0; }
.conversation-takeaways li::before {
  content: "▸";
  position: absolute;
  left: 0;
  color: var(--brand-2);
  font-weight: 700;
}

.conversation-notes-block { margin-top: 14px; }
.conversation-notes-toggle {
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  background: none;
  border: none;
  color: var(--brand-2);
  cursor: pointer;
  padding: 0;
}
.conversation-notes-toggle:hover { text-decoration: underline; }
.conversation-notes-body { margin-top: 10px; padding-top: 12px; border-top: 2px solid var(--bg); }
.conversation-notes-text {
  font-size: 14px;
  line-height: 1.65;
  color: var(--ink);
  white-space: pre-wrap;
}

.conversation-footer {
  display: flex;
  justify-content: flex-end;
  gap: 4px;
  margin-top: 14px;
}

/* Takeaway input rows in the conversation editor modal */
.takeaway-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.takeaway-row { display: flex; align-items: center; gap: 6px; }
.takeaway-input { flex: 1 1 auto; }
/* ---- Modal ---- */
.modal-overlay {
  position: fixed; inset: 0;
  background: rgba(27, 40, 68, 0.35);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  z-index: 50;
}
.modal-panel {
  background: var(--panel);
  border: 2px solid var(--ink);
  padding: 22px;
  width: 100%; max-width: 460px;
  /* Scroll a tall modal inside itself instead of overflowing the viewport
     (fixes the New-TODO modal "hanging" off the bottom). */
  max-height: calc(100vh - 48px);
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 12px;
}
/* The TODO modal carries the most fields (component/type/title/description/plan
   + a two-column blocks matrix) — give it room so nothing is cramped. */
#todo-form { max-width: 680px; }
.modal-panel h2 { font-size: 18px; font-weight: 600; }
.modal-panel label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; font-weight: 600; }
.modal-panel .row { display: flex; gap: 12px; }
/* min-width:0 lets a flex column shrink below its content width, so a <select>
   with long option text fills its column instead of growing past the panel
   (the "hanging contents"). */
.modal-panel .row label { flex: 1; min-width: 0; }
.field-group-title {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  border-top: 2px solid var(--bg);
  padding-top: 10px;
  margin-top: 2px;
}
.small { font-size: 11px; }
.pitch-mix { display: flex; flex-direction: column; gap: 6px; }
.pitch-mix-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-size: 13px;
  font-weight: 300;
}
.pitch-mix-row input { width: 64px; flex-shrink: 0; }
.modal-panel input,
.modal-panel select,
.modal-panel textarea {
  font-family: inherit;
  font-size: 13px;
  font-weight: 300;
  color: var(--ink);
  background: var(--panel);
  border: 2px solid var(--ink);
  border-radius: 0;
  padding: 7px 9px;
  /* Fill the column, don't grow to content — a multi-select's long option text
     must not widen the control past the panel. */
  width: 100%;
  box-sizing: border-box;
}
.modal-panel input:focus,
.modal-panel select:focus,
.modal-panel textarea:focus { outline: none; box-shadow: 0 0 0 2px var(--brand-2); }
.modal-panel textarea { resize: vertical; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 4px; }

/* ── Link picker (blocks / blocked by) ─────────────────────────────────────
 * The native <select multiple> it replaces is still in the DOM as the record;
 * this is only the control. Sharp rectangles, ink borders, no rounded corners —
 * same vocabulary as every other field. */
.lp-native { display: none; }
.link-picker { display: flex; flex-direction: column; gap: 8px; }
.lp-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.lp-none { font-size: 12px; font-weight: 300; color: var(--muted); }
/* A chip is the whole hit target: tapping it removes the link, and the × says
   so. Two targets (chip + separate ×) at this size would both be too small. */
.lp-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  color: #fff;
  background: var(--ink);
  border: 2px solid var(--ink);
  border-radius: 0;
  padding: 6px 8px;
  min-height: 32px;
  cursor: pointer;
}
.lp-chip:hover { background: var(--danger, #d64545); border-color: var(--danger, #d64545); }
.lp-x { font-size: 14px; line-height: 1; opacity: 0.8; }
.lp-search {
  font-family: inherit;
  font-size: 13px;
  color: var(--ink);
  background: #fff;
  border: 2px solid var(--ink);
  border-radius: 0;
  padding: 0.55rem 0.6rem;
  width: 100%;
  box-sizing: border-box;
}
.lp-search:focus { outline: none; box-shadow: 0 0 0 2px var(--brand-2); }
.lp-results {
  display: flex;
  flex-direction: column;
  max-height: 220px;
  overflow-y: auto;
}
.lp-results:not(:empty) { border: 2px solid var(--ink); }
.lp-result {
  font-family: inherit;
  font-size: 12px;
  text-align: left;
  color: var(--ink);
  background: #fff;
  border: none;
  border-bottom: 2px solid var(--ink);
  padding: 10px;
  cursor: pointer;
}
.lp-result:last-child { border-bottom: none; }
.lp-result:hover { background: var(--ink); color: #fff; }
.lp-empty { font-size: 12px; font-weight: 300; color: var(--muted); padding: 10px; }

@media (max-width: 640px) {
  /* Thumb-sized rows, and a taller result list — the sheet has the room now. */
  .lp-search, .lp-result { min-height: 44px; }
  .lp-chip { min-height: 40px; font-size: 13px; }
  .lp-results { max-height: 40vh; }
}

/* ── Modals become SHEETS on a phone ────────────────────────────────────────
 *
 * Not a narrower dialog — a different object. A centered panel floating on a
 * dimmed page is a desktop idiom: it says "the page is still there behind me",
 * which is true on a 1500px screen and a lie on a 390px one, where the panel IS
 * the screen. Shrinking it to fit keeps 24px of decorative gutter, a shadow
 * nobody can see past, and a Save button that scrolls away below eleven fields.
 *
 * So on mobile it fills the viewport, the title bar stays put, the fields stack
 * one per row, and the actions dock to the bottom where a thumb already is.
 */
@media (max-width: 640px) {
  .modal-overlay {
    padding: 0;
    align-items: stretch;
    justify-content: stretch;
  }
  .modal-panel,
  #todo-form {
    max-width: none;
    width: 100%;
    max-height: 100vh;
    /* Full bleed: a border on a full-screen sheet draws a frame around the
       whole phone, which reads as a rendering bug rather than an edge. */
    border: none;
    padding: 0;
    gap: 0;
  }
  /* The title bar holds its place while the fields scroll under it, so you never
     lose track of which record you are editing halfway down a long form. */
  .modal-panel h2 {
    position: sticky;
    top: 0;
    z-index: 2;
    margin: 0;
    padding: 16px;
    background: var(--panel);
    border-bottom: 2px solid var(--ink);
    font-size: 17px;
  }
  /* One field per row. The three-up component/sub-component/system grid put
     three selects in 342px, which wrapped "Sub-component" onto two lines and
     left the three controls on different baselines. */
  .modal-panel .row {
    flex-direction: column;
    gap: 14px;
    padding: 0 16px;
  }
  .modal-panel > label,
  .modal-panel > .field-group-title { padding: 0 16px; }
  .modal-panel .row + .row,
  .modal-panel > label { margin-top: 14px; }
  .modal-panel label { font-size: 13px; }

  /* Native selects arrive as iOS pills. Same treatment the backlog's sort
     control already gets — appearance:none takes the corners AND the arrow, so
     the caret is redrawn. */
  .modal-panel select {
    -webkit-appearance: none;
    appearance: none;
    border-radius: 0;
    padding-right: 1.7rem;
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0h10L5 6z' fill='%231b2844'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.6rem center;
  }
  /* 44px thumb targets, and a description you can actually see while typing. */
  .modal-panel input,
  .modal-panel select { min-height: 44px; }
  .modal-panel textarea { min-height: 120px; }

  /* The blocks matrix was two native multi-selects side by side, ~170px each,
     rendering "#PLATFORM-CORE-W…" — every option truncated past the point of
     being identifiable, which makes the control decorative. Stacked and full
     width they are at least readable and scrollable. A native <select multiple>
     is still a poor touch control; replacing it with a searchable picker is
     tracked separately rather than bolted on here. */
  .modal-panel select[multiple] {
    -webkit-appearance: none;
    appearance: none;
    background-image: none;
    min-height: 132px;
    padding-right: 0.6rem;
  }

  /* Docked, not floating at the end of a scroll. Save is the reason the sheet is
     open; it should never be something you have to go looking for. */
  .modal-actions {
    position: sticky;
    bottom: 0;
    margin-top: auto;
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
    background: var(--panel);
    border-top: 2px solid var(--ink);
  }
  .modal-actions .btn { flex: 1 1 0; min-height: 48px; }
}

/* ---- Login page ---- */
.login-body {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.login-shell {
  width: 100%;
  max-width: 380px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
}
.login-logo {
  display: flex;
  align-items: center;
  gap: 10px;
}
.login-logo .title {
  font-family: 'Tinos', serif;
  font-weight: 700;
  font-style: italic;
  font-size: 2.5rem;
  line-height: 1;
}
.login-panel {
  width: 100%;
  background: var(--panel);
  border: 2px solid var(--ink);
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.login-panel h2 { font-size: 18px; font-weight: 600; }
.login-panel label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; font-weight: 600; }
.login-panel input {
  font-family: inherit;
  font-size: 13px;
  font-weight: 300;
  color: var(--ink);
  background: var(--panel);
  border: 2px solid var(--ink);
  border-radius: 0;
  padding: 8px 9px;
  width: 100%;
}
.login-panel input:focus { outline: none; box-shadow: 0 0 0 2px var(--brand-2); }
.login-panel .btn { width: 100%; text-align: center; }
.login-error { font-size: 12px; color: var(--error); min-height: 1em; }

/* ---- Generic modal helpers ---- */
.modal-actions .spacer { flex: 1; }
.btn.danger-outline {
  background: var(--panel);
  color: var(--error);
  border-color: var(--error);
}
.btn.danger-outline:hover { background: var(--error); color: #fff; box-shadow: 0 3px 0 var(--ink); }

/* ---- Cloud tab: unified Component/Sub-component/System/TODO tree ----
 * A page-local box, NOT the pinned app-sidebar treatment — this is
 * content-area sub-navigation, not primary app nav. Azure DevOps-style:
 * structural nodes + the todos themselves live in one expand/collapse tree
 * (see renderTodoTree), one card per top-level component rather than a
 * single continuous list. */
.todo-tree { display: flex; flex-direction: column; gap: 10px; }
.todo-card {
  background: var(--panel);
  border: 2px solid var(--ink);
}
/* Every expandable level is its own card, and every card is a SIBLING
   inside .todo-tree — a sub-component's/system's card is never placed
   inside its parent's card element. margin-left (not padding-left on the
   row, not DOM nesting) is what conveys the hierarchy; vertical spacing
   between every card still comes from .todo-tree's own flex gap. */
.todo-card-nested-1 { margin-left: 24px; }
.todo-card-nested-2 { margin-left: 48px; }
.todo-card-nested-3 { margin-left: 72px; }
.icon-btn {
  font-family: inherit;
  font-size: 11px;
  line-height: 1;
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  padding: 3px 5px;
  flex-shrink: 0;
}
.icon-btn:hover { color: var(--brand-2); }
.add-component-btn {
  display: block;
  width: calc(100% - 24px);
  margin: 10px 12px 4px;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  background: none;
  border: 2px dashed var(--muted);
  color: var(--muted);
  cursor: pointer;
  padding: 6px 10px;
}
.add-component-btn:hover { border-color: var(--brand-2); color: var(--brand-2); }
.add-subcomponent-btn:hover { border-color: var(--brand-2); color: var(--brand-2); }

/* ---- Toast notifications (#SITESHQ12) ---- */
/* Reusable brand-consistent toast: fixed bottom-right stack, same visual
   language as the rest of HQ (ink border, no radius, drop-shadow-on-hover
   affordance via the .btn family's flat aesthetic). showToast() in app.js
   is the only entry point — any future save/action can call it. */
.toast-container {
  position: fixed;
  bottom: 20px;
  right: 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 500;
  pointer-events: none;
}
.toast {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 220px;
  max-width: 360px;
  background: #fff;
  border: 2px solid var(--ink);
  border-left: 4px solid var(--ink);
  box-shadow: 0 3px 0 var(--ink);
  padding: 10px 14px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast-success { border-left-color: var(--brand-3); }
.toast-error   { border-left-color: var(--error); }

/* ---- Company > Presentations (#SITESHQ21) — Marp-based decks ---- */
.presentation-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 18px;
}
.presentation-card {
  background: var(--panel);
  border: 2px solid var(--ink);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.presentation-card:hover { transform: translateY(-4px); box-shadow: 0 4px 0 var(--ink); }
.presentation-card-thumb {
  width: 100%;
  aspect-ratio: 1280 / 720;
  overflow: hidden;
  position: relative;
  background: var(--bg);
  border: 2px solid var(--ink);
}
.presentation-card-thumb .marpit { transform-origin: top left; }
.presentation-card-thumb-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  font-size: 12px;
}
.presentation-card-title {
  font-size: 15px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.presentation-card-meta { font-size: 12px; color: var(--muted); }
.presentation-card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  margin-top: auto;
  padding-top: 6px;
  border-top: 2px solid var(--bg);
}

/* Editor — title + a tall monospace textarea, not a modal (markdown wants
   the full pane width, unlike the small title/link/description forms). */
.pres-title-input {
  font-family: inherit;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  background: var(--panel);
  border: 2px solid var(--ink);
  padding: 9px 12px;
  margin-bottom: 10px;
  width: 100%;
}
.pres-title-input:focus { outline: none; box-shadow: 0 0 0 2px var(--brand-2); }
.pres-markdown-editor {
  font-family: 'Courier New', monospace;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--panel);
  border: 2px solid var(--ink);
  padding: 14px;
  width: 100%;
  min-height: 60vh;
  resize: vertical;
}
.pres-markdown-editor:focus { outline: none; box-shadow: 0 0 0 2px var(--brand-2); }

/* Visual slide editor (#SITESHQ21) — layout-picker + per-slide field forms,
   built on top of the same Markdown storage; "Raw Markdown" above is the
   plain-textarea fallback. */
.pres-editor-tabs { display: flex; gap: 2px; margin-bottom: 12px; }
.pres-tab-btn {
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  background: var(--panel);
  color: var(--muted);
  border: 2px solid var(--ink);
  cursor: pointer;
  padding: 7px 14px;
}
.pres-tab-btn.active { background: var(--ink); color: #fff; }

.pres-slide-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.pres-slide-card { background: var(--panel); border: 2px solid var(--ink); }
.pres-slide-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  cursor: pointer;
}
.pres-slide-thumb {
  flex: 0 0 auto;
  width: 220px;
  height: 124px;
  overflow: hidden;
  position: relative;
  background: var(--bg);
  border: 2px solid var(--ink);
}
.pres-slide-thumb .marpit { transform-origin: top left; }
.pres-slide-meta { flex: 1 1 auto; font-size: 13px; font-weight: 600; }
.pres-slide-actions { display: flex; gap: 2px; flex-shrink: 0; }

/* Expanded slide: fields on the left, a much larger live preview on the
   right — side by side so you can see the slide while you're editing it,
   not just in the small row thumbnail above. Wraps on narrow screens. */
.pres-slide-editor {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  border-top: 2px solid var(--bg);
  padding: 14px 16px 16px;
}
.pres-slide-editor-form {
  flex: 1 1 320px;
  min-width: 280px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.pres-slide-editor-preview {
  flex: 0 0 auto;
  position: sticky;
  top: 10px;
  align-self: flex-start;
}
.pres-slide-preview-box {
  width: 720px;
  max-width: 100%;
  aspect-ratio: 1280 / 720;
  overflow: hidden;
  position: relative;
  background: var(--bg);
  border: 2px solid var(--ink);
  box-shadow: 0 3px 0 var(--ink);
}
.pres-slide-preview-box .marpit { transform-origin: top left; }

.pres-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; font-weight: 600; }
.pres-field input[type="text"],
.pres-field textarea,
.pres-field select {
  font-family: inherit;
  font-size: 13px;
  font-weight: 300;
  color: var(--ink);
  background: var(--panel);
  border: 2px solid var(--ink);
  padding: 7px 9px;
}
.pres-field textarea { resize: vertical; font-family: 'Courier New', monospace; }
.pres-field input:focus, .pres-field textarea:focus, .pres-field select:focus {
  outline: none; box-shadow: 0 0 0 2px var(--brand-2);
}

.pres-list-field, .pres-rows-field { display: flex; flex-direction: column; gap: 6px; }
.pres-list-row, .pres-rows-row { display: flex; gap: 6px; }
.pres-list-row input, .pres-rows-row input { flex: 1; }
.pres-list-remove {
  font-family: inherit;
  background: none;
  border: 2px solid var(--ink);
  color: var(--error);
  cursor: pointer;
  padding: 0 8px;
  flex-shrink: 0;
}

/* Layout picker modal — a grid of rendered previews, not a text list. Each
   item's title attribute carries the layout's description (a tooltip —
   the thumbnail is what you're actually choosing by). */
.layout-picker-panel { max-width: 1020px; max-height: 80vh; overflow-y: auto; }
.layout-picker-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 14px;
}
.layout-picker-item {
  font-family: inherit;
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--panel);
  border: 2px solid var(--ink);
  cursor: pointer;
  padding: 8px;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.layout-picker-item:hover { transform: translateY(-3px); box-shadow: 0 3px 0 var(--ink); }
.layout-picker-thumb {
  width: 100%;
  aspect-ratio: 1280 / 720;
  overflow: hidden;
  position: relative;
  background: var(--bg);
  border: 2px solid var(--ink);
}
.layout-picker-thumb .marpit { transform-origin: top left; }
.layout-picker-label { font-size: 12px; font-weight: 600; text-align: center; }

/* Preview ("Open") — every slide stacked and scaled down, read-only. Marpit
   renders each deck as a fixed 1280x720 <section>; scaling the whole
   .marpit wrapper down (transform-origin top left) keeps its real layout
   instead of re-flowing it into something markdown-it never designed. */
.presentation-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  padding: 10px 0 40px;
}
.presentation-preview .marpit {
  transform-origin: top center;
  box-shadow: 0 3px 0 var(--ink);
  border: 2px solid var(--ink);
}

/* Fullscreen presenter mode — one slide at a time, dark surround (the one
   deliberate frame-break from "no dark mode", same exception the brand
   template itself makes for code blocks). */
.present-overlay {
  position: fixed;
  inset: 0;
  background: #0b1220;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.present-stage {
  flex: 1 1 auto;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.present-stage .marpit { box-shadow: 0 6px 0 rgba(0, 0, 0, 0.4); }
.present-controls {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px;
}
.present-nav-btn, .present-exit-btn {
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  background: var(--panel);
  color: var(--ink);
  border: 2px solid var(--ink);
  cursor: pointer;
  padding: 6px 14px;
}
.present-nav-btn:hover, .present-exit-btn:hover { background: var(--bg); }
.present-counter { font-size: 13px; font-weight: 600; color: #fff; min-width: 60px; text-align: center; }
.toast-info    { border-left-color: var(--brand-2); }


/* ---- Backlog: the list column ----
 * The controls row and the tree, in one stack. The gap is the
   tree's own row gap rather than a larger separation, which is what makes the
   controls read as the first row of the list instead of a band sitting above
   it. .toolbar's own margin-bottom is dropped for the same reason — two
   spacings for one gap is how a row stops looking like a row. */
.backlog-main {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}
.backlog-main > .toolbar { margin-bottom: 0; }
/* Says WHICH of the two reasons the list is empty. "No tasks" over a filter
   with closed ones hidden is a half-truth, and the fix is one toggle away. */
.todo-empty {
  font-size: 14px;
  color: var(--muted);
  padding: 32px 4px;
}

/* ===========================================================================
   THE ARRIVAL — revealed content rises 3px into place, staggered down its own
   column.

   Deliberately colourless. A triad-tinted version was built and rejected: the
   colour had to be positional to avoid claiming a meaning, and a positional
   colour on a board where colour already carries type, priority and state is
   one signal too many. The movement alone says the thing appeared.

   WHEN is decided by an element's position in its own column, set from JS —
   the cards sit inside a labelled group, so a CSS :nth-child would count the
   label. The stagger caps at the seventh row so a long column still lands
   inside the budget.
   ========================================================================= */
@keyframes drift-arrive {
  from { opacity: 0; transform: translateY(-3px); }
  to   { opacity: 1; transform: translateY(0); }
}

.epic-body.arrives .todo-item,
.epic-body.arrives .epic-description > *,
.todo-detail.arrives {
  animation-name: drift-arrive;
  animation-duration: 200ms;
  animation-timing-function: cubic-bezier(.2, .7, .3, 1);
  animation-fill-mode: backwards;
  animation-delay: calc(var(--i, 0) * 28ms);
}

/* ---- Structure modal: the registry, three levels deep ----
 * Depth by indent AND weight: indentation alone stops being legible once the
 * names are long enough to wrap. */
.structure-tree {
  max-height: 55vh;
  overflow-y: auto;
  border: 2px solid var(--ink);
  background: var(--panel);
  padding: 4px 0;
}
.structure-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 300;
  color: var(--ink);
  padding: 4px 12px;
}
.structure-row:hover { background: var(--bg); }
.structure-row.depth-0 { font-weight: 700; margin-top: 6px; }
.structure-row.depth-1 { padding-left: 28px; }
.structure-row.depth-2 { padding-left: 44px; font-size: 12px; color: var(--muted); }
.sr-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* An empty group is a real fact about the backlog, so the zero is shown rather
   than hidden — blanking it would make the group look unmeasured instead of
   clear. */
.sr-count {
  flex-shrink: 0;
  font-size: 10px;
  font-weight: 700;
  border: 2px solid var(--ink);
  background: #fff;
  padding: 0 5px;
}
/* Always visible, not hover-revealed: this modal exists to be edited from, so
   hiding its only controls until hover would hide the point of the panel. */
.sr-actions { display: flex; gap: 2px; flex-shrink: 0; }
.structure-empty { font-size: 13px; color: var(--muted); padding: 8px 12px; }

/* The controls row stops being a row. Below this width the search box was being
   crushed to 27px — flex:1 1 auto with min-width:0 shrinks to nothing while the
   sort/closed/new cluster refuses to give up any width — and both labels wrapped
   onto three lines. Search takes its own full-width row; the cluster spreads
   across a second one. */
@media (max-width: 640px) {
  .backlog-toolbar { flex-wrap: wrap; gap: 10px; }
  /* display:contents dissolves the wrapper so the sort control becomes a direct
     flex item beside the search box, without moving anything in the DOM. The
     alternative was hoisting the <select> out of .toolbar-actions for one
     breakpoint, which would have meant the markup describing a layout that only
     exists below 640px. */
  .toolbar-actions { display: contents; }
  /* Row one: search takes the room, sort takes what it needs. */
  .backlog-search { flex: 1 1 auto; }
  .search-input { max-width: none; }
  .sort-select { flex: 0 0 auto; }
  /* Row two: the primary action, full width — the one thing you actually tap. */
  .toolbar-actions .btn { flex: 1 0 100%; }
  /* The two secondary actions share one row instead of taking a full-width band
     each. Three stacked buttons push the first task off a phone screen, which is
     the same mistake the tree made. */
  #structure-btn, #new-epic-btn { flex: 1 1 40%; }
  /* Neither label may wrap; each is a label, not a paragraph. Getting this wrong
     is how "+ New task" once ran 3px past a 390px viewport and scrolled the
     whole page sideways. */
  .sort-select,
  .toolbar-actions .btn { white-space: nowrap; min-height: 44px; }
  /* The panel is right-aligned to its trigger, which sits mid-row here rather
     than at the right edge — anchoring to the viewport's right keeps it on
     screen at any position. */
  .ms-panel { right: auto; left: 0; min-width: 200px; }
}


/* ---- Epic headings on the board ---- */

/* ---- THE LATCH ----
 * BRAND.html's "inverse: raised at rest" — the hero CTA, the configurator
 * steppers — used for a STATE rather than a moment. Those spring back the
 * instant you let go; this one stays down for as long as the epic is open, so
 * open is a POSITION and not only a colour. An open row sits physically lower
 * than its neighbours, which is the one thing a flat control can never do. */
.epic-latch {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  text-align: left;
  font-family: inherit;
  font-size: 1rem;
  color: var(--ink);
  background: var(--panel);
  border: 2px solid var(--ink);
  border-radius: 0;
  padding: 9px 12px;
  cursor: pointer;
  user-select: none;
  /* Already up, waiting to be pushed. */
  transform: translateY(-3px);
  box-shadow: 0 3px 0 var(--ink);
  /* Springy on release per the raised-at-rest rule, but 160ms rather than the
     marketing site's 320ms: this is a row in a list of forty, and the full
     overshoot reads as sluggish at that density. */
  transition: transform .16s cubic-bezier(.34, 1.56, .64, 1),
              box-shadow .16s cubic-bezier(.34, 1.56, .64, 1),
              background-color .1s ease, color .1s ease;
}
/* NO HOVER RAISE. Raised-at-rest has exactly two positions — up, and pressed —
   and the button starts in the first. Lifting it further borrows the gesture
   that belongs to flat-at-rest controls, and it costs the control its premise:
   if height means open, height must not also mean "the pointer is here". The
   brand's own raised-at-rest specimen declares no :hover for this reason. */
.epic-latch:active { transform: translateY(0); box-shadow: 0 0 0 var(--ink); }
/* Latched. Declared AFTER :active deliberately — same specificity, so source
   order is what keeps an open row held down. */
.epic-latch[aria-expanded="true"] {
  transform: translateY(0);
  box-shadow: 0 0 0 var(--ink);
  background: var(--ink);
  color: #fff;
}
.epic-latch[aria-expanded="true"] .epic-count { color: var(--on-ink, #c8d2e6); }
.epic-latch:focus-visible { outline: 2px solid var(--brand-2); outline-offset: 2px; }
/* The plate is the separation; the tree's own 10px gap is the rest. */
.epic-latch + .epic-body { margin-top: 6px; }

.epic-title {
  font-weight: 600;
  letter-spacing: 0.01em;
}

.epic-count {
  font-size: 0.8em;
  opacity: 0.6;
  font-variant-numeric: tabular-nums;
  margin-left: auto;
}

/* ---- Finished epics: everything under them is closed ----
 *
 * They are most of the board and none of them is work, so they sit at the
 * bottom behind a divider and read quieter than the live ones. Dimmed rather
 * than hidden: "what has this project already settled" is a real question, and
 * the answer is the reason many of the open cards are shaped the way they are.
 *
 * An epic with NO cards is not finished and does not come here — see
 * epicIsFinished. */
.epic-finished-divider {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin: 2.5rem 0 0.5rem;
  padding-top: 0.75rem;
  border-top: 2px solid var(--ink);
}
.efd-label {
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
}
.efd-count {
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Dimmed as a whole, so the heading and its brief recede together. Opacity
   rather than a colour per element: these carry type badges, priority badges
   and code spans whose colours are set in a dozen rules, and dimming the
   container is the only thing that reaches all of them at once. */
.epic-latch.finished,
.epic-body.finished {
  opacity: 0.55;
}
/* Back to full strength on hover and on keyboard focus: dimmed must not mean
   unreadable for someone who has come here deliberately. Opacity only — the
   latch must not move on hover. */
.epic-latch.finished:hover,
.epic-latch.finished:focus-visible,
.epic-body.finished:hover {
  opacity: 1;
}

/* Brief on the left, cards on the right: reading a description and scanning a
   task list are different jobs, and stacking them pushes the second below the
   fold of the first. */
.epic-body {
  display: flex;
  gap: 1.5rem;
  align-items: flex-start;
  margin-bottom: 0.75rem;
}

.epic-body .epic-description {
  flex: 0 0 45%;
  min-width: 0;
  font-size: 0.9em;
  opacity: 0.8;
  line-height: 1.5;
  /* Scrolls within itself so a long brief cannot outrun its task list. */
  max-height: 24em;
  overflow-y: auto;
  padding-right: 0.5rem;
  /* Full column height. NOT `height: 100%` — .epic-body is a flex row with
     align-items: flex-start and no definite height of its own, so a percentage
     has nothing to resolve against and computes to the content height. Measured
     before this line: a one-line brief stayed 22px beside a 278px task column.
     align-self is what overrides flex-start for this one item. Still capped by
     the max-height above, so a long brief scrolls rather than stretching the
     row. */
  align-self: stretch;
}

.epic-body .epic-tasks {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* Flush, each card's own 2px border meets its neighbour's and the pair reads as
   one 4px rule — a run of cards looks like a table rather than a stack of
   separate pieces of paper. */
.epic-task-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* A section change is a bigger break than a row change. */
.epic-task-group + .epic-task-group { margin-top: 1rem; }

/* Finished work is the record, not the work: label, cards, badges and keys all
   dim, because the cards carry a dozen colours set in as many rules and dimming
   them one at a time would mean chasing all of them.

   Dim the CHILDREN, not the group. Opacity on a parent cannot be undone by a
   child — a card inside a group at 0.3 renders at 0.3 however much opacity it
   declares for itself — so a per-card hover is only possible if each child
   carries its own value. The rendered result is identical; the difference is
   that this one can be reversed for a single row. */
.epic-task-group.done > * { opacity: 0.5; }
.epic-task-group.done > .todo-item { transition: opacity .1s ease; }

/* Reading one is deliberate, so restore it fully. The record stays quiet until
   you point at a specific line of it, and an expanded card is being read by
   definition — it must not stay dim while its detail is open. */
.epic-task-group.done > .todo-item:hover,
.epic-task-group.done > .todo-item:focus-within,
.epic-task-group.done > .todo-item.expanded { opacity: 1; }

/* Do NOT compound. A finished EPIC's body is already dimmed, and every card in
   it is closed by definition — 0.55 x 0.5 lands at 0.275, dimmer than the
   quietest thing on the board is meant to be. */
.epic-body.finished .epic-task-group.done > * { opacity: 1; }

/* A section label, not a heading — 11px, upper, 0.08em, 600, muted, per the
   brand's structural-signpost rule. */
.epic-tasks-label {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin: 0;
}
.etl-count { font-variant-numeric: tabular-nums; }

/* Nothing to read, so the cards take the width rather than leaving a gap. */
.epic-body.no-description .epic-tasks { flex-basis: 100%; }

/* One column when there is not room for two — the description first, since it
   is the context for everything under it. */
@media (max-width: 900px) {
  .epic-body { display: block; }
  .epic-body .epic-description {
    flex: none;
    max-height: 12em;
    margin-bottom: 0.75rem;
    padding-right: 0;
  }
}


/* Pipe tables from descriptions. Scrolls inside its column so a wide table
   cannot push the board sideways. */
.md-table {
  border-collapse: collapse;
  margin: 0.5rem 0;
  font-size: 0.95em;
  display: block;
  max-width: 100%;
  overflow-x: auto;
}
.md-table th,
.md-table td {
  border: 1px solid currentColor;
  border-color: color-mix(in srgb, currentColor 20%, transparent);
  padding: 0.25rem 0.5rem;
  text-align: left;
  vertical-align: top;
}
.md-table th { font-weight: 600; }

} /* @layer page */

/* ── Docs editor ─────────────────────────────────────────────────────────────
 *
 * TWO SIDEBARS, which is the shape Company > Brand already has: the view is
 * full-bleed, so the shell's padding comes off and the documentation's own ink
 * column sits flush against HQ's. The difference from Brand is that this one is
 * not framed — the tree is markup fetched from the website and dropped in, so it
 * takes sidenav.css, which HQ already carries for its own column, and the two
 * columns are the same component at the same width by construction.
 *
 * Everything else lives to its right and keeps the padding the shell would have
 * given it, so only the column is flush.
 *
 * THE EDITING SURFACE IS A FRAME, and it loads the docs' OWN stylesheets over
 * /api/docs/asset. So nothing here dresses the article: it is dressed by the
 * same brand.css, global.css and docs.css the page is, in the same order. HQ
 * carrying its own approximation of them was a second stylesheet claiming to be
 * the docs', and it would have drifted from the first change made to either. */
.docs-shell {
  display: flex;
  align-items: stretch;
  height: 100%;
  min-height: 0;
}

/* ---- The second sidebar ---- */
/* The second column is shared/components/subnav/ — its ground, its width, its
   scrolling and its hidden masthead are all the component's. HQ says nothing
   about it here, which is the point of it having become one. */

/* ---- Everything to its right ----
 *
 * The document, and nothing else with a shape. No padding, no panes and no
 * gaps: the frame is the whole area, and the two pieces of chrome are taken out
 * of flow and laid on top of it. This is the positioning context they use. */
.docs-work {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  position: relative;
}
.docs-drafts > summary {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
  margin: 0 0 6px;
}

/* ---- The tools ----
 *
 * The shelf is shared/components/topbar/, the same one the documentation wears
 * across the top of its pages. HQ says only what sits to its left — its own
 * column and then the documentation tree, two sidenavs — and the component
 * supplies the ground, the height and the triad.
 *
 * The article's top padding is measured off it, not restated here; see
 * syncCanvasInset(). */
.docs-tools {
  --topbar-left: calc(2 * var(--sidenav-w));
  flex-wrap: wrap;
  gap: 4px;
}
/* THE BRAND'S PUSHBUTTON, at toolbar size. brand.css's .btn-raised sits up at
   rest on a 6px shadow and slams flat when pressed; these are a third the size,
   so the lift is 2px and the shadow matches it. Same gesture, same springy
   release, one object.

   `.on` is that same flat state made to STAY — the button is not being pressed,
   it is already down, because what it does is what the caret is sitting in.
   Which buttons can be down together is not decided here: the class is set from
   the document, so H2/H3/H4 exclude each other because a block is one of them,
   and bold and italic do not because a word can be both. */
.docs-tools button {
  font-family: inherit;
  font-size: 12px;
  line-height: 1;
  padding: 6px 8px;
  min-width: 30px;
  background: var(--panel);
  color: var(--ink);
  border: 2px solid var(--ink);
  cursor: pointer;
  transform: translateY(-2px);
  box-shadow: 0 2px 0 var(--ink);
  transition: transform .1s cubic-bezier(.34, 1.56, .64, 1),
              box-shadow .1s cubic-bezier(.34, 1.56, .64, 1),
              background .15s ease, color .15s ease;
}
.docs-tools button:hover { background: var(--bg); }
/* Down: under the finger, or because that is what the selection already is. */
.docs-tools button:active,
.docs-tools button.on {
  transform: translateY(0);
  box-shadow: 0 0 0 var(--ink);
}
.docs-tools button.on { background: var(--ink); color: #fff; }
.docs-tools button.on code { color: inherit; }
.docs-tools code { font-family: var(--mono); font-size: 11px; background: none; }
.docs-tools-sep { width: 1px; align-self: stretch; background: var(--ink-subtle); margin: 0 3px; }
.docs-tool-save { font-weight: 600; border-color: var(--ink) !important; }
/* A save says when, or nothing. A refusal says what — the server's own sentence,
   because "invalid" tells an author nothing they can act on. */
.docs-status { font-size: 12px; color: var(--muted); margin-left: auto; padding-right: 4px; }
.docs-status.ok { color: var(--brand-3); }
.docs-status.bad { color: var(--error, #d64545); font-weight: 600; }

/* ---- Where the page is filed ----
 * Stacked, because these live in the corner drawer, which is narrow. */
.docs-meta { display: grid; gap: 8px; margin-bottom: 10px; }
.docs-meta label { display: block; font-size: 11px; color: var(--muted); }
.docs-meta input {
  width: 100%;
  margin-top: 3px;
  font-family: var(--mono);
  font-size: 13px;
  padding: 6px 8px;
  border: 2px solid var(--ink);
  background: var(--panel);
  color: var(--ink);
}
/* THE CANVAS IS THE DOCUMENT, and it is the whole work area. It carries
   .docs-content, the class the docs' own stylesheet targets, so the article is
   dressed here the way it is dressed on the page.

   No border and no background of its own: a box around it would say the page is
   a preview of something else, and the article's own ground is loaded inside
   the frame with the rest of the docs' stylesheets. */
.docs-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: none;
  display: block;
}

/* ---- The corner drawer ----
 *
 * Everything ABOUT the page rather than in it. Shut, it is one word in the
 * bottom-left; open, it is a panel that covers part of the article — which is
 * the right trade, because none of this is needed while writing and all of it
 * is needed occasionally.
 *
 * Pinned to the BOTTOM so it grows upward: anchored at the top it would push
 * its own summary off the bottom edge as the drafts list filled. */
.docs-page-drawer {
  position: absolute;
  left: 16px;
  bottom: 16px;
  z-index: 2;
  max-width: min(420px, calc(100% - 32px));
  border: 2px solid var(--ink);
  background: var(--bg);
  box-shadow: 0 4px 0 var(--ink);
}
.docs-page-drawer > summary {
  cursor: pointer;
  padding: 7px 12px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink);
}
.docs-page-drawer-body {
  padding: 0 12px 12px;
  max-height: min(60vh, 520px);
  overflow: auto;
}
/* The markup the canvas is producing — a drawer, not a pane. It is for checking
   what a block became and for pasting a hand-tuned <pre> in, not for writing. */
.docs-pane-markup { margin-top: 10px; }
.docs-pane-markup > summary {
  cursor: pointer;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
}
.docs-markup {
  margin: 6px 0 0;
  max-height: 220px;
  overflow: auto;
  padding: 10px 12px;
  border: 2px solid var(--ink-subtle);
  background: var(--bg);
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}
.docs-drafts { margin-top: 10px; }
.docs-drafts > summary { cursor: pointer; }
.docs-draft-list { list-style: none; margin: 6px 0 0; padding: 0; max-height: 140px; overflow: auto; }
.docs-draft {
  display: block; width: 100%; text-align: left;
  background: var(--panel); border: 2px solid var(--ink-subtle);
  padding: 6px 8px; margin-bottom: 4px; cursor: pointer; font-family: inherit;
}
.docs-draft:hover { border-color: var(--ink); }
.docs-draft.active { border-color: var(--brand-1); }
.docs-draft-title { display: block; font-weight: 600; font-size: 13px; }
.docs-draft-path { display: block; font-size: 11px; color: var(--muted); font-family: var(--mono); }

/* ---- Preview ---- */
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: 20px 24px;
  border: 2px solid var(--ink);
  background: var(--panel);
}
/* The refusal, where the preview would be. A <pre> because the message carries
   the offending markup and its own line breaks. */
  margin: 0; white-space: pre-wrap;
  font-family: var(--mono); font-size: 12px;
  color: var(--error, #d64545);
}

/* Enough of the article's shape to judge it by. */
/* The docs' own highlight palette, so a block reads the way it will ship. */

@media (max-width: 1000px) {
  /* Below this the second sidebar is most of the screen, and HQ's own column has
     already come out of its pinning at this same breakpoint. */
  .docs-nav-host { display: none; }
  .docs-work { padding: 16px; }
}
