/* ============================================================
   Life Base — v3 styles: Board (canvas) + Chat + app-nodes
   ============================================================ */

/* ---- the board's tools live in the corner, and wait to be asked ----
   They were centred at the bottom of the window, the way Figma docks its
   toolbar. Figma can afford that: its canvas is the whole window and the
   bar is the app. Ours floats over a board whose middle and bottom is
   where the work is, so a permanent pill sat on top of the thing being
   used — the commonest complaint about board pages.

   Top right, and quiet until reached for. At rest it is faint enough to
   stop competing with the board and solid enough that nobody has to
   remember it is there; the pointer anywhere near it brings it fully up.
   The hover target is deliberately BIGGER than the pill (the padding on
   the container), so it lights up before the pointer arrives instead of
   at the moment it lands — the same trick Notion's corner controls use.

   `left:auto` is not needed: the old centring lived on this rule and is
   simply gone. */
#board-ribbon{
  display:none;position:fixed;top:0;right:0;
  padding:14px 18px 22px 26px;align-items:center;z-index:60;
  opacity:.3;transition:opacity .16s ease-out;
}
#board-ribbon.on{display:flex}
#board-ribbon:hover,#board-ribbon:focus-within{opacity:1}
/* while a picker is open the toolbar is being used — it does not fade out
   from under the menu it opened (board.js sets this) */
#board-ribbon.busy{opacity:1}
.board-pill.docked{position:static;transform:none;padding:5px 7px;
  background:rgba(255,255,255,.96);backdrop-filter:blur(16px);
  border:1px solid #e3e1de;border-radius:11px;box-shadow:0 6px 22px rgba(20,14,6,.14)}
.board-pill.docked .bp-btn{width:30px;height:30px;border-radius:6px}
.board-pill.docked .bp-btn svg{width:18px;height:18px}

/* ================= BOARD ================= */
/* #page-body carries cursor:text for stacked pages (styles.css, ID
   specificity) — on board pages the same element hosts the canvas,
   so re-pin the arrow cursor here at equal-or-higher specificity */
#page-body.board-host{cursor:default}
/* =========================================================================
   THE BOARD'S GROUND — WHITE. Settled 2026-07-28 after a full comparison.
   Do not "restore" a warm board. Read this first.
   Full history: high-level/iterations/00-running-log.md, entries (b)–(e).

   ---- THE DECISION ----
   White is the platform surface. It is congruent with the rest of the
   app — stacked pages, chat pages, panels and chrome are all white or
   near-white — and a board that matches them reads as one product
   instead of one screen with a different floor. That congruence is
   worth more than what a tinted board buys, and it was chosen with the
   trade-off fully measured and on the table.

   The trade-off is real and is stated here so nobody has to rediscover
   it. It is NOT a defect to be fixed by changing this rule.

   ---- WHAT THE TRADE-OFF IS, measured ----
   App-node paper is `#fbfaf8` = 95.7% relative luminance. White is 100%.
   So the board is BRIGHTER than the brightest pixel of a cream app, and
   the eye takes the brightest thing in view as its white point: it
   anchors on the board, judges the app's cream against true white, and
   the app reads very slightly washed. On a ground BELOW the app's paper
   (~84%) the anchor flips to the app and the same pixels read clean.
   That is the entire mechanism, and it is why the effect is real rather
   than imagined.

   ---- WHERE THE FIX GOES INSTEAD: the app-nodes, not the board ----
   The right answer to a pale app on a white board is to give THAT APP an
   edge — which is per-app work, incremental, and far cheaper than
   re-tinting the whole platform for every page. Two levers, in order:
     1. The node edge treatment below — a tight, dense shadow and a
        stronger hairline than a warm board would need. On white the edge
        is all a pale app has, so it is deliberately heavier here.
     2. The app's own palette, when an app is next worked on. An app
        whose surface is nearly white will always struggle against a
        white board; an app that carries its own color never does.
   Cost check, and it is the reason for the call: fixing N apps once is
   smaller than tinting every board page forever.

   ---- WHAT WAS BUILT AND COMPARED (so it isn't redone) ----
   Five grounds, mounted with the REAL node modules and flipped in place
   with the node shadow held equal. Rig: `lab-ground-compare.html`.
     · WHITE + a stronger node edge ..... shipped. The choice.
     · "barely warm" (#fbfaf8 top) ...... dead level with the app paper,
       so neither wins. Half-worked, which is worse than failing.
     · white + a warm glow under each app  local warmth only; not enough.
     · white + a warm MAT under each app   best of the white variants,
       but hangs furniture off every node.
     · warm board (#efece8 → #e3ded6) ... won on pure legibility of the
       apps, and was passed over for the congruence reason above. Also
       measured near-achromatic (chroma 7–13) — it read dull.
   Higher-chroma warm variants ("sand", "clay") were built and are still
   in the lab if the question is ever genuinely reopened. It is not open.

   ---- NOT the fix for this; do not re-litigate as ground problems ----
   The glyph-thinning opt-out below (a separate and larger cause of
   "washed out"), and `isolation` on .b-obj (overlapping apps' internal
   layers interleaving). Both fixed independently.

   ---- implementation note ----
   ONE background layer. applyView() still writes both background slots,
   so a second layer needs no JS change — and a second layer given only
   one size value gets tiled at 22px, which is the bug that guards
   against. The dots are warmed slightly off pure grey so the surface
   doesn't read cold.
   ========================================================================= */
.board-host{
  position:relative;overflow:hidden;background-color:#fff;
  background-image:radial-gradient(circle,#e0dedb 1px,transparent 1.35px);
  background-size:22px 22px;
  background-position:0 0;
  box-shadow:inset 0 1px 3px rgba(60,50,40,.04);
  cursor:default;flex:1;min-height:100%;
}
.board-viewport{position:absolute;left:0;top:0;transform-origin:0 0}

/* floating pill toolbar */
.board-pill{
  position:absolute;top:14px;left:50%;transform:translateX(-50%);z-index:30;
  display:flex;align-items:center;gap:2px;
  background:#fff;border:1px solid #e8e6e3;border-radius:999px;
  padding:4px 6px;box-shadow:0 6px 20px rgba(0,0,0,.12),0 1px 4px rgba(0,0,0,.07);
}
.bp-btn{
  width:32px;height:32px;border-radius:50%;
  display:inline-flex;align-items:center;justify-content:center;
}
.bp-btn svg{width:17px;height:17px}
.bp-btn:hover{background:#f0efed}
.bp-btn:active{background:#e4e3e1}
.bp-sep{width:1px;height:18px;background:#e5e3e1;margin:0 5px}

/* objects */
/* Every object is its own sealed layer. Without this, an object is
   `position:absolute; z-index:auto`, which does NOT start a stacking
   context — so the nearest one is `.board-viewport` (it has a transform),
   and EVERY z-index inside EVERY app competes in that one global pile.
   The typewriter stacks up to 40 internally; the kanban up to 90. Overlap
   them and their layers interleave: the typewriter's lamp and desk paint
   through the kanban, the kanban's cards paint through the typewriter,
   and the result reads exactly like one of them has gone translucent.
   `isolation:isolate` seals each object's internals inside its own box
   without touching layout or the order objects paint in. It also means a
   future app can use any z-index it likes and never leak. */
.b-obj{position:absolute;user-select:none;cursor:grab;isolation:isolate}
.b-obj.sel{z-index:5}
.board-host.grabbing, .board-host.grabbing .b-obj,
.board-host.grabbing .b-editable, .board-host.grabbing .b-shape-text{cursor:grabbing}
.b-editable{
  width:100%;height:100%;outline:none;overflow:hidden;
  font-family:var(--content-font);font-size:15px;line-height:1.45;
  color:#1e1e1e;user-select:text;cursor:inherit;padding:2px;
  /* board text is a document too (see styles.css "the page is a
     document, not an interface") — undo the global glyph-thinning
     so stickies and text objects read at full weight, like .c-body */
  -webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto;
}
.b-editable.editing{cursor:text}
/* NO BOX AROUND BOARD TEXT — just the words and a caret.
   `outline:none` was only ever written as `.c-body > .pm-editor`, which
   is a stacked page. On a board the editor mounts inside `.b-editable`,
   missed that rule, and wore the browser's DEFAULT focus ring: a blue
   rectangle that sat there for the rest of the session because edit mode
   never ended either (see the focusout note in board.js). */
.b-editable > .pm-editor,
.b-shape-text > .pm-editor{outline:none;white-space:pre-wrap;word-break:break-word}
/* a text object is sized to its text, so nothing is ever clipped out of
   sight — the height follows the words (fitTextHeight in board.js) */
.b-text .b-editable{background:transparent;overflow:visible}
.b-sticky{
  border-radius:6px;box-shadow:0 4px 14px rgba(0,0,0,.13);
}
.b-sticky-body{width:100%;height:100%;padding:12px 12px 10px;overflow:hidden}
.b-sticky .b-editable{font-size:14px;color:#4a3f16}
.b-shape svg{display:block}
/* built-in centered text inside every shape */
.b-shape-text{
  position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  text-align:center;padding:10%;overflow:hidden;outline:none;
  font-family:var(--content-font);font-size:15px;line-height:1.45;
  color:#1e1e1e;cursor:inherit;
}
.b-shape-text.editing{cursor:text;user-select:text}
.b-table{border-collapse:collapse;width:100%;height:100%;background:#fff}
.b-table td{
  border:1px solid #d8d5d2;padding:6px 9px;min-width:60px;
  font-family:var(--content-font);font-size:14px;outline:none;user-select:text;cursor:text;
}
.b-media{border-radius:4px;overflow:hidden}
.b-media-img{width:100%;height:100%;object-fit:cover;display:block;pointer-events:none}
.b-node-body{
  width:100%;height:100%;overflow:hidden;position:relative;
  /* ---- AN APP CANNOT DRAW OUTSIDE ITS NODE (2026-08-13) ----
     `contain: paint` makes this box the containing block for every
     absolutely AND FIXED positioned descendant. That second half is the
     point: `position: fixed` inside an app is measured against the browser
     window, so a sheet, a toast or a drag ghost written for a page that
     owned its tab lands somewhere else entirely on a board — and if the
     node happens to be scaled (fitScale), it is measured against the
     SCALED box and drawn at the wrong size on top of that. That is what
     "the Today's Tasks app-node is completely broken, it's glitching, a
     bit of it shows up but it's not really showing up" was.

     The task manager's three escapees are fixed at the source (its build
     script now refuses `position: fixed` outright). This is the wall
     behind that fix, so the next app to arrive with one is a contained
     mistake rather than a broken surface. Costs nothing: this box already
     clipped, which is the only other thing paint containment does. */
  contain:paint;
  background:#fbfaf8;border-radius:10px; /* the canonical app-node paper */
  /* An app-node is CONTENT, not chrome. The app thins every glyph globally
     (styles.css) and the page title, the page body, the chat and board text
     each opt back out — app-node interiors were the one content surface
     that got missed, so every word inside every node rendered thinner and
     greyer than the identical code does in the prototype lab. That is most
     of what "washed out" meant. (running log 2026-07-28b §1) */
  -webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto;
  /* elevation, same weight the prototype lab gives its stage: a node is
     a lit object resting on the canvas, not a rectangle pasted on it.
     The shadow is warm-tinted (brown, not black) so it deepens the
     scene's own palette instead of greying it.
     Tightened and darkened for the white ground: a big soft shadow needs
     something warm to fall onto, and on white it reads as grey haze
     rather than depth. A closer, denser one reads as lift. The hairline
     is heavier here than it would be on a warm board — with white doing
     none of the separating, the edge is all a pale app has. */
  box-shadow:0 18px 44px rgba(50,40,30,.20), 0 3px 10px rgba(50,40,30,.12);
  /* the same hairline the page's blocks wear (styles-v6.css). It was
     twice this weight and warm, which on a board full of white apps read
     as a box drawn around each one rather than as the edge of the app. */
  outline:1px solid rgba(0,0,0,.06);
  /* THE APP IS NOT A HANDLE (2026-08-13). This used to be `cursor:grab`
     until the node was selected, and the cursor was telling the truth:
     dragging here really did move the app. It no longer does — the
     interior belongs to the app from the first press — so the grab hand
     would now be a promise the board does not keep. The edge strips
     below are where the hand belongs.

     `user-select:text` for the same reason, and unconditionally: a live
     app's words were selectable only after you had selected the node,
     which is a state nobody can see. .b-obj sets user-select:none for
     shapes and stickies; an app must not inherit it. */
  cursor:auto;
  user-select:text;-webkit-user-select:text;
}
/* ---- the edge strips: where "move me" lives ----
   11px outside the node's border and 3px inside it, on all four sides,
   present whether or not the node is selected. Corners are left to the
   selection handles, which stack above these when they exist. */
.b-nodegrab{position:absolute;z-index:4;cursor:move;border-radius:3px;
  background:transparent;transition:background-color .12s ease;}
.b-nodegrab:hover{background:rgba(74,155,232,.30);}
.b-nodegrab-n{left:14px;right:14px;top:-11px;height:14px;}
.b-nodegrab-s{left:14px;right:14px;bottom:-11px;height:14px;}
.b-nodegrab-w{top:14px;bottom:14px;left:-11px;width:14px;}
.b-nodegrab-e{top:14px;bottom:14px;right:-11px;width:14px;}
/* inside the app there is no board to move it on, and in full view the
   strips would be a frame drawn around the whole window */
.board-host.node-focus .b-nodegrab,
.board-host.board-fullview .b-nodegrab{display:none;}

/* ---- "enter the node" — focus mode (running log 2026-07-28b, idea b) ----
   The prototype lab is one object, at its designed size, at 100%, with calm
   space around it. The board can't BE that — it's a place with many objects
   at any scale — so it gets a gesture that produces it on demand. Nothing
   here triggers on hover: a canvas that moves its own camera because the
   pointer crossed something is the most disorienting thing a canvas can do. */
.board-host.node-focus .b-obj{
  opacity:.12;transition:opacity .26s ease;pointer-events:none;
}
.board-host.node-focus .b-obj.node-focused{opacity:1;pointer-events:auto}

/* ---- FULL VIEW IS THE WHOLE WINDOW (2026-08-06) ----
   Opening a node used to zoom the camera inside the board's own column,
   so the app was framed by whatever was left after the sidebar and the
   canvas padding had taken theirs — and the dotted ground stayed on show
   all around it. It read as a bigger node, not as a full view.

   The host is pinned over the window instead, and its ground goes dark
   for the duration. What is left beside a node whose shape doesn't match
   the window's now reads as a darkened room rather than as margin, which
   is the difference between "still on the board" and "inside the app".

   #page-body carries an ID rule (styles.css) — hence the ID here, so this
   wins without !important. */
#page-body.board-host.board-fullview{
  position:fixed;inset:0;z-index:1200;border-radius:0;
  background-color:#17140f;background-image:none;box-shadow:none;
  animation:b-fullview-in .22s ease-out;
}
@keyframes b-fullview-in{from{opacity:.4}to{opacity:1}}
/* nothing else on the board competes with the app while it is open.
   The other objects were dimmed to 12%, which was right over a white
   ground and is wrong over a dark one — a pale node at 12% on near-black
   is a grey smudge beside the app. In full view they go entirely. */
#page-body.board-host.board-fullview .b-obj:not(.node-focused){opacity:0}
#page-body.board-host.board-fullview .board-pill,
body.lb-board-fullview #board-ribbon{display:none}
/* the selection frame is chrome; in focus the node is meant to read as the
   lab's stage, not as a selected object */
.board-host.node-focus .b-selbox,
.board-host.node-focus .b-tblctl,
.board-host.node-focus .b-swatches{opacity:0;pointer-events:none;transition:opacity .2s ease}
.board-host.node-focus .board-pill{opacity:.2;transition:opacity .2s ease}
.board-host.node-focus .board-pill:hover{opacity:1}
/* A mode needs an obvious way out, stated plainly and NEVER IN THE WAY.
   It was centred at the foot of the board, which on a node that now fills
   the window is on top of the app — the typewriter's space bar, exactly.
   Same corner and same quiet-until-reached-for rule as the toolbar and as
   a page block's way out (styles-v6, `.nb-open-exit`). */
.b-focus-exit{
  position:absolute;top:16px;right:18px;z-index:40;
  display:flex;align-items:center;gap:7px;
  background:rgba(252,251,249,.92);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
  border:1px solid rgba(60,50,40,.10);border-radius:999px;
  padding:6px 13px;box-shadow:0 10px 30px rgba(50,40,30,.14);
  font-size:12px;color:#6b6763;cursor:pointer;user-select:none;
  opacity:0;transition:opacity .26s ease;
}
.b-focus-exit.on{opacity:.42}
.b-focus-exit.on:hover{opacity:1;background:#fff;color:#3a3836}
.b-focus-exit kbd{
  font:inherit;font-size:11px;background:rgba(60,50,40,.07);
  border-radius:4px;padding:1px 5px;color:#4d4a46;
}
/* "Open" — the discoverable door in, and the board's half of the rule a
   page block follows (styles-v6, "THE TOP STRIP").

   INSIDE the node's top-right corner, shown when the pointer is in the
   node's top strip. Both halves matter:

     · INSIDE, because a control that floats outside the node has empty
       canvas between it and the node, and crossing that gap drops the
       hover. That is what made this button run away from the cursor when
       it sat at -36px.
     · TOP STRIP, not plain hover, because it covers the app. On hover it
       was up the entire time the node was being used, which is noise you
       cannot dismiss. Reach the top of the node and it is there; work
       anywhere else in it and there is nothing on screen but the app.

   The button is deep inside the strip that reveals it, so moving toward
   it can only make the test more true. */
.b-openbtn{
  position:absolute;right:8px;top:8px;
  display:inline-flex;align-items:center;gap:6px;
  height:28px;padding:0 11px 0 9px;border-radius:999px;
  background:rgba(255,255,255,.96);backdrop-filter:blur(10px);
  border:1px solid #e2dfdb;color:#4d4a46;cursor:pointer;
  font-size:12px;font-weight:500;letter-spacing:.01em;white-space:nowrap;
  box-shadow:0 2px 10px rgba(50,40,30,.16);
  opacity:0;pointer-events:none;
  transition:opacity .14s ease, background-color .12s ease;
}
.b-openbtn svg{width:14px;height:14px;display:block;flex:none}
/* ---- ON THE EDGE, NOT ON THE APP (2026-08-13) ----
   `.b-node.sel` used to reveal it too, and that was survivable while a
   node was only selected deliberately. It is not survivable now: under
   "the app is not a handle" a press ANYWHERE inside an app selects its
   node, so simply using an app left Open sitting on top of it for as long
   as you kept using it. Reported as "why are the options like Open showing
   when I'm using the app — it should only show on hovering on the edges".

   `nb-top` is exactly that: the pointer is in the node's top strip. Reach
   for the edge and the door appears; work in the middle and there is
   nothing on screen but the app. */
.b-node.nb-top .b-openbtn{opacity:1;pointer-events:auto}
.b-openbtn:hover{background:#fff;color:#2f2d2b}
/* inside the app, the door you came through is not something you need */
.board-host.node-focus .b-openbtn{opacity:0;pointer-events:none}

/* a selected app-node: marked, but not fenced. The frame's grab handles
   appear only when the pointer is actually on the frame — while you're
   using the app they'd be pure noise, and reaching for one is a rare,
   deliberate act. The invisible drag strips stay live throughout, so
   moving the node never waits for the handles to show. */
.b-selbox.quiet{border-color:rgba(74,155,232,.45)}
.b-selbox.quiet .b-hd{opacity:0;transition:opacity .13s ease}
.b-selbox.quiet.near{border-color:#4a9be8}
.b-selbox.quiet.near .b-hd{opacity:1}
/* selection chrome */
.b-selbox{
  position:absolute;z-index:6;pointer-events:none;
  border:1.5px solid #4a9be8;border-radius:2px;
}
.b-hd{
  position:absolute;width:10px;height:10px;background:#fff;border:1.5px solid #4a9be8;
  border-radius:50%;pointer-events:auto;
}
.b-hd-nw{left:-6px;top:-6px;cursor:nwse-resize}
.b-hd-n{left:calc(50% - 5px);top:-6px;cursor:ns-resize}
.b-hd-ne{right:-6px;top:-6px;cursor:nesw-resize}
.b-hd-e{right:-6px;top:calc(50% - 5px);cursor:ew-resize}
.b-hd-se{right:-6px;bottom:-6px;cursor:nwse-resize}
.b-hd-s{left:calc(50% - 5px);bottom:-6px;cursor:ns-resize}
.b-hd-sw{left:-6px;bottom:-6px;cursor:nesw-resize}
.b-hd-w{left:-6px;top:calc(50% - 5px);cursor:ew-resize}
/* frame strips: the move-drag surface along each edge of the selection
   box. They sit just outside the border (clear of the object's live
   interior) and leave the corners to the resize handles. */
.b-mv{position:absolute;pointer-events:auto;cursor:move}
.b-mv-n{left:10px;right:10px;top:-6px;height:7px}
.b-mv-s{left:10px;right:10px;bottom:-6px;height:7px}
.b-mv-e{top:10px;bottom:10px;right:-6px;width:7px}
.b-mv-w{top:10px;bottom:10px;left:-6px;width:7px}
/* connector endpoint reorient handles */
.b-end{cursor:grab}
.b-marquee{
  position:absolute;z-index:25;background:rgba(74,155,232,.10);
  border:1px solid rgba(74,155,232,.5);pointer-events:none;
}

/* recolor strip */
.b-swatches{
  position:absolute;z-index:30;display:flex;gap:6px;background:#fff;
  border:1px solid #e8e6e3;border-radius:999px;padding:6px 8px;
  box-shadow:0 4px 14px rgba(0,0,0,.10);
}
.b-swatch{
  width:20px;height:20px;border-radius:50%;border:1px solid rgba(0,0,0,.14);cursor:pointer;
}
.b-swatch:hover{transform:scale(1.12)}
.b-swatch.sel{box-shadow:0 0 0 2px #fff,0 0 0 4px #4a9be8}

/* shapes popover (Free Form style) */
.sp-pop{
  position:absolute;z-index:40;width:350px;background:#fff;
  border:1px solid #e6e4e1;border-radius:12px;
  box-shadow:0 14px 40px rgba(0,0,0,.16),0 2px 8px rgba(0,0,0,.07);
  padding-bottom:10px;
}
.sp-caret{
  position:absolute;top:-7px;left:50%;transform:translateX(-50%);
  width:14px;height:7px;overflow:hidden;
}
.sp-caret::before{
  content:"";position:absolute;left:1px;top:1px;width:10px;height:10px;background:#fff;
  border:1px solid #e6e4e1;border-radius:2px 0 0 0;transform:rotate(45deg);transform-origin:center;
}
/* the pill is docked at the FOOT of the board, so a popover almost
   always opens upward and its caret has to hang off the bottom edge
   pointing down. Same 10px square rotated 45deg, flipped. */
.sp-caret.down{top:auto;bottom:-7px}
.sp-caret.down::before{top:-4px;border-radius:0 0 2px 0}
.sp-search{
  margin:10px 12px 8px;height:34px;border:1px solid #b7d4ee;border-radius:8px;
  display:flex;align-items:center;padding:0 10px;gap:8px;
}
.sp-search:focus-within{border-color:#8ab8e6;box-shadow:0 0 0 3px rgba(122,167,217,.16)}
.sp-search svg{width:14px;height:14px;flex:none}
.sp-search input{flex:1;border:0;outline:none;font-size:13px;color:#242424;background:transparent}
.sp-search input::placeholder{color:#a19f9d}
.sp-grid{
  display:grid;grid-template-columns:repeat(4,1fr);gap:2px;
  max-height:340px;overflow-y:auto;padding:2px 10px;
}
.sp-cell{
  aspect-ratio:1;display:flex;align-items:center;justify-content:center;
  border-radius:8px;padding:10px;
}
.sp-cell:hover{background:#f4f3f2}
.sp-cell svg{width:100%;height:100%}

/* node picker (same Apple popover anatomy as the / panel:
   440x362, centered search pill, 160px category column, and one
   written vertical list of app rows — icon left, name beside it;
   row styles are the shared .ip-* set in styles-v2.css) */
.np-pop{
  width:440px;height:362px;padding:0;display:flex;flex-direction:column;
  background:rgba(250,250,249,.97);backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);
  border:1px solid rgba(0,0,0,.09);border-radius:17px;
  box-shadow:0 18px 55px rgba(0,0,0,.19),0 3px 12px rgba(0,0,0,.08);
}
.np-searchwrap{flex:none;display:flex;justify-content:center;padding:9px 12px 0}
.np-search{
  width:200px;height:26px;border:1px solid #d9d9d7;border-radius:13px;background:#fff;
  display:flex;align-items:center;padding:0 9px;gap:6px;
}
.np-search:focus-within{border-color:#7cb3ec;box-shadow:0 0 0 3.5px rgba(0,122,255,.18)}
.np-search svg{width:12px;height:12px;flex:none}
.np-search input{flex:1;min-width:0;border:0;outline:none;font-size:12.5px;color:#242424;background:transparent;padding:0}
.np-search input::placeholder{color:#9c9a97}
.np-body{display:flex;flex:1;min-height:0;margin-top:9px;border-top:1px solid rgba(0,0,0,.05)}
.np-cats{
  width:160px;flex:none;border-right:1px solid rgba(0,0,0,.06);
  padding:8px 6px;overflow-y:auto;
}
.np-cat{
  height:20px;display:flex;align-items:center;padding:0 18px;border-radius:10px;
  font-size:13px;color:#3f3d3b;white-space:nowrap;margin-bottom:2px;cursor:default;
}
.np-cat:hover{background:rgba(0,0,0,.045)}
.np-cat.sel{background:#00bde5;color:#fff;font-weight:600}
.np-content{flex:1;min-width:0;overflow-y:auto;padding:8px}
.np-empty{font-size:12px;color:#a19f9d;text-align:center;padding:26px 10px}

/* ================= CHAT ================= */
/* same #page-body cursor:text override as the board — the chat
   surface is read/click, the textarea sets its own I-beam */
#page-body.chat-host{cursor:default}
.chat-host{flex:1;min-height:100%;background:#fff}
.chat-root{display:flex;flex-direction:column;height:100%}
.chat-list{
  flex:1;overflow-y:auto;padding:28px 24px 30px;
  /* the conversation is reading matter, same as the page body — undo
     the global glyph-thinning here; header/composer stay interface */
  -webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto;
}
.chat-empty{
  height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:8px;color:#a19f9d;
}
.chat-empty-t{font-size:17px;color:#6e6e6e;font-weight:500}
.chat-empty-s{font-size:13px}
.chat-empty-s b{color:#7a7a7a}

/* ---- a reply is set exactly like a page (2026-08-06) ----
   This was Charter at 17.5px/1.65 — a serif, sized up to compensate for
   Charter's short lowercase. Two typographic systems in one product,
   and moving between a page and a chat page felt like moving between
   two apps. Now it is the page's own face, size and leading, read from
   the same variables, so the two can never drift apart again.

   ---- and the LEADING is the chat's own, not the page's ----
   A page is scanned: headings, bullets, short lines, and 1.5 is right
   for that. A reply is read straight through in long paragraphs, and
   prose wants a little more air between its lines than notes do. 1.6 is
   the difference, and it is the one number here that deliberately does
   NOT come from the page. */
.chat-msg{
  max-width:700px;
  /* 26px between messages, and the copy button below is out of flow, so
     this number IS the gap. It used to be 20px plus 28px of invisible
     button, which is why a reply sat 80px under the question. */
  margin:0 auto 26px;position:relative;
  font-family:var(--content-font);
  font-size:var(--lb-body);line-height:1.6;color:var(--content-ink);
}
.chat-msg.user{display:flex;flex-direction:column;align-items:flex-end}
.chat-bubble{
  background:#f3f3f3;border-radius:18px;padding:9px 16px;
  max-width:72%;white-space:pre-wrap;word-break:break-word;
}
.chat-msgctx{display:flex;gap:5px;margin-top:5px;flex-wrap:wrap;justify-content:flex-end}
.chat-minichip{
  font-family:-apple-system,"Segoe UI",sans-serif;font-size:11px;color:#8a8886;
  background:#f7f7f7;border:1px solid #ececec;border-radius:999px;padding:2px 8px;
}
.chat-text{white-space:pre-wrap;word-break:break-word}
.chat-text.pending{color:#a19f9d;animation:chat-pulse 1.1s ease-in-out infinite}
@keyframes chat-pulse{0%,100%{opacity:.4}50%{opacity:1}}
/* ---- the copy button lives IN the gap, it does not create one ----
   It is invisible until you hover the message, and it was sitting in
   normal flow the whole time: 22px of button plus 6px of margin under
   every single message, showing nothing. Two thirds of the 80px between
   a question and its answer was this. Out of flow, it costs nothing
   when hidden and appears inside the 26px already between messages. */
.chat-copy{
  position:absolute;bottom:-24px;
  display:inline-flex;align-items:center;gap:5px;padding:3px 4px;
  border-radius:6px;background:transparent;border:0;cursor:pointer;
  font-family:-apple-system,"Segoe UI",sans-serif;font-size:12px;color:#6e6e6e;
  opacity:0;transition:opacity .12s;
}
/* it belongs to the edge its message is written from */
.chat-msg.user .chat-copy{right:0}
.chat-msg.assistant .chat-copy{left:0}
.chat-msg:hover .chat-copy, .chat-copy.copied{opacity:1}
.chat-copy svg{width:14px;height:14px}
.chat-copy:hover{background:#f3f3f3;color:#242424}
.chat-copy.copied{color:#1a7f37}

/* rendered markdown inside assistant messages */
/* same principle as the page: a heading takes its own height above
   itself and almost nothing below. Ratios rather than pixels, so the
   whole reply scales together if the body size ever moves again. */
.chat-text p{margin:0 0 12px}   /* prose, not notes: wider than the page's 8px */
.chat-text p:last-child{margin-bottom:0}
.chat-text .md-h1{font-size:1.45em;font-weight:700;margin:1.1em 0 .3em;line-height:1.3}
.chat-text .md-h2{font-size:1.22em;font-weight:700;margin:1.1em 0 .3em;line-height:1.3}
.chat-text .md-h3{font-size:1.06em;font-weight:700;margin:1em 0 .3em;line-height:1.35}
.chat-text .md-h1:first-child,.chat-text .md-h2:first-child,.chat-text .md-h3:first-child{margin-top:0}
.chat-text strong{font-weight:700}
.chat-text em{font-style:italic}
.chat-text code{
  font-family:"SF Mono",Menlo,Consolas,monospace;font-size:.86em;
  background:#f3f2f1;border:1px solid #eceae7;border-radius:4px;padding:1px 5px;
}
.chat-text .md-code{
  background:#f7f6f4;border:1px solid #eceae7;border-radius:8px;
  padding:10px 14px;margin:0 0 12px;overflow-x:auto;
  font-family:"SF Mono",Menlo,Consolas,monospace;font-size:14.5px;line-height:1.55;
  white-space:pre-wrap;word-break:break-word;
}
.chat-text .md-code code{background:none;border:0;padding:0;font-size:inherit}
.chat-text .md-ul,.chat-text .md-ol{margin:0 0 12px;padding-left:1.7em}
.chat-text .md-ul li,.chat-text .md-ol li{margin:0;padding:var(--lb-listpad) 0}
.chat-text .md-quote{
  margin:0 0 12px;padding:2px 0 2px 15px;border-left:3px solid #d8d5d2;color:#5b5a59;
}
.chat-text a{color:#2b88d8;text-decoration:none;border-bottom:1px solid #bcd7f0}
.chat-text a:hover{border-bottom-color:#2b88d8}

.chat-inputwrap{
  flex:none;padding:6px 24px 18px;position:relative;
}
.chat-chips{
  display:none;gap:6px;flex-wrap:wrap;max-width:700px;margin:0 auto 8px;
}
.chat-chip{
  display:inline-flex;align-items:center;gap:6px;
  font-size:12px;color:#44413f;background:#f3f3f3;border:1px solid #e5e5e5;
  border-radius:999px;padding:4px 10px;
}
.chat-chip-x{color:#a19f9d;font-size:10px;cursor:pointer}
.chat-chip-x:hover{color:#242424}
.chat-field{
  max-width:700px;margin:0 auto;position:relative;
  border:1px solid #d9d9d7;border-radius:26px;background:#fff;
  box-shadow:0 2px 10px rgba(0,0,0,.05);
  display:flex;align-items:flex-end;padding:6px 8px 6px 18px;
}
.chat-field:focus-within{border-color:#b9b9b6}
.chat-input{
  flex:1;border:0;outline:none;resize:none;background:transparent;
  /* what you type and what comes back are the same words in the same
     face, so the composer is not a different kind of text from the reply */
  font-family:var(--content-font);
  font-size:var(--lb-body);line-height:var(--lb-lh);color:var(--content-ink);
  padding:8px 0;max-height:160px;
}
.chat-input::placeholder{color:#a19f9d}
.chat-send{
  width:34px;height:34px;border-radius:50%;background:#0d0d0d;flex:none;
  display:inline-flex;align-items:center;justify-content:center;cursor:pointer;border:0;
}
.chat-send svg{width:15px;height:15px}
.chat-send:disabled{opacity:.25;cursor:default}
/* while a reply is coming the same button is a stop button — a filled square
   in the circle, the shape everyone already reads as "stop this" */
.chat-send.stopping{background:#3a3a3a}
.chat-send.stopping:hover{background:#565656}

/* @ picker */
.chat-at{
  position:absolute;left:24px;right:24px;bottom:calc(100% - 2px);z-index:40;
  max-width:700px;margin:0 auto;background:#fff;
  border:1px solid #e5e3e1;border-radius:12px;max-height:300px;overflow-y:auto;
  box-shadow:0 10px 30px rgba(0,0,0,.14);padding:5px;
}
.chat-at-h{font-size:11px;color:#a19f9d;padding:6px 10px 4px}
.chat-at-row{
  display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:6px;
  font-size:13px;color:#242424;
}
.chat-at-row:hover{background:#f4f3f2}
.chat-at-kind{flex:none}
.chat-at-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.chat-at-sub{font-size:11px;color:#a19f9d;flex:none}
.chat-at-count{
  font-size:10.5px;color:#a19f9d;background:#f3f3f3;border-radius:999px;padding:1px 7px;flex:none;
}
.chat-at-empty{padding:12px;font-size:12px;color:#a19f9d;text-align:center}

/* ---- agentic chat: tool status ---- */
.chat-status{
  font-size:12.5px;color:#8a8886;font-style:italic;margin:0 0 7px;
  display:flex;align-items:center;gap:6px;
}
.chat-status::before{
  content:"";width:6px;height:6px;border-radius:50%;background:#c9a3e0;
  animation:chat-pulse 1.1s ease-in-out infinite;
}
@keyframes chat-pulse{0%,100%{opacity:.35;transform:scale(.85)}50%{opacity:1;transform:scale(1)}}

/* ---------- 04 §4.7 — alignment guides ---------- */
.b-guides{position:absolute;inset:0;pointer-events:none;z-index:30}
.b-guide{position:absolute;background:#4a9be8;opacity:.55}
.b-guide.v{width:1px;top:0;bottom:0;height:100000px}
.b-guide.h{height:1px;left:0;right:0;width:100000px}

/* ---------- 04 §4.5 — table row/col controls ---------- */
.b-tblctl{position:absolute;display:flex;gap:4px;z-index:25}
.b-tblctl button{
  font:11.5px -apple-system,"Segoe UI",sans-serif;color:#5b5a59;background:rgba(255,255,255,.95);
  border:1px solid #d8d5d2;border-radius:6px;padding:3px 8px;cursor:pointer;
  box-shadow:0 1px 4px rgba(0,0,0,.08);
}
.b-tblctl button:hover:not(:disabled){background:#f4f2f0}
.b-tblctl button:disabled{opacity:.4;cursor:default}


/* ---------- THE NAV'S APP PICKER (2026-08-19) ----------
   "Add app" at the foot of panel 2 opens the app's OWN picker, not a menu
   the nav invented. `.np-pop` and everything under it are the board's
   node-picker rules above; these are the few things a nav-hosted copy
   needs that a board-hosted one does not.

   It is `position:fixed` and lives on <body>, because the board's version
   is positioned inside the board's own host and this one has no host —
   and because a panel measured against anything that scrolls would drift
   away from the button it grew out of. */
.np-pop.lb-app-pop{ position: fixed; z-index: 1200; }

/* ---- THE STEM ----
   The picker grows OUT of the button that opened it. Without it a 440px
   panel simply appears near the bottom-left and nothing says which press
   produced it.

   Drawn as a path rather than as a rotated square. `.sp-caret` is a 10px
   square turned 45deg inside a 14x7 box with `overflow:hidden`, which is
   right at the TOP of a popover and reads as a rounded notch at the
   bottom of one — two clipped corners and a radius that points nowhere.
   Two straight lines meeting at a point is the shape everybody means by
   "stem". It sits 1px into the panel so the fill covers the panel's own
   border and the strokes continue the outline instead of crossing it. */
.np-stem{
  position: absolute; bottom: -8px; left: 0; width: 20px; height: 9px;
  display: block; pointer-events: none;
}
