/* CAM Proposal Hub — internal app styles (camindustrial.net brand: CAM blue #003DA6, Helvetica) */
:root {
  --green: #003DA6; --green-soft: #eef3fb; --amber: #9a7d00; --amber-soft: #fff2a8;
  --ink: #222; --muted: #636569; --line: #d9dee6; --bg: #f5f7fa; --foot: #272727;
}
* { box-sizing: border-box; }
body { margin: 0; font-family: Helvetica, Arial, "Segoe UI", sans-serif; color: var(--ink);
       background: var(--bg); font-size: 15px; line-height: 1.45; }
a { color: var(--green); text-decoration: none; }
h1 { font-size: 24px; margin: 0 0 4px; color: var(--green); }
h2 { font-size: 18px; margin: 18px 0 8px; color: var(--green); }
.mono { font-family: Consolas, monospace; }
.tiny { font-size: 12px; } .muted { color: var(--muted); } .r { text-align: right; }
.err { color: #a3222d; font-weight: bold; }

.topbar { display: flex; justify-content: space-between; align-items: center;
  background: #fff; color: var(--ink); padding: 8px 22px;
  border-bottom: 3px solid var(--green); }
.topbar nav a { color: var(--green); margin-left: 18px; font-weight: 700; }
/* The interim "who is using this browser" control. Amber when nobody has been
   chosen, because until then the dashboard cannot open on anyone's work and a
   new proposal has no owner to notify. */
.topbar nav a.whoami { font-weight: 500; color: var(--muted); }
.topbar nav a.whoami b { color: var(--green); font-weight: 700; }
.topbar nav a.whoami.unset { background: var(--amber-soft); color: #6b5600;
  border-radius: 5px; padding: 2px 9px; }
.topbar nav a.whoami.unset b { color: #6b5600; }
.brand { display: flex; align-items: center; gap: 10px; color: var(--ink); }
.brand img { height: 40px; display: block; }
.brand-mark { background: var(--green); color: #fff; font-weight: 900; padding: 1px 8px;
  border-radius: 4px; letter-spacing: 1px; }
.brand-sub { font-weight: 700; letter-spacing: 2px; font-size: 12px; color: var(--muted); }
.brand.big { justify-content: center; margin-bottom: 6px; }
.brand.big img { height: 52px; }

.wrap { max-width: 1080px; margin: 26px auto; padding: 0 20px; }
/* Interim widening (owner review 2026-08-20 item 3) until the dashboard
   layout rework lands; effectively full width on 1080p-1440p displays. */
.wrap.wide { max-width: 1720px; }
/* Edge-to-edge, like Proposify's editor (owner review item 17). */
.wrap.full { max-width: none; }
.page-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px;
  flex-wrap: wrap; margin-bottom: 10px; }
/* ------- editor: FROZEN proposal panel (owner review 2026-08-26) -------
   Excel freeze-panes, applied to the row that carries the work: the number,
   the title/client/contact/rep fields, the Teamwork link and the
   Preview / Tracking / Send buttons all stay put while the document scrolls.
   `.wrap.full` is the editor and ONLY the editor (editor.html:7 is its sole
   user; every other page renders base.html's `.wrap.in-shell`), so no other
   page shifts. The CAM brand bar deliberately scrolls away — it is navigation,
   not work, and freezing it too would spend a quarter of a 768px laptop on
   chrome. One line here if that turns out to be wrong.
   ⛔ Do NOT give this bar an overflow value: it would cut off the Teamwork
   typeahead results, which are absolutely positioned inside it. */
.wrap.full > .page-head {
  /* ⛔ 55, ABOVE `.save-alert`'s 50, and this is not arbitrary. A z-index on
     this sticky bar makes it a STACKING CONTEXT, so everything inside it -
     including the compact panel's absolutely positioned drop-down, whatever
     z-index that carries - paints at THIS number. `#saveAlert` is a SIBLING at
     50, sticking to exactly where the drop panel opens, so at 45 the unsaved-
     work alarm painted straight over the fields: measured, five of the eight
     header inputs hit-tested to `saveAlert` and swallowed clicks and keystrokes
     silently - in the one state where a rep has just been told their work is
     unsaved. Found by review 2026-08-27.
     No new overlap is created by raising it: closed, the bar is 48px and the
     alarm sticks at 48px; at rest the bar is 185px and the alarm sticks at
     185px. The Library panel (60) still wins over both, and <dialog> lives in
     the top layer regardless. */
  position: sticky; top: 0; z-index: 55;
  background: #fff; margin-bottom: 0;
  padding: 10px 0 8px; border-bottom: 1.5px solid var(--line);
  box-shadow: 0 4px 10px -8px rgba(0, 0, 0, .35);
}
/* ------- editor: COMPACT the frozen panel once scrolled (owner 2026-08-27) ----
   The frozen panel above costs ~185px, about a quarter of a 1366x768 laptop.
   Asked whether that was too much, the owner chose "make it compact when
   scrolled". Once the document is scrolled the panel drops to one ~50px row
   (the number, a read-only title/client/rep summary, and the buttons) and the
   eight editable fields fold into a drop panel that opens on hover, on
   keyboard focus, or from the Details button.

   ⛔ The drop panel is ABSOLUTELY POSITIONED, and that is the whole trick. A
   `position: sticky` box is still in normal flow, so expanding these fields in
   flow would shove the entire document down ~138px under the pointer every
   time the mouse crossed the top bar. Absolute keeps the FROZEN height at the
   compact value, which is what --ed-head-h publishes and what the outline
   rail, the unsaved-work alarm and every section's scroll-margin key off. If
   you ever make this expand in flow, all four of those start moving on hover.

   ⛔ Do NOT swap `max-height: 0` for `display: none` or `visibility: hidden`.
   Both remove the fields from the tab order, and then :focus-within can never
   fire — a keyboard user would have no way in but the Details button. Clipped
   is enough: overflow:hidden makes the children unclickable, so there are no
   invisible hit targets, and the moment focus lands inside, the rule below
   opens the panel.

   There is deliberately NO transition. A transition on max-height with
   overflow flipping to visible spills content mid-flight, and it makes every
   geometry assertion in tests/test_editor_browser.py depend on a timer. */
.wrap.full > .page-head.is-compact {
  padding: 6px 0 5px;
  /* A flex container WRAPS in preference to shrinking, so a realistic 118-char
     title pushed the whole button row onto a second line and the "48px" bar
     measured 94px at 1366x768 and 116px at 1024x700 - it gave back 91px instead
     of 137px on exactly the proposals whose names are long. nowrap forces
     `.head-main` to shrink and the summary to ellipsise instead. */
  flex-wrap: nowrap;
}
.wrap.full > .page-head.is-compact .row-end { flex: 0 0 auto; }
/* The proposal NUMBER must never wrap. With `.head-main` a nowrap flex row, a
   long title squeezed the h1 until "CAM-2026-0140" broke onto THREE lines and
   the compact bar measured 87px instead of 48px. The number is short, fixed
   width and the one label a rep uses to identify the document: it is the
   summary's job to give ground, not the number's. */
.wrap.full > .page-head.is-compact .head-main h1 {
  flex: 0 0 auto; white-space: nowrap; }
/* ⚠️ Everything here is scoped to `.is-compact`. `.head-main` deliberately
   gets NO base rule: giving the flex item a `min-width: 0` at rest lets it
   shrink below its content and changes how the eight header fields wrap, i.e.
   it would move the resting panel's height - the one number this whole feature
   is measured against. */
.wrap.full > .page-head.is-compact .head-main {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: nowrap;
  flex: 1 1 auto; min-width: 0;
  /* A hover target the pointer can actually rest on, and the reason the
     summary and number share one line rather than stacking to ~50px. */
  min-height: 30px; padding: 3px 0; }
/* The summary is compact-only: at rest the real fields are right there. */
.head-summary { display: none; }
.wrap.full > .page-head.is-compact .head-summary {
  display: block; font-size: 13.5px; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.head-summary b { color: var(--ink); font-weight: 700; }
.wrap.full > .page-head:not(.is-compact) #headMore { display: none; }
.wrap.full > .page-head.is-compact .head-fields {
  /* Opens BELOW the unsaved-work alarm when one is showing, rather than over
     it. --ed-alarm-h is published by syncOutlineTop and is 0px almost always.
     The z-index fix above is still load-bearing as a backstop: the alarm is
     multi-line and can GROW after the panel is already open, and the fields
     must win that race rather than going dead again. */
  position: absolute; top: calc(100% + var(--ed-alarm-h, 0px));
  left: 0; right: 0; z-index: 46;
  margin: 0; padding: 0 14px;
  background: #fff; border: 0 solid var(--line);
  border-radius: 0 0 10px 10px;
  box-shadow: 0 14px 24px -14px rgba(0, 0, 0, .5);
  /* `box-sizing: border-box` is global, and a border-box height is clamped UP
     to padding+border, so a border left on here keeps the closed panel 1-2px
     tall - a hairline under the bar, and enough to make "clipped to nothing"
     read as 1 instead of 0. The border arrives with the open state. */
  max-height: 0; overflow: hidden;
}
/* ⛔ `.fields-hover` is set by JS AFTER A DWELL, and a bare `:hover` is
   deliberately NOT used here any more.
   With `:hover`, merely BRUSHING the bar dropped the panel over the top ~113px
   of the canvas - the block tool row and the rich-text format bar of whatever
   block was at the top of the viewport. Review reproduced the whole sequence:
   sweep the pointer up past the bar, come back down to click where the format
   bar was, and the click lands in the client-facing proposal TITLE, where the
   next sentence typed is silently autosaved. Nothing under the pointer changed
   appearance before the click. A dwell means a deliberate hover opens it and a
   sweep past does not.
   The fields are DOM children of .head-main even while absolutely positioned,
   so hovering the OPEN panel keeps it open (JS cancels the close on re-enter). */
.wrap.full > .page-head.is-compact.fields-hover .head-fields,
.wrap.full > .page-head.is-compact .head-main:focus-within .head-fields,
.wrap.full > .page-head.is-compact.fields-open .head-fields {
  max-height: 60vh; overflow: visible; padding: 10px 14px 12px;
  border-width: 0 1.5px 1.5px;
}

.row-end { display: flex; gap: 8px; justify-content: flex-end; align-items: center; flex-wrap: wrap; }

.btn { display: inline-block; border: 1.5px solid var(--green); color: var(--green);
  background: #fff; border-radius: 7px; padding: 7px 14px; font-weight: 700; cursor: pointer;
  font-size: 14px; }
.btn:hover { background: var(--green-soft); }
.btn.primary { background: var(--green); color: #fff; }
.btn.primary:hover { filter: brightness(1.12); }
.btn.small { padding: 3px 10px; font-size: 12.5px; }
/* ⛔ R9 (review 2026-08-27). THE THIRD TIME THIS TRAP HAS SHIPPED on this
   product, after .tw-box and .stack. An author-origin `display` BEATS the
   user-agent's `[hidden] { display: none }`, and `.btn` sets
   `display: inline-block` - so EVERY button hidden via the `hidden` attribute
   in JS rendered normally and was fully clickable. It bit #scopeIntake, whose
   whole point is to appear only once a Teamwork project is linked: on an
   unlinked proposal the button was visible, and clicking it produced a bare
   error. The guarding test asserted the ATTRIBUTE (`:not([hidden])`), so it
   stayed green - assert getComputedStyle for anything toggled this way.
   ⚠️ Any new class that sets `display` needs its own `[hidden]` rule. */
.btn[hidden] { display: none; }

.filters { display: flex; gap: 8px; margin: 12px 0; }
.filters input[type=search] { flex: 1; }
input, select, textarea { font: inherit; padding: 8px 10px; border: 1.5px solid var(--line);
  border-radius: 7px; background: #fff; }
input:focus, select:focus { outline: 2px solid var(--green); border-color: var(--green); }

table.list { width: 100%; border-collapse: collapse; background: #fff; border: 1.5px solid var(--line);
  border-radius: 8px; overflow: hidden; }
table.list th { background: var(--green); color: #fff; text-align: left; padding: 7px 10px; font-size: 13px; }
table.list td { border-top: 1px solid var(--line); padding: 8px 10px; vertical-align: middle; }

.chip { display: inline-block; border-radius: 4px; padding: 1px 8px; font-size: 11.5px;
  font-weight: 800; letter-spacing: 0.4px; background: #eee; color: #555; }
.chip.s-draft { background: #ececec; color: #555; }
.chip.s-sent { background: #e8f0fb; color: #2a4e78; }
.chip.s-viewed { background: var(--amber-soft); color: #6b5600; }
.chip.s-accepted, .chip.s-won { background: #c8f7c5; color: #14401f; }
.chip.s-lost { background: #f7d4d4; color: #7a1e1e; }
.chip.mode { background: var(--green-soft); color: var(--green); }
.chip.warn { background: #f7d4d4; color: #7a1e1e; }
.chip[class*="ev-"] { background: var(--green-soft); color: var(--green); }
.chip.ev-accepted { background: #c8f7c5; color: #14401f; }
.chip.ev-pricing_changed { background: var(--amber-soft); color: #6b5600; }

.login-body { display: grid; place-items: center; min-height: 100vh; background: var(--green); }
.login-card { background: #fff; border-radius: 12px; padding: 34px 38px; width: 360px;
  text-align: center; box-shadow: 0 18px 60px rgba(0,0,0,.35); }
.login-card form { display: flex; flex-direction: column; gap: 10px; margin-top: 12px; }

dialog { border: none; border-radius: 12px; padding: 22px 24px; width: 430px; max-width: 92vw;
  box-shadow: 0 18px 60px rgba(0,0,0,.35); }
dialog::backdrop { background: rgba(10, 30, 24, .55); }
.stack { display: flex; flex-direction: column; gap: 10px; }
/* An author-origin `display` BEATS the user-agent's `[hidden] { display: none }`.
   This is the same trap that made `.tw-box[hidden]` render on hosts with no
   Teamwork credential (2026-08-26); the New-proposal dialog's "Someone else"
   fields are a `.stack` and would have shown permanently without this. */
.stack[hidden] { display: none; }
.stack label { display: flex; flex-direction: column; gap: 4px; font-weight: 700; font-size: 13px; }

.stat-row { display: flex; gap: 12px; margin: 14px 0; flex-wrap: wrap; }
.stat { background: #fff; border: 1.5px solid var(--line); border-radius: 10px;
  padding: 12px 18px; min-width: 150px; }
.stat-n { font-size: 22px; font-weight: 900; color: var(--green); }
.stat-l { font-size: 12px; color: var(--muted); }
.accept-box { background: #c8f7c5; border: 2px solid #1a7a3c; color: #14401f;
  border-radius: 9px; padding: 10px 14px; margin: 10px 0; }

/* ------- editor ------- */
.small-h { font-size: 15px; color: var(--muted); }
.head-fields { display: flex; gap: 8px; margin-top: 4px; flex-wrap: wrap; }
.title-input { font-size: 17px; font-weight: 800; width: 380px; }
.client-input { width: 220px; }
/* B3's scope row. `flex-basis: 100%` gives it its own line inside the wrapping
   `.head-fields` row rather than letting a two-line textarea stretch the row of
   single-line inputs beside it.
   ⛔ It lives INSIDE the drop panel, which is absolutely positioned - that is
   what keeps `--ed-head-h` from moving when the panel opens, and a starred
   check asserts exactly that. Do not move this row out into the bar. */
.head-scope { flex: 1 1 100%; display: flex; gap: 8px; align-items: flex-start; }
.head-scope[hidden] { display: none; }
/* ⚠️ R22 (review 2026-08-27). ONE ROW, NOT TWO.
   The resting frozen panel is the number the owner was explicitly asked about
   and judged too much at 185px - the whole compact-on-scroll feature exists
   because of that answer. A `rows="2"` textarea here added 70px (38%) to it,
   and on a short proposal the document never scrolls far enough for the compact
   state to engage at all, so that 70px was simply gone. It is one line at rest
   and grows only while the rep is actually in it (`:focus`), which is when the
   space is worth spending. `field-sizing` is progressive: browsers without it
   keep the fixed single-line height and still scroll internally. */
.scope-input { flex: 1 1 auto; min-width: 240px; resize: vertical;
  font: inherit; line-height: 1.45;
  height: 2.1em; min-height: 2.1em; field-sizing: content; max-height: 2.1em;
  transition: max-height .12s ease; }
.scope-input:focus { max-height: 7em; height: auto; }
.scope-side { display: flex; flex-direction: column; gap: 3px; align-items: flex-start;
  flex: 0 0 auto; }
/* The two free-text owner boxes, revealed only by "Someone else". An
   author-origin `display` beats the UA's `[hidden] { display: none }`, so the
   explicit rule is required or they show permanently - the trap that already
   bit `.tw-box` and `.stack`. */
.ed-owner-other { display: inline-flex; gap: 8px; }
.ed-owner-other[hidden] { display: none; }
/* Unsaved-work alarm. Deliberately loud and sticky: the failure it reports
   (an expired session, a locked proposal, a conflicting save) used to render
   as the word "saved", and a rep could lose a morning without a single
   visible cue. It stays on screen until a save actually succeeds. */
.save-alert {
  position: sticky; top: 0; z-index: 50;
  margin: 10px 0; padding: 10px 14px; border-radius: 6px;
  background: #f7d4d4; color: #7a1e1e; border: 1px solid #d99;
  font-size: 13px; line-height: 1.45;
}
.save-alert a { color: #7a1e1e; }
/* Only the CANVAS alarm parks below the frozen panel. `.save-alert` is a shared
   class: `#unfilledWarn` inside the Send dialog wears it too, and offsetting
   that by the panel height pushed it ~185px down the dialog, over the share
   URL the rep came there to copy. Scope by id, not by class. */
#saveAlert { top: var(--ed-head-h, 0px); }
#saveState.bad { color: #b02020; font-weight: 700; }

#blocks { margin-top: 14px; }
/* scroll-margin-top: the outline's click handler calls scrollIntoView, and with
   the proposal panel frozen the block's own heading would land BEHIND it. This
   is what keeps "click a section, see that section" true. */
.blk { background: #fff; border: 1.5px solid var(--line); border-radius: 10px;
  margin: 10px 0; position: relative;
  scroll-margin-top: calc(var(--ed-chrome-h, var(--ed-head-h, 0px)) + 18px); }
/* Block header bar (owner review items 19 + 22): the tools are always visible
   (no hover-only fade), sized to be hit reliably, and a collapsed block keeps
   showing its own heading here. */
.blk-head { display: flex; align-items: center; gap: 10px; padding: 6px 8px 6px 12px;
  flex-wrap: wrap; }
.blk:not(.collapsed) .blk-head { border-bottom: 1px solid var(--line); }
.blk-headlabel { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-weight: 700; font-size: 13px; visibility: hidden; }
.blk.collapsed .blk-headlabel { visibility: visible; }
.blk-tools { display: flex; gap: 4px; flex: none; }
.blk-tools button { border: 1px solid var(--line); background: #fff; border-radius: 5px;
  cursor: pointer; font-size: 12.5px; padding: 4px 9px; }
.blk-tools button:hover { background: var(--green-soft); }
.blk-tools .t-del { color: #a3222d; }
.blk-type { background: var(--green); color: #fff; flex: none;
  font-size: 10px; font-weight: 800; letter-spacing: 1px; border-radius: 4px; padding: 1px 7px; }
.blk-body { padding: 14px 18px; }
.blk [contenteditable] { outline: none; min-height: 34px; }
.blk [contenteditable]:focus { background: #fcfefd; }
/* Owner item 26 asked for Word/Outlook or at least Gmail level formatting, so
   the bar now wraps rather than overflowing off the block. */
.rt-toolbar { display: flex; gap: 4px; padding: 6px 12px; border-bottom: 1px solid var(--line);
  flex-wrap: wrap; align-items: center; }
.rt-toolbar .sep { width: 1px; align-self: stretch; background: var(--line); margin: 2px 4px; }
/* Reflects the caret's real state via queryCommandState: without this the bar
   tells you nothing about the text you are standing in, which is most of what
   makes a toolbar feel like Word rather than a row of buttons. */
.rt-toolbar button.on { background: var(--green); border-color: var(--green); color: #fff; }
.rt-toolbar button { border: 1px solid var(--line); background: #fff; border-radius: 5px;
  cursor: pointer; min-width: 30px; padding: 2px 8px; font-size: 13px; }
.rt-toolbar button:hover { background: var(--green-soft); }

table.pt { width: 100%; border-collapse: collapse; margin-top: 6px; }
table.pt th { background: var(--green-soft); color: var(--green); font-size: 12px; text-align: left;
  padding: 5px 6px; }
table.pt td { border-top: 1px solid var(--line); padding: 4px 6px; }
table.pt input[type=text] { width: 100%; padding: 4px 6px; }
table.pt input[type=number] { width: 76px; padding: 4px 6px; }
table.pt select { padding: 4px 6px; }
table.pt .del { color: #a3222d; cursor: pointer; border: none; background: none; font-weight: 900; }
.pt-foot { display: flex; gap: 10px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
.pl-search { position: relative; flex: 1; min-width: 260px; }
.pl-search input { width: 100%; }
.pl-results { position: absolute; z-index: 30; left: 0; right: 0; top: 100%; background: #fff;
  border: 1.5px solid var(--green); border-radius: 0 0 8px 8px; max-height: 240px; overflow: auto; }
.pl-results div { padding: 6px 10px; cursor: pointer; font-size: 13px; border-top: 1px solid var(--line); }
.pl-results div:hover { background: var(--green-soft); }
.badge { display: inline-block; font-size: 10px; font-weight: 900; border-radius: 3px;
  padding: 0 5px; letter-spacing: .5px; vertical-align: 1px; }
.badge.validated, .badge.firm { background: #c8f7c5; color: #14401f; }
.badge.budgetary { background: var(--amber-soft); color: #6b5600; }
.addbar { display: flex; gap: 8px; align-items: center; margin: 14px 0 4px;
  border: 2px dashed var(--line); border-radius: 10px; padding: 10px 14px; background: #fff;
  flex-wrap: wrap; }

/* ------- editor: section outline over ONE canvas (owner decision 2026-08-09) ------- */
/* Outline sized up + reorder arrows in it (owner review item 18); the sticky
   column also carries the ADD BLOCK rail (item 20) so both float beside the
   canvas while scrolling. */
/* The canvas is bounded at 1560px: full-window on 1080p-1440p displays (the
   owner's edge-to-edge ask) without letting an ultrawide stretch prose to
   3000px lines the 860px client page will re-wrap anyway. */
/* Rail width is a rep preference (--ed-rail-w, dragged on the divider and kept
   in that browser's localStorage). The DEFAULT stays 290px so nothing about the
   shipped canvas width moves for anyone who never touches the handle. */
.ed-layout { display: grid;
  grid-template-columns: var(--ed-rail-w, 290px) 12px minmax(0, 1560px); gap: 6px;
  align-items: start; }
/* The divider. A real hit target and keyboard-operable, deliberately kept to
   a short grab handle near the top of the rail: it must never be taller than
   the rail it resizes, or it becomes the tallest item in the grid and puts a
   scrollbar on an otherwise short document. */
.ed-split { position: sticky;
  top: calc(var(--ed-head-h, 0px) + 14px); margin-top: 14px;
  /* A GRAB HANDLE, not a full-height rule. It used to use the rail's own
     height formula, which on a short proposal made the splitter 827px against
     a 445px rail - the tallest thing in the grid, and the only reason the page
     had a scrollbar at all. */
  height: 160px;
  border: 0; padding: 0; background: none; cursor: col-resize;
  /* A pointerdown here must not also start a touch scroll or a text selection. */
  touch-action: none; -webkit-user-select: none; user-select: none;
  display: flex; align-items: center; justify-content: center; }
.ed-split::before { content: ""; width: 3px; height: 44px; border-radius: 3px;
  background: var(--line); }
.ed-split:hover::before, .ed-split:focus-visible::before { background: var(--green);
  height: 100%; }
.ed-split:focus-visible { outline: 2px solid var(--green); outline-offset: 1px; }
body.ed-dragging { cursor: col-resize; user-select: none; }
/* Flex column so the SECTIONS list scrolls INTERNALLY and the ADD BLOCK rail
   stays pinned on screen — as the last child of one scrolling box, a 16-section
   document pushed the whole rail below the fold on a 768px-tall laptop. */
/* The pinned ADD BLOCK rail lives at the BOTTOM of this box, so the box must
   fit the viewport even at scroll 0, when it starts below the page head rather
   than at its stuck offset. Hard-coding `100vh - 40px` assumed a short head:
   one extra wrapped row of header fields pushed the rail below the fold again
   (caught by the 18-section 1366x768 case in tests/test_editor_browser.py).
   editor.js publishes the real layout offset as --ed-outline-top.
   ⛔ 2026-08-26: that offset MUST be measured from a NON-STICKY element.
   It used to be summed off `.outline` itself, and Chromium's offsetTop on a
   position:sticky box TRACKS THE STUCK POSITION: measured on a 16-section
   document at 1366x768, it read 284px at rest and 3199px once scrolled past.
   `calc(100vh - 3199px - 16px)` clamps to 0px, so the whole rail became a 16px
   empty sliver — and it never came back, because the measurement only re-ran on
   resize. The comment that used to sit here asserted the opposite and was wrong.
   The `max(220px, ...)` floor is deliberate belt-and-braces: whatever a future
   measurement does, the rail can no longer vanish outright. Note the floor also
   makes the failure quieter, so the test that guards this asserts the MEASURED
   OFFSET, not just the resulting height. */
.outline { position: sticky; top: calc(var(--ed-head-h, 0px) + 14px);
  background: #fff; border: 1.5px solid var(--line);
  border-radius: 10px; padding: 8px 0 6px;
  max-height: max(220px, calc(100vh - var(--ed-outline-top, 26px) - 16px));
  display: flex; flex-direction: column; overflow: hidden; margin-top: 14px; }
.outline-head { flex: none; font-size: 10.5px; font-weight: 900; letter-spacing: 1.5px;
  color: var(--muted); padding: 4px 14px 6px; }
/* shrink 1000 vs the ADD BLOCK rail's 1: when the box is short of room the
   SECTIONS list gives ground FIRST (it is a scrolling list, that is its job)
   and the buttons keep their full height until the list hits its 60px floor.
   This weighting pairs with `.side-add { flex: 0 1 auto }` below and was added
   in the same change: making the button rail shrinkable ALSO let this list grow
   into the free space and squeeze the buttons (measured 364px list against
   155px of a 304px button rail on a 1600x900 window). Neither half is correct
   without the other. */
.outline ul { list-style: none; margin: 0; padding: 0;
  flex: 1 1000 auto; min-height: 60px; overflow-y: auto; }
.outline li { display: flex; gap: 7px; align-items: center; padding: 8px 10px 8px 12px;
  cursor: pointer; font-size: 13.5px; border-left: 3px solid transparent; }
.outline li:hover { background: var(--green-soft); }
.outline li.active { border-left-color: var(--green); background: var(--green-soft); }
.o-type { font-size: 9px; font-weight: 900; color: #fff; background: var(--green);
  border-radius: 3px; padding: 0 4px; letter-spacing: .5px; flex: none; }
/* Section names wrap to TWO lines before they clip (owner review 2026-08-26:
   "Materials, Equipme..." told him nothing). Two lines names every seeded
   section in full at the default 290px rail; the divider handles the rest. */
.o-label { flex: 1; min-width: 0; overflow: hidden; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  line-clamp: 2; }
/* Real hit targets with visible bounds on hover: three adjacent reorder
   controls whose mis-click silently rewrites and autosaves the block order
   must not be 14px glyphs 2px apart. */
.o-tools { display: flex; gap: 4px; flex: none; margin-left: auto; }
.o-tools button { border: 1px solid transparent; background: none; cursor: pointer;
  color: var(--muted); flex: none; font-size: 13px; padding: 2px 6px; border-radius: 4px; }
.o-tools button:hover { color: var(--green); border-color: var(--green);
  background: var(--green-soft); }
.side-add-head { border-top: 1.5px solid var(--line); margin-top: 8px; padding-top: 10px; }
/* `flex: none` made this unshrinkable, so on a short window (measured: a 620px
   viewport caps the box at 320px while the buttons alone need 304px plus the
   headings and the list's 60px floor) the overflow was simply clipped by
   `.outline`'s overflow:hidden and Library / Cost Summary could not be reached
   at all. It may shrink and scroll now; see the shrink weighting on the list
   above, which has to change with it. */
.side-add { flex: 0 1 auto; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: 6px; padding: 2px 12px 10px;
  align-items: stretch; }
.side-add .btn { text-align: left; }
/* Narrow window: one column, and the rail moves ABOVE the canvas instead of
   disappearing. It used to be `display: none`, which produced the same
   "the panel is gone" report as the C1 collapse did — from a different cause,
   on a zoomed-in browser (150% zoom on a 1440px screen is a 960px viewport). */
@media (max-width: 950px) {
  .ed-layout { grid-template-columns: minmax(0, 1fr); }
  .ed-split { display: none; }
  /* Not sticky and not height-capped by the viewport here: it sits above the
     canvas in one column, so it may be as tall as its content. A 300px cap was
     tried and crushed the SECTIONS list to its 60px floor - present but
     useless, which is not an improvement on being hidden. */
  .outline { position: static; max-height: none; margin-top: 10px; }
  .outline ul { max-height: 260px; }
  /* The buttons go in a row rather than a 10-deep column that pushes the
     document off the bottom of a narrow screen. */
  .side-add { flex-direction: row; flex-wrap: wrap; overflow: visible; }
  .side-add .btn, .side-add select { flex: 0 1 auto; }
}
.blk.collapsed .blk-body { display: none; }
.blk.collapsed { border-style: dashed; }
.blk.flash { outline: 3px solid var(--green); outline-offset: 2px; }
.blk-tools .pb-on, .blk-tools .pb-on:hover { background: var(--green); color: #fff;
  border-color: var(--green); }

/* cover + drawings authoring */
.cov-fields { display: flex; gap: 16px; align-items: center; margin: 10px 0 6px; flex-wrap: wrap; }
.cov-up { font-size: 12.5px; font-weight: 700; color: var(--muted); }
.cov-up input { font-size: 12px; }
.cov-parties-ed { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 6px; }
.cov-parties-ed label { font-size: 12.5px; font-weight: 700; color: var(--muted);
  display: flex; flex-direction: column; gap: 4px; }
.cov-parties-ed textarea { font-size: 13px; resize: vertical; }
.dr-sheet { display: flex; gap: 8px; align-items: center; }
.dr-sheet .dcap { flex: 1; }

/* ------- editor: CAM fee table (priced groups) ------- */
.cam-head { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 4px; }
.cam-head input, .cam-head select { font-size: 13.5px; padding: 5px 8px; }
.cam-title { flex: 1 1 240px; font-weight: 800; }
.cam-header { flex: 1 1 300px; }
.grp { border: 1.5px solid var(--line); border-radius: 8px; padding: 10px 12px; margin: 8px 0;
  background: #fafbfc; }
.grp-row1 { display: flex; gap: 8px; align-items: center; }
.g-desc { flex: 1; font-weight: 700; }
.g-amt-wrap { display: flex; align-items: center; gap: 3px; font-weight: 700; color: var(--muted); }
.g-amount { width: 118px; text-align: right; }
.grp-tools { display: flex; gap: 4px; }
.grp-tools button { border: 1px solid var(--line); background: #fff; border-radius: 5px;
  cursor: pointer; font-size: 12px; padding: 2px 7px; }
.grp-row2 { display: flex; gap: 16px; align-items: center; margin-top: 7px; flex-wrap: wrap; }
.g-suffix { flex: 1; min-width: 220px; font-size: 13px; }
.grp-row2 label { display: flex; gap: 5px; align-items: center; font-size: 12.5px;
  color: var(--muted); font-weight: 700; }
.g-lines { width: 100%; margin-top: 8px; font-size: 13.5px; line-height: 1.5; resize: vertical; }
.cam-foot { display: flex; gap: 10px; align-items: center; justify-content: space-between;
  margin-top: 8px; flex-wrap: wrap; }
.cam-total { font-weight: 800; color: var(--green); font-size: 15px; }
.cam-total .tiny { font-weight: 400; }
.drop-list { list-style: none; margin: 6px 0; padding: 0; }
.drop-list li { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
#shareUrl { width: 100%; font-family: Consolas, monospace; font-size: 12.5px; }
/* ------- content library: slide-over panel ------- */
#libPanel { position: fixed; top: 0; right: 0; bottom: 0; width: 420px; max-width: 94vw;
  background: #fff; border-left: 2px solid var(--green); box-shadow: -14px 0 40px rgba(0,0,0,.18);
  z-index: 60; display: flex; flex-direction: column; }
#libPanel[hidden] { display: none; }
.lib-head { display: flex; justify-content: space-between; align-items: center;
  padding: 12px 16px; background: var(--green); color: #fff; }
.lib-close { border: none; background: none; color: #fff; font-size: 15px; cursor: pointer; }
.lib-filters { display: flex; gap: 8px; padding: 10px 12px; border-bottom: 1.5px solid var(--line); }
.lib-filters input[type=search] { flex: 1; min-width: 0; }
.lib-filters select { max-width: 170px; }
.lib-list { flex: 1; overflow-y: auto; }
.lib-row { padding: 9px 14px; border-bottom: 1px solid var(--line); cursor: pointer; }
.lib-row:hover { background: var(--green-soft); }
.lib-row-head { display: flex; gap: 8px; align-items: baseline; }
.lib-row-name { font-weight: 700; flex: 1; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.lib-row .chip { flex: none; }
.lib-prev { font-size: 12px; color: var(--muted); margin: 4px 0 6px; display: none;
  max-height: 130px; overflow: hidden; }
.lib-row.open .lib-prev { display: block; }
.lib-row .btn { display: none; }
.lib-row.open .btn { display: inline-block; }
.lib-hint { padding: 8px 14px; border-top: 1.5px solid var(--line); margin: 0; }
.chip.saved { background: #e8f0fb; color: #2a4e78; }
.chip.imgs-placeholder { background: var(--amber-soft); color: #6b5600; }

/* labelled image placeholder (library sections whose artwork wasn't extractable) */
.img-ph { border: 2px dashed var(--line); border-radius: 8px; padding: 26px 18px;
  text-align: center; color: var(--muted); font-size: 13px; font-weight: 700;
  background: repeating-linear-gradient(45deg, #fafbfc, #fafbfc 12px, #f2f4f7 12px, #f2f4f7 24px); }

/* ------- Cost Summary import: review + markup dialog ------- */
dialog.wide { width: 1080px; max-width: 96vw; }
.cs-markupbar { display: flex; gap: 16px; align-items: center; flex-wrap: wrap;
  background: var(--green-soft); border: 1.5px solid var(--green); border-radius: 8px;
  padding: 10px 14px; }
.cs-markupbar label { flex-direction: row; align-items: center; gap: 8px;
  font-size: 15px; font-weight: 800; color: var(--green); }
.cs-rollup { margin-left: auto; text-align: right; }
.cs-tablewrap { max-height: 46vh; overflow: auto; border: 1.5px solid var(--line);
  border-radius: 8px; }
.cs-table { font-size: 13px; }
.cs-table thead th { position: sticky; top: 0; z-index: 2; }
.cs-table .cs-vhead td { background: var(--green-soft); color: var(--green); }
.cs-table tr.cs-off td { opacity: .4; }
.cs-table tr.cs-unpriced { background: #fffbe8; }
/* ★ D2b (2026-08-27): a negative markup is allowed and must be LOUD. The row
   is tinted, the price cell carries a chip, and the roll-up states the loss in
   dollars - a rep can nod at "are you sure?" without noticing a number. */
.cs-table tr.cs-under { background: #fdeeee; }
.cs-underwarn { margin-top: 6px; padding: 7px 11px; border-radius: 6px;
  background: #f7d4d4; color: #7a1e1e; border: 1px solid #d99;
  font-size: 12.5px; line-height: 1.4; text-align: left; }
/* The same fact, kept visible in the editor for as long as it stays true.
   Rep-side only: cs_unit_cost never survives _viewer_ctx. */
/* ⛔ NOT `white-space: nowrap`, and the wrapper reserves its line.
   Two separate defects, both measured by review 2026-08-27:
   (1) nowrap inside an auto-layout <td> raised the Unit $ column's MIN-CONTENT
       width by ~77px. Between about 950px and 1100px of window that pushed the
       pricing table wider than its column, the whole page gained a horizontal
       scrollbar, and the row's own overflow menu and delete button went off
       screen - 48px off at 1024x760. Wrapping instead means the min-content
       width is the longest WORD, not the whole string.
   (2) the badge appearing or clearing changed the cell's width and the row's
       height WHILE THE REP WAS TYPING in the price box: measured 77px sideways
       and 22px down at 900x700, on the keystroke that crosses cost. `.bc-note`
       reserves its line up front on any row that HAS a recorded cost, so
       appearing and clearing move nothing.
   Keep the shortfall on screen: it is the loud half of D2b. */
.bc-note { display: block; }
.bc-note.can-warn { min-height: 17px; }
.below-cost { display: inline-block; margin-top: 3px; padding: 1px 6px;
  border-radius: 4px; background: #f7d4d4; color: #7a1e1e; max-width: 100%;
  font-size: 10.5px; font-weight: 800; letter-spacing: .4px;
  overflow-wrap: anywhere; }
.cs-table .cs-price b { color: var(--green); }
.cs-variants { display: flex; gap: 14px; flex-wrap: wrap; margin: 4px 0 8px; }
.cs-variants label { flex-direction: row; align-items: center; gap: 5px; }
#csBtn { cursor: pointer; }

/* itemized-pricing detail sub-row: supply / condition / zone (appendix fields) */
table.pt td.nowrap { white-space: nowrap; }
table.pt .xtra { border: 1px solid var(--line); background: #fff; border-radius: 5px;
  cursor: pointer; padding: 2px 6px; margin-right: 4px; }
table.pt .xtra.on { background: var(--green); color: #fff; border-color: var(--green); }
tr.prow-x td { background: #fafbfc; }
tr.prow-x label { font-weight: 700; color: var(--muted); margin-right: 10px; }
tr.prow-x select, tr.prow-x input { font-size: 12.5px; padding: 3px 6px; }
.tiny-select select { font-size: 14px; font-weight: 800; color: var(--green); border: none; }

/* ---------------------------------------------------------------- teamwork
   Link a proposal to ONE Teamwork project (owner items 7-9). Every element
   here is hidden unless the host has Teamwork credentials, so an unconfigured
   pilot host shows no dead controls. */
.tw-box { position: relative; display: inline-flex; align-items: center; gap: 6px; }
/* An author-origin `display` beats the user-agent's `[hidden] { display: none }`,
   so both of these rendered on hosts with NO Teamwork credential — the exact
   "no dead controls" promise the comment above makes. Found 2026-08-26. */
.tw-box[hidden], .tw-quoted[hidden] { display: none; }
.tw-box .client-input { min-width: 230px; }
/* ⚠️ R20 (review 2026-08-27): 280px of results is tall enough to cover the
   Teamwork project box AND the required owner-rep dropdown in the create
   dialog, so a click aimed at the owner select hit-tested to a `.tw-row` and
   silently replaced the client the rep had typed. 150px is four to five rows -
   enough to choose from, short enough that the controls underneath stay
   reachable - and the list scrolls for the rest. This is the same class of
   trapdoor as the bare :hover that dropped a panel over the canvas. */
.tw-results { position: absolute; top: 100%; left: 0; z-index: 40; min-width: 340px;
  max-height: 150px; overflow-y: auto; background: #fff; border: 1px solid #ccc;
  border-radius: 5px; box-shadow: 0 6px 18px rgba(0,0,0,.13); }
.tw-row { padding: 6px 10px; cursor: pointer; border-bottom: 1px solid #f0f0f0; }
.tw-row:last-child { border-bottom: 0; }
.tw-row:hover { background: #eef4fd; }
.tw-row.muted { cursor: default; }
.tw-row.muted:hover { background: #fff; }
.tw-quoted { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 10px; background: #f6f8fb; border: 1px solid #dde4ee; border-radius: 5px; }

/* ------- New-proposal dialog: Teamwork client + project pickers (A1-A4) -----
   `position: relative` on a wrapper INSIDE the label, not on the label itself:
   the result list is absolutely positioned against it, and anchoring it to the
   label would have put the list over the label's own text.

   ⛔ Both `[hidden]` rules are load-bearing, not tidiness. An author-origin
   `display` BEATS the user-agent's `[hidden] { display: none }` - the exact
   trap that made `.tw-box` render on hosts with no Teamwork credentials
   (2026-08-26) and `.stack` render its owner fields permanently (2026-08-27).
   `.np-tw` is a `.stack` (flex) and `.np-combo` is a block, so without these
   the whole picker would show on an unconfigured host as a dead control. */
.np-tw[hidden] { display: none; }
.np-combo { position: relative; display: block; }
.np-combo[hidden] { display: none; }
.np-combo .tw-results { min-width: 100%; }
/* The dialog is a small box and the result list is absolutely positioned, so it
   would be clipped by the dialog's own overflow without this. */
#newModal form { overflow: visible; }
/* ⛔ THIS BOX MUST NEVER CHANGE THE DIALOG'S HEIGHT.
   Measured 2026-08-27: it used to be `hidden` until a project was picked, and
   committing a typed project ID on blur made it appear - which happens on the
   MOUSEDOWN of the Create button. The button then moved between mousedown and
   mouseup, so the browser fired no click at all and Create silently did
   nothing. (Found by A/B: submitting worked with the project box untouched and
   failed with it touched.) Same family as the compact panel being absolutely
   positioned so opening it cannot shove the document.
   So the space is RESERVED: the box is always in flow at a stable height and
   only its text changes. `overflow: auto` keeps an unusually long warning from
   growing it. */
.np-prefill { background: #f6f8fb; border: 1px solid #dde4ee; border-radius: 5px;
  padding: 6px 9px; line-height: 1.5;
  min-height: 3.4em; max-height: 3.4em; overflow: auto; }
.np-prefill[hidden] { display: none; }
.np-prefill b { font-weight: 700; }
.np-prefill .np-warn { color: #8a5a00; }
.np-tw-hint { font-weight: 400; }
.np-tw-hint[hidden] { display: none; }
/* D-51 (owner, 2026-10-16): the intake's scope of work, shown ONLY when the
   dialog was opened from an intake. It is un-hidden the moment the dialog opens,
   before the intake is read, and only its text changes afterwards, so it never
   changes the dialog's height under a click (the ⛔ above). A `.stack label` is
   flex, so this [hidden] rule is load-bearing. */
.np-scope[hidden] { display: none; }
.np-scope textarea { font: inherit; font-weight: 400; min-height: 4.6em; resize: vertical; }
.np-scope .tiny { font-weight: 400; }
.tw-quoted label { display: inline-flex; align-items: center; gap: 6px; margin: 0; }

/* Status-ladder buckets on the dashboard. Deliberately cooler than the
   proposal-status chips so the two ladders never read as the same thing. */
.chip.tw-leads, .chip.tw-prospecting { background: #ececec; color: #555; }
.chip.tw-proposal { background: #e8f0fb; color: #2a4e78; }
.chip.tw-quoted { background: var(--amber-soft); color: #6b5600; }
.chip.tw-won { background: #c8f7c5; color: #14401f; }
.chip.tw-completed { background: #d9e8d5; color: #2f4a2a; }
.chip.tw-closed, .chip.tw-internal { background: #eee; color: #777; }
.chip.tw-other { background: #f2e6f7; color: #5b2a72; }

/* ------- left rail + page shell (owner review item 5) ------- */
/* Grid rather than a float so the rail cannot overlap a wide table, and it
   collapses to a horizontal strip on a narrow screen instead of eating the
   content width. */
/* The rail costs width, and owner item 3 was a complaint ABOUT lost width, so
   it only stays vertical where there is room to spare. Below 1280px it becomes
   a horizontal strip above the content and the table gets the whole viewport,
   which is the common laptop case. */
.shell { display: grid; grid-template-columns: 186px minmax(0, 1fr); align-items: start; }
.sidenav { position: sticky; top: 0; display: flex; flex-direction: column; gap: 2px;
  padding: 22px 10px; }
.sidenav a { display: block; padding: 8px 12px; border-radius: 7px; font-weight: 700;
  color: var(--ink); font-size: 14px; }
.sidenav a:hover { background: #fff; }
.sidenav a.on { background: var(--green); color: #fff; }
/* In-shell pages get their horizontal position from the grid cell, so drop the
   auto margins that would re-centre them inside it. The width CAP is left
   alone: the tracking page deliberately keeps the 1080px reading measure, and
   only pages that ask for it (.wide) run the full width. */
.wrap.in-shell { margin-left: 0; margin-right: 0; }
.wrap.in-shell.wide { max-width: none; }
@media (max-width: 1280px) {
  .shell { grid-template-columns: minmax(0, 1fr); }
  .sidenav { position: static; flex-direction: row; flex-wrap: wrap; padding: 10px 20px 0; }
}
/* An in-shell page is already inset by the rail, so it does not need the outer
   20px gutter as well - that was pure lost width on the very table item 3 was
   about. */
.wrap.in-shell { padding-left: 14px; padding-right: 18px; }

/* ------- dashboard: rep chips + filter rows + activity (items 1-4, 6) ------- */
.chiprow { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin: 6px 0 10px; }
/* One CLICK, not a dropdown plus a select (owner item 1). */
a.repchip { display: inline-flex; gap: 6px; align-items: center; border: 1.5px solid var(--line);
  background: #fff; border-radius: 20px; padding: 4px 12px; font-size: 13px; font-weight: 700;
  color: var(--ink); }
a.repchip:hover { border-color: var(--green); }
a.repchip.on { background: var(--green); border-color: var(--green); color: #fff; }
a.repchip .n { font-weight: 400; opacity: .75; font-size: 12px; }
a.repchip.me::before { content: "★"; font-size: 11px; }
.filters.two { flex-wrap: wrap; }
.filters .fgroup { display: flex; gap: 4px; align-items: center; }
.filters .fgroup label { font-size: 12px; color: var(--muted); }
/* Activity beside the table (owner item 4) only where that does not fight item
   3. A 320px panel plus a 186px rail on a 1366px laptop left the proposals
   table NARROWER than before this rework, which is the opposite of what was
   asked, so below 1500px the panel moves under the table and keeps its full
   width there. */
.dash-cols { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 18px;
  align-items: start; }
@media (max-width: 1500px) { .dash-cols { grid-template-columns: minmax(0, 1fr); } }
.panel { background: #fff; border: 1.5px solid var(--line); border-radius: 10px; padding: 12px 14px; }
.panel h2 { margin-top: 0; font-size: 15px; }
.feed { list-style: none; margin: 0; padding: 0; }
.feed li { border-top: 1px solid var(--line); padding: 7px 0; font-size: 13px; }
.feed li:first-child { border-top: 0; }
.feed .when { color: var(--muted); font-size: 11.5px; }
.due-strip { background: var(--amber-soft); border: 1.5px solid #e2c65a; border-radius: 8px;
  padding: 8px 12px; margin-bottom: 10px; font-size: 13.5px; }
.due-strip a { font-weight: 700; }
.numodd { color: var(--muted); font-size: 11px; cursor: help; }

/* ------- follow-ups (item 12) ------- */
.fup { display: grid; grid-template-columns: 92px 1fr auto; gap: 8px; align-items: baseline;
  border-top: 1px solid var(--line); padding: 7px 0; font-size: 13.5px; }
.fup:first-child { border-top: 0; }
.fup.overdue .fup-date { color: #a1231f; font-weight: 700; }
.fup.done { opacity: .62; }
.fup.done .fup-note { text-decoration: line-through; }
.fup-date { font-variant-numeric: tabular-nums; }

/* ------- rich text toolbar (item 26) ------- */
.rt-bar { display: flex; gap: 3px; flex-wrap: wrap; padding: 4px; border: 1.5px solid var(--line);
  border-bottom: 0; border-radius: 8px 8px 0 0; background: #fbfcfe; }
.rt-bar button { border: 1px solid transparent; background: none; border-radius: 5px; cursor: pointer;
  font: inherit; font-size: 13px; min-width: 28px; padding: 3px 7px; color: var(--ink); }
.rt-bar button:hover { background: var(--green-soft); border-color: var(--line); }
.rt-bar button.on { background: var(--green); color: #fff; }
.rt-bar .sep { width: 1px; background: var(--line); margin: 2px 3px; }
.rt-area { min-height: 90px; padding: 9px 11px; border: 1.5px solid var(--line);
  border-radius: 0 0 8px 8px; background: #fff; }
.rt-area:focus { outline: 2px solid var(--green); border-color: var(--green); }
.rt-area p:first-child { margin-top: 0; }
.rt-area p:last-child { margin-bottom: 0; }
.rt-area ul, .rt-area ol { padding-left: 22px; }

/* ------- metrics + address book ------- */
.mgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.kv { display: grid; grid-template-columns: 1fr auto; gap: 6px; font-size: 13.5px; }
.kv div { border-top: 1px solid var(--line); padding: 5px 0; }
.kv div:nth-child(-n+2) { border-top: 0; }
.bar { height: 7px; background: var(--green-soft); border-radius: 4px; overflow: hidden; }
.bar > i { display: block; height: 100%; background: var(--green); }

/* A pasted table has to be legible where it lands, or keeping the tags in the
   sanitiser buys nothing. Deliberately plain: the client-facing look comes from
   viewer.css, this is just the authoring view. */
.rt-area table, .rt-body table { border-collapse: collapse; width: 100%; margin: 8px 0; }
.rt-area th, .rt-area td, .rt-body th, .rt-body td {
  border: 1px solid var(--line); padding: 4px 7px; text-align: left; vertical-align: top; }
.rt-area th, .rt-body th { background: var(--green-soft); font-weight: 700; }
