/* app/app.css — the prototype's own styles, carved from task-manager.html
   2026-08-28. tokens/globals.css stays the only source of colour. */
* { box-sizing: border-box; }
/* THE LAYER ORDER (design audit A1, 2026-09-06). Every floating surface takes
   its z-index from here, bottom to top, so two surfaces never argue about who
   is on top and a new one is placed by picking its rung, not a number. The
   rungs are 8–12 apart so a scrim or a grip can sit one below or above its
   surface with calc(). The order it encodes: chrome, then the sidebar, then the
   SIDE PANELS (peek, discussion), then the hover card and the dashboard's
   floating tile, then DIALOGS (settings, share, trash, history, quick look),
   then CONFIRMS (a confirm is asked over a dialog), then toasts, then the item
   under the hand in a drag, then MENUS (the most recent interaction wherever
   they open — a menu opened from a dialog lands on top of it), then the
   lightbox, then sign-in, which covers everything.
   Before this the discussion panel (113) sat above the settings and trash
   dialogs (80, 95): their close and Restore buttons were drawn under it. And
   the phone's menu sheet (90) sat under its own scrim (123): a tap on any row
   hit the scrim and closed the menu. */
:root {
  --z-bar: 60;       /* the top bar, the selection bar */
  --z-nav: 70;       /* the sidebar; its grip +1; the phone drawer's scrim -1 */
  --z-side: 80;      /* the peek; the discussion panel +4 sits over the peek; a side panel's scrim -1 */
  --z-hover: 88;     /* the hover card */
  --z-float: 90;     /* a dashboard tile in flight */
  --z-dialog: 100;   /* settings, share, trash, history, quick look (+2); their scrim -1 */
  --z-confirm: 112;  /* a confirm or a small editor, over any dialog; its scrim -1 */
  --z-toast: 116;    /* the one notice */
  --z-drag: 118;     /* the item under the hand, over whatever it is dragged across */
  --z-menu: 125;     /* menus and sheets; the sheet's scrim -2; a sub-menu +2 */
  --z-lightbox: 130; /* the enlarged picture */
  --z-auth: 140;     /* sign-in, its box +1 */
}
/* THE TYPE RAMP AND THE RADIUS RAMP (design audit C1, 2026-09-06). Twenty text
   sizes and fourteen radii had grown by accretion, six of the sizes half a
   pixel from a neighbour; two panels that should match differed by a hair.
   Every font-size and border-radius in this file is now one of these steps.
   A new size is a step here, or it is not a size. The snaps that were made:
   8 -> 9; 9.5 and most 10.5 -> 10 (counts and capitals), the rest of 10.5 and
   11.5 -> 11/12 (small words); 12.5 -> 13 where it was body text or a
   control, 12 where it sat beside a 13 and the difference carried meaning
   (a sub-page row, a property's key, a card's property line, a list
   marker, code, the dashboard's dense tables); 13.5 -> 14; 14.5 -> 15;
   17 -> 18; 20 and 22 -> 21. Radii 1 and 3 -> 2, 5 -> 6, 7 and 9 -> 8,
   14 -> 16. */
:root {
  --t-micro: 9px;    /* a count in a badge, initials in a small avatar */
  --t-caps: 10px;    /* uppercase micro-labels: table heads, chips, weekday rows, rail counts */
  --t-label: 11px;   /* a field group's label, a note under a row, header controls */
  --t-small: 12px;   /* secondary text beside body, form labels, hints */
  --t-body: 13px;    /* body: rows, cells, buttons, fields, menus */
  --t-lead: 14px;    /* a dialog's title, a message, a callout */
  --t-large: 15px;   /* a sheet's rows, the third heading */
  --t-h3: 18px;      /* the second heading, the property dialog's title */
  --t-h2: 21px;      /* the first heading, a title in the peek and on the phone */
  --t-h1: 26px;      /* the page title */
  /* THE TWO SIZE MARKS a person can put on selected text: relative,
     because the mark rides on whatever block carries it — small in a heading
     is still a heading. The one em pair in the ramp, written here so it is
     written once. */
  --t-mark-sm: .85em;
  --t-mark-lg: 1.3em;
  --r-hair: 2px;     /* a hairline's ends, a flag, a swatch */
  --r-xs: 4px;       /* a tick, a small mark */
  --r-sm: 8px;       /* a chip's corner, a cell control, a bar — very rounded, his style (2026-10-09) */
  --r-md: 12px;      /* a button, a field, a menu row, a card's control */
  --r-lg: 15px;      /* a card, a box */
  --r-xl: 18px;      /* a menu, a panel */
  --r-2xl: 24px;     /* the page card, the sheet's top */
  --r-pill: 999px;   /* a chip, a switch */
}
/* The scrollbar gutter is always reserved, scrollbar or not. Without it, any
   view tall enough to scroll shifts the centred page 5px sideways the moment it
   appears — switching views read as the layout jumping. */
/* THE TYPEFACE (his style choice, 2026-10-09): Source Sans 3, shipped in
   app/fonts/ under the SIL Open Font License, so no outside service is
   contacted. Latin and extended Latin only; Arabic falls through to the
   device's own face, exactly as before. One variable file per style covers
   every weight. */
@font-face { font-family: "Source Sans 3"; font-style: normal; font-weight: 200 900; font-display: swap;
  src: url(fonts/SourceSans3-Upright-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: "Source Sans 3"; font-style: normal; font-weight: 200 900; font-display: swap;
  src: url(fonts/SourceSans3-Upright-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
@font-face { font-family: "Source Sans 3"; font-style: italic; font-weight: 200 900; font-display: swap;
  src: url(fonts/SourceSans3-Italic-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: "Source Sans 3"; font-style: italic; font-weight: 200 900; font-display: swap;
  src: url(fonts/SourceSans3-Italic-latin-ext.woff2) format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
html { scrollbar-gutter: stable; }
body {
  margin: 0;
  font-family: "Source Sans 3", ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  background: var(--ink); color: var(--paper); -webkit-font-smoothing: antialiased;
}
/* BOTH GAPS ARE ONE NUMBER; THE PAGE'S WIDTH ABSORBS THE SIDEBAR (his
   corrections, 2026-09-03: the space between the sidebar and the page
   "should be maintained", then "you fixed the left side but not the right
   side"). `margin: 0 auto` centered the page and made the START gap
   breathe; a width cap made the END gap breathe. Neither survives: the
   wrap fills the space between the sidebar and the window at this constant
   padding, so expanding or collapsing the sidebar moves only the page's
   WIDTH, never its distance from anything. This also IS the rule ("collapse
   .. to increase the size of the page") by construction — the rail's own
   max-width rule became redundant and is gone with the cap. */
.wrap { max-width: none; margin: 0; padding: 0 20px 36px; }

/* ---------------- header ---------------- */
.topbar {
  position: sticky; top: 0; z-index: var(--z-bar);
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  padding: 12px 20px; border-bottom: 1px solid var(--line);
  background: rgb(var(--c-ink) / .95); backdrop-filter: blur(8px);
}
/* The heading is a PATH, so it is weighted like the trail it replaced: the parts
   above you recede, the separators recede further, and the page you are on is the
   only one at full strength. It keeps the heading's own size — this is still the
   top of the window — and wraps rather than overflowing on a narrow one. */
/* THE HEADING IS THE TRAIL, SO IT IS THE TRAIL'S STYLING — same 12px, same
   weight, same gap, same button padding and hover. CEO, 2026-08-25: "i believe i
   told you make the path the same style as it was before the location change ..
   why is the font bigger?". He had said it once already; I matched the colours
   and left a heading's 15px and weight 600, which is a title's weight on a path.
   The values are copied from `.crumbs` rather than chosen again, so the two
   cannot drift. `margin-bottom` is not copied: the trail sat above a title inside
   a card, this sits in a row. */
.topbar h1 { display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
  font-size: var(--t-small); font-weight: 400; color: var(--mute); margin: 0;
  letter-spacing: 0; min-width: 0; }
.topbar h1 .up { color: var(--mute); }
.topbar h1 .sep { color: var(--ghost); }
.topbar h1 .here { color: var(--paper); padding: 3px 6px; }
/* A part that IS a place looks pressable and answers hover, exactly as the
   trail's buttons did. */
.topbar h1 button.up { border: 0; background: none; font: inherit; font-size: var(--t-small);
  color: var(--mute); cursor: pointer; padding: 3px 6px; border-radius: var(--r-sm); }
.topbar h1 button.up:hover { background: var(--panel2); color: var(--paper); }
.spacer { margin-inline-start: auto; }

/* The base carries a real look, not just the shared metrics. `.btn` on its own
   used to fall through to the browser's native button — grey fill, black text,
   system border — so a variant class that did not exist produced an OS control
   in the middle of the panel rather than something obviously wrong in the house
   style. A missing variant now degrades to the ghost button. */
.btn {
  border-radius: var(--r-md); font-size: var(--t-body); cursor: pointer; font-family: inherit;
  appearance: none; -webkit-appearance: none;
  background: transparent; color: var(--mute); border: 1px solid var(--line); padding: 6px 10px;
  transition: background-color .15s, border-color .15s, color .15s;
}
.btn:hover { background: var(--panel); color: var(--paper); }
.btn-primary { background: var(--wk); color: var(--onaccent); border: 0; padding: 7px 14px; font-weight: 600; }
.btn-primary:hover { background: var(--wkhover); }
.btn-ghost { background: transparent; color: var(--mute); border: 1px solid var(--line); padding: 6px 10px; }
.btn-ghost:hover { background: var(--panel); color: var(--paper); }
.btn-danger { background: transparent; color: var(--bad); border: 1px solid rgb(var(--c-bad) / .5); padding: 6px 10px; font-weight: 600; }
.btn-danger:hover { background: rgb(var(--c-bad) / .1); }
.ctl {
  display: inline-flex; align-items: center; gap: 6px;
  border: 1px solid var(--line); border-radius: var(--r-md); padding: 6px 10px;
  font-size: var(--t-small);
  color: var(--mute); background: transparent; cursor: pointer; font-family: inherit; text-decoration: none;
}
.ctl:hover { border-color: var(--mute); color: var(--paper); }
/* The selected look belongs to controls that are genuinely one of a set — the
   theme toggle, where Dark stays "Dark" and pressed reports which is in force.
   It does NOT belong to a button whose label changes to name its next action:
   the expand control says "Shrink to a side card" once expanded, so it is an
   action, not a state, and carrying aria-pressed there both painted it as
   permanently selected and made a reader announce "Shrink, pressed". */
.ctl[aria-pressed="true"] { color: var(--paper); border-color: var(--mute); background: var(--panel2); }

/* every interactive thing gets a visible focus ring — the module has real keyboard features */
:is(button, a, input, select, [tabindex]):focus-visible { outline: none; box-shadow: 0 0 0 2px var(--focus-ring); }





/* ---------------- table block ---------------- */
.blk.table { align-items: flex-start; }
.tblwrap { position: relative; flex: 1; min-width: 0; padding-inline-end: 26px; padding-bottom: 26px; }
/* Nothing scrolls sideways at page level (constraint 4): a wide table scrolls
   inside its own container, exactly as the board's table does. */
.tblscroll { overflow-x: auto; overscroll-behavior-x: contain; border: 1px solid var(--line); border-radius: var(--r-lg); }
.btable { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--t-body); }
/* MIDDLE, NOT TOP. CEO, 2026-08-27: "the dots on the left of the block needs to
   always be vertically centered to the full hight of the block". A cell
   stretched by a taller neighbour used to pin its lone block to the top edge,
   dots and all — measured 30px above the cell's middle. Centring the CELL is
   what centres them: a lone block sits mid-cell, and a cell holding several
   blocks still reads top-down because its stack fills the height anyway. */
.btable th, .btable td { border-inline-end: 1px solid var(--line); border-bottom: 1px solid var(--line);
  padding: 0; vertical-align: middle; min-width: 90px; }
.btable tr > *:last-child { border-inline-end: 0; }
.btable tbody tr:last-child > * { border-bottom: 0; }
.btable thead th { background: var(--panel2); font-weight: 600; text-align: start; }
.tcell { padding: 7px 9px; min-height: 20px; outline: none; color: var(--paper); }
.tcell:focus { background: rgb(var(--c-wk) / .1); box-shadow: inset 0 0 0 2px var(--focus-ring); border-radius: var(--r-xs); }
.tcell:empty::before { content: attr(data-ph); color: var(--ghost); }
/* The two adders sit outside the frame, so they never cover a cell. They only
   appear on hover, with their space already reserved. */
.tbladd { position: absolute; display: flex; align-items: center; justify-content: center;
  border: 1px dashed var(--line); background: var(--panel); color: var(--ghost);
  border-radius: var(--r-sm); cursor: pointer; padding: 0; opacity: 0; transition: opacity .12s; }
.tbladd.col { inset-inline-end: 0; top: 0; bottom: 26px; width: 20px; }
.tbladd.row { inset-inline-start: 0; inset-inline-end: 26px; bottom: 0; height: 20px; }
.blk.table:hover .tbladd, .blk.table:focus-within .tbladd { opacity: 1; }
.tbladd:hover { border-style: solid; border-color: var(--wk); color: var(--wksoft); }


.vcard[draggable="true"] { cursor: grab; }
.vcard.dragging, .cal-item.dragging { opacity: .4; }
.kcol.kover .kbody { outline: 2px dashed rgb(var(--c-wk) / .7); outline-offset: 3px; border-radius: var(--r-lg); }
.cal-item[draggable="true"] { cursor: grab; }
.cal-cell.dayover { background: rgb(var(--c-wk) / .18); box-shadow: inset 0 0 0 2px var(--wk); }

/* ---------------- standalone pages ---------------- */
.navempty { font-size: var(--t-label); color: var(--ghost); padding: 4px 8px 6px; line-height: 1.5; }
:root.navrail .navempty { display: none; }
/* ONE PAGE FRAME. A private page and an item opened as a page are the same
   object to a reader — a document in this system — so they are the same shape
   on screen: the same card, the same padding, the same measure, the title in
   the same place. They were drawn by two functions that had each picked their
   own geometry, and the difference was visible the moment anyone opened one
   after the other.
   The 940px measure is the one the full-width peek already uses. A card as wide
   as a 1320px page gives a text line nobody can track back to the start of;
   anything that genuinely needs the width — a table block, a wide embed —
   scrolls inside itself, which is the house rule anyway. */
/* The column menu and the "+" that adds one. Both hold their space and appear
   on hover like every other `.act`, so a header does not resize under the
   pointer. */
.colmenu { flex-shrink: 0; width: 20px; height: 20px; margin-inline-end: 6px;
  display: flex; align-items: center; justify-content: center; border: 0; background: none;
  padding: 0; border-radius: var(--r-sm); color: var(--ghost); cursor: pointer;
  opacity: 0; transition: opacity .12s; }
.tt thead th:hover .colmenu, .tt thead th:focus-within .colmenu { opacity: 1; }
.colmenu:hover { background: var(--panel2); color: var(--paper); }
.colplus { width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
  border: 1px dashed var(--line); background: none; padding: 0; border-radius: var(--r-sm);
  color: var(--ghost); cursor: pointer; margin-inline: auto; }
.colplus:hover { border-color: var(--wk); color: var(--wksoft); }
/* A property cell in the table is the same control as in the property list, in
   less room: the label is the column, so the control carries no label of its own. */
.pcell .cin, .pcell .cellin { width: 100%; }
/* A date in a cell is the same borderless field the rest of the table uses. */
.pcell .dfield { background: none; border: 0; }
.pcell .dfield:hover { background: var(--panel2); }
.pcell .tagcell { flex-wrap: nowrap; }
.pcell .person { max-width: 100%; }
.pcell input { width: 100%; }
@media (prefers-reduced-motion: reduce) { .colmenu { transition: none; } }
/* A property row the owner added. The options control keeps its space so the
   row does not resize under the pointer, and appears on hover like every other
   `.act` in this product. */
.pageprops .prow { padding-inline-end: 0; }
.propmore { flex-shrink: 0; opacity: 0; transition: opacity .12s; }
.prow:hover .propmore, .prow:focus-within .propmore { opacity: 1; }
.propcheck { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: none; color: var(--onaccent);
  cursor: pointer; padding: 0; }
.propcheck.on { background: var(--wk); border-color: var(--wk); }
.plink { font-size: var(--t-body); color: var(--wksoft); }
@media (prefers-reduced-motion: reduce) { .propmore { transition: none; } }
/* A link to something in the portal. Reads as one object, not as text with a
   URL under it — there is nothing to copy here, only somewhere to go. */
.gotolink { display: flex; align-items: center; gap: 9px; width: 100%; text-align: start;
  border: 1px solid var(--line); background: var(--panel2); border-radius: var(--r-md);
  padding: 9px 12px; font: inherit; font-size: var(--t-lead); color: var(--paper); cursor: pointer; }
.gotolink:hover { border-color: var(--wk); }
.gotoicon { flex-shrink: 0; width: 18px; height: 18px; display: flex; align-items: center;
  justify-content: center; color: var(--wksoft); }
.gotoname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-decoration: underline;
  text-decoration-color: var(--line); text-underline-offset: 3px; }
.linkempty.gone { color: var(--ghost); }
/* The reference names itself in place. It IS the empty state it replaces, so it
   borrows `.linkempty`'s shape rather than inventing a second one — the field
   sits exactly where the name will sit, and nothing moves when it commits. */
.refnaming {
  display: flex; align-items: center; gap: 8px;
  border: 1px dashed var(--line); border-radius: var(--r-lg); padding: 12px;
  font-size: var(--t-body); color: var(--ghost);
}
.refnaming:focus-within { border-color: rgb(var(--c-wk) / .6); }
/* Same input vocabulary as every other name typed in place (`.nin`, `.pgtitle`,
   `.pnewname`): transparent until it is used, and the focus ring is the ring. */
.refnamein { flex: 1; min-width: 0; border: 0; background: transparent; font: inherit;
  font-size: var(--t-body); color: var(--paper); padding: 2px 4px; border-radius: var(--r-sm); outline: none; }
.refnamein::placeholder { color: var(--ghost); }
.refnamein:focus { background: var(--ink); box-shadow: 0 0 0 2px var(--focus-ring); }
/* The whole body is the target when a page is dragged over it — the link lands
   at the end, so there is nothing to aim at. */
.linkdrop { outline: 2px dashed rgb(var(--c-wk) / .55); outline-offset: -8px; border-radius: var(--r-xl); }
/* ONE CARD PER PAGE, AND A BOARD IS A PAGE.
   The frame used to be drawn by whoever happened to be rendering: `.pagewrap`
   drew a card round the head, then `.toolbar` drew the top of a SECOND card and
   `.ttwrap` or `.viewpane` closed it. On a board — and on any board page — the
   two cards met in the middle and the seam read as a heavy line across the
   screen a third of the way down, which no plain page has. It was the two
   systems showing through again: the head belonged to the page, the table
   belonged to the board, and each brought its own box.
   The frame is now one element, `.pagecard`, wrapped round both hosts in the
   markup. Everything inside it — head, properties, view chips, table — is one
   surface, and the parts below the head are the page's content in exactly the
   way blocks are on a document page. */
/* LINE DIVIDERS, NOT BOXES (his style, 2026-10-09): the border is made
   transparent rather than removed, so the page sits exactly where it did. */
.pagecard { border: 1px solid transparent; border-radius: var(--r-xl); background: transparent; overflow: hidden;
  /* A table's pinned cells must be opaque, so they wear the surface they sit
     on: the page ground here; the panel colour everywhere else (the side page). */
  --tt-ground: var(--ink); --tt-head: var(--ink); }
/* Not a frame any more, just the head's group inside the card. `overflow` goes
   with the border: the card clips, and a second clipping ancestor here would
   silently cut anything the head ever needs to hang outside itself. */
.pagewrap { background: none; }
/* A PAGE IS NOT A PANEL, AND MUST NOT BORROW ITS SCROLLER.
   This body used to carry `.pbody` as well, to pick up the block editor's
   file-drop target. `.pbody` is the peek's scrolling middle: `overflow-y: auto`
   plus `overscroll-behavior: contain`. On a page that is a scroll container the
   height of its own content — so it never scrolls — and `contain` stops the
   wheel from passing through to the page behind it. The result was a page that
   simply did not scroll under the pointer. The drop target is now found by
   either class instead. */
.pagebody { padding: 22px 26px 44px; }
/* The head is followed, OUTSIDE the shell, by the board chrome in the same
   card. Its own bottom padding would then be a band of nothing between the
   properties and the view chips — the same 44px that used to be the last thing
   before a card edge, now stranded mid-card. The gap is the rule's 6px, and the
   toolbar's own padding does the rest.
   This replaced `.nocontent`, which trimmed the same padding for a board page
   whose card held only a trail, a title and properties. That is now this case
   by name, and the old rule had become unreachable: every remaining page has
   content, because `renderBlocks` always draws at least an "Add a block". */
.pagebody.contentbelow { padding-bottom: 0; }
/* No cap. A measure of 940 was a reading decision, and on a wide screen it paid
   for it with two empty bands inside the card — the page looked like it had
   been laid out for a narrower window and left there. The card is already
   bounded by `.wrap` at 1320px, which is where line length is decided; inside
   it, the content uses the width it is given. */
.pagewrap .inner { max-width: none; }
/* The head sits inside that column, not across the card, so the title starts on
   the same line as the text under it. Its rule is the top of the document. */
.pagehead { display: flex; align-items: center; gap: 8px; min-height: 30px; margin-bottom: 10px; }
/* ONE TITLE ROW FOR EVERY PAGE: icon, then title, then whatever that kind of
   page puts beside it. The icon used to sit on a line of its own above the
   title on a private page and be absent altogether on an item page — two
   answers to the same question, which is the thing being fixed here.
   The gap below is 22px because a title with the content pressed against it
   reads as a label on the first block rather than as the name of the page. */
/* The title row and the line under it are one group, and the air below the page
   title is the GROUP's margin. Put it on either child and a page with a
   subtitle would open differently from a page without one. */
.pagetop { margin-bottom: 22px; }
/* ---------------- the cover and the photo ---------------- */
/* The cover bleeds to the card's own edges — it is a banner, not a figure in
   the text column — by walking back over .pagebody's padding. Fixed height:
   content never decides geometry, so a page with a cover starts at the same
   place whatever the image is. */
.pagecover {
  height: 190px;
  margin: -22px -26px 18px;
  background-size: cover; background-position: center;
  /* An explicit background-size (the side view holding the page's scale)
     would tile without this; `cover` never did, so nothing else changes. */
  background-repeat: no-repeat;
  border-start-start-radius: inherit; border-start-end-radius: inherit;
}
.photorow { display: flex; align-items: flex-start; gap: 22px; min-width: 0;
  /* Air between the head and the body — it measured 2px (CEO, 2026-08-27:
     "there needs to be more space between the head and the body"). */
  margin-block-end: 26px; }
/* THE BODY SPANS TO THE IMAGE'S EDGE (CEO: "the image should be alined with
   the body elements .. extending the body elements to match the image .. the
   width"). Block text used to start a grip-gutter to the right of the photo;
   the gutter now lives in the card's padding, so content and photo share one
   left edge. Desktop only — the phone's padding is too thin to hold a gutter.
   Direct child of .inner only, so the bodies inside table cells keep their
   own geometry. */
@media (min-width: 768px) {
  .inner > [data-blocks] { margin-inline-start: -24px; }
}
.photocol { flex: 1; min-width: 0; }
/* A portrait, not an icon: large enough to be somebody, cropped rather than
   squashed. It spans the name and the properties beside it (his words). */
.pagephoto { flex-shrink: 0; }
.pagephoto img {
  display: block; width: 128px; height: 128px; object-fit: cover;
  border-radius: var(--r-2xl); border: 1px solid var(--line);
}
/* THE IMAGE IS ALWAYS LEVEL WITH THE TITLE. CEO, 2026-08-30: "the image should
   always be aligned to the top of the page title .. it should be fixed and the
   space should be accounted for after the fix".
   It used to ride UP over the banner's lower edge when the page had a cover —
   the profile-page look, identity in front of scenery — which put it 58px above
   the title it stands beside (measured: image top 266, title top 324). Without a
   cover the two were exact, so the same page had two different geometries
   depending on whether somebody had set a banner. The overlap is gone and the
   row is one row again; `align-items: flex-start` on `.photorow` is what levels
   them, and it levels them everywhere.
   The space it used to steal is now held open rather than reclaimed: the cover
   keeps its own margin below it and the image starts there, so nothing moves as
   a cover is added or removed except the cover itself. */
.pagecover + .pagehead + .photorow .pagephoto img { box-shadow: var(--shadow-card); }
@media (max-width: 767.98px) {
  .pagecover { height: 120px; margin: -16px -12px 14px; }
  .pagephoto img { width: 96px; height: 96px; }
  /* The phone's ride-up went with the desktop one — same rule, same reason. */
}
.pagetop .itemhead { margin-bottom: 0; align-items: center; gap: 10px; }
.pagesub { margin: 4px 0 0; font-size: var(--t-small); color: var(--ghost); }
/* The editable form. It sits in the layout whether or not it has anything in it
   — a line that appeared only once written would shift the whole page down the
   first time somebody used it. Borderless until hovered or focused, so an empty
   subtitle reads as space rather than as an unfilled form. */
.pgsub { display: block; width: 100%; margin: 4px 0 0; padding: 2px 6px;
  font: inherit; font-size: var(--t-small); line-height: 1.5; color: var(--ghost);
  background: transparent; border: 1px solid transparent; border-radius: var(--r-sm); }
.pgsub::placeholder { color: var(--ghost); opacity: .55; }
.pgsub:hover { background: var(--panel2); }
.pgsub:focus { background: var(--ink); border-color: transparent; outline: 0;
  box-shadow: 0 0 0 2px var(--focus-ring); color: var(--paper); }
.pageicon { flex-shrink: 0; width: 32px; height: 32px; display: flex; align-items: center;
  justify-content: center; border: 0; background: none; padding: 0; border-radius: var(--r-md);
  color: var(--mute); cursor: pointer; }
.pageicon:hover { background: var(--panel2); color: var(--paper); }
.pageicon svg { width: 22px; height: 22px; }
/* The peek's title row is top-aligned (its pen and chip hang off the first
   line), so the glyph is nudged onto that line rather than the box's top. */
.peek .pageicon { width: 26px; height: 26px; margin-top: 1px; }
.peek .pageicon svg { width: 18px; height: 18px; }
.pagewrap .itemhead h2 { font-size: var(--t-h1); }
.pgtitle { flex: 1; min-width: 0; border: 0; background: none; font: inherit; font-size: var(--t-h1);
  font-weight: 600; letter-spacing: -.02em; color: var(--paper); padding: 2px 6px; border-radius: var(--r-md);
  outline: none; margin: 0; margin-inline: -6px 0; }
.pgtitle:hover { background: var(--panel2); }
.pgtitle:focus { background: var(--ink); box-shadow: 0 0 0 2px var(--focus-ring); }
.pgtitle::placeholder { color: var(--ghost); font-style: italic; }
/* A title that cannot be typed in (a reader's page, the dashboard) is still the
   title: paper ink like every other, only the typing affordances gone. `.ro`
   alone is the read-only CELL and would paint it as a muted 12.5px figure. */
.pgtitle.ro { color: var(--paper); padding-inline-start: 6px; }
.pgtitle.ro:hover { background: none; }
.pgfrom { font-size: var(--t-small); color: var(--ghost); }
/* The cover and the image are controls when the reader may edit (their rows
   open on click — `wireMediaTargets`); the hand is told so. The keyboard ring
   comes from the [tabindex] rule above, the same as every other control. */
[data-mediabtn] { cursor: pointer; }

/* Read-only where it is read-only: the state is visible rather than discovered
   by typing into something that will not take it. */
.pagebody.locked { opacity: .8; pointer-events: none; }
/* LOCKED MEANS CANNOT EDIT, NEVER CANNOT READ (CEO, 2026-08-31: "regular
   members still cant click pages linked in another page .. they cant click
   the subpage in the head of the page"). The blanket rule above made every
   view-only page pointer-dead — links, sub-page rows, the page menu, the
   discussion composer, even a wide table's own scrollbar. Everything that
   READS or TALKS is switched back on by name; the write affordances stay
   dead, which is all the blanket was ever for. */
.pagebody.locked :is(a, [data-goto2], [data-pgpeek], [data-pgcrumb],
  [data-pgfav], [data-pgmenu], [data-ql], .klink, .ttwrap, [data-astable],
  [data-say], [data-send], [data-psay], [data-psend], [data-punsay], .composer,
  [data-deadback], [data-deadpriv], .prevtable) {
  pointer-events: auto;
}

/* ---------------- the portal nav ----------------
 * Anchored to the edge of the screen rather than to the page's centred column,
 * because it is not part of this board — it is the way between the parts of the
 * portal. Everything else is inset past it, including the top bar, so the nav
 * reads as the frame and the board reads as what is inside it.
 *
 * PortalNav.tsx in the real app is the same three presentations: a labelled
 * column, an icon rail, a bottom bar on a phone. One difference worth carrying
 * across deliberately — it renders NO chrome at 0 or 1 destination, on the
 * grounds that navigation between one place is decoration. Here it is drawn at
 * one on purpose, so the shape is visible while the second module is still to
 * come. When it lands, this behaves as PortalNav already does.
 */
/* The person's own width, when they have set one. The rail and the phone
   rules below are more specific, so folding still overrides whatever was
   dragged — a width is a preference for the expanded panel, not a way to defeat
   the two presentations that exist because the screen is narrow. */
/* The floor here is `NAV_W.min` in nav.js and moves with it — 176px, which is what the full logo needs. The clamp exists so a stale or
   hand-edited preference can never produce a panel too narrow to read. */
:root { --navw: clamp(176px, var(--navw-user, 208px), 420px); }
body { padding-inline-start: var(--navw); }
.appnav {
  position: fixed; inset-block: 0; inset-inline-start: 0; z-index: var(--z-nav);
  width: var(--navw); display: flex; flex-direction: column; gap: 2px;
  padding: 12px 10px; background: var(--panel); border-inline-end: 1px solid var(--line);
  overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain;
}
/* One height in both presentations. Expanded it is a row, folded it is a stack,
   and left to size themselves the two differ by about 25px — so every row below
   sat at a different height depending on the fold, which reads as the whole nav
   twitching when it is toggled. */
/* The mark does not move. Its box is the same width, at the same offset, in both
   presentations — 5px in from a nav that keeps its 10px padding either way — so
   folding the panel changes what is beside the mark and never the mark itself.
   Its centre also lines up with the centre of the icons below it. */
/* THE EDGE IS THE CONTROL, AND IT IS BIGGER THAN IT LOOKS.
   Seven pixels of target for a one-pixel line: the border reads as a hairline,
   but nobody can reliably hit a hairline. It sits outside the nav rather than
   inside it, because the nav scrolls and an absolutely-positioned child would
   scroll away with the tree — and be clipped by its overflow-x anyway. */
.navgrip {
  position: fixed; inset-block: 0; inset-inline-start: calc(var(--navw) - 3px);
  width: 7px; z-index: calc(var(--z-nav) + 1); cursor: col-resize; touch-action: none;
}
.navgrip::after {
  content: ''; position: absolute; inset-block: 0; inset-inline-start: 3px; width: 1px;
  background: var(--wk); opacity: 0; transition: opacity .12s;
}
.navgrip:hover::after, .navgrip:focus-visible::after, .navgrip.resizing::after { opacity: 1; }
.navgrip:focus-visible { outline: none; }
/* Nothing to resize when the width is not the reader's to choose. */
:root.navrail .navgrip { display: none; }
@media (prefers-reduced-motion: reduce) { .navgrip::after { transition: none; } }
@media (max-width: 767.98px) { .navgrip { display: none; } }
/* Pinned, because the tree can now make this column far taller than the screen.
The mark is also the control that unfolds the rail — scrolling it out
   of reach would hide the only way back. */
/* ON THE TOP BAR'S OWN LINE (CEO, 2026-08-31: "fix the vertical alignment of
   the logo .. this should reflect on both the full and the small logo"). The
   panel's 12px top padding plus a 64px row centred the mark at 44px while the
   bar across the split centres at 27.5px — the logo read as sagging. The row
   now cancels the padding and takes the bar's height, so the mark, the fold
   arrow and the crumb sit on one line in both presentations. */
.navbrand { display: flex; align-items: center; gap: 8px; padding: 0 5px;
  margin-block-start: -12px; min-height: 55px; flex-shrink: 0;
  /* -12, not 0: the panel is its own scroller, and a sticky top of 0 clamps
     to the inside of its 12px padding — pushing the row right back down. */
  position: sticky; top: -12px; z-index: 1; background: var(--panel); }
/* THE NAME, AS TEXT (brand.js). It is `--paper` on the dark ground and the
   same token's dark value on the light one, so it reads on both themes with no
   image and no hex fill. globals.css's rule still holds: a pale mark on the
   panel, never an accent, and no module's colour standing in for the name. */
.navmark { flex-shrink: 0; width: 30px; height: 30px; border-radius: var(--r-md); padding: 0;
  background: none; border: 0;
  color: var(--paper); display: flex; align-items: center; justify-content: center; }
button.navmark { cursor: pointer; }
button.navmark:hover { background: var(--panel2); color: var(--wksoft); }
.brandmark { display: block; font-size: var(--t-h3); font-weight: 700; line-height: 1; }
/* THE NAME SITS IN THE PANEL'S NARROWEST WIDTH (`NAV_W.min` in nav.js — the
   two are commented at each other). `min-width: 0` and the ellipsis mean a
   longer name is cut with a mark rather than pushing the fold button out. */
.navbrandfull { display: flex; align-items: center; min-width: 0; flex: 1; color: var(--paper); }
.brandfull { display: block; min-width: 0; font-size: var(--t-h3); font-weight: 700; line-height: 30px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* A fixed box, so the word and the rule that replaces it on the rail occupy the
   same height and nothing below them moves when the nav is folded. */
/* EACH FOLDER AND ITS CONTENTS, AS ONE THING (CEO, 2026-08-29:
   "clearly indicates each folder and its content"). He offered a filled box
   like the current page's, an outline, or anything that belongs here; it is
   an OUTLINE, and the reason is a rule already written into this file: the
   filled pill is the one fill in this column and the only answer to "where
   am I" (see `.navitem.on`). A second fill wrapping five groups would make
   the pill compete with its own container. A hairline says the same thing
   and takes nothing from it — and it is the shape this product already uses
   for a container (`.prevwrap`, the cards): 1px of `--line`, radius 10.
   The heading keeps its own hover inside the box, so the group reads as a
   box and the heading still reads as a control. */
/* ONE HOVER, ONE LIT STATE, EVERYWHERE IN THE SIDEBAR (his correction,
   2026-09-04: "fix it .. not only the dashboard .. they are all following the
   same style"). A row, a folder heading, a top row: whatever the pointer is
   over, or whatever is open, fills as a BAND across its box from border to
   border — never a pill floating inside a frame. So the box carries no
   inline padding (the rows carry it, so nothing moves), clips at its own
   corners. Since the boxes became dividers (his style, 2026-10-09) there is
   no frame for a fill to float inside, so the fills are rounded like every
   other corner (`--r-md`). */
/* A FOLDER IS DIVIDED FROM THE NEXT BY A HAIRLINE, NOT BOXED (his style,
   2026-10-09). The border stays, transparent, so nothing shifts; only its top
   edge is drawn. */
.navfoldergroup { border: 1px solid transparent; border-block-start-color: var(--line); border-radius: 0; overflow: hidden;
  padding: 2px 0 4px; margin-block-end: 8px;
  /* A flex item that clips (`overflow: hidden`) loses its automatic minimum
     height and SHRINKS below its content when the column is full — the two
     top boxes squeezed to 18px with their rows clipped away. Never shrink;
     the panel scrolls. */
  flex-shrink: 0; }
/* A shut folder is a bar, not an empty frame: the box shrinks to the
   heading and loses the padding that would leave a hollow strip below it. */
.navfoldergroup.shut { padding-block-end: 2px; }
/* The heading's own top margin belonged to a flat list; inside a box the
   box's padding is the spacing, and the margin would push it off its own
   top edge. */
.navfoldergroup .navgroup { margin-block-start: 0; border-radius: var(--r-md); }
/* THE FOLDER HEADING IS A ROW, NOT A CAPTION. It was 10px ghost caps
   starting in the icons' column, floating between rows a third larger than
   itself — "they look lost". Now it shares the rows' grid: the same twisty
   gutter, a glyph in the same 18px box, and the name at the same offset as
   every name below it. Brighter than the rows on purpose — a heading that is
   dimmer than its contents reads as disabled — and it takes a hover, because
 it IS a control: toggle, drag, right-click. The space above
   each one is a MARGIN rather than padding, so the hover fills the row and
   not the gap. */
/* The offsets are the ROW's, written out rather than left to a flex gap:
   a row puts its icon 5px past the twisty (2px row gap + 3px cell padding)
   and its name 7px past the icon, so the heading does the same and the two
   columns line up to the pixel. `gap: 0` because those margins ARE the gaps. */
.navgroup { display: flex; align-items: center; gap: 0; font-size: var(--t-small); font-weight: 700; color: var(--mute);
  min-height: 30px; padding-inline: 4px 12px; margin-block-start: 12px;
  border-radius: var(--r-md); box-sizing: border-box; flex-shrink: 0; }
.navgroup:first-of-type { margin-block-start: 0; }
.navfolder:hover { background: var(--panel2); color: var(--paper); }
.navfolder .folicon { color: var(--ghost); margin-inline-start: 5px; }
.navfolder:hover .folicon { color: var(--wksoft); }
/* For an admin the glyph is a button — it must not gain a button's
   box, only a button's cursor and a mark that it can be pressed. */
button.folicon { border: 0; background: none; padding: 0; cursor: pointer; border-radius: var(--r-sm); }
button.folicon:hover { background: rgb(var(--c-wk) / .18); color: var(--paper); }
.navfolder .folname { padding-inline-start: 7px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.navfolder .navinput { margin-inline-start: 7px; }
/* The folder headings: a twisty like the rows', draggable to arrange
   for yourself, and on the trash a COUNT — a badge only for a real positive
   figure; an empty trash says it is empty by saying nothing. */
.navfolder[draggable="true"] { cursor: grab; }
.navfolder.dragging { opacity: .4; }
/* The NAME clips, never the glyph beside it (which is a span too). */
.navfolder .navinput { font-size: var(--t-body); font-weight: 600; letter-spacing: 0; text-transform: none; }
.trashcount { margin-inline-start: auto; flex-shrink: 0; font-size: var(--t-micro); font-weight: 700;
  color: var(--mute); border: 1px solid var(--line); border-radius: var(--r-pill); padding: 1px 6px;
  letter-spacing: .02em; }
/* The head's word for a trashed page — a state, not a control. */
.deadnote { font-size: var(--t-small); font-weight: 700;
  color: var(--mute); border: 1px solid var(--line); border-radius: var(--r-pill); padding: 4px 10px;
  flex-shrink: 0; }
/* The rows carry no standing button (CEO, on a column of RESTORE
   pills: "remove it all togeather") — right-click, long-press and the drag
   out are the affordances, like every other row here. */
.trashrow .navname { color: var(--mute); flex: 1; min-width: 0; font-size: var(--t-body); }
/* THE OPEN ONE IS NOT MUTED. The rule above greys every trash row on
   purpose — they are deleted and should read that way — but it is set on the
   NAME, which is more specific than the `.navitem.on` colour that marks the
   open page. So marking the row open changed its background and left its name
   grey: half a highlight, which reads as a bug rather than a state. */
.trashrow .navitem.on .navname { color: var(--paper); font-weight: 600; }
.trashrow[draggable="true"] { cursor: grab; }
.trashrow.dragging { opacity: .4; }
button.navtrashall { display: block; width: 100%; text-align: start; background: none; border: 0;
  cursor: pointer; font: inherit; font-size: var(--t-label); color: var(--ghost); }
button.navtrashall:hover { color: var(--paper); }
:root.navrail .trashcount, :root.navrail .trashrow { display: none; }
/* `auto` on the first of them pushes the group to the end; a second control
   beside it must not push again or the two separate across the heading. */
.navadd + .navadd { margin-inline-start: 0; }
.navadd { margin-inline-start: auto; width: 22px; height: 22px; display: flex; align-items: center;
  justify-content: center; border: 0; background: none; padding: 0; border-radius: var(--r-sm);
  color: var(--ghost); cursor: pointer; }
.navadd:hover { background: var(--panel2); color: var(--paper); }
/* The share control on a folder's heading. It is a `.navadd` like the
   plus beside it — same box, same hover — with the COUNT of people riding on
   it when there are any, so the heading answers "who can see this" without
   opening anything. A badge only for a real positive figure: nobody yet
   draws no number, which is how it says nobody. */
.navshare { position: relative; }
/* The pinned row's tack — quiet, beside the name, never a colour alone. */
.pinmark { display: inline-flex; align-items: center; color: var(--ghost); flex-shrink: 0; }

/* The bell's unread count — a badge only for a real positive figure. */
#bell { position: relative; }
#bell .belln { position: absolute; inset-block-start: -4px; inset-inline-end: -4px;
  min-width: 15px; height: 15px; padding: 0 3px; border-radius: var(--r-pill);
  background: var(--bad); color: var(--onaccent); font-size: var(--t-micro); font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center; }
/* AT ZERO THERE IS NO DOT (two reports, 2026-08-31). The `hidden` attribute
   only wins when no author rule sets display — the inline-flex above beat it,
   so an empty red circle sat on the bell for everyone with nothing unread. */
#bell .belln[hidden] { display: none; }
/* .navshare and its .fshrn counter are gone from the folder headings (CEO,
   2026-08-30); the folder menu's Share row is the way in now. */
/* The row holds the destination and its menu. The menu only appears on hover or
   focus, with its space reserved, so nothing moves under the pointer. */
.navrow { display: flex; align-items: center; gap: 2px; border-radius: var(--r-md); padding-inline: 4px; }
/* The band: the ROW fills, not the button inside it. */
.navrow:hover, .navrow.on { background: var(--panel2); }
.navrow[draggable="true"] { cursor: grab; }
.navrow.dragging { opacity: .4; }
/* Beside is a line, drawn on the edge it will land against. */

/* A drop that cannot happen, shown while the pointer is over it and again when
   it is released. Colour is not doing the work alone: the cursor becomes
   no-drop, the row shakes, and the reason arrives in words. */
.navrow.refuse { box-shadow: inset 0 0 0 1px rgb(var(--c-bad) / .6); background: rgb(var(--c-bad) / .08); }
@keyframes work-refuse {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-4px); }
  45% { transform: translateX(4px); }
  70% { transform: translateX(-2px); }
}
.navrow.refused { animation: work-refuse 380ms ease-in-out; }
/* THE SAME INSERTION EDGE THE REST OF THE PRODUCT USES. The table, the timeline
   and the block editor all mark "it lands here" with a 2px inset edge; the
   sidebar briefly grew a line-and-dot of its own, which made one list behave
   like a different product. One vocabulary, everywhere. */
/* THE SHELF HEADING TAKES THE SAME EDGE, because it sits in the same column and a
   drag passes straight through it. Between the last board row and the first
   private page there are 38 pixels of heading, and lighting the whole heading
   there broke the line the drag had been drawing the whole way down — one list,
   two different marks, at the one point where the reader is furthest from home.
   Extended onto `.navgroup` rather than given a style of its own: it is the same
   mark, so it is the same declaration. */
/* THE ONE DRAG. The float is the item as it is, under the hand; the
   source dims where it was; the ghost is the item as it WILL BE where it lands
   — a real row in the list, so the neighbours move to make the room. Green is
   a copy. `refuse` on a target is the red edge it always was. */
/* `!important` on the geometry, deliberately: the float keeps the item's own
   classes so it looks like the item, and some of those (`.blk`) set
   `position: relative` at the same specificity — a block's float then sat
   at the foot of the document instead of under the hand (seen 2026-09-05). */
.dragfloat { position: fixed !important; inset-inline-start: 0 !important; inset-block-start: 0 !important; margin: 0 !important;
  z-index: var(--z-drag); pointer-events: none; box-shadow: var(--shadow-card); opacity: .82; transform-origin: 0 0; border-radius: var(--r-md); background: var(--panel); }
.dragfloat.blk { max-inline-size: 60vw; }
.dragsource { opacity: .35; }
body.kitdragging { cursor: grabbing; user-select: none; -webkit-user-select: none; }
/* NO HOVER WASH WHILE DRAGGING. A real pointer hovers whatever it crosses, and
   a lit row or heading under a drag reads as "into this" — the very thing the
   ghost exists to say precisely, and the wash he had removed from the heading
   once already (5.x, "the private page section still highlights the full
   page"). Only the ghost speaks. */
body.kitdragging .navrow:hover, body.kitdragging .navgroup:hover, body.kitdragging .navtopgroup:hover,
body.kitdragging .navitem:hover, body.kitdragging .navfolder:hover { background: transparent; }
body.kitdragging * { cursor: grabbing !important; }
.dragghost { pointer-events: none; }
/* THE SHAPE ONLY, STEPPED IN TO ITS LEVEL (his correction, 2026-09-05: "the
   highlight should not show the name .. there needs to be an indicator when
   dragging to make a subpage .. making it indent on the left to reflect the
   sub level"). The ghost keeps a row's height and carries no words or icon;
   its start edge sits where a row at that depth begins its icon, so one
   level deeper is one visible step in. */
.navrow.dragghost { background: rgb(var(--c-wk) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .7); border-radius: var(--r-sm);
  margin-inline-start: calc(21px + var(--nd, 0px)); padding-inline-start: 0; }
.navrow.dragghost > * { visibility: hidden; }
.navrow.dragghost.copy { background: rgb(var(--c-good) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-good) / .7); }
.navgroup.dragghost { background: rgb(var(--c-wk) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .7); }
.navfoldergroup.dragghost { border-color: rgb(var(--c-wk) / .7); background: rgb(var(--c-wk) / .10); }
/* THE GHOSTS OF EVERY OTHER SURFACE: the shape only, the size the
   item will take, stepped to its level where a level exists. */
.tt tbody tr.dragghost td { padding: 0; border-top: 1px solid var(--line); }
.tt tbody tr.dragghost .rowghost { display: block; block-size: 100%; min-block-size: 30px; border-radius: var(--r-sm);
  background: rgb(var(--c-wk) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .7); }
.tt tbody tr.dragghost.copy .rowghost { background: rgb(var(--c-good) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-good) / .7); }
.tlrow.dragghost { background: rgb(var(--c-wk) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .7); border-radius: var(--r-sm);
  margin-inline-start: var(--pad, 0px); }
.tlrow.dragghost.copy { background: rgb(var(--c-good) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-good) / .7); }
.flowcard.dragghost, .vcard.dragghost { background: rgb(var(--c-wk) / .16); border: 1px solid rgb(var(--c-wk) / .7); border-radius: var(--r-lg); }
.flowcard.dragghost.copy, .vcard.dragghost.copy { background: rgb(var(--c-good) / .16); border-color: rgb(var(--c-good) / .7); }
.flowcard.dragghost { inline-size: 290px; display: block; }
.flownode.dragghost { pointer-events: none; }
.flowkids.ghostkids { padding-block-start: 18px; }
/* The ghost's own connector — the stub up to its generation's rail, and the
   rail's reach to it — in the highlight's colour, so the shape is drawn
   joined to the card it will hang under (his picture, 2026-09-05). */
.flowkids > .flownode.dragghost::after,
.flowkids > .flownode.dragghost::before { border-color: rgb(var(--c-wk) / .7); }
.flowkids > .flownode.dragghost.copy::after,
.flowkids > .flownode.dragghost.copy::before { border-color: rgb(var(--c-good) / .7); }
.cal-item.dragghost { min-block-size: 22px; background: rgb(var(--c-wk) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .7); }
.tt thead th.dragghost { background: rgb(var(--c-wk) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .7); }
.viewbtn.dragghost { display: inline-block; block-size: 28px; background: rgb(var(--c-wk) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .7); border-radius: var(--r-md); }
.epopt.dragghost { background: rgb(var(--c-wk) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .7); border-radius: var(--r-md); }
.blk.dragghost { min-block-size: 28px; background: rgb(var(--c-wk) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .7); border-radius: var(--r-sm); }
.blk.dragghost.copy { background: rgb(var(--c-good) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-good) / .7); }
/* Beside a block: the shape of the column it would join, on that side. */
.blk .dragghost.side { position: absolute; inset-block: 0; inline-size: 42%; border-radius: var(--r-sm); pointer-events: none;
  background: rgb(var(--c-wk) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .7); }
.blk .dragghost.side.start { inset-inline-start: 0; }
.blk .dragghost.side.end { inset-inline-end: 0; }
.chatpanel .dragghost.chip { display: inline-block; inline-size: 120px; block-size: 24px; border-radius: var(--r-pill); margin: 4px;
  background: rgb(var(--c-wk) / .16); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .7); }
/* Hover washes, silenced while dragging, on the surfaces that have them. */
body.kitdragging .tt tbody tr:hover td, body.kitdragging .tlrow:hover, body.kitdragging .vcard:hover, body.kitdragging .cal-item:hover,
body.kitdragging .blk:hover, body.kitdragging .epopt:hover, body.kitdragging .viewbtn:hover { background: transparent; }
/* A grip for the finger (his rule: a touch drag starts from a grip): drawn
   only where the pointer is coarse; a mouse picks the row up anywhere. Its
   own name, not `.navgrip`: that is the resize separator beside the sidebar,
   and from 2026-09-05 to 2026-09-27 the two shared one class, so this
   `display: none` hid the separator and nobody could resize the sidebar. */
.rowgrip { display: none; flex: 0 0 18px; align-items: center; justify-content: center; color: var(--ghost); touch-action: none; }
@media (pointer: coarse) { .navrow .rowgrip, .trashrow .rowgrip { display: inline-flex; } }
.navrow.dropbefore, .navgroup.dropbefore { box-shadow: inset 0 2px 0 var(--wk); }
.navrow.dropafter, .navgroup.dropafter { box-shadow: inset 0 -2px 0 var(--wk); }
.navrow .navitem { flex: 1; min-width: 0; }
.navmore { flex-shrink: 0; width: 24px; height: 24px; display: flex; align-items: center;
  justify-content: center; border: 0; background: none; padding: 0; border-radius: var(--r-sm);
  color: var(--ghost); cursor: pointer; opacity: 0; transition: opacity .12s; }
.navrow:hover .navmore, .navrow:focus-within .navmore { opacity: 1; }
.navmore:hover { background: var(--panel2); color: var(--paper); }
:root.navrail .navrow { justify-content: center; }
:root.navrail .navmore, :root.navrail .navadd { display: none; }
/* The twisty keeps its own column so names line up whether or not a row has
   children — nothing shifts sideways as branches open. */
.navtw { flex-shrink: 0; width: 15px; height: 22px; display: flex; align-items: center;
  justify-content: center; border: 0; background: none; padding: 0; border-radius: var(--r-xs);
  color: var(--ghost); cursor: pointer; }
/* The twisty takes its column out of the name's width, so the padding it sits
   beside is given back — otherwise "Projects and Work" starts ellipsising the
   moment the board becomes expandable. */
.navrow .navtw + .navitem { padding-inline-start: 3px; gap: 7px; }
.navtw:hover { color: var(--paper); }
/* Closed points along the reading direction, so it mirrors; open points down,
   which is the same in both. */
[dir='rtl'] .navtw[aria-expanded="false"] svg { transform: scaleX(-1); }
.navtw.blank { pointer-events: none; }
.navrow.navsub { padding-inline-start: calc(4px + var(--nd, 0px)); }
.navrow.navsub .navitem { font-size: var(--t-small); min-height: 30px; padding-block: 5px; }
.navrow.dropinto { box-shadow: inset 0 0 0 1px var(--wk); background: rgb(var(--c-wk) / .12); }
/* `.navgroup.shelfover` used to live here — a wash and a ring across the whole
   heading. Gone, and not replaced: a heading offered as a target now draws the
   same insertion edge every row draws. The refusal below keeps its wash because
   it is a different signal, and one nobody has asked to change. */
.navgroup.refuse { background: rgb(var(--c-bad) / .1); border-radius: var(--r-sm);
  box-shadow: inset 0 0 0 1px rgb(var(--c-bad) / .5); }
.navgroup.refused { animation: work-refuse 380ms ease-in-out; }
.tt tbody tr.refuse td { background: rgb(var(--c-bad) / .09); }
.tt tbody tr.refused { animation: work-refuse 380ms ease-in-out; }
/* Not the current page — the branch the current page is in. No fill, so the one
   filled pill in the list is still the answer to "where am I". */
.navitem.inbranch { color: var(--paper); font-weight: 600; }
.navitem.inbranch .navicon { color: var(--wksoft); }
.navname.done { text-decoration: line-through; color: var(--ghost); }
/* The rail has 56px and no names; a tree in it would be a column of identical
   glyphs. The branches fold away with the labels. */
:root.navrail .navsub, :root.navrail .navtw { display: none; }
.navitem {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: start;
  border: 0; background: none; font: inherit; font-size: var(--t-body); color: var(--mute);
  padding: 8px; border-radius: var(--r-md); cursor: pointer; min-height: 36px; text-decoration: none;
}
.navitem:hover { color: var(--paper); }
/* Current is a filled pill and nothing else. An inset rule on a rounded box
   leaves a curved sliver at the corners, which reads as a rendering fault rather
   than as emphasis. Colour is not carrying the state alone — aria-current says
   it, and the weight changes with it. */
.navitem.on { color: var(--paper); font-weight: 600; }
.navicon { flex-shrink: 0; width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; color: var(--ghost); }
.navitem.on .navicon { color: var(--wksoft); }
.navname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The row being named. It is the row, not a dialog over it: same height, same
   icon, same indent, so the name is edited where the name lives and the list
   does not move under the pointer when the field comes and goes. */
.navitem.renaming { cursor: default; background: var(--panel2); }
.navname.navinput {
  flex: 1; min-width: 0; border: 0; background: var(--ink); color: var(--paper);
  font: inherit; font-size: var(--t-body); padding: 3px 6px; margin-inline: -6px 0;
  border-radius: var(--r-sm); outline: none; box-shadow: 0 0 0 2px var(--focus-ring);
}
.navsoon { margin-inline-start: auto; font-size: var(--t-small);
  color: var(--ghost); border: 1px solid var(--line); border-radius: var(--r-pill); padding: 1px 6px; }
.navfoot { margin-top: auto; padding-top: 10px; font-size: var(--t-label); color: var(--ghost); }

/* THE RAIL IS A CLASS, NOT A BREAKPOINT.
   It is reached two ways — someone folds it, or the screen is too narrow to
   carry names — and both produce the same thing, so both set the same class.
   Written as a media query as well it would be two definitions of one state,
   and they would drift. The name becomes the tooltip AND the accessible label;
   a title attribute is not a label. */
:root.navrail { --navw: 56px; }
:root.navrail .appnav { align-items: stretch; }
/* `.navwordmark` was the typed word beside the placeholder; it went with the
   real logo, and the rail draws the square mark instead. */
:root.navrail .navname, :root.navrail .navbrandfull,
:root.navrail .navsoon, :root.navrail .navfoot { display: none; }
/* The heading keeps its box on the rail and loses its word: a group boundary
   still has to be visible when there is more than one group, and hiding the row
   outright moved every destination 38px up the moment the nav was folded. */
/* ON THE RAIL THE GLYPH IS WHAT IS LEFT: the name goes, the folder's
   own icon stays and marks the group — which is what a rail is for. The
   hairline that used to stand in for the whole heading is gone with it, since
   there is now something real to see. */
/* No boxes on the rail: at 56px a border around each group is four
   nested frames and no room for any of them. A HAIRLINE between groups says
   the same thing in the space there is — and it has to say something, because
   with the names gone a folder and a page were the same shape (the CEO, on
   the two panels side by side: "why is their styling different"). */
/* `border-radius: 0` matters: the box's 10px radius curved the ends of this
   hairline into little hooks, which read as fragments of a frame rather than
   as a rule between two groups. */
:root.navrail .navfoldergroup { border: 0; border-radius: 0; padding: 0; margin-block-end: 4px; }
:root.navrail .navfoldergroup + .navfoldergroup {
  border-block-start: 1px solid var(--line); padding-block-start: 6px; margin-block-start: 6px; }
/* The folder's glyph IS the toggle on the rail, so it takes a control's box
   and a control's hover — the destinations below it are buttons too, and a
   heading that looked like plain art would be the one thing there that does
   not answer a press. */
:root.navrail .navfolder .folicon { width: 36px; height: 32px; margin-inline: auto;
  border-radius: var(--r-md); color: var(--ghost); }
:root.navrail .navfolder .folicon:hover { background: var(--panel2); color: var(--wksoft); }
:root.navrail .navgroup { justify-content: center; padding-inline: 0; margin-block-start: 8px; }
:root.navrail .navgroup .folname, :root.navrail .navgroup .navinput,
:root.navrail .navgroup .trashcount { display: none; }
:root.navrail .navfolder .folicon { margin-inline-start: 0; }
:root.navrail .navitem { justify-content: center; padding: 9px 0; width: 36px; margin-inline: auto; }
:root.navrail .navbrand { justify-content: flex-start; }
.navfold {
  margin-inline-start: auto; flex-shrink: 0; width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  border: 0; background: none; padding: 0; border-radius: var(--r-sm); color: var(--ghost); cursor: pointer;
}
.navfold:hover { background: var(--panel2); color: var(--paper); }
/* No chevron on the rail: the mark is the way back out, so a second control
   pointing the same way would be one too many in 56px. */
:root.navrail .navfold { display: none; }
[dir='rtl'] .navfold svg { transform: scaleX(-1); }
/* PHONE: THE SIDEBAR HIDES ITSELF AND IS REVEALED FROM THE SIDE.
   CEO, 2026-08-24: "it should not be a bottom bar .. it should be an auto hide
   side bar .. it can be dropdown or side reveal".

   WHAT THE BOTTOM BAR GOT WRONG. It put one cell per sidebar row into a grid
   with `grid-auto-columns: 1fr` — unlimited implicit columns — so with ten
   boards the cells ran off the right edge and everything past it was
   unreachable. The comment above it claimed a grid cannot exceed its column
   count, which is true only if the column count is fixed, and it was not.

   A HIDDEN SIDEBAR HAS NO SUCH LIMIT: it is a vertical list, and a vertical list
   gains height instead of overflowing — the argument `PortalNav.tsx` already
   makes for the same problem. It is also the SAME sidebar, with the same tree,
   the same twisties and the same drag, rather than a second navigation that has
   to be kept in step by hand.

   Side reveal rather than a dropdown: the nav is a tree that can be tall, and a
   drawer with its own scroll holds depth where a dropdown hanging off the top
   has to fight the page for height. `inset-block: 0` with the drawer scrolling
   internally is hard constraint 5 — a fixed overlay must fit the viewport.

   Mirrored explicitly for RTL: the drawer leaves from whichever edge it sits
   against, so the transform is stated for both directions rather than assumed. */
@media (max-width: 767.98px) {
  :root { --navw: 0px; }
  body { padding-inline-start: 0; }
  .appnav {
    inset-block: 0; inset-inline-start: 0; inset-inline-end: auto;
    width: min(84vw, 320px); max-width: 320px;
    flex-direction: column; padding: 12px 10px;
    border-inline-end: 1px solid var(--line);
    box-shadow: var(--shadow-card);
    transform: translateX(-101%);
    transition: transform .18s ease;
    visibility: hidden;
  }
  [dir='rtl'] .appnav { transform: translateX(101%); }
  :root.navopen .appnav,
  [dir='rtl'] :root.navopen .appnav { transform: translateX(0); visibility: visible; }
  /* The scrim is what makes it a drawer rather than a panel that has landed on
     the page: a press anywhere outside closes it, which is where people press. */
  .navscrim {
    position: fixed; inset: 0; z-index: calc(var(--z-nav) - 1); background: rgb(var(--c-scrim) / 0.5);
    opacity: 0; pointer-events: none; transition: opacity .18s ease;
  }
  :root.navopen .navscrim { opacity: 1; pointer-events: auto; }
  /* The drawer is the full sidebar, so nothing in it is hidden the way the
     bottom bar hid the brand, the groups and the shelves. */
  .navfold { display: none; }
  :root.navrail .navname, .navname { display: block; }
  :root.navrail .navitem { width: auto; margin-inline: 0; }
  /* Folding is a desktop decision about a column that does not exist here. Left
     to apply, someone who folded the rail on their laptop opened the drawer to
     an unlabelled column of icons. */
  .navitem { min-height: 44px; }
  .navgrip { display: none; }
}
/* The control that reveals it. Desktop has the sidebar in front of it already,
   so this exists only where the sidebar does not. */
.navshow { display: none; }
@media (max-width: 767.98px) {
  .navshow {
    display: inline-flex; align-items: center; justify-content: center;
    width: 40px; height: 40px; flex: none;
    border: 1px solid var(--line); border-radius: var(--r-lg);
    background: var(--panel); color: var(--paper);
  }
  .navshow:hover { background: var(--panel2); }
}

/* ---------------- toolbar ---------------- */
/* The toolbar is a fixed set of controls and must not reflow. The sort status
   used to live here and pushed Export onto a second line the moment anyone
   sorted — a row of buttons that moves under the pointer is worse than one that
   is slightly cramped. The status now has its own strip below. */
.toolbar {
  display: flex; flex-wrap: nowrap; align-items: center; gap: 8px;
  padding: 12px;
  overflow-x: auto; overscroll-behavior-x: contain;
}
.toolbar > * { flex-shrink: 0; }
.toolbar .search { flex-shrink: 1; min-width: 130px; }
/* Constraint 5: a fixed panel must fit the viewport, so it is capped and scrolls
   inside itself rather than running off the bottom edge. */
.adminpanel {
  position: fixed; z-index: var(--z-dialog); inset-inline: 0; margin: 0 auto; top: 50%;
  transform: translateY(-50%); width: min(560px, calc(100vw - 32px));
  max-height: calc(100dvh - 48px); display: flex; flex-direction: column;
  border: 1px solid var(--line); border-radius: var(--r-2xl); background: var(--panel);
  box-shadow: var(--shadow-card); overflow: hidden;
}
.adminbody { overflow-y: auto; padding: 6px 0; }
.admrow { display: flex; align-items: flex-start; gap: 16px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
.admrow:last-child { border-bottom: 0; }
.admrow > div { flex: 1; min-width: 0; }
.admrow .btn { flex-shrink: 0; margin-top: 2px; }
.admname { font-size: var(--t-lead); font-weight: 600; color: var(--paper); }
.userlist { display: flex; flex-direction: column; gap: 2px; margin: 10px 0; }
.urow { display: flex; align-items: center; gap: 9px; padding: 6px 8px; border-radius: var(--r-md); }
.urow:hover { background: var(--panel2); }
.uname { flex: 1; min-width: 0; font-size: var(--t-body); display: flex; flex-direction: column; overflow: hidden; }
.uemail { font-size: var(--t-label); color: var(--ghost); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.invitebar { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.invitebar input, .invitebar select {
  flex: 1; min-width: 130px; border: 1px solid var(--line); background: var(--ink);
  border-radius: var(--r-md); padding: 7px 9px; color: var(--paper); font: inherit; font-size: var(--t-body); outline: none;
}
.invitelist { display: flex; flex-direction: column; gap: 4px; margin-top: 10px; }
.invrow { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; padding: 7px 8px; border: 1px solid var(--line); border-radius: var(--r-md); }
.invwho { font-size: var(--t-small); color: var(--paper); display: flex; align-items: center; gap: 6px; }
/* The link itself stays off screen (design audit B9): Copy carries it. It is
   shown, selected, only when the clipboard refused — the one case a person has
   to copy by hand. */
.invrow .invurl { display: none; }
.invrow.showurl .invurl { display: block; flex-basis: 100%; order: 9; }   /* on its own line beneath, so the buttons keep theirs */
.invrow .btn { margin-inline-start: auto; }
.invrow .btn + .btn { margin-inline-start: 0; }
.invurl { flex: 1; min-width: 160px; border: 0; background: var(--ink); border-radius: var(--r-sm); padding: 6px 8px;
  color: var(--mute); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--t-label); outline: none; }
.advadd { margin-top: 12px; }
.advadd summary { font-size: var(--t-small); color: var(--ghost); cursor: pointer; }
.advadd summary:hover { color: var(--paper); }
.newuser { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.newuser input, .newuser select {
  flex: 1; min-width: 110px; border: 1px solid var(--line); background: var(--ink);
  border-radius: var(--r-md); padding: 7px 9px; color: var(--paper); font: inherit; font-size: var(--t-body); outline: none;
}
.newuser input:focus, .newuser select:focus { box-shadow: 0 0 0 2px var(--focus-ring); }

/* ---------------- sign in ---------------- */
.authback { position: fixed; inset: 0; z-index: var(--z-auth); background: var(--ink); }
.authbox {
  position: fixed; z-index: calc(var(--z-auth) + 1); inset-inline: 0; margin: 0 auto; top: 50%;
  transform: translateY(-50%); width: min(360px, calc(100vw - 32px));
  max-height: calc(100dvh - 40px); overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--r-2xl); background: var(--panel);
  box-shadow: var(--shadow-card); padding: 24px;
}
.authbox h2 { margin: 0 0 6px; font-size: var(--t-h3); font-weight: 600; letter-spacing: -.02em; }
.authbox p { margin: 0 0 16px; font-size: var(--t-body); line-height: 1.6; color: var(--mute); }
.authbox label { display: block; font-size: var(--t-small); color: var(--ghost); margin-bottom: 12px; }
.authbox input, .authinput {
  display: block; width: 100%; margin-top: 6px; border: 1px solid var(--line); background: var(--ink);
  border-radius: var(--r-md); padding: 10px 12px; color: var(--paper); font: inherit; font-size: var(--t-lead);
  text-transform: none; letter-spacing: normal; outline: none;
}
.authbox input:focus, .authinput:focus { box-shadow: 0 0 0 2px var(--focus-ring); }
.authbox .btn { width: 100%; min-height: 42px; margin-top: 4px; }
.autherr { color: var(--bad); font-size: var(--t-body); margin: -6px 0 12px; }
.authnote { font-size: var(--t-small); color: var(--ghost); margin: 12px 0 0; line-height: 1.5; }
.admdesc { margin: 5px 0 0; font-size: var(--t-body); line-height: 1.6; color: var(--mute); }
.admdesc strong { color: var(--paper); }
.search {
  display: flex; align-items: center; gap: 7px; border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 5px 10px; background: var(--ink); min-width: 190px;
}
.search input { border: 0; background: none; color: var(--paper); font: inherit; font-size: var(--t-body); outline: none; width: 100%; }
.search input::placeholder { color: var(--ghost); }
.stat { font-size: var(--t-label); color: var(--ghost); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ---------------- table ---------------- */
/* No frame of its own — see `.pagecard`. The header band already draws the line
   under the chips: `.tt th` is panel2 with a bottom border, so the table starts
   with an edge whether or not anything is drawn above it. */
.ttwrap { overflow: hidden; }
.ttscroll { overflow-x: auto; overscroll-behavior-x: contain; }
.tt { width: 100%; table-layout: fixed; border-collapse: separate; border-spacing: 0; font-size: var(--t-body); }
.tt th {
  position: sticky; top: 0; z-index: 3; background: var(--tt-head, var(--panel2)); text-align: start; white-space: nowrap;
  padding: 9px 12px; font-size: var(--t-small); font-weight: 600; color: var(--ghost); border-bottom: 1px solid var(--line);
}
.tt td { padding: 0 12px; height: 44px; border-bottom: 1px solid var(--line); vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tt tbody tr:hover td { background: var(--panel2); }
.tt thead th { padding: 0; }
/* The header height is fixed and the arrow slot is always in the layout, empty
   or not. Drawn only when sorted, a 13px glyph is taller than a 10px uppercase
   line box, so the whole header — and the table under it — stepped down a pixel
   the moment anyone sorted. Reserve the space instead of reacting to it. */
/* The header's single row. Its height is the header's height, fixed, whatever is
   in it — see renderHeaders. */
.thcell { display: flex; align-items: center; height: 34px; min-width: 0; }
.sortbtn {
  display: flex; align-items: center; gap: 5px; flex: 1; min-width: 0; height: 34px;
  border: 0; background: none; cursor: pointer; font: inherit;
  padding: 0 12px; text-align: start; white-space: nowrap; line-height: 1;
  font-size: var(--t-small); font-weight: 600;
  color: var(--ghost);
}
/* The label is the only thing allowed to give way. The icon, the arrow (a fixed
   slot, see above) and the options control all keep their size, so a narrow
   column ellipsises the word rather than wrapping or growing. */
.thicon { flex-shrink: 0; display: flex; align-items: center; color: var(--ghost);
  width: 13px; height: 13px; }
.thicon svg { width: 13px; height: 13px; }
.sortbtn.on .thicon { color: var(--wksoft); }
.thlabel { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sortbtn .arrow { flex-shrink: 0; }
.sortbtn:hover { color: var(--paper); background: rgb(var(--c-line) / .5); }
.sortbtn.on { color: var(--wksoft); }
.sortbtn[draggable="true"] { cursor: grab; }
.sortbtn[draggable="true"]:active { cursor: grabbing; }
/* Where the column will land, drawn on the edge it will land against. */
.tt thead th.dropbefore { box-shadow: inset 2px 0 0 var(--wk); }
.tt thead th.dropafter { box-shadow: inset -2px 0 0 var(--wk); }
.tt thead th.coldragging .sortbtn { opacity: .45; }
.tt thead th { position: sticky; top: 0; }
.tt thead th[data-th] { position: sticky; }
.colgrip {
  position: absolute; top: 0; bottom: 0; inset-inline-end: -4px; width: 9px;
  cursor: col-resize; z-index: 6; touch-action: none;
}
.colgrip::after {
  content: ''; position: absolute; top: 7px; bottom: 7px; inset-inline-start: 4px;
  width: 2px; border-radius: var(--r-hair); background: transparent; transition: background-color .12s;
}
.colgrip:hover::after, .colgrip.dragging::after { background: var(--wk); }
/* While dragging, the whole document keeps the resize cursor and stops selecting
   text — otherwise the pointer flickers and the drag highlights the table. */
body.resizing { cursor: col-resize; user-select: none; }
body.resizing * { cursor: col-resize !important; }
.sortbtn .arrow { display: flex; align-items: center; justify-content: center; flex-shrink: 0; width: 13px; height: 13px; }
.rolechip {
  font-size: var(--t-label); font-weight: 700;
  border-radius: var(--r-pill); padding: 2px 6px; background: var(--panel2); color: var(--ghost);
}
.rolechip.adm { background: rgb(var(--c-wk) / .18); color: var(--wksoft); }
/* ONE SELECTED LOOK, FOR EVERY KIND OF THING.
   A selected task, a selected sidebar page and a selected block are the same
   state, so they carry the same wash and the same edge. Written once: changing
   how "selected" looks changes it in all three places, and there is no way for
   one surface to drift into its own idea of it. The wash is the work accent at
   10%, which clears its contrast floor on both grounds; the inline edge is what
   makes a run of them read as one block rather than three stripes. */
.tt tbody tr.sel td,
.navrow.sel,
.blk.sel { background: rgb(var(--c-wk) / .1); }
.navrow.sel, .blk.sel { border-radius: var(--r-sm); box-shadow: inset 2px 0 0 var(--wk); }
.tt tbody tr.sel td:first-child { box-shadow: inset 2px 0 0 var(--wk); }
/* A block is inline-padded by its indent, so the wash needs to reach past the
   grip rather than stopping at the text. */
.blk.sel { outline: 1px solid rgb(var(--c-wk) / .35); outline-offset: 1px; }

/* WHAT IS SELECTED AND WHAT CAN BE DONE WITH IT.
   Fixed inside the viewport, never taller than it (constraint 5), and it cannot
   scroll the page sideways because it is a flex row that shrinks. Above the
   phone's bottom navigation, so it never buries the way out. */
.selbar {
  position: fixed; inset-inline: 12px; bottom: 12px; z-index: var(--z-bar);
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  max-width: 640px; margin-inline: auto;
  padding: 8px 10px; border-radius: var(--r-xl);
  background: var(--panel2); border: 1px solid var(--line);
  box-shadow: 0 8px 30px rgb(var(--c-scrim) / .45);
  animation: work-fade-in .12s ease-out;
}
.selbar .selcount { font-size: var(--t-body); font-weight: 600; color: var(--paper); }
@media (prefers-reduced-motion: reduce) { .selbar { animation: none; } }
@media (max-width: 767.98px) {
  /* Clear of the bottom bar, which is 62px plus the safe area. */
  .selbar { bottom: calc(70px + env(safe-area-inset-bottom)); inset-inline: 8px; }
}

/* pinned name column, so depth stays legible however far the table scrolls */
.tt .name { position: sticky; inset-inline-start: 0; z-index: 2; background: var(--tt-ground, var(--panel)); }
/* The footer sticks to the bottom of the scroll box the way the header sticks to
   the top. Its name cell is sticky on both axes at once — bottom and inline
   start — so it stays put whichever way the table is scrolled. */
.tt tfoot td { position: sticky; bottom: 0; z-index: 4; background: var(--tt-ground, var(--panel));
  border-top: 1px solid var(--line); border-bottom: 0; }
.tt tfoot td.name { z-index: 5; }
/* The row sets --pad from its depth. Unchanged geometry — this used to be an
   inline padding on the cell; it moved to the row so the phone layout can
   indent both of the row's lines by the same amount. */
.ncell { padding-inline-start: var(--pad, 12px); }
.tt tbody tr:hover .name { background: var(--panel2); }
.tt tbody tr.sel .name { background: rgb(var(--c-panel2)); }
.tt th.name { z-index: 40; background: var(--tt-head, var(--panel2)); }
.tt .name::after { content: ''; position: absolute; inset-block: 0; inset-inline-end: 0; width: 1px; background: var(--line); }

.ncell { display: flex; align-items: center; gap: 6px; min-width: 0; }
/* The row itself is grabbed now, so the whole row dims while it is in flight. */
tr.dragging td { opacity: .4; }
tr[draggable="true"], .tlrow[draggable="true"] { cursor: grab; }
tr.dragging, .tlrow.dragging { cursor: grabbing; }
.tlrow.dragging { opacity: .4; }

/* Beside is a line; inside is a filled row. Two different shapes so the answer
   to "what will this do" never depends on reading a 2px edge. */
.tt tbody tr.dropbefore td { box-shadow: inset 0 2px 0 var(--wk); }
.tt tbody tr.dropafter td { box-shadow: inset 0 -2px 0 var(--wk); }
.tt tbody tr.dropinto td { background: rgb(var(--c-wk) / .16); }
.tt tbody tr.dropinto .name { box-shadow: inset 3px 0 0 var(--wk); }
/* the same three answers in the timeline, drawn the same three ways */
.tlrow.dropbefore { box-shadow: inset 0 2px 0 var(--wk); }
.tlrow.dropafter { box-shadow: inset 0 -2px 0 var(--wk); }
.tlrow.dropinto { background: rgb(var(--c-wk) / .16); box-shadow: inset 3px 0 0 var(--wk); }
.tw { flex-shrink: 0; width: 18px; height: 18px; display: flex; align-items: center; justify-content: center; border: 0; background: none; padding: 0; color: var(--ghost); cursor: pointer; border-radius: var(--r-xs); }
.tw:hover { background: var(--line); color: var(--paper); }
.tw.blank { cursor: default; }
.tw.blank:hover { background: none; }
/* The twisty is a real touch target on a phone, not a 18px desktop one. */
@media (max-width: 767.98px) {
  .tt .tw { width: 34px; height: 40px; margin-inline-start: -6px; }
  .tt .tw svg { width: 15px; height: 15px; }
}
/* The glyph sits in a fixed box and the SVG is forced to fill it.
   `ICO` entries were authored at 13, 14 and 15px, so an icon carrying its own
   width made the name shift sideways the moment someone changed it. Same rule as
   the sort arrow: reserve the space, never let the content set the geometry. */
.gl {
  flex-shrink: 0; width: 22px; height: 22px; margin-inline-start: -3px;
  color: var(--ghost); display: flex; align-items: center; justify-content: center;
  border: 0; background: none; padding: 0; border-radius: var(--r-sm); cursor: pointer;
}
.gl svg { width: 15px; height: 15px; }
.gl:hover { color: var(--paper); background: rgb(var(--c-line) / .6); }
.migrid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 2px; padding: 4px 3px; }
.miicon {
  display: flex; align-items: center; justify-content: center; height: 32px;
  border: 0; background: none; color: var(--mute); border-radius: var(--r-md); cursor: pointer;
}
.miicon svg { width: 16px; height: 16px; }
.mhint { padding: 0 9px 6px; font-size: var(--t-label); line-height: 1.5; color: var(--ghost); max-width: 280px; }
.menu textarea {
  width: 100%; min-width: 250px; border: 0; background: var(--ink); border-radius: var(--r-sm);
  padding: 8px; color: var(--paper); font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--t-small); line-height: 1.5; outline: none; resize: vertical;
}
.miicon:hover { background: var(--panel2); color: var(--paper); }
.miicon.on { background: rgb(var(--c-wk) / .18); color: var(--wksoft); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .5); }

/* the name is always an input, so typing never rebuilds the row under the caret */
.nin {
  flex: 1; min-width: 0; border: 0; background: none; font: inherit; font-size: var(--t-body);
  color: var(--paper); padding: 5px 6px; border-radius: var(--r-sm); outline: none;
}
.nin:hover { background: rgb(var(--c-line) / .5); }
.nin:focus { background: var(--ink); box-shadow: 0 0 0 2px var(--focus-ring); }
.nin.done { color: var(--ghost); text-decoration: line-through; }
.nin::placeholder { color: var(--ghost); font-style: italic; }

/* The name OPENS the item. Renaming is a separate, explicit act behind the
   pencil — clicking a title and landing in a text field is the wrong default
   when the title is the way into the thing. */
.nlink {
  flex: 1; min-width: 0; text-align: start; border: 0; background: none;
  font: inherit; font-size: var(--t-body); color: var(--paper); padding: 5px 6px;
  border-radius: var(--r-sm); cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nlink:hover { background: rgb(var(--c-line) / .5); text-decoration: underline; text-underline-offset: 2px; }
.nlink.done { color: var(--ghost); text-decoration: line-through; }
.nlink.untitled { color: var(--ghost); font-style: italic; }
.pen { opacity: 0; transition: opacity .12s; flex-shrink: 0; }
tr:hover .pen, tr:focus-within .pen,
.tlrow:hover .pen, .tlrow:focus-within .pen { opacity: 1; }



/* ---------------- references ---------------- */
.minote { display: block; font-size: var(--t-label); color: var(--ghost); margin-top: 1px; }
/* A select's choice drawn as the chip it becomes (menu `tone` rows). */
.mi .mtone { display: inline-flex; align-items: center; gap: 5px;
  padding: 1.5px 9px; border-radius: var(--r-pill); font-size: var(--t-small); font-weight: 600; }
.refcard { border: 1px solid var(--line); border-radius: var(--r-xl); background: var(--panel2); overflow: hidden; }
/* Inherited is drawn quieter than owned, the same 55% the inherited property
   fields use, so "this came from somewhere else" reads the same way everywhere. */
.refcard.inh { background: transparent; }
.refcard.missing { border-style: dashed; }
.refhead { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--line); }
.refcard.missing .refhead { border-bottom: 0; }
.refhead .linkicon { color: var(--ghost); display: flex; }
.refname { font-size: var(--t-body); font-weight: 600; color: var(--paper); }
.refsrc { display: inline-flex; align-items: center; gap: 3px; border: 1px solid var(--line);
  background: none; border-radius: var(--r-pill); padding: 1px 8px; font: inherit; font-size: var(--t-label);
  color: var(--ghost); cursor: pointer; }
.refsrc:hover { color: var(--paper); border-color: var(--wk); }
.refsrc.as-text { border: 0; padding: 0; cursor: default; }
.refedit { border: 0; background: none; font: inherit; font-size: var(--t-label); color: var(--ghost);
  cursor: pointer; padding: 2px 6px; border-radius: var(--r-sm); }
.refedit:hover { color: var(--wksoft); background: rgb(var(--c-wk) / .1); }
/* THE PEN IS THE WORD. CEO, 2026-08-26: "i dont want the edit button to be here
   .. just a pen or pencel icon will do". Sized to the same box the word occupied
   so the row's geometry does not change, and it carries an `aria-label` because a
   `title` is not a label. */
.refedit.pen { display: inline-flex; align-items: center; justify-content: center;
  padding: 2px 5px; }
.refedit.pen svg { display: block; }
.refbody { padding: 2px 4px 6px; }
.refbody.plain { padding: 10px 12px; font-size: var(--t-body); color: var(--paper); display: flex; align-items: center; gap: 8px; }
/* Read-only until someone says which way they mean to edit it. Not hidden — a
   value you can see but not touch is the honest state for something owned by a
   page above this one. */
/* Locked means "not yours to type in yet", not "invisible to the pointer".
   `pointer-events: none` meant a press landed on the card behind it and nothing
   happened at all — no caret, no reason, no refusal. The body takes the press and
   answers it; the blocks inside stay untypeable. */
.refbody.locked { opacity: .72; cursor: pointer; }
.refbody.locked .btext, .refbody.locked .bgrip,
.refbody.locked button, .refbody.locked input { pointer-events: none; }
.refempty { padding: 12px; display: flex; align-items: center; gap: 12px; }
.refempty p { margin: 0; font-size: var(--t-small); color: var(--ghost); }
.reflog { padding: 6px 10px; border-top: 1px solid var(--line); font-size: var(--t-label); color: var(--ghost); }

/* ---------------- Dropbox preview and quick look ---------------- */
.dbxframe { position: relative; min-height: 260px; border-top: 1px solid var(--line); background: var(--panel2); }
.dbxframe iframe { width: 100%; height: 100%; min-height: 260px; border: 0; display: block; }
.dbxwait { display: flex; align-items: center; justify-content: center; height: 260px;
  font-size: var(--t-small); color: var(--ghost); }
/* Nothing to show is worth one line, not a screen of empty. The card above still
   opens the folder, so this is a note, not a failure. */
.dbxframe.dbxoff { min-height: 0; }
.dbxmsg { display: block; padding: 12px 14px; font-size: var(--t-small); color: var(--ghost); }
.pdfhead .linkmeta { min-width: 0; flex: 1; }
.pdfhead .linkname, .pdfhead .linkhost { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qlbox {
  position: fixed; z-index: calc(var(--z-dialog) + 2); inset: 24px; margin: auto;
  width: min(1100px, calc(100vw - 48px)); height: min(760px, calc(100dvh - 48px));
  display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--r-2xl);
  background: var(--panel); box-shadow: var(--shadow-card); overflow: hidden;
}
.qlbody { flex: 1; min-height: 0; display: flex; }
.qlbody .dbxframe { flex: 1; border-top: 0; }
.qlbody .dbxframe iframe { height: 100%; }
.qlframe { flex: 1; width: 100%; border: 0; background: var(--panel2); }
@media (max-width: 767.98px) { .qlbox { inset: 8px; width: calc(100vw - 16px); height: calc(100dvh - 16px); } }

/* ---------------- templates ---------------- */
.tkey { margin-inline-start: auto; font-size: var(--t-caps); color: var(--ghost); border: 1px solid var(--line);
  border-radius: var(--r-xs); padding: 0 4px; line-height: 15px; flex-shrink: 0; }
.tpl .tname { display: flex; align-items: center; gap: 6px; }
.tplmore { border-style: dashed; }
.tplpanel { width: min(680px, calc(100vw - 32px)); }
.tpledit .adminbody { padding-bottom: 20px; }
.tplsec { padding: 10px 14px 14px; border-top: 1px solid var(--line); }
/* The public log. Three columns that always line up — who, what, when — because
   the question is usually "who touched this" and a ragged left edge makes that
   a search rather than a glance. */
.actlist { list-style: none; margin: 0; padding: 0; }
.actrow { display: grid; grid-template-columns: minmax(84px, 22%) 1fr auto; gap: 10px;
  align-items: baseline; padding: 9px 14px; border-block-end: 1px solid var(--line);
  font-size: var(--t-body); line-height: 1.5; }
.actrow:last-child { border-block-end: 0; }
.actwho { font-weight: 600; color: var(--paper); overflow-wrap: anywhere; }
.actwhat { color: var(--mute); overflow-wrap: anywhere; }
.actwhen { color: var(--ghost); font-size: var(--t-label); white-space: nowrap; }
.actempty { margin: 0; padding: 14px; font-size: var(--t-body); color: var(--mute); }
@media (max-width: 767.98px) {
  /* The columns give way before the content does: who and what stack, and the
     time keeps its own line rather than squeezing the sentence. */
  .actrow { grid-template-columns: 1fr; gap: 2px; }
}
.tplsectitle { display: flex; align-items: center; gap: 10px; font-size: var(--t-small); font-weight: 600;
  color: var(--paper); margin-bottom: 2px; min-height: 28px; }
.tplsectitle .btn { margin-inline-start: auto; }
/* THE RULE ROW (his correction, 2026-09-06): a switch, the trigger's face, the
   sentence over a quiet line, one menu — the member row's shape. The switch
   is a small track with a knob, the one place this product draws a toggle:
   the state is a colour AND a position, never colour alone. */
.rulrow { align-items: center; gap: 12px; padding: 10px 0; }
.rulrow .rulsent { white-space: normal; line-height: 1.45; }
.rulrow.isoff .rulsent { color: var(--ghost); }
.rulrow .rulface { color: var(--mute); display: inline-flex; align-items: center; justify-content: center; }
.rulrow .rulmore { flex-shrink: 0; inline-size: 30px; padding-inline: 0; opacity: .7; }
.rulrow:hover .rulmore, .rulrow:focus-within .rulmore { opacity: 1; }
.rulstate { flex-shrink: 0; inline-size: 34px; text-align: center; }
.tgl { position: relative; display: inline-flex; flex-shrink: 0; inline-size: 34px; block-size: 20px; cursor: pointer; }
.tgl input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.tgltrack { position: absolute; inset: 0; border-radius: var(--r-pill); background: rgb(var(--c-line) / .9);
  border: 1px solid var(--line); transition: background .15s; }
.tgltrack::after { content: ''; position: absolute; inset-block-start: 2px; inset-inline-start: 2px;
  inline-size: 14px; block-size: 14px; border-radius: 50%; background: var(--paper); opacity: .85;
  transition: transform .15s; }
.tgl input:checked + .tgltrack { background: rgb(var(--c-wk) / .85); border-color: rgb(var(--c-wk) / .9); }
.tgl input:checked + .tgltrack::after { transform: translateX(14px); opacity: 1; }
[dir="rtl"] .tgl input:checked + .tgltrack::after { transform: translateX(-14px); }
.tgl input:focus-visible + .tgltrack { outline: 2px solid rgb(var(--c-wk) / .7); outline-offset: 2px; }
.tplsectitle .kcount { margin-inline-start: 4px; }
/* The rules section's two doors sit together: the library, then Add. */
.tplsectitle .rulebtns { display: flex; gap: 8px; align-items: center; margin-inline-start: auto; }
.tplsectitle .rulebtns .btn { margin-inline-start: 0; }
/* THE AUTOMATION LIBRARY (his correction: "it needs a side bar to
   organize it"): the settings panel's rail-and-body inside the one frame,
   wider and taller than a dialog because it is a place, not a question. The
   rail scrolls on its own; the shelf is the templates' card grid. */
.libpanel { width: min(900px, calc(100vw - 32px)); height: min(620px, calc(100dvh - 48px)); display: flex; flex-direction: column; }
.libpanel .setwrap { flex: 1; min-height: 0; }
.libpanel .setnav { overflow-y: auto; }
/* The rail is as wide as its longest category, so no category is cut (design
   audit B6); the label span ellipsises and the count keeps its column. */
.libpanel .setnav { inline-size: 224px; }
.setnavrow .tntxt { flex: 1; min-width: 0; }
.setnavrow .kcount { flex: none; }
.libbody { display: flex; flex-direction: column; }
.libbody .libsearch { margin: 12px 14px 0; }
.libbody .libsearch input { flex: 1; }
.libbody .tplsec { flex: 1; }
.libbody .confirmrow { padding: 8px 14px 12px; }
.rcpcard .tpldesc { -webkit-line-clamp: 3; }
/* A recipe card has one corner control, not two, so its name keeps the room. */
.rcpcard .tname { padding-inline-end: 30px; align-items: flex-start; }
/* A recipe's name is a sentence's worth — two lines, then the ellipsis. */
.rcpcard .tntxt { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.3; }
.libread .tplmeta { margin-top: 0; }
/* The read view: a padded page of its own inside the library's frame, its
   buttons resting at the foot whatever the sentence's length. */
.libread { display: flex; flex-direction: column; gap: 10px; flex: 1; min-height: 0; padding: 16px 18px 14px; overflow-y: auto; }
.libread .rulsent { font-size: var(--t-lead); color: var(--paper); line-height: 1.5; }
.libread .confirmrow { margin-top: auto; padding-top: 12px; }
/* Cards, the same shape the empty page offers, so the shelf and the page someone
   lands on read as one thing rather than a list and a set of buttons. */
.tplgrid { display: grid; gap: 8px; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); margin-top: 8px; }
.tplcard { position: relative; display: flex; border: 1px solid var(--line); border-radius: var(--r-xl); background: var(--panel2); }
.tplcard:hover { border-color: rgb(var(--c-wk) / .6); }
.tplgo {
  display: flex; flex-direction: column; gap: 5px; width: 100%; text-align: start;
  justify-content: flex-start;
  border: 0; background: none; font: inherit; padding: 11px 12px; cursor: pointer; border-radius: var(--r-xl);
}
/* Stretched link: the whole card is the target, the two controls sit above it. */
.tplgo::after { content: ''; position: absolute; inset: 0; border-radius: var(--r-xl); }
.tplcard .tname { padding-inline-end: 56px; display: flex; align-items: center; gap: 6px; font-size: var(--t-body); font-weight: 500; color: var(--paper); }
.tplcard .tname svg { flex-shrink: 0; color: var(--ghost); }
.tntxt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tpldesc { font-size: var(--t-small); color: var(--ghost); line-height: 1.4;
  /* Two lines, then an ellipsis. A card that grows with its description makes
     the row it sits in grow with it, and one long line leaves gaps beside every
     short one. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.tplmeta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: auto; padding-top: 2px; }
.tplowner { font-size: var(--t-label); color: var(--ghost); }
/* Both corner controls keep their space reserved and only fade in, so nothing
   moves under the pointer as a card is approached. */
.tplstar, .tplmenu {
  position: absolute; top: 6px; z-index: 1; width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  border: 0; background: none; padding: 0; border-radius: var(--r-sm); color: var(--ghost); cursor: pointer;
}
.tplstar { inset-inline-end: 34px; opacity: 0; transition: opacity .12s; }
.tplmenu { inset-inline-end: 4px; opacity: 0; transition: opacity .12s; }
.tplcard:hover .tplstar, .tplcard:hover .tplmenu,
.tplcard:focus-within .tplstar, .tplcard:focus-within .tplmenu { opacity: 1; }
.tplstar.on { opacity: 1; color: var(--warn); }
.tplstar.on svg { fill: currentColor; }
.tplstar:hover, .tplmenu:hover { background: rgb(var(--c-line) / .6); color: var(--paper); }
/* The share panel lists people, and a person is a row. */
.tplrow { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--line); }
.tplrow:last-child { border-bottom: 0; }
.tplmain { min-width: 0; flex: 1; }
.tplname { font-size: var(--t-body); color: var(--paper); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tplform { display: flex; flex-direction: column; gap: 10px; padding: 12px 14px; }
.tplform label { display: flex; flex-direction: column; gap: 4px; font-size: var(--t-small); color: var(--mute); }
.tplform input { border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel2);
  color: var(--paper); font: inherit; font-size: var(--t-body); padding: 7px 9px; }
.tplwhat { border: 1px solid var(--line); border-radius: var(--r-lg); padding: 8px 10px; margin: 0; }
.tplwhat legend { font-size: var(--t-label); color: var(--ghost); padding: 0 4px; }
.tplwhat label { flex-direction: row; align-items: center; gap: 8px; font-size: var(--t-body); color: var(--paper);
  padding: 3px 0; }
.tplwhat input:disabled + span, .tplwhat label:has(input:disabled) { color: var(--ghost); }
.tpleblocks { padding: 0 14px; }

/* ---------------- the side peek ---------------- */
/* W-14a: the task side-peek slides in and the backdrop fades. Both keyframes
   are the real ones out of globals.css, so this matches the module. A card at
   the inline end is the default because the board behind it is the context you
   came from; expanding is for when the item IS the work. */
.peekback {
  position: fixed; inset: 0; z-index: calc(var(--z-side) - 1);
  background: rgb(var(--c-scrim) / .6);
}
/* THE SCRIM SITS ONE RUNG UNDER THE SURFACE IT BELONGS TO. Every host writes
   the scrim as the sibling right before its box, so the box's class says which
   rung: a dialog's scrim dims the side panels beneath it, a confirm's scrim
   dims the dialog it was asked over. `.over` is the older explicit mark for
   "over a dialog" and means the same as a confirm's scrim. */
.peekback:has(+ .adminpanel) { z-index: calc(var(--z-dialog) - 1); }
.peekback:has(+ .qlbox) { z-index: calc(var(--z-dialog) + 1); }
.peekback:has(+ .confirmbox), .peekback.over { z-index: calc(var(--z-confirm) - 1); }
.peek {
  position: fixed; inset-block: 0; inset-inline-end: 0; z-index: var(--z-side);
  min-width: 340px; display: flex; flex-direction: column;
  background: var(--panel); border-inline-start: 1px solid var(--line);
  box-shadow: var(--shadow-card);
}
/* Constraint 5: a fixed overlay must fit the viewport. inset-block:0 with the
   body scrolling internally is what guarantees the foot is always reachable. */
/* .peek.full no longer spans the screen: the stops divide the
   CONTENT AREA, so 100% ENDS AT THE SIDEBAR — the width says so and needs no
   inset override. The phone block below still takes the whole screen. */
/* The draggable edge. Wide enough to hit, invisible until pointed at. */
.peekgrip {
  position: absolute; inset-block: 0; inset-inline-start: -3px; width: 9px;
  cursor: col-resize; z-index: 9; touch-action: none;
}
.peekgrip::after {
  content: ''; position: absolute; inset-block: 0; inset-inline-start: 3px; width: 2px;
  background: transparent; transition: background-color .12s;
}
.peekgrip:hover::after, body.resizing .peekgrip::after { background: var(--wk); }
/* The jump between stops is animated so a snap reads as a snap rather than a
   glitch. WHILE DRAGGING it is off: the edge follows the pointer now,
   and a transition there is a panel trailing behind the hand — the magnet
   snap mid-drag stays visible as the jump it is. */
@media (prefers-reduced-motion: no-preference) {
  .peek { transition: width .12s ease-out; }
  .peek.enter { transition: none; }
}
body.resizing .peek { transition: none; }
/* The slide belongs to OPENING the peek, not to redrawing it. Left on .peek it
   replayed on every re-render — renaming, checking a box, posting a message —
   and the panel read as reloading itself. It is now only on .enter, which is
   set the first time an item is shown and never again. */
@media (prefers-reduced-motion: no-preference) {
  .peekback.enter { animation: work-fade-in 150ms ease-out; }
  .peek.enter { animation: work-peek-in 200ms ease-out; }
  .peek.full.enter { animation: work-fade-in 150ms ease-out; }
}
/* Icon-only, so the label has to be carried by aria-label — a title attribute
   is not announced by most screen readers, the finding HomeLink already records. */
.iconbtn { padding: 0; gap: 0; width: 30px; height: 30px; justify-content: center; align-items: center; }
.phead .ctl[data-close] { padding: 0; gap: 0; width: 30px; height: 30px; justify-content: center; align-items: center; }
.cornerarrow { display: flex; align-items: center; justify-content: center; }
[dir='rtl'] .cornerarrow { transform: scaleX(-1); }
.phead {
  display: flex; align-items: center; gap: 8px; flex-shrink: 0;
  padding: 10px 14px; border-bottom: 1px solid var(--line); background: var(--panel);
}
.pbody { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 20px; }
.peek.full .pbody { padding: 28px 20px 60px; }
/* Was capped at 940 for the same reason and dropped for the same one — a
   full-width peek is a page, and pages do not have bands down their sides. */

.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: var(--t-small); color: var(--mute); margin-bottom: 16px; }
.crumbs button { border: 0; background: none; font: inherit; font-size: var(--t-small); color: var(--mute); cursor: pointer; padding: 3px 6px; border-radius: var(--r-sm); }
.crumbs button:hover { background: var(--panel2); color: var(--paper); }
.crumbs .sep { color: var(--ghost); }
.crumbs .here { color: var(--paper); padding: 3px 6px; }
/* Inline in the page's control row rather than a band of its own, and it gives
   way before the controls do — a long branch must not push Share off the edge. */
.pgcrumbs { margin-bottom: 0; min-width: 0; overflow: hidden; flex-wrap: nowrap; }
.pgcrumbs .root { padding: 3px 6px; color: var(--ghost); }
.pgcrumbs button, .pgcrumbs .here { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.itemhead { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 4px; }
.itemhead h2 { margin: 0; font-size: var(--t-h2); font-weight: 600; letter-spacing: -.02em; flex: 1; min-width: 0; }
.peek.full .itemhead h2 { font-size: var(--t-h1); }
.itemhead h2.untitled { color: var(--ghost); font-style: italic; }
.titlein {
  flex: 1; min-width: 0; font: inherit; font-size: var(--t-h2); font-weight: 600; letter-spacing: -.02em;
  background: var(--ink); border: 0; border-radius: var(--r-md); padding: 2px 8px; color: var(--paper); outline: none;
  box-shadow: 0 0 0 2px var(--focus-ring);
}

.fields { display: grid; gap: 2px; margin: 18px 0; }
.frow { display: flex; align-items: center; gap: 12px; min-height: 36px; }
.fkey { width: 120px; flex-shrink: 0; font-size: var(--t-small); color: var(--ghost); }
.fval { flex: 1; min-width: 0; }
.fval input {
  width: 100%; border: 0; background: none; font: inherit; font-size: var(--t-body); color: var(--paper);
  padding: 6px 8px; border-radius: var(--r-sm); outline: none;
}
.fval input:hover { background: var(--panel2); }
.fval input:focus { background: var(--ink); box-shadow: 0 0 0 2px var(--focus-ring); }
.notes {
  width: 100%; min-height: 92px; resize: vertical; border: 1px solid var(--line); background: var(--ink);
  border-radius: var(--r-lg); padding: 10px 12px; color: var(--paper); font: inherit; font-size: var(--t-body);
  line-height: 1.65; outline: none;
}
.notes:focus { box-shadow: 0 0 0 2px var(--focus-ring); }
.notes::placeholder { color: var(--ghost); }

.kidlist { border: 1px solid var(--line); border-radius: var(--r-xl); overflow: hidden; }
.kid { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--line); }
.kid:last-child { border-bottom: 0; }
.kid:hover { background: var(--panel2); }
.kid button.klink { flex: 1; min-width: 0; text-align: start; border: 0; background: none; font: inherit; font-size: var(--t-body); color: var(--paper); cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kid button.klink:hover { text-decoration: underline; text-underline-offset: 2px; }
.kid button.klink.done { color: var(--ghost); text-decoration: line-through; }
.kid .kcount { font-size: var(--t-caps); color: var(--ghost); flex-shrink: 0; }
.sectitle { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 12px; }
.sectitle h3 { margin: 0; font-size: var(--t-large); font-weight: 600; letter-spacing: -.01em; }
.sectitle .tile { display: flex; height: 28px; width: 28px; flex-shrink: 0; align-items: center; justify-content: center; border-radius: var(--r-md); background: rgb(var(--c-wk) / .14); color: var(--wksoft); }
.sectitle .cnt { border: 1px solid var(--line); border-radius: var(--r-pill); background: var(--panel2); padding: 2px 8px; font-size: var(--t-label); font-weight: 600; color: var(--mute); }
.emptykids { padding: 22px 14px; text-align: center; font-size: var(--t-body); color: var(--mute); border: 1px dashed rgb(var(--c-line) / .8); border-radius: var(--r-xl); }

/* ---------------- page properties, Notion-style ---------------- */
/* Compact rows, muted labels, the value doing the talking. A property row is
   quiet until you point at it — the filled information is what you came to
   read, not the chrome around it. */
.props { display: grid; gap: 1px; margin: 14px 0 6px; }
.prow { display: flex; align-items: center; gap: 8px; min-height: 32px; border-radius: var(--r-sm); padding: 0 4px; }
.prow:hover { background: rgb(var(--c-panel2) / .6); }
.pkey {
  display: flex; align-items: center; gap: 7px; width: 150px; flex-shrink: 0;
  font-size: var(--t-small); color: var(--ghost);
}
.pkey svg { flex-shrink: 0; }

/* THE EMPTY PROPERTY ROW. It has to read as an invitation rather than as a
   property with no name, so the label is a field and the row is quiet until it is
   hovered or focused — the same way the waiting block is a line and not a button.
   It holds the same height as a real row, so nothing moves when it becomes one. */
.prow.pnew { opacity: .6; }
.prow.pnew:hover, .prow.pnew:focus-within { opacity: 1; }
.pnewname {
  background: transparent; border: 0; color: var(--paper); font: inherit;
  font-size: var(--t-small); padding: 2px 4px; border-radius: var(--r-sm); min-width: 0; width: 100%;
}
.pnewname::placeholder { color: var(--ghost); }
/* A column heading being renamed. Same metrics as the label it replaces so the
   header does not change height (reserve space), and the same transparent-then-
   focus-ring treatment as every other in-place name. */
.threname { width: 100%; min-width: 0; border: 0; background: transparent;
  font: inherit; font-size: var(--t-small); font-weight: 600; color: var(--paper); padding: 3px 6px;
  border-radius: var(--r-sm); outline: 0; }
.threname:focus { background: var(--ink); box-shadow: 0 0 0 2px var(--focus-ring); }
/* Where a row's properties come from, in the place the naming field would have
   been — so the answer is at the point the question is asked. */
/* Same dashed empty-state shape as `.linkempty`, because it is one: the place a
   property would have been named, saying why it cannot be. */
.pfromboard { margin: 6px 0 0; padding: 12px; font-size: var(--t-body); line-height: 1.5;
  color: var(--ghost); border: 1px dashed var(--line); border-radius: var(--r-lg); }
.pnewname:hover { background: var(--panel2); }
.pnewname:focus { background: var(--ink); outline: 0; box-shadow: 0 0 0 2px var(--focus-ring); }
.pnewtype {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; flex-shrink: 0; padding: 0;
  background: transparent; border: 0; border-radius: var(--r-sm); color: var(--mute); cursor: pointer;
}
.pnewtype:hover { background: var(--panel2); color: var(--paper); }
.pnewhint { color: var(--ghost); font-size: var(--t-small); }


.pval { flex: 1; min-width: 0; }
.pval input {
  width: 100%; border: 0; background: none; font: inherit; font-size: var(--t-body); color: var(--paper);
  padding: 5px 7px; border-radius: var(--r-sm); outline: none;
}
.pval input:hover { background: var(--panel2); }
.pval input:focus { background: var(--ink); box-shadow: 0 0 0 2px var(--focus-ring); }
.pval input::placeholder { color: var(--ghost); }

.notesline {
  width: 100%; border: 0; background: none; resize: none; overflow: hidden;
  font: inherit; font-size: var(--t-lead); line-height: 1.7; color: var(--mute);
  padding: 7px 8px; border-radius: var(--r-md); outline: none; min-height: 34px;
  margin: 2px 0 10px;
}
.notesline:hover { background: rgb(var(--c-panel2) / .6); }
.notesline:focus { background: var(--ink); box-shadow: 0 0 0 2px var(--focus-ring); color: var(--paper); }
.notesline::placeholder { color: var(--ghost); }

.rule { height: 1px; background: var(--line); margin: 14px 0 6px; }

/* ---------------- the page body: blocks ---------------- */
.blocks { margin-top: 2px; }
.blk { position: relative; display: flex; align-items: flex-start; gap: 4px; border-radius: var(--r-sm); }
/* A FIELD BLOCK IS ITS CONTROL. It borrows `.prow` for its shape, and `.prow`
   reserves a 150px label column because a property row lines its names up — a
   block has no name to line up, so the column would be 150px of nothing before
   every date. */
.blk.field .pval { flex: 1; min-width: 0; }
.blk:hover { background: rgb(var(--c-panel2) / .45); }
/* THE GRIP IS CENTRED ON WHATEVER IT IS GRIPPING.
   CEO, 2026-08-25: "the dots needs to be always centered vertically". `.blk` is
   `align-items: flex-start` so that a paragraph running to three lines keeps its
   first line beside the grip — right for text, wrong for the grip itself, which
   was pinned to the top of a reference card several times its own height and read
   as belonging to the card's first line rather than to the card.
   `align-self` overrides the row's alignment for this one child, so the text
   still starts at the top and the dots sit in the middle of any height. The old
   `margin-top: 2px` was nudging it against a single line of text and fights
   centring, so it goes. */
.bgrip {
  opacity: 0; flex-shrink: 0; width: 20px; height: 26px; display: flex; align-items: center;
  justify-content: center; border: 0; background: none; color: var(--ghost); cursor: pointer; border-radius: var(--r-xs);
  align-self: center;
}
.blk:hover .bgrip, .blk:focus-within .bgrip { opacity: 1; }
.bgrip:hover { background: var(--line); color: var(--paper); }
.bmark { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 18px; margin-top: 6px; color: var(--mute); font-size: var(--t-body); }
.btext {
  flex: 1; min-width: 0; outline: none; padding: 4px 6px; border-radius: var(--r-xs);
  font-size: var(--t-lead); line-height: 1.7; color: var(--paper); word-break: break-word;
}
/* A PLACEHOLDER IS A PROMPT, AND A PROMPT IS FOR SOMEBODY WHO IS ALREADY THERE.
   CEO, 2026-08-25: "this needs to be remove from the default block .. it should
   only shows up when the user clicks to type but has not typed anything".
   Every empty block used to carry "Type '/' for commands", so the line waiting at
   the bottom of every page in the product was permanently announcing a shortcut
   nobody had asked for — the busiest screen carrying an instruction on it at all
   times. Shown on FOCUS and empty, which is exactly the moment the sentence is
   useful and the only moment it is. */
.btext:empty:focus::before { content: attr(data-ph); color: var(--ghost); pointer-events: none; }
.btext:focus { box-shadow: 0 0 0 2px var(--focus-ring); }

.blk.h1 .btext { font-size: var(--t-h2); font-weight: 600; letter-spacing: -.02em; }
.blk.h1 { margin-top: 18px; }
.blk.h2 .btext { font-size: var(--t-h3); font-weight: 600; letter-spacing: -.015em; }
.blk.h2 { margin-top: 14px; }
.blk.h3 .btext { font-size: var(--t-large); font-weight: 600; letter-spacing: -.01em; }
.blk.h3 { margin-top: 10px; }
/* A toggle heading IS the heading, with a triangle. The rules are shared rather
   than duplicated so the two can never drift into different sizes. */
.blk.tg1 .btext { font-size: var(--t-h2); font-weight: 600; letter-spacing: -.02em; }
.blk.tg1 { margin-top: 18px; }
.blk.tg2 .btext { font-size: var(--t-h3); font-weight: 600; letter-spacing: -.015em; }
.blk.tg2 { margin-top: 14px; }
.blk.tg3 .btext { font-size: var(--t-large); font-weight: 600; letter-spacing: -.01em; }
.blk.tg3 { margin-top: 10px; }
/* The triangle sits on the heading's baseline rather than the body text's. */
.blk.tg1 .bmark.tgl, .blk.tg2 .bmark.tgl, .blk.tg3 .bmark.tgl { align-self: center; }

.bmark.num { font-variant-numeric: tabular-nums; font-size: var(--t-small); color: var(--mute); }
.bmark.tgl { border: 0; background: none; cursor: pointer; color: var(--ghost); padding: 0; transition: transform .12s; }
.bmark.tgl.open { transform: rotate(90deg); }
/* Empty reads as empty: dimmed, and it does not turn. Same space either way, so
   filling a toggle moves nothing on the line. */
.bmark.tgl.empty { color: var(--line); }
.bmark.tgl.empty:hover { color: var(--ghost); }
.bmark.tgl:hover { color: var(--paper); }

.blk.todo .box {
  flex-shrink: 0; margin-top: 8px; height: 15px; width: 15px; border-radius: var(--r-xs);
  border: 1.5px solid var(--line); display: flex; align-items: center; justify-content: center;
  cursor: pointer; background: none; padding: 0; color: transparent;
}
.blk.todo .box.on { border-color: var(--good); background: rgb(var(--c-good) / .18); color: var(--good); }
.blk.todo .btext.on { color: var(--ghost); text-decoration: line-through; }

.blk.quote { border-inline-start: 3px solid var(--line); margin: 6px 0; }
.blk.quote .btext { color: var(--mute); font-style: italic; }

.blk.callout {
  background: rgb(var(--c-wk) / .09); border: 1px solid rgb(var(--c-wk) / .28);
  border-radius: var(--r-lg); padding: 8px 12px 8px 8px; margin: 8px 0;
}
.blk.callout .bmark { color: var(--wksoft); font-size: var(--t-caps); }
.blk.callout .btext { color: var(--paper); font-size: var(--t-lead); }

.blk.code { background: var(--panel2); border: 1px solid var(--line); border-radius: var(--r-lg); margin: 8px 0; padding: 6px 10px 6px 4px; }
.blk.code .btext {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--t-small);
  line-height: 1.7; white-space: pre-wrap; color: var(--paper);
}

/* media --------------------------------------------------------------- */
.blk.link { align-items: flex-start; }
.linkwrap { flex: 1; min-width: 0; padding: 4px 0; }
.linkfig { margin: 0; }
.blkimg, .blkvid { max-width: 100%; display: block; border-radius: var(--r-lg); border: 1px solid var(--line); }
.blkaud { width: 100%; }
.embed { position: relative; padding-top: 56.25%; border-radius: var(--r-lg); overflow: hidden; border: 1px solid var(--line); background: var(--ink); }
.embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.linkcard {
  display: flex; align-items: center; gap: 10px; text-decoration: none;
  border: 1px solid var(--line); border-radius: var(--r-lg); padding: 11px 12px;
  background: var(--panel2); color: var(--paper);
}
.linkcard:hover { border-color: rgb(var(--c-wk) / .6); }
.linkcard.bad { border-color: rgb(var(--c-bad) / .5); color: var(--bad); cursor: pointer; }
.linkicon { flex-shrink: 0; display: flex; color: var(--wksoft); }
.linkmeta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.linkname { font-size: var(--t-body); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.linkhost { font-size: var(--t-label); color: var(--ghost); }
.linkcard > svg:last-child { flex-shrink: 0; color: var(--ghost); }

.pdfhead {
  display: flex; align-items: center; gap: 10px;
  border: 1px solid var(--line); border-radius: var(--r-lg) var(--r-lg) 0 0;
  background: var(--panel2); padding: 9px 12px;
}
.pdfhead + .pdfframe { border-radius: 0 0 var(--r-lg) var(--r-lg); border-top: 0; }
.pdftgl, .pdfopen {
  flex-shrink: 0; border: 0; background: none; padding: 0; font: inherit; font-size: var(--t-label);
  color: var(--ghost); cursor: pointer; text-decoration: none;
}
.pdftgl:hover, .pdfopen:hover { color: var(--wksoft); text-decoration: underline; }
/* Resizable because a one-page memo and a forty-page portfolio do not want the
   same height, and the reader knows which they have. */
.pdfframe {
  border: 1px solid var(--line); border-radius: var(--r-lg); overflow: hidden;
  background: var(--ink); height: 520px; min-height: 160px; resize: vertical;
}
.pdfframe iframe { width: 100%; height: 100%; border: 0; display: block; }


.filedrop { outline: 2px dashed rgb(var(--c-wk) / .8); outline-offset: -8px; background: rgb(var(--c-wk) / .06); }
.filedrop::after {
  content: 'Drop to add these files to the page';
  position: sticky; bottom: 12px; display: block; margin: 12px auto 0; width: fit-content;
  background: var(--wk); color: var(--onaccent); font-size: var(--t-small); font-weight: 600;
  padding: 7px 14px; border-radius: var(--r-pill);
}
[data-missing] { outline: 1px dashed rgb(var(--c-line)); min-height: 0; height: 0; display: none; }
.filemissing { padding: 10px 12px; font-size: var(--t-small); color: var(--ghost); border: 1px dashed var(--line);
  border-radius: var(--r-lg); background: var(--panel2); }
.linkopen.off, .pdfopen.off { color: var(--ghost); cursor: default; text-decoration: none; pointer-events: none; }

.linkempty {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  border: 1px dashed var(--line); border-radius: var(--r-lg); padding: 12px;
  font-size: var(--t-body); color: var(--ghost);
}
.linkempty:hover { border-color: rgb(var(--c-wk) / .6); color: var(--wksoft); }
.linkcap { font-size: var(--t-small); color: var(--mute); margin-top: 6px; }
.linkbar { display: flex; align-items: center; gap: 10px; margin-top: 6px; opacity: 0; transition: opacity .12s; }
.blk.link:hover .linkbar, .blk.link:focus-within .linkbar { opacity: 1; }

/* A RECOGNISED EMAIL, PHONE NUMBER OR PERSON, STYLED ONCE.
   The same in a block, in a discussion message and in a reference body, so a
   recognised thing never looks like two different things depending on where it
   was typed. Underlined rather than coloured alone — status is never colour
   alone here, and neither is a link. */
.rlink { color: var(--wksoft); text-decoration: underline; text-underline-offset: 2px; }
.rlink:hover { color: var(--accent2); }
.mention {
  color: var(--wksoft); background: rgb(var(--c-wk) / .14);
  border-radius: var(--r-xs); padding: 0 3px; font-weight: 600;
}
.linkopen { font-size: var(--t-label); color: var(--ghost); text-decoration: none; }
.linkopen:hover { color: var(--wksoft); text-decoration: underline; }
.linkedit { border: 0; background: none; padding: 0; font: inherit; font-size: var(--t-label); color: var(--ghost); cursor: pointer; }
.linkedit:hover { color: var(--wksoft); text-decoration: underline; }

.blk.div { padding: 8px 0; }
.blk.div .line { flex: 1; height: 1px; background: var(--line); }

/* A BLOCK THE READER HAS SELECTED is `.blk.sel`, which this product has had
   all along (it draws rows, pages and blocks alike) — the second
   set of styles that had been added beside it was deleted. The only thing kept from that
   attempt is the line below: with no caret in a selected block, nothing should
   blink in it. */
.blk.sel .btext { caret-color: transparent; }

/* ================= DISCUSSING A PART OF A PAGE =================
   A marked stretch of text, and a block with a conversation on it. The wash is
   the product's own accent family, not a new colour, and the mark carries an
   UNDERLINE as well as the wash so it is not colour alone saying "there is
   something here". */
.dmark {
  background: rgb(var(--c-warn) / .18);
  box-shadow: inset 0 -2px 0 rgb(var(--c-warn) / .7);
  border-radius: var(--r-hair); cursor: pointer;
}
.dmark:hover { background: rgb(var(--c-warn) / .3); }
/* A mark whose message has gone is not a highlight — it is the words it wraps. */
.dmark.dead { background: none; box-shadow: none; cursor: auto; }
/* A BLOCK with a conversation on it. EVERYTHING STAYS INSIDE THE BLOCK: the
   card clips (`.pagecard` is `overflow: hidden`, and deliberately), so a bar
   hung in the margin outside it is drawn nowhere and can be pressed by nobody
   — which is exactly what the first cut did, found by a probe whose press
   landed on the page behind. The start bar is an inset shadow, so it costs no
   layout and nothing shifts when a discussion appears. */
.blk.discussed {
  position: relative; border-radius: var(--r-sm);
  background: rgb(var(--c-warn) / .10);
  box-shadow: inset 3px 0 0 rgb(var(--c-warn) / .8);
}
[dir="rtl"] .blk.discussed { box-shadow: inset -3px 0 0 rgb(var(--c-warn) / .8); }
.blk.discussed:hover { background: rgb(var(--c-warn) / .16); }
/* The mark you press, at the block's own end and inside it. The text under it
   stays editable — a block is not "selected" because somebody is talking about
   it — so the conversation gets its own target rather than the whole body. */
/* THE PAGE STEPS ASIDE WHILE THE PANEL IS OPEN. The panel is a fixed
   overlay, so without this the end of every block — and the mark you press to
   reach its conversation — sits underneath it, unreachable at exactly the
   moment somebody is reading the conversation. Only the page card, only on a
   desktop: on a phone the panel is nearly the whole width and stepping aside
   would leave nothing to step onto. */
@media (min-width: 768px) {
  :root.chatopen .pagecard { margin-inline-end: min(400px, 92vw); }
}
.blk > .dbar {
  position: absolute; inset-block-start: 3px; inset-inline-end: 4px;
  width: 24px; height: 24px; padding: 0; display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--r-sm); background: rgb(var(--c-warn) / .18);
  color: var(--warn); cursor: pointer;
}
.blk > .dbar svg { width: 14px; height: 14px; }
.blk > .dbar:hover { background: rgb(var(--c-warn) / .32); }
.blk > .dbar:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The flash both directions end in. Reduced motion gets the colour with no
   movement, which still answers "which one". */
@keyframes dflash { 0% { background: rgb(var(--c-warn) / .38); } 100% { background: transparent; } }
.dflash { animation: dflash 1.4s ease-out; border-radius: var(--r-sm); }
@media (prefers-reduced-motion: reduce) { .dflash { animation: none; background: rgb(var(--c-warn) / .2); } }
/* The chip on a message, and the one in the draft. Same shape as a linked page
   (`.chatpg`, `.dchip`), because it is the same idea. */
.manchor {
  display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
  margin-block-end: 4px; padding: 3px 8px; border: 1px solid var(--line);
  border-inline-start: 3px solid rgb(var(--c-warn) / .8);
  border-radius: var(--r-sm); background: var(--panel2); color: var(--mute);
  font: inherit; font-size: var(--t-small); text-align: start; cursor: pointer;
}
.manchor:hover { color: var(--paper); border-color: var(--ghost); }
.manchor > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dchip.danchor { border-inline-start: 3px solid rgb(var(--c-warn) / .8); }

/* inline marks */
/* Size marks are global, not `.btext`-scoped: a block's html is drawn in the
   hover card, the print view and the peek as well, and a mark that held in
   one place and vanished in another would read as lost. */
.sz-sm { font-size: var(--t-mark-sm); }
.sz-lg { font-size: var(--t-mark-lg); }
.btext code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
  background: var(--panel2); border-radius: var(--r-xs); padding: 1px 5px; color: var(--wksoft);
}
.btext a { color: var(--wksoft); text-underline-offset: 2px; }
.btext s { color: var(--ghost); }

/* ================= A ROW OF BLOCKS =================
   Six across on the desktop, two across on a phone, and a lone leftover takes the
   whole width rather than sitting at half with a gap beside it — CEO, 2026-08-25,
   on what happens to the odd one: "the leftover will be alone", and full width is
   the recommendation he took.

   THE PHONE RULE IS CSS, NOT A SECOND GROUPING IN JS. `flex-wrap` turns six
   columns into three pairs on its own, in the same left-to-right order, so there
   is no phone-shaped copy of the layout logic to fall out of step with the
   desktop one. `:last-child:nth-child(odd)` is exactly "there was nothing to pair
   me with", which is the only case that wants the full width. */
.brow { display: flex; align-items: stretch; gap: 0; width: 100%; }
.bcol { flex: var(--w, 1) 1 0; min-width: 0; display: flex; flex-direction: column; }
/* A column's blocks keep their own left padding; the column supplies the gutter,
   so two paragraphs side by side are not touching. */
/* FLUSH BOTTOMS. CEO, 2026-08-25: "if one block in a row is higher than other
   blocks other blocks hight will be adjusted to be flushed with the higher one".
   `align-items: stretch` on the row gives every column the tallest one's height;
   these two carry that height down through the block to the editable area, so a
   short paragraph beside a tall one is a tall box you can click anywhere in —
   rather than a short box with dead space under it, which is the same defect as a
   press that lands on nothing.
   Blocks NOT in a row are untouched by any of this and stay full width, which is
   what dissolving a row of one in `normaliseRows` protects. */
.bcol > .blk { flex: 1; align-items: stretch; }
.bcol > .blk > .btext { flex: 1; }
.brow > .bcol + .bcol > .blk, .brow .bsplit + .bcol > .blk { padding-inline-start: 4px; }

/* THE HANDLE BETWEEN TWO COLUMNS. Six pixels wide with the rule drawn down the
   middle of it: a 1px target cannot be hit, and reserving the six keeps the
   columns from shifting when it appears. Invisible until the row is hovered, for
   the same reason the block grip is. */
.bsplit { flex: 0 0 6px; align-self: stretch; cursor: col-resize; position: relative;
  opacity: 0; transition: opacity .12s; }
.brow:hover .bsplit, .bsplit.dragging { opacity: 1; }
.bsplit::before { content: ''; position: absolute; inset-block: 2px; inset-inline-start: 2px;
  width: 2px; border-radius: var(--r-hair); background: var(--line); }
.bsplit:hover::before, .bsplit.dragging::before { background: var(--wk); }
/* On the snap point, so it is visible that it will land evenly. */
.bsplit.snapped::before { background: var(--good); }

/* A DIVIDER IN A ROW STANDS UP. CEO, 2026-08-25: "if the divider block is dragged
   in a row with more blocks becomes vertical not horozontal". It is still "a
   block like everything else" — its own column, counting toward the six — it is
   only drawn on its side, because a horizontal rule a third of the page wide
   reads as a mistake rather than a division. */
.bcol > .blk.div { align-items: stretch; }
.bcol > .blk.div .line { flex: 0 0 2px; width: 2px; height: auto; min-height: 24px;
  align-self: stretch; margin: 2px auto; background: var(--line); }

/* THE EDGES OF A BLOCK MEAN BESIDE. The insertion mark is vertical there, because
   the thing it promises is a column, not a line above or below. */
.blk.dropleft { box-shadow: inset 2px 0 0 var(--wk); }
.blk.dropright { box-shadow: inset -2px 0 0 var(--wk); }
.dropcopy.dropleft { box-shadow: inset 2px 0 0 var(--good); }
.dropcopy.dropright { box-shadow: inset -2px 0 0 var(--good); }

/* drag target */
.blk.dropbefore { box-shadow: inset 0 2px 0 var(--wk); }
.blk.dropafter { box-shadow: inset 0 -2px 0 var(--wk); }

/* ================= OPTION COPIES, AND IT LOOKS THE SAME EVERYWHERE =================
   CEO, 2026-08-24: "if the user option drags anywhere it make a copy", and "if a
   fucking user option drags a block .. it is copied .. this is consistent through
   out the fucking system".

   One gesture with one meaning gets one appearance, written once here rather than
   four times in four wirings — which is how the sidebar ended up with a green wash
   and a COPY tag while the board and the block editor showed nothing at all, so the
   same key looked like three different features.

   TWO SIGNALS, NOT ONE. They answer different questions and they were welded into a
   single class, which is what made a copy into a gap indistinguishable from a move:
     - the tag says THIS IS A COPY, and rides every zone, including the gaps;
     - the wash says IT LANDS INSIDE THIS ROW, and only appears with `dropinto`.
   The insertion edge keeps its shape and only changes colour, so the mark still
   reads as the same mark. Colour alone would not be enough on its own — the tag is
   the glyph that carries it, which is the house rule for any state.

   Placed after every surface's own drop rules so it wins on order, not on weight. */
.dropcopy.dropbefore, .tt tbody tr.dropcopy.dropbefore td { box-shadow: inset 0 2px 0 var(--good); }
.dropcopy.dropafter, .tt tbody tr.dropcopy.dropafter td { box-shadow: inset 0 -2px 0 var(--good); }
.dropcopy.dropinto, .tt tbody tr.dropcopy.dropinto td {
  background: rgb(var(--c-good) / .14); box-shadow: inset 0 0 0 1px rgb(var(--c-good) / .5);
}
.tt tbody tr.dropcopy.dropinto .name { box-shadow: inset 3px 0 0 var(--good); }
/* The tag needs a containing block, and every row here is a plain flow element.
   Reserved rather than inserted: it is positioned out of flow, so no row changes
   height or width when it appears and nothing under the pointer moves. */
.navrow.dropcopy, .blk.dropcopy, .tlrow.dropcopy, .tt tbody tr.dropcopy .name { position: relative; }
.navrow.dropcopy::after, .blk.dropcopy::after, .tlrow.dropcopy::after,
.tt tbody tr.dropcopy .name::after {
  content: 'COPY'; position: absolute; inset-inline-end: 8px; top: 50%;
  transform: translateY(-50%); font-size: var(--t-micro); font-weight: 700;
  letter-spacing: .08em; color: var(--good); pointer-events: none;
}
/* On a HEADING the tag follows the word instead of sitting at the inline end,
   because the end is where the trash and plus buttons already are. Out of flow
   either way, so neither the word nor the buttons move when it appears. */
.navgroup.dropcopy > span:first-child { position: relative; }
.navgroup.dropcopy > span:first-child::after {
  content: 'COPY'; position: absolute; inset-inline-start: 100%; top: 50%;
  transform: translateY(-50%); margin-inline-start: 8px;
  font-size: var(--t-micro); font-weight: 700; letter-spacing: .08em;
  color: var(--good); pointer-events: none;
}
.bgrip { cursor: grab; }
.bgrip:active { cursor: grabbing; }
.menu.slash .mi.cur { background: var(--panel2); box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .5); }

/* The waiting line's gutter. It has no grip — there is nothing to drag yet —
   but it holds the same space, so the line sits exactly where the block it is
   about to become will sit. Reserve space; never let content decide geometry. */
.ghostgrip { display: inline-block; width: 13px; height: 13px; flex-shrink: 0; }

.addblk {
  display: inline-flex; align-items: center; gap: 6px; border: 0; background: none;
  padding: 6px 8px; margin-top: 6px; font: inherit; font-size: var(--t-body); color: var(--ghost);
  cursor: pointer; border-radius: var(--r-sm);
}
.addblk:hover { color: var(--wksoft); background: rgb(var(--c-wk) / .1); }

/* ---------------- templates ---------------- */
.tplbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 10px 0 4px; }
.tpls { display: grid; gap: 8px; grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); margin-top: 10px; }
.tpl {
  border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel2);
  padding: 11px 12px; text-align: start; cursor: pointer; font: inherit;
  transition: border-color .15s, background-color .15s;
}
.tpl:hover { border-color: rgb(var(--c-wk) / .6); background: rgb(var(--c-wk) / .08); }
.tpl .tname { font-size: var(--t-body); font-weight: 600; color: var(--paper); display: flex; align-items: center; gap: 7px; }
.tpl .tdesc { font-size: var(--t-label); color: var(--ghost); margin-top: 4px; line-height: 1.5; }
.blankpage {
  border: 1px dashed rgb(var(--c-line) / .9); border-radius: var(--r-xl); padding: 18px;
}
.blankpage p { margin: 0 0 4px; font-size: var(--t-body); color: var(--mute); }
.blankpage .hint { font-size: var(--t-small); color: var(--ghost); }


/* ---------------- view switcher ---------------- */
.viewbar { display: inline-flex; gap: 2px; background: transparent; border-radius: var(--r-lg); padding: 3px; }
.viewbtn {
  display: inline-flex; align-items: center; gap: 6px; border: 0; background: none;
  padding: 6px 11px; border-radius: var(--r-md); font: inherit; font-size: var(--t-body);
  color: var(--mute); cursor: pointer; white-space: nowrap;
}
.viewbtn:hover { color: var(--paper); }
/* The chosen view is underlined, not lifted into a box (his style, 2026-10-09). */
.viewbtn.on { background: none; color: var(--paper); box-shadow: inset 0 -2px 0 rgb(var(--c-wk)); border-radius: 0; }
.viewbtn.on svg { color: var(--wksoft); }
/* The strip's editor affordance and its reorder marks. The marks are
   the column headers' own, physical for the same reason theirs are. */
.viewbtn.viewedit { color: var(--ghost); }
.viewbtn.viewedit:hover { color: var(--paper); }
.viewbtn.dropbefore { box-shadow: inset 2px 0 0 var(--wk); }
.viewbtn.dropafter { box-shadow: inset -2px 0 0 var(--wk); }
/* A chip being renamed IS the field: same footprint as the chip it
   replaces, so the strip does not shift under the caret. */
.viewbtn.viewrenaming { background: var(--panel); box-shadow: var(--shadow-card); }
.viewrename {
  background: none; border: 0; outline: none; font: inherit; font-size: var(--t-body);
  color: var(--paper); width: 10ch; padding: 0;
  border-block-end: 1px solid var(--wk); border-radius: 0;
}

/* Board, Calendar and Timeline sit in the same card as the table they replace.
   This one used to draw a full 12px box under a toolbar whose bottom border was
   removed on the assumption the TABLE would close it — so switching to an alt
   view left an unclosed box above a rounded one. Gone with the rest of the
   second card. */
/* A NESTED FOLDER IS A ROW, NOT A SHELF.
   The markup is identical — one renderer, his rule 8 — and the difference is
   written here, which is where a difference that is only visual belongs.
   A top-level shelf is a HEADING over a section: a bordered box, and a name
   in small caps. A folder inside one is an ITEM in that section, and dressing
   it as a heading made three things wrong at once, all of them visible in the
   first screenshot: boxes nested inside boxes three deep; names in caps with
   letter-spacing, which at an indent left four of five truncated to
   "CLIENT WO…"; and a glyph-plus-caps row that read louder than the pages
   under it. So at depth the box goes, the caps go, and the row takes the
   same size and weight as a page row — which is what it sits beside. */
.navfoldergroup.navnested { border: 0; border-radius: 0; margin-block-end: 0;
  padding: 0; overflow: visible; }
.navfoldergroup.navnested > .navgroup {
  font-size: var(--t-body); font-weight: 600; letter-spacing: 0;
  text-transform: none; min-height: 28px; margin-block-start: 0;
  border-radius: var(--r-md);
  /* The step is the page rows' own, so a tree of folders and a tree of pages
     keep one rhythm rather than each inventing its own. */
  padding-inline-start: calc(var(--fdepth, 1) * 12px); }
.navfoldergroup.navnested > .navgroup .folname { color: var(--paper); }
/* Its contents step in with it, so what is inside a folder reads as inside
   it rather than as a sibling that happens to follow. */
.navfoldergroup.navnested > .navrow,
.navfoldergroup.navnested > .navsub { padding-inline-start: calc(var(--fdepth, 1) * 12px); }
/* The empty note lines up under the NAME of the folder it belongs to, not
   under the twisty: a sentence that starts further left than the folder it is
   about reads as belonging to the folder above it. */
.navfoldergroup.navnested > .navempty { padding-inline-start: calc(26px + var(--fdepth, 1) * 12px); }
/* THE SERVER-IS-BEHIND STRIP. Loud enough to be read and quiet enough
   to work under: the warning ground, full width, above everything, and no
   control on it — there is nothing the reader can press, the fix is a restart.
   It uses the tokens like everything else; no literal colour.
   TEXT IS `--paper`, NOT `--ink`. `--ink` is the page GROUND in this set, and
   using it put near-black on a dark amber wash: 13.77:1 now against 1.27:1
   before, which was no text at all. Measured in the browser, both themes
   (dark 13.77:1, light 12.74:1 on the band), not judged from a screenshot —
   the screenshot looked fine. */
.stalebar { background: rgb(var(--c-warn) / 0.16); color: var(--paper);
  border-block-end: 1px solid rgb(var(--c-warn) / 0.5);
  padding: 9px 16px; font-size: var(--t-small); line-height: 1.45;
  text-align: center; }
.stalebar[hidden] { display: none; }
.viewpane { overflow: hidden; }
/* THE DASHBOARD VIEW'S PANE. The other alt views draw inside the
   table's card and clip to it. This one holds the widget grid, which has its
   own tiles, its own resize overlay drawn 6px outside itself, and a tile that
   floats above everything while it is dragged — all of which the card's
   `overflow: hidden` would cut off. It also needs the same breathing room
   above the first tile that the dashboard page gives it. */
.viewpane.dashpane { overflow: visible; padding-block: 4px 14px; }
.vempty { font-size: var(--t-small); color: var(--ghost); padding: 10px 4px; margin: 0; }
.vnote { font-size: var(--t-small); color: var(--ghost); }
.vblocked { padding: 34px 24px; text-align: center; }
.vblocked p { font-size: var(--t-body); color: var(--mute); margin: 0; }

/* cards shared by Board and Timeline */
.vcard {
  display: flex; flex-direction: column; gap: 7px; width: 100%; text-align: start;
  border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel2);
  padding: 10px 11px; cursor: pointer; font: inherit;
}
.vcard:hover { border-color: rgb(var(--c-wk) / .6); }
.vcrumb { font-size: var(--t-caps); color: var(--ghost); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vname { font-size: var(--t-body); font-weight: 500; color: var(--paper); line-height: 1.4; }
.vname.done { color: var(--ghost); text-decoration: line-through; }
.vmeta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }

/* ---------------- Board ---------------- */
.kanban { display: flex; gap: 12px; padding: 14px; overflow-x: auto; overscroll-behavior-x: contain; align-items: flex-start; }
.kcol { flex: 0 0 268px; display: flex; flex-direction: column; gap: 10px; }
.khead { display: flex; align-items: center; gap: 8px; }
.kcount { font-size: var(--t-label); color: var(--ghost); font-variant-numeric: tabular-nums; }
.kbody { display: flex; flex-direction: column; gap: 8px; }

/* ---------------- Calendar ---------------- */
.calhead { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 12px 14px; border-bottom: 1px solid var(--line); }
/* The week rows SHARE the height sizeScroller gives the grid, so the whole
   month is on screen at once — no row may refuse to shrink, which is
   why the cells' floor is 0 on desktop and a crowded day scrolls inside its
   own cell instead of growing the row. The phone keeps flowing rows: two
   scroll areas fighting for one thumb is worse than a tall page there. */
.cal-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); grid-template-rows: auto; grid-auto-rows: minmax(0, 1fr); }
.cal-dow { padding: 8px 10px; font-size: var(--t-small); color: var(--ghost); border-bottom: 1px solid var(--line); }
.cal-cell { min-height: 0; overflow-y: auto; border-inline-end: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: 6px; display: flex; flex-direction: column; gap: 4px; }
.cal-cell.out { background: rgb(var(--c-panel2) / .35); }
/* ---------------- the three page blocks ---------------- */
/* The card: the page's image AS A THUMBNAIL BESIDE THE CONTENT — a card, not
   a banner (CEO, 2026-08-28: "i dont want a fucking banner for the page card
   i want a fucking page card"). Panel ground, hairline, radius from the kit. */
.pagecardbtn {
  display: flex; flex-direction: row; align-items: stretch; width: 100%; text-align: start;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 0; overflow: hidden; cursor: pointer; font: inherit; color: var(--paper);
}
.pagecardbtn:hover { border-color: rgb(var(--c-wk) / .5); }
.pcimg { flex: 0 0 96px; min-height: 96px; background-size: cover; background-position: center;
  border-inline-end: 1px solid var(--line); }
.pcbody { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; min-width: 0; }
.pcname { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; color: var(--paper); }
.pcname svg { color: var(--wksoft); }
.pcrow { display: flex; gap: 10px; font-size: var(--t-small); }
.pclab { color: var(--mute); min-width: 96px; flex-shrink: 0; }
/* --paper, NOT --ink: --ink is the PAGE GROUND (near-black on dark, near-
   white on light), so ink-coloured text on the card's panel was invisible in
   both themes — latent since the block shipped (2026-08-28), exposed the
   first time the flow chart put a real value in the row (his "you broke the
   title", 2026-09-02). Paper on panel is the measured body pairing. */
.pcval { color: var(--paper); overflow-wrap: anywhere; }
/* The preview: the whole body, read-only, scrolling inside its own frame —
   hard constraint 4's shape. The inner content is inert; the scroll lives on
   the wrapper, which is not. */
.prevwrap { border: 1px solid var(--line); border-radius: var(--r-lg); overflow: hidden; background: var(--panel); }
.prevhead { width: 100%; border-block-end: 1px solid var(--line); border-radius: 0; }
/* The word that says the table below is withholding rows — a filtered
   preview must never pass for the whole board. A word, not a colour: status
   never travels on colour alone. */
.prevflt { margin-inline-start: auto; flex-shrink: 0; font-size: var(--t-small); font-weight: 700; color: var(--mute);
  border: 1px solid var(--line); border-radius: var(--r-pill); padding: 2px 8px; }
/* Most of a screen, not a slot (CEO, 2026-08-28) — the body has to be
   READABLE in place, and the frame scrolls when it runs longer. */
/* Both axes: a wide table inside the preview scrolls HERE (constraint 4 —
   the page level never scrolls sideways). 420px is the DEFAULT height (CEO,
   2026-08-28); the grip below stretches or shrinks it, stored on the block. */
.prevscroll { max-height: 420px; overflow: auto; }
.prevgrip { height: 9px; cursor: row-resize; touch-action: none;
  border-block-start: 1px solid var(--line); background: var(--panel2); }
.prevgrip:hover { background: rgb(var(--c-wk) / .25); }
/* The as-is table scrolls SIDEWAYS inside its own wrap (hard constraint 4;
   CEO, 2026-08-28: "horozontal scrowlling should work on page side view") —
   in the side view there is no outer scroller to lean on, and
   the preview is interactive, so an inner scroller works there too. */
.prevtable { overflow-x: auto; }
.prevtable .tt { table-layout: fixed; min-width: 100%; }
.prevtable th { text-align: start; font-size: var(--t-small); color: var(--ghost); padding: 8px 10px;
  border-block-end: 1px solid var(--line); }
/* pointer-events, not `inert`: inert removed the content from hit-testing so
   the wheel had nothing to grab and the preview would not scroll under the
   pointer. pointer-events lets the wheel land while every click passes dead
   through — and the data- hooks are stripped anyway, so there is nothing to
   click into. */
.prevbody { padding: 8px 14px; }
.prevrow { display: flex; align-items: center; gap: 8px; padding-block: 5px;
  border-block-end: 1px solid rgb(var(--c-line) / .5); font-size: var(--t-body); }
.prevrow .done { text-decoration: line-through; color: var(--mute); }
.prevrow svg { color: var(--mute); flex-shrink: 0; }
.prevempty { color: var(--mute); font-size: var(--t-body); padding-block: 8px; }

/* Today is ONLY the circle on the number (CEO, 2026-08-28: "the cell should
   be the same as the rest") — the washed cell that sat here is gone. */
.cal-num { font-size: var(--t-label); color: var(--ghost); font-variant-numeric: tabular-nums; }
/* TODAY WEARS THE FILLED CIRCLE every calendar people know (CEO, 2026-08-28:
   "todays date should be highlighted" — the tinted wash alone did not read).
   onaccent on a wk fill measures 6.64:1 dark, 6.32:1 light — computed from
   the triplets, both clear of the 4.5 floor. */
.cal-cell.today .cal-num {
  color: var(--onaccent); background: var(--wk); font-weight: 700;
  display: inline-flex; align-items: center; justify-content: center;
  align-self: start;      /* the cell stretches its children; the circle must not become a bar */
  min-width: 20px; height: 20px; padding-inline: 5px; border-radius: var(--r-pill);
}
.cal-item {
  display: flex; align-items: center; gap: 5px; width: 100%; text-align: start;
  border: 0; border-radius: var(--r-sm); background: var(--panel2); color: var(--paper);
  padding: 4px 6px; font: inherit; font-size: var(--t-label); cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cal-item:hover { background: rgb(var(--c-wk) / .18); }
.cal-item.late { color: var(--bad); background: rgb(var(--c-bad) / .12); }
.cal-item svg { flex-shrink: 0; }

.hovercard {
  position: fixed; z-index: var(--z-hover); width: 292px; pointer-events: none;
  border: 1px solid var(--line); border-radius: var(--r-xl); background: var(--panel);
  box-shadow: var(--shadow-card); padding: 12px 14px;
}
@media (prefers-reduced-motion: no-preference) { .hovercard { animation: work-fade-in 120ms ease-out; } }
.hccrumb { font-size: var(--t-label); color: var(--ghost); margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hcname { font-size: var(--t-lead); font-weight: 600; color: var(--paper); line-height: 1.35; }
.hcname.done { color: var(--ghost); text-decoration: line-through; }
.hcchips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
.hcrows { margin-top: 10px; display: flex; flex-direction: column; gap: 5px; }
.hcrow { display: flex; gap: 10px; align-items: flex-start; font-size: var(--t-small); }
.hckey { width: 74px; flex-shrink: 0; color: var(--ghost); }
.hcval { flex: 1; min-width: 0; color: var(--paper); display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
.hcval .person { padding: 0; font-size: var(--t-small); }
.hcval .av { height: 18px; width: 18px; font-size: var(--t-micro); }
.hcnames { font-size: var(--t-small); color: var(--mute); }

/* ---------------- Timeline ---------------- */
.tlwrap { display: flex; flex-direction: column; min-height: 0; }
.tlrows { padding: 6px 0 8px; overflow-y: auto; }
.tlfoot { display: flex; align-items: center; gap: 12px; padding: 2px 14px 4px;
  border-top: 1px solid var(--line); background: var(--panel); flex-shrink: 0; }
.tlrow.newrow { padding-block: 0; }
.tlrow.newrow:hover { background: none; }
/* Collapsed: the tasks inside still draw, as thin lines on the project's own
   track. Folding a project should hide the detail, not the fact that there is
   work in there and roughly when it falls. */
.tlmini {
  position: absolute; min-width: 3px; border: 0; padding: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  color: var(--onaccent); background: var(--wk);
  /* the hairline is what carves a child out of the parent it sits on; without it
     two bands of the same colour read as one thick bar with a ragged edge */
  box-shadow: 0 0 0 1px rgb(var(--c-panel) / .7);
}
.tlmini.done { background: var(--good); }
.tlmini.late { background: var(--bad); }
.tlmini:hover { filter: brightness(1.12); box-shadow: 0 0 0 1px rgb(var(--c-panel) / .7), 0 0 0 3px rgb(var(--c-wk) / .3); }
.tlmini svg { opacity: .9; }
.tlrow { display: flex; align-items: center; gap: 12px; padding: 4px 14px; }
.tlrow:hover { background: var(--panel2); }
/* The timeline's left side IS the table's name cell — same markup, same rules.
   The only thing this adds is a width, and it takes that from the table's own
   Name column, so dragging that column's edge resizes both. */
.tlrow > .ncell, .tlfoot > .ncell { flex: 0 0 var(--namew, 260px); overflow: hidden; }
.tltrack { position: relative; flex: 1; height: 26px; background: var(--panel2); border-radius: var(--r-sm); }
.tlbar {
  position: absolute; top: 3px; bottom: 3px; min-width: 22px; border: 0; border-radius: var(--r-sm);
  background: var(--wk); color: var(--onaccent); cursor: pointer; display: flex;
  align-items: center; justify-content: center; padding: 0 6px;
  container-type: inline-size;
}
/* The bar's name (design audit A3): drawn where it fits, dropped by the bar's
   own width where it does not, so a short bar is a glyph and never a "…". */
.tlbarin { display: flex; align-items: center; justify-content: center; gap: 5px; min-width: 0; max-width: 100%; }
.tlbarname { font-size: var(--t-label); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.tlbarout { position: absolute; inset-block: 0; inset-inline-start: 100%; margin-inline-start: 6px; display: none; align-items: center;
  font-size: var(--t-label); font-weight: 600; color: var(--paper); white-space: nowrap; max-width: 220px; overflow: hidden; text-overflow: ellipsis; pointer-events: none; }
.tlbar.endside .tlbarout { inset-inline-start: auto; inset-inline-end: 100%; margin-inline: 0 6px; }
@container (max-width: 64px) { .tlbarname { display: none; } .tlbarout { display: flex; } }
/* THE RULER (design audit A3): a rung of ticks over the tracks, the same ticks
   as hairlines down every track, and today's line. The axis row wears the
   row's own geometry (name cell, gap, track), so a tick and a bar on the same
   date share a pixel on every width, the phone's 40% track included. */
.tlaxis { padding-block: 0; min-height: 24px; align-items: flex-end; }
.tlaxis:hover { background: none; }
.tlaxistrack { background: none; height: 24px; border-radius: 0; }
.tltick { position: absolute; top: 6px; bottom: 0; border-inline-start: 1px solid var(--line); }
.tltick b { position: absolute; inset-block-end: 3px; inset-inline-start: 5px; font-size: var(--t-small); font-weight: 600; color: var(--ghost); white-space: nowrap; }
.tltick.today { border-inline-start-color: var(--wk); }
.tltick.today b { color: var(--wk); }
.tlgrid { position: absolute; top: 0; bottom: 0; border-inline-start: 1px solid rgb(var(--c-line) / .8); pointer-events: none; }
.tlgrid.today { border-inline-start-color: var(--wk); }
.tlbar.late { background: var(--bad); }
.tlbar.done { background: var(--good); }

/* ---------------- discussion ---------------- */
.thread { display: flex; flex-direction: column; gap: 2px; margin-bottom: 10px; }
.msg { display: flex; gap: 10px; padding: 8px; border-radius: var(--r-lg); }
.msg:hover { background: rgb(var(--c-panel2) / .55); }
.msg .body { flex: 1; min-width: 0; }
.mhead { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.mhead .who { font-size: var(--t-body); font-weight: 600; color: var(--paper); }
.mhead .when { font-size: var(--t-label); color: var(--ghost); font-variant-numeric: tabular-nums; }
.mtext { font-size: var(--t-lead); line-height: 1.65; color: var(--mute); margin-top: 2px; white-space: pre-wrap; word-break: break-word; }
.msg .del { opacity: 0; flex-shrink: 0; }
.msg:hover .del { opacity: 1; }

.composer { display: flex; gap: 10px; align-items: flex-start; border: 1px solid var(--line); border-radius: var(--r-xl); padding: 10px; background: var(--ink); }
.composer:focus-within { box-shadow: 0 0 0 2px var(--focus-ring); }
.composer textarea {
  flex: 1; min-width: 0; border: 0; background: none; resize: none; overflow: hidden;
  font: inherit; font-size: var(--t-lead); line-height: 1.65; color: var(--paper);
  outline: none; min-height: 22px; padding: 2px 0;
}
.composer textarea::placeholder { color: var(--ghost); }
.composer .send { flex-shrink: 0; align-self: flex-end; }
.nothread { font-size: var(--t-body); color: var(--ghost); padding: 4px 8px 12px; }

/* overlapping avatars so a row of five people costs the width of two */
.stack { display: inline-flex; align-items: center; }
.stack .av { height: 22px; width: 22px; font-size: var(--t-micro); margin-inline-end: -6px; box-shadow: inset 0 0 0 1px rgba(255,255,255,.1), 0 0 0 2px var(--panel); }
.stack .av:last-child { margin-inline-end: 0; }
.pval .stack { margin-inline-end: 8px; }

/* ---------------- who else is here ----------------
   Presence, drawn in two places: the faces of everyone reading this page, and a
   quiet edge on the block whoever is writing has their caret in.

   THE STRIP SITS BEFORE THE SPACER ON PURPOSE. Everything after the spacer is
   pinned to the inline end, so a strip placed there would push the identity and
   theme buttons sideways every time somebody arrived or left — a control moving
   under the pointer as a colleague opens a page. Placed before it, the strip
   grows into the gap and nothing else moves. Its height is held open whether or
   not anybody is there, so the header never changes height either. */
.herewho { display: inline-flex; align-items: center; min-height: 22px; }
.herewho .av { box-shadow: inset 0 0 0 1px rgba(255,255,255,.1), 0 0 0 2px var(--ink); }
/* The faces are `aria-hidden`, because a screen reader reading a stack of
   initials says "MS NA" and means nothing by it. The sentence beside them is
   what gets announced, and only when it changes. */
.sronly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

/* The block a colleague has their caret in. `::before`, because `::after` on a
   block is already the Option-copy tag and the two can be true at once. Both the
   edge and the name are out of flow, so a block does not move or change size when
   somebody starts writing in it.

   `--guest` is set on the element from the same `avColor()` that draws that
   person's avatar, so the edge, the name and their face are one colour. That
   palette is the theme-fixed avatar palette — identity, not surface, and the one
   documented exception to colour coming from `globals.css`. */
.blk.guest { box-shadow: inset 0 0 0 1px var(--guest); }
.blk.guest::before {
  content: attr(data-guest); position: absolute; inset-block-start: -7px;
  inset-inline-start: 8px; z-index: 2;
  font-size: var(--t-label); font-weight: 700;
  background: var(--guest); color: rgba(255,255,255,.95);
  padding: 1px 5px; border-radius: var(--r-pill); pointer-events: none; white-space: nowrap;
}

.dfield {
  border: 0; background: none; font: inherit; font-size: var(--t-body); color: var(--paper);
  padding: 5px 7px; border-radius: var(--r-sm); outline: none; color-scheme: dark;
}
:root[data-theme='light'] .dfield { color-scheme: light; }
.dfield:hover { background: var(--panel2); }
.dfield:focus { background: var(--ink); box-shadow: 0 0 0 2px var(--focus-ring); }

/* Overdue never travels on colour alone — it carries the alert glyph and the
   word, so it survives greyscale and a colour-blind reader. */
.overdue { display: inline-flex; align-items: center; gap: 5px; color: var(--bad); font-weight: 600; font-size: var(--t-body); }
.duecell { font-size: var(--t-body); color: var(--mute); font-variant-numeric: tabular-nums; }
.ro { font-size: var(--t-body); color: var(--mute); font-variant-numeric: tabular-nums; padding-inline-start: 7px; }

.cellbtn { border: 0; background: none; padding: 4px 6px; margin: 0 -6px; border-radius: var(--r-md); cursor: pointer; font: inherit; color: inherit; display: inline-flex; align-items: center; }
.cellbtn:hover { background: rgb(var(--c-line) / .55); }
.dash { color: var(--ghost); }
.menu input[type=date] { color-scheme: dark; }
:root[data-theme='light'] .menu input[type=date] { color-scheme: light; }
.datewrap { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.datewrap .dfield { border: 1px solid transparent; }
.datewrap .overdue { font-size: var(--t-small); font-weight: 700; background: rgb(var(--c-bad) / .15); border-radius: var(--r-pill); padding: 2px 8px; }
.tagcell { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
/* THE CROSS BELONGS TO ITS CHIP. It sat INSIDE the chip until 2026-08-30, when
   the chip became the button that opens the picker (CEO: "the users should just
   click the tag") and a button may not contain another one. Moved out, it has to
   stay against the value it clears rather than drift into the row — so the pair
   closes up. It no longer wears the chip's colour, because it is no longer
   inside it; muted is right for a control that removes rather than carries. */
.tagcell.filled { gap: 0; }
.tagx { border: 0; background: none; color: inherit; cursor: pointer; padding: 3px 6px 3px 4px; font-size: var(--t-small); opacity: .55; }
.tagx:hover { opacity: 1; }
.tagadd { display: inline-flex; align-items: center; gap: 4px; border: 1px dashed var(--line); background: none; color: var(--ghost); border-radius: var(--r-pill); padding: 2px 8px; font: inherit; font-size: var(--t-small); cursor: pointer; }
.tagadd:hover { color: var(--wksoft); border-color: rgb(var(--c-wk) / .6); }


.whoami { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); border-radius: var(--r-pill); padding: 3px 8px 3px 3px; background: none; cursor: pointer; font: inherit; font-size: var(--t-small); color: var(--mute); }
.whoami:hover { border-color: var(--mute); color: var(--paper); }

.cin { width: 100%; border: 0; background: none; font: inherit; font-size: var(--t-body); color: var(--paper); padding: 5px 6px; border-radius: var(--r-sm); outline: none; }
.cin:hover { background: rgb(var(--c-line) / .5); }
.cin:focus { background: var(--ink); box-shadow: 0 0 0 2px var(--focus-ring); }
.cin::placeholder { color: var(--ghost); }
.cin.inh { opacity: .55; }

/* A NUMBER PROPERTY LOOKS LIKE EVERY OTHER PROPERTY.
   CEO, 2026-08-25: "since all page properties are left lined .. the numbers need
   to be as well". A right-aligned figure is the convention for a column of
   numbers, and it was the wrong convention to import here: a property list is
   read ACROSS — name, then value — not down, so one value sitting against the far
   edge of its row reads as a different kind of thing rather than as a number.
   The whole visual treatment is gone. What makes a Number a Number is what it
   ACCEPTS — see `data-numonly` — and how a board column sorts it. Nothing about
   how it is drawn. */

.chip {
  display: inline-flex; align-items: center; gap: 4px; border-radius: var(--r-pill); padding: 3px 9px;
  font-size: var(--t-small); font-weight: 700;
  white-space: nowrap; border: 0; cursor: pointer; font-family: inherit;
}
.chip.inh { opacity: .55; }
.c-neutral { background: var(--panel2); color: var(--mute); }
.c-accent  { background: rgb(var(--c-wk) / .15); color: var(--wksoft); }
.c-good    { background: rgb(var(--c-good) / .15); color: var(--good); }
.c-warn    { background: rgb(var(--c-warn) / .15); color: var(--warn); }
.c-bad     { background: rgb(var(--c-bad) / .15); color: var(--bad); }
.c-violet  { background: rgb(var(--c-accent2) / .15); color: var(--accent2); }
.c-empty   { background: none; color: var(--ghost); box-shadow: inset 0 0 0 1px var(--line); }
/* Was never defined, so Low's chip rendered bare — found in the CEO's
   2026-08-27 screenshot while everything else wore a wash. */
.c-ghost   { background: rgb(var(--c-ghost) / .15); color: var(--ghost); }

.person { display: inline-flex; align-items: center; gap: 7px; min-width: 0; border: 0; background: none; padding: 3px 6px; border-radius: var(--r-pill); cursor: pointer; font: inherit; font-size: var(--t-body); color: var(--paper); }
.person:hover { background: rgb(var(--c-line) / .5); }
.person.inh { opacity: .55; }
.av { flex-shrink: 0; display: flex; align-items: center; justify-content: center; height: 22px; width: 22px; border-radius: var(--r-pill); font-size: var(--t-micro); font-weight: 700; color: rgba(255,255,255,.95); box-shadow: inset 0 0 0 1px rgba(255,255,255,.1); }
.av.none { background: var(--panel2); color: var(--ghost); box-shadow: inset 0 0 0 1px var(--line); }

/* row actions, revealed on hover or focus so they do not add permanent noise */
/* Sits at the inline end of its cell. flex-end follows the writing direction, so
   this stays correct when the layout mirrors — do not swap it for a left/right
   value. The column also lost a button when the overflow "..." moved to
   right-click, so its reserved width came down with it. */
.acts { display: flex; align-items: center; justify-content: flex-end; gap: 2px; opacity: 0; transition: opacity .12s; }
tr:hover .acts, tr:focus-within .acts { opacity: 1; }
.act { width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border: 0; background: none; color: var(--ghost); border-radius: var(--r-sm); cursor: pointer; }
.act:hover { background: var(--line); color: var(--paper); }
.act.danger:hover { background: rgb(var(--c-bad) / .15); color: var(--bad); }
/* Desktop reaches the row menu by right-clicking the row. The phone has no
   right-click, so its own media query puts the button back. */
@media (min-width: 768px) { .act[data-more] { display: none; } }

/* ---------------- confirm ---------------- */
/* Constraint 5: capped and centred so it can never run past the bottom edge. */
.confirmbox {
  position: fixed; z-index: var(--z-confirm); inset-inline: 0; margin: 0 auto; top: 50%;
  transform: translateY(-50%); width: min(420px, calc(100vw - 32px));
  max-height: calc(100dvh - 48px); overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--r-2xl); background: var(--panel);
  box-shadow: var(--shadow-card); padding: 20px;
}
.confirmbox h3 { margin: 0 0 8px; font-size: var(--t-large); font-weight: 600; letter-spacing: -.01em; line-height: 1.45; }
.confirmbox p { margin: 0 0 18px; font-size: var(--t-body); line-height: 1.6; color: var(--mute); }
/* The opt-in to a larger loss. It sits above the buttons, in the reading order
   of the sentence it qualifies, and its hit area is the whole label rather than
   the 13px box — a control that is hard to hit is a control that gets checked by
   accident. */
.confirmcheck {
  display: flex; align-items: flex-start; gap: 10px;
  margin: 0 0 18px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel2);
  font-size: var(--t-body); line-height: 1.5; color: var(--paper); cursor: pointer;
}
.confirmcheck input { margin-block-start: 2px; accent-color: var(--wk); flex: none; }
.confirmrow { display: flex; justify-content: flex-end; gap: 8px; }
.confirmrow .btn { min-height: 40px; padding-inline: 16px; }
/* The property-edit dialog (editPropertyDialog), in the product's OWN form
   voice: the title IS the name and is edited in place (the row is the field,
   2026-08-24), the type sits under it as the subtitle, and every field label
   sits ABOVE its field — the same block-label pattern the sign-in form set
   (`.authbox label`). Option rows keep their controls' SPACE at all times and
   reveal the quiet ones (move, remove) on hover by opacity — nothing shifts
   under a pointer reaching for it. */
.confirmbox.epbox { width: min(560px, calc(100vw - 48px)); }
.eptitle { display: block; width: 100%; border: 0; background: none; color: var(--paper);
  font: inherit; font-size: var(--t-h3); font-weight: 600; letter-spacing: -.02em;
  padding: 2px 6px; margin: -2px 0 2px -6px; border-radius: var(--r-sm); outline: none; }
.eptitle:hover { background: rgb(var(--c-panel2) / .45); }
.eptitle:focus { background: var(--panel2); box-shadow: 0 0 0 2px var(--focus-ring); }
.eptype { margin: 0 0 14px; font-size: var(--t-body); color: var(--mute); }
.epfield { margin-block: 14px; }
/* TWO LEVELS OF LABEL, ONE RULE EACH (design audit B1). A group of fields inside
   a form wears this micro-label — the property dialog's WHAT IT MEANS and
   OPTIONS, the rule builder's When, Only if, Then and Try it. A section of a
   panel (Shelves, People, Personal Info) is bold sentence case, `.tplsectitle`
   and the rail heads. */
.eplab, .tplsectitle.grplab { display: block; font-size: var(--t-small); font-weight: 700; color: var(--ghost); margin-block-end: 6px; }
.tplsectitle.grplab { display: flex; margin-block: 8px 2px; }
.epopts { border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 6px; max-height: 260px; overflow-y: auto; }
.epopt { display: flex; align-items: center; gap: 6px; padding: 3px 6px; border-radius: var(--r-md); }
.epopt:hover { background: rgb(var(--c-panel2) / .45); }
/* The swatch shows the colour people READ — the chip class's text tone, via
   currentColor — not the dark wash it sits on (CEO, 2026-08-27). */
.epswatch { width: 24px; height: 16px; border-radius: var(--r-pill); border: 0; cursor: pointer;
  flex-shrink: 0; padding: 0; background: currentColor; }
.epswatchmini { display: inline-block; width: 22px; height: 12px; border-radius: var(--r-pill);
  background: currentColor; padding: 0; }
.opticon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
.opticon svg { width: 100%; height: 100%; }
.epicon, .epdone, .epx, .epgrip { width: 26px; height: 26px; display: flex; align-items: center;
  justify-content: center; border: 0; background: none; color: var(--mute);
  border-radius: var(--r-sm); flex-shrink: 0; }
.epicon, .epdone, .epx { cursor: pointer; }
/* An option with no icon shows a ghost ring — a visible empty slot where the
   old 1.4-radius dot read as a speck of debris. */
.epicon-none { opacity: .45; }
.epopt:hover .epicon-none { opacity: .8; }
/* Faint at rest rather than invisible: the slot always existed (space is
   reserved, 5.x standing rule), but an INVISIBLE control's reserved space
   reads as a layout mistake — the row looked indented for no reason. */
.epgrip { cursor: grab; color: var(--ghost); opacity: .35; }
.epgrip:active { cursor: grabbing; }
.epicon:hover, .epdone:hover, .epx:hover { background: var(--line); color: var(--paper); }
.epdone.on { color: var(--good); }
.epx { opacity: 0; }
.epopt:hover .epx, .epopt:focus-within .epx,
.epopt:hover .epgrip, .epopt:focus-within .epgrip { opacity: 1; }
.epx:hover { color: var(--bad); }
/* The drop side: a hairline on the edge the drop will land against. */
.epopt.epover-top { box-shadow: inset 0 2px 0 0 var(--accent); }
.epopt.epover-btm { box-shadow: inset 0 -2px 0 0 var(--accent); }
/* The name is WRITTEN IN ITS OWN TONE — the row shows the style the option
   will actually wear (CEO, 2026-08-27: "the name next to it should reflect the
   style it will apear to be in"). The colour comes from the tone class on the
   input; the wash does not, because this is still a field being typed into. */
.eplabel { flex: 1; min-width: 0; border: 1px solid transparent; background: none;
  padding: 4px 6px; border-radius: var(--r-sm); font-size: var(--t-lead); }
.eplabel:hover { border-color: var(--line); }
.eplabel:focus { border-color: var(--accent); outline: none; background: var(--panel2); }
.epadd { margin-block-start: 6px; border-block-start: 1px solid var(--line); padding-block-start: 6px; }
.epadd input { width: 100%; background: var(--panel2); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 7px 9px; color: var(--paper); }
.epempty { color: var(--mute); font-size: var(--t-body); padding: 8px; margin: 0; }
/* The crop stage: the image at fit, the selection over it. The scrim
   outside the selection is painted by the selection's own giant shadow, so
   there is exactly one element to move. */
.cropstage { position: relative; margin: 0 auto; overflow: hidden; border-radius: var(--r-lg);
  border: 1px solid var(--line); touch-action: none; }
.cropstage img { display: block; width: 100%; height: 100%; user-select: none; }
.cropsel { position: absolute; border: 1.5px solid var(--accent); border-radius: var(--r-xs);
  box-shadow: 0 0 0 9999px rgb(var(--c-scrim) / .55); cursor: grab; }
.cropsel:active { cursor: grabbing; }
/* INSIDE the corner, never straddling it: half-out it was clipped by the
   stage's overflow whenever the selection was full-bleed — exactly the state
   every crop starts in — so the control could not be grabbed at all. */
.crophandle { position: absolute; inset-inline-end: 2px; inset-block-end: 2px;
  width: 14px; height: 14px; border-radius: 50%; background: var(--accent);
  border: 2px solid var(--paper); cursor: nwse-resize; }

.newrow td { height: 36px; }
.newbtn { display: inline-flex; align-items: center; gap: 6px; border: 0; background: none; padding: 4px 6px; font: inherit; font-size: var(--t-small); color: var(--ghost); cursor: pointer; border-radius: var(--r-sm); }
.newbtn:hover { color: var(--wksoft); background: rgb(var(--c-wk) / .1); }

.empty { padding: 44px 20px; text-align: center; }
.empty p { font-size: var(--t-body); color: var(--mute); margin: 0 0 14px; }

/* ---------------- menu ---------------- */
.menu {
  /* Above EVERY dialog, the add-member family (120) included: a menu opened
     FROM a dialog must land on top of it — the icon picker drew behind the
     property popup once (95), then behind the team editor (120), which is a
     control placed where it cannot be used. Menus are the most recent
     interaction wherever they open, so top of the ladder is their place. */
  position: fixed; z-index: var(--z-menu); min-width: 190px; max-height: 60vh; overflow-y: auto;
  border: 1px solid var(--line); border-radius: var(--r-xl); background: var(--panel);
  box-shadow: var(--shadow-card); padding: 5px;
}
.mi {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: start;
  border: 0; background: none; padding: 7px 9px; border-radius: var(--r-md);
  font: inherit; font-size: var(--t-body); color: var(--paper); cursor: pointer;
  /* ONE LINE, ALWAYS. A menu row that wraps reads as two entries, and the eye
     has to work out which lines belong together before it can choose. The menu
     takes the width the longest label needs instead — a menu is as wide as what
     is in it, which is the one dimension nobody is short of. */
  white-space: nowrap;
}
.mi > span { min-width: 0; }
/* The label may not shrink into an ellipsis either: a truncated action is a
   question rather than a choice. */
.mi > span:not(.tick):not(.michev):not(.minote):not(.mikeys) { overflow: visible; }
.mi:hover { background: var(--panel2); }
/* A SHORTCUT SITS ON THE ROW'S OWN LINE, HARD AGAINST THE END (CEO,
   2026-09-27: "if your going to add command shortcuts add it for all and right
   aline it in the same line as the action name"). It is NOT `.minote`, which is
   a second line of description — a colleague's email under their name — and is
   wanted there; a shortcut is a different thing and gets its own name. The auto
   margin puts it at the end, and a tick or a chevron after it sits beside it
   rather than fighting it for the same space. */
.mikeys { margin-inline-start: auto; padding-inline-start: 18px; font-size: var(--t-label);
  color: var(--ghost); white-space: nowrap; font-variant-numeric: tabular-nums; }
.mi .mikeys + .tick, .mi .mikeys + .michev { margin-inline-start: 8px; }
/* THE CURRENT ROW IS HIGHLIGHTED, NOT TICKED (CEO, 2026-09-27: "instead of a
   check how about you highlight the selection"). The wash and the colour are
   `.miicon.on`'s, which is what a menu in this product already uses to say
   "this one is on", so nothing new is invented; the weight is `.navitem.on`'s.
   Only rows that DECLARE `chosen` take it — every other menu keeps its tick. */
.mi.chosen { background: rgb(var(--c-wk) / .18); color: var(--wksoft); font-weight: 600;
  /* THE RING IS NOT DECORATION. Status never travels on colour alone here, and
     the tick that used to carry it is gone. Weight cannot stand in for it
     either: the Bold row is already 700 because it wears what it does, so on
     that row a heavier weight says nothing. `.miicon.on`'s ring is the shape
     this product already uses for "on" inside a menu, and it is a shape. */
  box-shadow: inset 0 0 0 1px rgb(var(--c-wk) / .5); }
.mi.chosen:hover { background: rgb(var(--c-wk) / .26); }
.mi.chosen .mikeys { color: var(--wksoft); opacity: .75; }
/* A ROW WEARING WHAT IT DOES (his "make the bold text bold, make the italics
   italic ect"). Relative units throughout, so a face rides on the menu's own
   size rather than fixing one, and the two size faces are the SAME TOKENS the
   marks themselves use — what the row shows is what the text becomes. */
.mface-b { font-weight: 700; }
.mface-i { font-style: italic; }
.mface-u { text-decoration: underline; text-underline-offset: 2px; }
.mface-s { text-decoration: line-through; }
/* The code row wears the CHIP the mark actually draws (`.btext code`), not
   just its typeface — at a glance the difference between a monospace word and
   a proportional one is nearly nothing, and the point of the face is to be
   read at a glance. */
.mface-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em;
  background: var(--panel2); border-radius: var(--r-xs); padding: 1px 5px; color: var(--wksoft); }
.mi.chosen .mface-code { background: rgb(var(--c-ink) / .35); }
.mface-sm { font-size: var(--t-mark-sm); }
.mface-lg { font-size: var(--t-mark-lg); }
.mi .tick { margin-inline-start: auto; color: var(--wksoft); display: flex; }
/* THE CHEVRON SAYS THERE IS MORE, before anyone has to discover it by hovering.
   It mirrors under RTL, because it points at where the flyout will open and that
   side changes with the direction. */
.mi .michev { margin-inline-start: auto; color: var(--ghost); display: flex; }
[dir='rtl'] .mi .michev svg { transform: scaleX(-1); }
.mi[aria-expanded='true'] { background: var(--panel2); }
.mi[aria-expanded='true'] .michev { color: var(--wksoft); }
/* The flyout is a menu like any other, so it inherits the surface, the border
   and the shadow; it only needs to sit above its parent. */
.menu.sub { z-index: calc(var(--z-menu) + 2); }
.mi.danger { color: var(--bad); }
.mi.danger:hover { background: rgb(var(--c-bad) / .1); }
/* An impossible move is shown with its reason, not hidden. A control that
   vanishes teaches nobody why it was unavailable. */
.mi.off { color: var(--ghost); cursor: default; }
.mi.off:hover { background: none; }
.menu { min-width: 190px; }
.msep { height: 1px; background: var(--line); margin: 5px 3px; }
.mhead { padding: 6px 9px 3px; font-size: var(--t-small); color: var(--ghost); }
.mi input { border: 0; background: var(--ink); border-radius: var(--r-sm); padding: 6px 8px; color: var(--paper); font: inherit; font-size: var(--t-body); width: 100%; outline: none; }

/* The people picker's typing box, drawn over the cell it is filling: the
   chips already chosen, then the input, where the name will finally sit
   (CEO, 2026-08-31). It carries .menu for the layer and the close paths;
   these overrides are a cell's geometry rather than a panel's. */
.menu.pplbox { display: flex; flex-wrap: wrap; align-items: center; gap: 5px;
  padding: 4px 8px; min-width: 0; max-width: min(420px, calc(100vw - 20px)); border-radius: var(--r-lg); }
.menu.pplbox input { border: 0; background: none; color: var(--paper); font: inherit;
  font-size: var(--t-body); flex: 1; min-width: 70px; padding: 3px 2px; outline: none; }
.pplchip { display: inline-flex; align-items: center; gap: 5px; max-width: 160px;
  padding: 2px 8px 2px 3px; border-radius: var(--r-pill); background: rgb(var(--c-line) / .5);
  font-size: var(--t-small); color: var(--paper); white-space: nowrap; }
.pplchip > span:last-child { overflow: hidden; text-overflow: ellipsis; }

/* ---------------- toast ---------------- */
.toast {
  position: fixed; inset-inline: 0; bottom: 22px; z-index: var(--z-toast);
  display: flex; justify-content: center; pointer-events: none;
}
.toast > div {
  pointer-events: auto; display: flex; align-items: center; gap: 12px;
  border: 1px solid var(--line); border-radius: var(--r-xl); background: var(--panel2);
  box-shadow: var(--shadow-card); padding: 10px 14px; font-size: var(--t-body); color: var(--paper);
  max-width: calc(100vw - 32px);
}
.toast button { border: 0; background: none; color: var(--wksoft); font: inherit; font-size: var(--t-body); font-weight: 600; cursor: pointer; }
/* THE HOLD (his rule, 2026-09-04): while the server works, the toast stays
   and a bar moves along its bottom edge, so a wait is never silence. */
.toast.busy > div { position: relative; overflow: hidden; }
.toast .tbar { position: absolute; inset-inline: 0; bottom: 0; height: 2px; }
.toast .tbar::after { content: ''; position: absolute; inset-block: 0; inline-size: 40%;
  inset-inline-start: -40%; background: var(--wk); animation: work-hold 1.4s ease-in-out infinite; }
@keyframes work-hold { to { inset-inline-start: 100%; } }
@media (prefers-reduced-motion: reduce) { .toast .tbar::after { animation: none; inline-size: 100%; inset-inline-start: 0; opacity: .6; } }

.warnbar { margin-top: 14px; border-radius: var(--r-lg); padding: 9px 12px; font-size: var(--t-small); border: 1px solid rgb(var(--c-warn) / .4); background: rgb(var(--c-warn) / .08); color: var(--warn); }

/* ==================================================================
   PHONE. Everything below 768px — the same breakpoint PortalNav.tsx
   uses to swap its sidebar for the bottom bar, so the product has one
   phone threshold rather than two.

   EVERY RULE IN THIS BLOCK IS GATED. Desktop does not move.
   ================================================================== */
@media (max-width: 767.98px) {

/* ---------------- the table stops being a table ----------------
 *
 * WHAT COLLAPSES IS THE COLUMNS, NOT THE TREE.
 *
 * Two other phone shapes were considered and both were rejected for the same
 * reason. Cards-with-a-breadcrumb (sketched in preview-subtasks.html) and
 * drill-in navigation each move depth off the list — the first makes it
 * implicit, the second makes it a tap away. What that costs: cascading sub-tasks shipped inside the side panel and
 * production measured 14 checklist rows and ZERO nested. The diagnosis was
 * "in a drawer, nesting is somewhere you go; in a table it is something you do
 * without leaving the list". A phone shape that hides depth behind a tap
 * rebuilds the drawer on the smaller screen and would reproduce that result.
 *
 * The second fact rules out the usual excuse for cards. preview-subtasks.html
 * measures that a 360px screen carries NINE levels of 18px indent before a
 * label drops under 120px. Width was never the constraint on depth. It is the
 * constraint on COLUMNS. So the nine columns collapse and the tree survives
 * intact: same indentation, same disclosure toggle in the list, same
 * "New sub-page" without leaving it.
 *
 * The row wraps to two lines instead of scrolling sideways. It gains height
 * rather than width, which is the argument PortalNav.tsx already makes for the
 * nav: a vertical list cannot outgrow its container the way a horizontal one
 * does, and height is cheap in the direction people already scroll.
 */
.tt { display: block; min-width: 0 !important; }
.tt colgroup, .tt thead { display: none; }
.tt tbody { display: block; }
/* Block layout on the phone, so the sticky lives on the tfoot itself rather than
   on cells that are no longer laid out as a row. */
.tt tfoot { display: block; position: sticky; bottom: 0; z-index: 4; background: var(--tt-ground, var(--panel)); }
.tt tfoot td { position: static; border-top: 0; }

/* --glyphs is the width of the disclosure toggle and the item glyph that sit
   before the name. The row is indented by the depth PLUS that run, and the name
   cell is pulled back over it, so line two starts under the name text rather
   than under the toggle. A meta line that begins to the left of the thing it
   describes reads as belonging to the row above it. */
.tt tbody tr {
  --glyphs: 54px;
  display: flex; flex-wrap: wrap; align-items: center; column-gap: 8px; row-gap: 4px;
  padding-block: 8px; padding-inline: calc(var(--pad, 12px) + var(--glyphs)) 8px;
  border-bottom: 1px solid var(--line); min-height: 56px;
}
.tt tbody tr > td {
  display: flex; align-items: center; height: auto; padding: 0;
  border-bottom: 0; overflow: visible;
}
/* Line one: the name takes the whole line so nothing else can join it, and the
   row menu holds the end. Its outer width is (line - 52px), the 44px menu plus
   the 8px gap, which is exactly what forces the fields onto line two. */
.tt tbody tr > td.name {
  flex: 1 1 auto; width: calc(100% + var(--glyphs) - 52px);
  margin-inline-start: calc(var(--glyphs) * -1);
  padding-inline-start: 0; position: static;
}
/* The row does the indenting on the phone, so the cell must not do it again. */
.tt .ncell { padding-inline-start: 0; }
.tt .name::after { display: none; }
.tt tbody tr > td.actscell { flex: 0 0 44px; order: 2; justify-content: flex-end; }

/* Line two: the fields that drive triage, in the row's own indent.
 * Owner, client and department are dropped here — they are the three fields
 * that INHERIT, so they print the same value on every row of a
 * project and buy no information at this width. Assignees is kept because it
 * is the one person-field that deliberately does NOT inherit, so it is the one
 * that varies row to row. All six dropped fields remain on the item page. */
/* Addressed by key, never by position: the columns can be reordered, so
   nth-child(2) is whatever the reader dragged there this morning. */
.tt tbody tr > td[data-col="owner"],
.tt tbody tr > td[data-col="client"],
.tt tbody tr > td[data-col="dept"] { display: none; }
.tt tbody tr > td[data-col="assignees"] { order: 3; }
.tt tbody tr > td[data-col="priority"] { order: 4; }
.tt tbody tr > td[data-col="due"] { order: 5; }
.tt tbody tr > td[data-col="status"] { order: 6; }

/* An unset field on line two draws as an em dash, and on a phone that dash has
   no column header over it to say what is unset — three of them in a row read
   as damage rather than as absence. In a table the dash is legible because the
   header names the column; off the table it names nothing, so an unset field
   simply does not draw here. Setting one is a tap away on the item page, which
   is where the other twelve properties live anyway. Status always draws: it has
   no empty state. */
.tt tbody tr > td:has(> .cellbtn > .dash),
.tt tbody tr > td:has(> .person > .dash) { display: none; }

/* The "New sub-page" affordance keeps its own line and its own indent. */
.tt tr.newrow { min-height: 0; }
.tt tr.newrow > td:not(.name) { display: none; }
.tt tr.newrow > td.name { width: 100%; }
.tt tr.newrow .newbtn { min-height: 40px; }

/* Sorting and column resizing are both header controls, and the header does
   not survive the collapse. The grips are disabled rather than merely hidden:
   a touch-drag on one writes into the per-user stored column widths
   and would corrupt the reader's desktop layout. */
.colgrip { display: none; }

/* Touch. The row is 56px; the controls inside it are brought up to match.
   The disclosure toggle is the control that makes the tree legible, so it is
   the one that gets the most: full row height, and wide enough to hit without
   catching the name next to it. */
.tw { width: 32px; height: 40px; border-radius: var(--r-sm); }
.pen { display: none; }          /* rename moved into the row sheet */
.acts { opacity: 1; }
.acts .act[data-add], .acts .act[data-del] { display: none; }  /* both are in the sheet */
.act { width: 44px; height: 44px; }
.tt td .chip, .tt td .cellbtn, .tt td .person { min-height: 34px; }
/* An unset priority or due date is a single em dash, which is a 23px target.
   The cell keeps the dash — "Not measured" is reserved for a figure
   that was sought, and an em dash is how genuinely-unset is drawn — but the
   button around it is widened to something a thumb can land on. The board is
   where triage happens, so a field drawn here has to be settable here. */
.tt td .cellbtn, .tt td .person { min-width: 44px; justify-content: center; }
.nlink, .nin { padding-block: 8px; font-size: var(--t-lead); }
/* The name is long-pressable, so the browser's own selection callout has to
   be kept out of the way or it lands on top of the sheet. */
.tt tbody tr > td.name { -webkit-touch-callout: none; user-select: none; }
.nin { user-select: text; }

/* ---------------- menus become bottom sheets ----------------
 * Anchored popovers are the desktop answer: on a 390px screen they clip, and
 * they land wherever the control is rather than where the thumb is. This is
 * the same shape PortalNav.tsx gives its "More" sheet below 768px — rounded
 * top, panel ground, capped at 80dvh, scrolling inside itself — and it uses
 * the same work-sheet-in keyframe out of globals.css, so the module has one
 * mobile idiom rather than a second one invented here.
 * The slash menu is excluded: it is caret-anchored on purpose, and a sheet
 * would cover the block being typed into. */
.menuscrim { position: fixed; inset: 0; z-index: calc(var(--z-menu) - 2); background: rgb(var(--c-scrim) / .6); }
.menu.sheet {
  position: fixed; inset-inline: 0; inset-block-start: auto; bottom: 0; z-index: var(--z-menu);
  max-width: none !important; width: auto;
  max-height: 80dvh; overflow-y: auto; overscroll-behavior: contain;
  border: 0; border-top: 1px solid var(--line);
  border-radius: var(--r-2xl) var(--r-2xl) 0 0; padding: 8px 8px calc(env(safe-area-inset-bottom) + 12px);
}
/* THE SHEET IS THE ONE PLACE A ROW MAY WRAP. "One line, always" holds where
   a menu can be as wide as its longest label; a sheet is the screen's width
   and no wider, so a long label (a rule's trigger, a page's name) was cut at
   the edge with no ellipsis — a choice nobody could read. Wrapped, in a 46px
   row with the sheet's spacing, it still reads as one entry. */
.menu.sheet .mi { min-height: 46px; font-size: var(--t-large); padding: 10px 12px; white-space: normal; }
.menu.sheet .mi > span:not(.tick):not(.michev) { overflow-wrap: anywhere; }
.menu.sheet .mi.off { min-height: 46px; padding: 10px 12px; }
.menu.sheet .mhead { padding: 10px 12px 4px; }
/* The "there is more" line sits under the list, so it reads as a footer rather
   than as a second heading. */
.mhead.slashmore { border-top: 1px solid var(--line); margin-top: 4px; padding-top: 6px; }
/* A cell of an advanced table is a body, so it needs the room a body needs and
   none of the page's own indent. The grip is hidden until the cell is hovered:
   a drag handle on every cell of a six-column table is more chrome than table. */
.tcellb { min-width: 120px; padding: 2px; }
.tcellb .blk { margin-inline-start: 0 !important; }
.tcellb .bgrip { opacity: 0; }
.tcellb:hover .bgrip, .tcellb .blk:focus-within .bgrip { opacity: 1; }
.tcellb .blk.h1 .btext, .tcellb .blk.tg1 .btext { font-size: var(--t-h3); }
.tcellb .blk.h2 .btext, .tcellb .blk.tg2 .btext { font-size: var(--t-large); }
.tcellb .blk.h1, .tcellb .blk.h2, .tcellb .blk.h3,
.tcellb .blk.tg1, .tcellb .blk.tg2, .tcellb .blk.tg3 { margin-top: 0; }
.menu.sheet .msep { margin: 6px 3px; }
.menu.sheet .mi input { padding: 10px 12px; font-size: var(--t-large); }

/* ---------------- the peek is the whole screen ----------------
 * Width stops are meaningless here: at a third the 340px floor already
 * takes the screen, so every stop looks the same. The drag edge does not
 * render at all rather than sitting there doing nothing; the Open button
 * stays — the full page is still a different place on a phone.
 * The stored width is NOT rewritten — it is the reader's desktop preference
 * and a phone visit must not overwrite it, so this is done
 * with !important over the inline width rather than in JavaScript. */
.peek { width: 100% !important; inset-inline: 0; min-width: 0; border-inline-start: 0; }
.peekgrip { display: none; }
.peek .pbody, .peek.full .pbody { padding: 16px 14px calc(env(safe-area-inset-bottom) + 40px); }
/* The way back up a level. It already names its destination rather than saying
   "Back" — the finding BackLink.tsx records — so it only needs room to breathe
   and permission to shrink instead of pushing Close off the edge. */
.phead { padding: 8px 10px; gap: 6px; }
.phead .ctl, .phead .btn { min-height: 44px; }
.phead .ctl[data-goto] { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.phead .ctl[data-close] { width: 44px; height: 44px; flex-shrink: 0; }
.itemhead h2, .peek.full .itemhead h2, .pagewrap .itemhead h2 { font-size: var(--t-h2); }
.pgtitle { font-size: var(--t-h2); }
.crumbs { font-size: var(--t-label); }
/* TWO ACROSS ON A PHONE, and the odd one out takes the width rather than half of
   it. CEO, 2026-08-25: "on mobile it would be two in a row". The stored split is
   ignored here on purpose — a 70/30 written for a six-column row means nothing
   once it is two-up, and honouring it would give one unreadable column. The
   handle goes too: there is nothing to drag when the pairing is not the author's.
   Wrapping preserves the left-to-right order, so a row of five reads 1 2 / 3 4 / 5. */
.brow { flex-wrap: wrap; }
.bcol { flex: 1 1 calc(50% - 3px); }
.brow > .bcol:last-child:nth-child(odd) { flex-basis: 100%; }
.bsplit { display: none; }
/* Sixteen property rows at 120px of label leave nothing for the value — and a
   118px label at 390px wraps to two lines with the value floating beside the
   middle of it, which reads as broken alignment rather than as a field. The
   label goes on its own line and the value under it, both with the full width:
   gain height, never width, the same answer the table rows give at this size.
   `flex: 0 0 118px` HAD TO GO WITH THE ROW DIRECTION. A flex basis is measured
   along the main axis, so in a column it is a HEIGHT — 118px of empty label. */
/* And the rows take their content's height and no more. `.props` is a grid, so
   a box taller than its content stretches the implicit rows to fill it. */
.props { align-content: start; }
/* A GRID, NOT A COLUMN, SO THE ROW MENU STAYS ON THE LABEL'S LINE. Stacked as a
   plain column the three-dots button became a third line of its own — 44px,
   because a touch target has to be 44px — and every property row was a hundred
   pixels tall. Label and menu share the first line; the value has the width to
   itself underneath. */
.prow { display: grid; grid-template-columns: 1fr auto; gap: 2px 8px;
  align-items: center; min-height: 44px; padding-block: 6px; }
.pkey { width: auto; flex: none; padding-top: 0; grid-column: 1; }
.propmore { grid-column: 2; grid-row: 1; opacity: 1; }
.pval { flex: none; width: 100%; grid-column: 1 / -1; }
.frow { flex-wrap: wrap; }
.fkey { width: auto; flex: 0 0 100px; }
.kid { padding: 12px 12px; flex-wrap: wrap; }
.composer { flex-wrap: wrap; }
.composer .send { align-self: stretch; width: 100%; min-height: 44px; }
.msg .del, .msg .radd, .linkbar { opacity: 1; }   /* no hover on touch; all are words, not clutter */
/* The block grip carries data-bmenu as well as data-bdrag — it is the only way
   into "turn into", duplicate, move and delete. Hiding it would leave the page
   body type-only on a phone, so it stays and is sized for a thumb. Its drag
   half is HTML5 drag and is simply inert on touch; the menu half is the part
   that matters, and the aria-label already says "Block actions". */
.bgrip { opacity: 1; width: 32px; height: 40px; margin-top: 0; }

/* ---------------- toolbar ----------------
 * Two controls on one 390px line is two cramped controls. The view chips take a
 * line of their own and scroll inside it; the filter gets the second. Creating a
 * project lives at the end of the list of projects, not up here. */
.toolbar { flex-wrap: wrap; overflow: visible; padding: 10px; gap: 8px; }
.toolbar .spacer { display: none; }
.viewbar {
  display: flex; flex: 1 0 100%; overflow-x: auto; overscroll-behavior-x: contain;
  justify-content: flex-start; scrollbar-width: none;
}
.viewbar::-webkit-scrollbar { display: none; }
.viewbtn { padding: 9px 12px; flex-shrink: 0; }
/* The input, not the border around it, is what a tap has to land on, so the
   field takes the height and the wrapper keeps only enough padding to draw. */
.toolbar .search { flex: 1 1 140px; min-width: 0; padding-block: 4px; }
.toolbar .search input { min-height: 36px; }
.toolbar .btn-primary { flex: 0 0 auto; min-height: 44px; padding-inline: 16px; }

/* ---------------- header ----------------
 * The identity control loses its name and keeps its avatar and role. It is a
 * prototype stand-in that disappears in production; the save
 * stamp does not, so that is what keeps the words. */
.wrap { padding: 0 12px 96px; }
.pagebody { padding: 16px 12px 40px; }
.topbar { padding: 10px 12px; gap: 8px; }
.topbar h1 { font-size: var(--t-label); }   /* the trail's phone size */
.whoami .whoname { display: none; }
.whoami { padding: 3px 8px 3px 3px; min-height: 40px; }
.ctl.iconbtn { width: 44px; height: 44px; }

/* ---------------- the other three views ---------------- */
/* Board already scrolls inside its own container, which is the right answer on
   a phone; the column is narrowed so a second one is visible at the edge and
   the strip reads as scrollable rather than as the only column there is. */
.kanban { padding: 12px; }
.kcol { flex: 0 0 82%; }
/* Timeline gave a fixed 240px to the name, which at 336px left 96px of track.
   The name takes a share instead of a fixed width. */
/* On the phone the name takes what is left rather than a fixed share: the cell
   carries a disclosure, a glyph and an indent before the first letter, and at a
   fixed 47% a fourth-level name came out as four characters. The track keeps a
   fixed 40% so every bar is still measured against the same width. */
.tlrow > .ncell, .tlfoot > .ncell { flex: 1 1 auto; min-width: 0; }
.tlrow > .tltrack { flex: 0 0 40%; }
/* The name cell is right beside a 40% track, so a name beside the bar would
   say it twice and run off the track's end. The glyph and the ruler stay. */
.tlbarout { display: none !important; }
.tlrow { padding-inline: 10px; }
.calhead { padding: 10px; }
.cal-cell { min-height: 72px; padding: 4px 3px; }
.cal-dow { padding: 6px 3px; font-size: var(--t-micro); }
.cal-item { font-size: var(--t-caps); padding: 3px 4px; }

/* ---------------- the keyboard strip ----------------
 * There is no keyboard, so the shortcut legend is noise on a phone. Removed at
 * the CEO's instruction. Every shortcut it names still works if a keyboard is
 * attached; only the legend goes. */

.tpls { grid-template-columns: 1fr; }
.adminpanel { width: calc(100vw - 20px); }
.admrow { flex-wrap: wrap; gap: 10px; }
}

/* The "showing only your work" strip (?mine=). Tokens only, no new colours. */
.minebar {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 10px; padding: 8px 12px;
  border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--panel2); color: var(--ink);
  font-size: var(--t-small); font-weight: 600;
}
.minebar button {
  border: 1px solid var(--line); background: transparent; color: var(--ink);
  border-radius: var(--r-pill); padding: 3px 10px; font: inherit; cursor: pointer;
}
.minebar button:hover { background: var(--panel); }
.minebar button:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/* THE ONE TOP ROW: a link or a button in the folder heading's clothes
   — only the family is inherited, so the row's own 10.5px caps hold. */
.navtoprow { text-decoration: none; cursor: pointer; width: 100%; text-align: start; border: 0;
  background: none; font-family: inherit; }
.navtoprow:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: var(--r-md); }
/* HOVER AND LIT ARE ONE STATE ON ONE TILE: the GROUP fills, the row inside
   stays clear, the glyph turns — a pill filling inside a frame is the
   box-in-a-box he refused twice (2026-09-04). */
/* The row above the shelves is a destination, not a divided group: no line
   of its own, and its hover fill keeps its rounding. */
.navtopgroup { border-color: transparent; border-radius: var(--r-lg); }
.navtopgroup:hover, .navtopgroup.on { background: var(--panel2); }
.navtopgroup:hover .navtoprow, .navtopgroup.on .navtoprow { background: none; color: var(--paper); }
.navtopgroup:hover .folicon, .navtopgroup.on .folicon { color: var(--wksoft); }

/* The icon picker: tabs over one grid, a search above them. */
.iconpicker { width: 340px; }
.iconpicker .icsearch input { width: 100%; background: var(--panel2); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 6px 9px; font: inherit; font-size: var(--t-body); color: var(--paper); }
/* One row, stepped with chevrons (his correction, 2026-08-31) — the rail
   slides, never wraps, and hides its scrollbar the way the viewbar does. */
.iconpicker .ictabs { display: flex; flex-wrap: nowrap; align-items: center; gap: 2px; margin: 4px 6px; }
.iconpicker .ictabrail { flex: 1; min-width: 0; overflow-x: auto; scrollbar-width: none; flex-wrap: nowrap; }
.iconpicker .ictabrail::-webkit-scrollbar { display: none; }
.iconpicker .icstep { border: 0; background: none; color: var(--ghost); cursor: pointer;
  padding: 4px 2px; display: inline-flex; flex-shrink: 0; border-radius: var(--r-sm); }
.iconpicker .icstep:hover { color: var(--paper); background: var(--panel2); }
.iconpicker .icstep:disabled { opacity: .35; cursor: default; background: none; }
/* The chevrons point along the reading direction, so they mirror with it. */
[dir="rtl"] .iconpicker .icstep svg { transform: scaleX(-1); }
.iconpicker .icgrid { max-height: 300px; overflow-y: auto; }

/* The notification row's lead (his flip of 2026-08-31): the unread
   dot first, the page's icon against its name. The dot's slot is held on read
   rows so the titles keep one edge. */
.nrow { display: flex; align-items: center; gap: 8px; color: var(--mute); }
.ndot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.ndot.on { background: var(--bad); }
.nico { display: flex; }

/* A history line from a sub-page names the page it happened on. */
.acton { color: var(--ghost); }

/* The board's automations (2026-09-01). A rule is drawn as its sentence — the
   sentence IS the rule's display form, monday's lesson — so the row wraps
   rather than truncating: a rule cut short is a rule misread. */
.rulsent { white-space: normal; line-height: 1.5; }
.ruloff { color: var(--ghost); text-decoration: line-through; }
.rulswitch { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-small);
  color: var(--mute); flex: none; cursor: pointer; }
/* One action of the builder: what to do, then its blanks, side by side and
   wrapping on a narrow screen. The controls match .tplform's inputs so the
   builder reads as one form. */
.rulact { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.rulact > input, .rulact > select { flex: 1 1 140px; min-width: 0; }
.rulact > .btn { flex: none; }
.tplform select, .rulact select { border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--panel2); color: var(--paper); font: inherit; font-size: var(--t-body); padding: 7px 9px; }
/* THE ONE DROPDOWN (app/pick.js, design audit A2). The native select stays
   underneath, hidden; the face in front wears the word and opens the
   product's own menu. The face takes the skin the select wore in each place
   (the rules that follow name the places), and the wrap takes the select's
   room in the row, so nothing moves when the control changes hands. */
.pick { position: relative; display: inline-flex; min-width: 0; max-width: 100%; vertical-align: middle; }
.pick > select { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; pointer-events: none; }
.pickbtn { display: inline-flex; align-items: center; gap: 6px; width: 100%; min-width: 0; text-align: start; cursor: pointer;
  border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel2); color: var(--paper);
  font: inherit; font-size: var(--t-body); padding: 7px 9px; transition: border-color .15s; }
.pickbtn:hover { border-color: var(--mute); }
.pickbtn svg { flex: none; color: var(--mute); }
.pickword { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pickbtn.placeholder .pickword { color: var(--mute); }
.pickbtn:disabled { opacity: .55; cursor: default; }
.rulact > .pick { flex: 1 1 140px; min-width: 0; }
.invitebar .pick, .newuser .pick { flex: 1; min-width: 130px; }
.invitebar .pickbtn, .newuser .pickbtn { background: var(--ink); font-size: var(--t-body); }
.addmember .invitebar .pick { min-width: 150px; }
.urow .pickbtn, .tplrow .pickbtn, .tplsectitle .pickbtn { background: var(--ink); padding: 4px 7px; font-size: var(--t-small); }
.tplsectitle .pick { flex: none; }

/* An image block's resize (Resize Images, 2026-09-01). The width the author
   chose is on the figure; `sized` is what lets it drive the image, so an
   image nobody resized keeps its natural size and never upscales. The clamp
   to the container is here, not in the stored number — a width picked on a
   desktop must not scroll a phone sideways (hard constraint 4). */
/* The figure hugs the image (`fit-content`), so the grips sit ON the image's
   edges — a resize handle floating at the far side of an empty figure reads
   as belonging to nothing. A stored width (inline style) overrides it. */
.imgfig { position: relative; inline-size: fit-content; max-inline-size: 100%; }
.imgfig.sized .blkimg { inline-size: 100%; }
/* The video wears the same figure and the same stored width — its
   height follows the file's own aspect, so a width is the whole answer. */
.imgfig.sized .blkvid { inline-size: 100%; }
/* A FRAMED figure — a PDF card, a Dropbox preview, an embedded player —
   keeps its full-width default: an image is its own intrinsic size, a frame
   is not, and fit-content would collapse it. A stored width still wins. */
.imgfig.framefig { inline-size: auto; }

/* ---------------- media lightbox (2026-09-04) ----------------
   Click an image or a video's enlarge button to preview it full-size. Links
   are untouched. Above the chat panel (z 113) and every menu (z 127). */
.blkimg, .chatimg { cursor: zoom-in; }
/* The enlarge button a video gets — its native controls own body clicks, so
   the button is how it enlarges. Absolute, so it reserves no flow space and
   shifts nothing; faded in on hover, and always shown where there is no hover
   (touch). */
.mediazoom { position: absolute; inset-block-start: 8px; inset-inline-end: 8px; z-index: 3;
  inline-size: 32px; block-size: 32px; min-height: 0; padding: 0; opacity: 0;
  background: rgb(var(--c-panel) / .82); transition: opacity .12s; }
.imgfig:hover .mediazoom, .chatvidwrap:hover .mediazoom, .mediazoom:focus-visible { opacity: 1; }
@media (hover: none) { .mediazoom { opacity: 1; } }
.chatvidwrap { position: relative; display: inline-block; max-inline-size: 100%; }
.chatvid { max-inline-size: 100%; display: block; border-radius: var(--r-lg); border: 1px solid var(--line); }
.lightbox { position: fixed; inset: 0; z-index: var(--z-lightbox); display: grid; place-items: center;
  padding: 24px; background: rgb(var(--c-scrim) / .85); animation: work-fade-in 150ms ease-out; }
.lightboxbox { position: relative; max-inline-size: 100%; max-block-size: 100%; line-height: 0; }
.lightboxmedia { display: block; max-inline-size: min(92vw, 1500px); max-block-size: 88vh;
  border-radius: var(--r-md); background: var(--panel); box-shadow: 0 24px 70px rgb(var(--c-scrim) / .6); }
.lightboxclose { position: absolute; inset-block-start: -14px; inset-inline-end: -14px; min-height: 0; }
/* Mid-drag, iframes must not eat the pointer: a drag crossing a PDF's or a
   player's frame would silently stall — the window stops hearing moves the
   moment the pointer is over another document. */
body.resizing iframe, body.resizing .dbxframe { pointer-events: none; }
/* MEDIA ARRIVES AT A CIVIL SIZE (his ask, 2026-09-03: "by default
   limit the size of images videos and whatever documents the user might
   upload or link .. will not excceed unless the user resizes it"). The caps
   bind ONLY unsized figures — a width somebody chose (.sized) wins whole,
   however large; that is the "unless". Two numbers, one place: 720 wide,
   440 tall. The width cap alone reins in the framed kinds — a PDF card
   keeps its own 520px frame height, an embed's aspect follows its width —
   and the height cap tames the tall image and the tall video, which scale
   to honour both. min(100%, …) keeps constraint 4: nothing outgrows a
   narrow column sideways. */
.imgfig:not(.sized) { max-inline-size: min(100%, 720px); }
.imgfig:not(.sized) .blkimg, .imgfig:not(.sized) .blkvid { max-block-size: 440px; }
/* The grips ride the image's edges, absolute so nothing shifts when they
   appear (the reserve-space rule), visible on hover and on keyboard focus. */
.imgrz { position: absolute; inset-block-start: 50%; transform: translateY(-50%);
  block-size: 44px; inline-size: 16px; padding: 0; border: 0; background: none;
  cursor: col-resize; opacity: 0; touch-action: none;
  /* Above the linkbar: its opacity puts the bar in the same stacking layer
     as this positioned grip, and it comes later in the markup, so on a short
     figure it was winning the hit-test — a grip you could see and not press. */
  z-index: 2; }
.imgrz::after { content: ''; display: block; margin: auto; inline-size: 5px; block-size: 32px;
  border-radius: var(--r-hair); background: var(--paper); box-shadow: 0 0 0 1px var(--panel); }
.imgrz[data-edge="start"] { inset-inline-start: 6px; }
.imgrz[data-edge="end"] { inset-inline-end: 6px; }
.imgfig:hover .imgrz, .imgrz:focus-visible { opacity: 1; }

/* The person's own settings (2026-09-01). A chosen photo fills the avatar
   circle; the identity colour stays painted underneath, so a photo that
   fails to load degrades to the colour, never to a hole. */
.av.avimg { overflow: hidden; }
.av.avimg img { inline-size: 100%; block-size: 100%; object-fit: cover; border-radius: 50%; display: block; }
.avbig { inline-size: 44px; block-size: 44px; border-radius: 50%; overflow: hidden; flex: none;
  display: inline-flex; align-items: center; justify-content: center; }
.avbig img { inline-size: 100%; block-size: 100%; object-fit: cover; }
.avbig .av { inline-size: 44px; block-size: 44px; font-size: var(--t-large); }
.setswitch { display: flex; align-items: center; gap: 8px; font-size: var(--t-body);
  color: var(--paper); padding: 6px 0; cursor: pointer; }

/* MY SETTINGS WEARS ITS OWN SIDEBAR (his correction, 2026-09-01). One section
   on screen at a time; the panel's HEIGHT is fixed, not content-sized, so
   switching sections never moves the dialog under the pointer (the
   reserve-space rule). On a narrow screen the sidebar becomes a rail across
   the top and scrolls inside itself, never the page. */
.setpanel { width: min(720px, calc(100vw - 32px)); height: min(540px, calc(100dvh - 48px)); }
.setwrap { display: flex; flex: 1; min-height: 0; }
.setnav { flex: none; inline-size: 172px; border-inline-end: 1px solid var(--line);
  padding: 10px 8px; display: flex; flex-direction: column; gap: 2px; }
.setnavrow { display: flex; align-items: center; gap: 9px; border: 0; background: none;
  color: var(--mute); font: inherit; font-size: var(--t-body); padding: 8px 10px; border-radius: var(--r-md);
  cursor: pointer; text-align: start; flex: none; }
.setnavrow:hover { color: var(--paper); background: var(--panel2); }
.setnavrow.on { color: var(--paper); background: var(--panel2); font-weight: 600; }
.setbody { flex: 1; min-width: 0; overflow-y: auto; }
.setbody .tplsec { border-top: 0; }
@media (max-width: 640px) {
  .setwrap { flex-direction: column; }
  .setnav { inline-size: auto; flex-direction: row; border-inline-end: 0;
    border-block-end: 1px solid var(--line); overflow-x: auto; padding: 8px; gap: 4px; }
  /* As a strip the rail has no room for its group headings (design audit C3):
     "My Settings" and "Shelves" were the first cell, wrapped, and said nothing
     a row does not. The rows keep their glyphs and counts. */
  .setnav .setnavhead { display: none; }
  .setnav .setnavrow { white-space: nowrap; }
}

/* AN OPEN FOLDER UNDERLINES ITS HEAD (his mock, 2026-09-01: "when the folder
   is opened add the line shown here"; collapsed shows none, and "shrink it
   like in the screen shot" — the line is INSET from both ends, not the row's
   full width, so it reads as a divider rather than a box edge). A pseudo-
   element rather than a border, so the head keeps its hover rounding. The
   portal row is folder-styled but has no body — never a line over nothing. */
.navfoldergroup:not(.shut):not(.navtopgroup) > .navfolder { position: relative; margin-block-end: 4px; }
.navfoldergroup:not(.shut):not(.navtopgroup) > .navfolder::after {
  /* No rule under the heading any more: the hairline between folders is the
     divider, and a second line here would draw two. */
  content: none;
}

/* The settings sidebar's group headings (his layout, 2026-09-01): "My
   settings" and, for an administrator, "Admin settings" — a heading with a
   hairline under it, the mock's own motif. The close floats in the body's
   corner because this panel has no head bar; the body makes room for it. */
.setnavhead { font-size: var(--t-lead); font-weight: 700; color: var(--paper);
  padding: 6px 10px 8px; border-block-end: 1px solid var(--line);
  margin-block-end: 6px; }
.setnavhead + .setnavhead, .setnavrow + .setnavhead { margin-block-start: 18px; }
.setbody { position: relative; }
.setclose { position: absolute; inset-block-start: 10px; inset-inline-end: 10px; z-index: 2; }
.setbody .tplsec { padding-inline-end: 46px; }
.setbody .admrow { padding-inline: 0; }
/* A member row's role dropdown wears the system's control (his screenshot:
   the browser's raw select) — the same skin every other select here wears. */
/* .tplrow too (his "why is the Can view styled like that", 2026-09-02):
   the share panels' per-row level selects were bare UA controls beside
   fully dressed rows. One rule for every row-mounted select. */
.urow select, .tplrow select, .tplsectitle select { border: 1px solid var(--line); background: var(--ink); border-radius: var(--r-md);
  padding: 4px 7px; color: var(--paper); font: inherit; font-size: var(--t-small); outline: none; }
.urow select:focus, .tplrow select:focus { box-shadow: 0 0 0 2px var(--focus-ring); }

/* The one button under the member list, FROZEN to the bottom (his correction,
   2026-09-01): sticky against the section's scroll, on its own ground so the
   rows slide beneath it, a hairline marking the edge. The negative margins
   cancel the section's padding so it spans and pins flush. */
.setfoot { position: sticky; inset-block-end: 0; margin: 12px -14px -14px;
  padding: 10px 14px; background: var(--panel); border-block-start: 1px solid var(--line);
  display: flex; justify-content: flex-end; }
/* The add-member popup is a confirm asked over the settings panel: it takes
   the confirm rung like every other, so it has no z-index of its own. */
.addmember .invitebar input, .addmember .invitebar select { min-width: 150px; }
/* `.peekback.over` — a scrim opened OVER the settings panel — is placed with
   the other scrims, beside `.peekback`, one rung under the confirm. */
/* The phone row: the country code is a compact dropdown, the number takes
   the room (his order, 2026-09-01: Bahrain first, the Gulf, then the world). */
.setphone .phonecc { flex: 0 0 96px; min-width: 0; display: inline-flex; align-items: center;
  justify-content: center; gap: 6px; border: 1px solid var(--line); background: var(--ink);
  border-radius: var(--r-md); padding: 7px 8px; color: var(--paper); font: inherit; font-size: var(--t-body);
  cursor: pointer; }
.flagimg { inline-size: 20px; block-size: 15px; border-radius: var(--r-hair); object-fit: cover;
  outline: 1px solid var(--line); flex: none; }
.setphone input { flex: 1 1 140px; }
.setphone .btn { flex: none; }

/* The Shared shelf's two runs: a quiet caps label over each, aligned
   with the names below it, drawn only when its run has rows. */
.navsub { font-size: var(--t-small); font-weight: 700;
  color: var(--ghost); padding-block: 7px 3px; padding-inline: 30px 8px; }
:root.navrail .navsub { display: none; }

/* THE BOARD EXPANDED OVER ITS OWN HEAD (CEO, 2026-09-02): the toggle at the
   toolbar's end hides #pageview — title, properties, blocks — and the views
   take the room. Per reader, per board, remembered like every view state. */
.pagecard.boardfull #pageview { display: none; }

/* THE ROLE CHIP AS A CONTROL (2026-09-02: "click on the roll of the user to
   change it"). Drawn as a button only where the press would be honored — the
   master's view, on everyone but themselves; everywhere else it stays the
   plain chip. Same coat as the span, plus the hand and a lift on hover. */
button.rolechip { border: 0; cursor: pointer; font-family: inherit; }
button.rolechip:hover { filter: brightness(1.2); }

/* THE TEAM'S FACE (2026-09-02: "teams should also have icons and a color
   code"): a small tile carrying the team's registry icon over one of the
   option-chip tones — the same colour vocabulary a select's options wear.
   Never colour alone: the icon and the name always ride with it. */
.tface { inline-size: 24px; block-size: 24px; border-radius: var(--r-md); flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center; }
button.tface { border: 1px solid var(--line); cursor: pointer; padding: 0; }
/* The team's name wears the TAG styling in its colour (his word,
   2026-09-02: "when the color is selected follow the same styling we
   already have for the tags") — the same .chip c-* pill a select's options
   wear, so one vocabulary answers for both. A span, not a control: the
   pencil is the way in, so the pointer must not promise otherwise. */
.tmnamechip { cursor: default; }
/* The in-place rename field: the row's own text, not a dialog's. */
.tmname { flex: 1; min-width: 80px; max-width: 300px; border: 1px solid var(--line);
  background: var(--ink); border-radius: var(--r-md); padding: 4px 8px; color: var(--paper);
  font: inherit; font-size: var(--t-small); outline: none; }

/* THE TEAMS SECTION KEEPS ONE COLUMN OF CONTROLS (his alignment, 2026-09-02):
   every icon button is the same 44px tile, so the pencil sits over the stars
   and both trashes share an edge. The title row and the member rows both lay
   out on a 10px gap, so the pairs align with no correction; the Add button
   below spans exactly the pair — 2 x 44px + 10px — and being the bar's last
   element it shares the pair's right edge whatever the bar's own gap is. */
.teamctl { inline-size: 44px; justify-content: center; flex-shrink: 0; }
.invitebar .tmadd { flex: 0 0 98px; justify-content: center; }

/* The property dialog's own icon sits beside the name (2026-09-02: "the
   fields need to be properly iconed"), and the name and type stack in one
   column beside it (his alignment, same day) — so the type line reads under
   the NAME, the member rows' avatar-and-two-lines shape. The title input's
   -6px pull-out and the type's bare text both land at the column's start,
   which is what keeps the two flush. */
.eprowtop { display: flex; align-items: flex-start; gap: 8px; margin-block-end: 14px; }
/* CENTRED ON THE TITLE LINE (his correction, 2026-09-02, twice), not on
   the name-and-type pair: the title input is 25px tall and pulls itself up
   2px (its own -2px margin), so the 24px tile starts 1.5px above the row to
   put the two centres on the same pixel — measured, not eyeballed. */
.eprowtop .tface { margin-block-start: -1.5px; }
.epnamecol { flex: 1; min-width: 0; }
.epnamecol .eptype { margin: 0; }

/* THE STAR IS THE DEFAULT (CEO, 2026-09-02): starred fills new pages,
   pressed again it clears — the team-leader star's grammar, on an option
   row. The done check beside it is on every dropdown's rows now, and it
   acts everywhere (nodeDone), so it is never a control that does nothing. */
.epstar { width: 26px; height: 26px; display: flex; align-items: center;
  justify-content: center; border: 0; background: none; color: var(--mute);
  border-radius: var(--r-sm); flex-shrink: 0; cursor: pointer; }
.epstar:hover { background: var(--line); color: var(--paper); }
.epstar.on { color: var(--warn); }

/* THE FLOW CHART VIEW. The lines are BORDERS ON THE LAYOUT — a rail
   across the top of each generation, a stub down to every child, a stem
   under every parent — all logical properties, so the chart mirrors under
   RTL with no coordinate math to disagree with the flex that places the
   cards. Siblings space with PADDING, not gap: the rails span each child's
   full box, so a gap would cut holes in the line. */
/* ROOM AT THE BOTTOM FOR ANOTHER PAGE (his rule, 2026-09-05: "there needs to
   be room for another page and then you add the padding"): the pane keeps a
   card's height (84px image slot + borders, 96px) plus a generation's rail
   (18px) plus the old 30px beneath the last row, so under any bottom card
   there is somewhere to drop a sub-page. */
.flowpane { overflow: auto; padding: 20px 10px 144px; }
.flowroots { display: flex; align-items: flex-start; inline-size: max-content;
  min-inline-size: 100%; justify-content: center; }
.flownode { display: flex; flex-direction: column; align-items: center;
  position: relative; padding-inline: 9px; }
.flowkids { display: flex; align-items: flex-start; padding-block-start: 18px;
  position: relative; }
.flowkids > .flownode::before { content: ''; position: absolute;
  inset-block-start: -18px; inset-inline: 0; border-block-start: 1.5px solid var(--line); }
.flowkids > .flownode:first-child::before { inset-inline-start: 50%; }
.flowkids > .flownode:last-child::before { inset-inline-end: 50%; }
.flowkids > .flownode:only-child::before { display: none; }
.flowkids > .flownode::after { content: ''; position: absolute;
  inset-block-start: -18px; inset-inline-start: 50%; block-size: 18px;
  border-inline-start: 1.5px solid var(--line); }
/* The stem from a parent down to its generation's rail; the fold pill sits
   ON it, so the line reads through the control. */
.flownode > .flowfold { position: relative; z-index: 1; margin-block-start: 6px; }
.flownode > .flowfold::before { content: ''; position: absolute;
  inset-block-start: -7px; inset-inline-start: 50%; block-size: 7px;
  border-inline-start: 1.5px solid var(--line); z-index: -1; }

/* THE CARD IS THE PAGE CARD BLOCK (his correction, 2026-09-02) — the
   `.pagecardbtn` classes verbatim, with flow's own few rules on top: a
   fixed width so a generation lays out, the ICON AT IMAGE SIZE in the
   image slot when the page has no photo (his flow spec), and both lines
   held to ONE line with an ellipsis, the dash keeping an empty property's
   height so every card is the same card. */
.flowcard.pagecardbtn { inline-size: 290px; }   /* "a little" wider, 2026-09-02 */
.flowcard .pcimg { flex-basis: 84px; min-block-size: 84px; }
/* A glyph needs less room than a picture (design audit C4): the slot that
   holds the page's icon is 60px, not the cover's 84, so a card without a cover
   is mostly its words. A cover keeps the full slot. */
.flowcard .pcimg.flowico { display: flex; align-items: center; justify-content: center; flex-basis: 60px; min-block-size: 64px;
  color: var(--ghost); background: rgb(var(--c-panel2) / .6); }
.flowcard .flowico svg { width: 30px; height: 30px; }
.flowcard .pcbody { padding: 10px 12px; gap: 4px; justify-content: center; min-inline-size: 0; flex: 1; }
.flowcard .pcname { font-size: var(--t-body); }
.flowcard .pcname > span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.flowcard .pcname.done > span { text-decoration: line-through; color: var(--mute); }
.flowcard .pcrow { min-block-size: 20px; align-items: center; font-size: var(--t-small); }
/* The label is the property's ICON, not its word (his correction,
   2026-09-02) — the column's own glyph, the name on hover. BOTH
   rows lead with the same fixed 16px slot and the same gap, so the name
   and the value share one text edge TO THE PIXEL (his alignment, same
   day, measured in the suite) — with or without a photo: a bare card
   keeps the empty slot, the two rows indenting together. */
.flowcard .pcname { gap: 8px; }
.flowcard .pcrow { gap: 8px; }
.flowcard .flowplab, .flowcard .pclab { min-width: 0; inline-size: 16px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center; color: var(--ghost); }
.flowcard .pcname .flowplab svg { color: var(--wksoft); }
.flowcard .pcval { white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  display: flex; align-items: center; min-inline-size: 0; }
.flowcard .pcval > * { max-inline-size: 100%; overflow: hidden; text-overflow: ellipsis; }
.flowcard.dragging { opacity: .45; }
/* The drop zones wear the table's own marks — on the INLINE edges, since
   the siblings stand side by side (drag sideways to reorder, 2026-09-02):
   before is the reading-order side, so the pair flips under RTL with the
   zones themselves. */
.flowcard.dropbefore { box-shadow: inset 3px 0 0 0 var(--accent); }
.flowcard.dropafter { box-shadow: inset -3px 0 0 0 var(--accent); }
[dir="rtl"] .flowcard.dropbefore { box-shadow: inset -3px 0 0 0 var(--accent); }
[dir="rtl"] .flowcard.dropafter { box-shadow: inset 3px 0 0 0 var(--accent); }
.flowcard.dropinto { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.flowfold { display: inline-flex; align-items: center; gap: 3px; border: 1px solid var(--line);
  border-radius: var(--r-pill); padding: 2px 8px; background: var(--panel); color: var(--mute);
  font: inherit; font-size: var(--t-label); font-weight: 700; cursor: pointer; }
.flowfold:hover { color: var(--paper); border-color: var(--ghost); }

/* The roster action's knobs: a small column of rows inside the
   action — where a member files, the photo switch, the icon, then one row
   per account field mapped into a column. */
.rosterknobs { display: flex; flex-direction: column; gap: 6px; flex: 1; min-inline-size: 0; }
.rosterknobs .rulact { margin: 0; }

/* LOCKED AND HIDDEN. The chips on the page head: the lock for
   everyone (it explains every refused edit); Hidden only for admins and the
   owner, whose business it is. The row's small lock rides beside the name. */
.guardchip { display: inline-flex; align-items: center; gap: 4px; border-radius: var(--r-pill);
  padding: 2px 8px; font-size: var(--t-small); font-weight: 700; background: var(--panel2); color: var(--mute);
  margin-inline-end: 6px; }
.rowlock { display: inline-flex; align-items: center; color: var(--ghost);
  margin-inline-end: 4px; flex-shrink: 0; }

/* THE SUB-PAGES SECTION IN THE BOARD'S GRAMMAR: twisty, icon, name,
   the count of what waits inside, children nesting by the same indent the
   table uses, and the maker's row last. */
.kid { padding-inline-start: calc(14px + var(--pad, 0px)); }
.ktw { inline-size: 22px; block-size: 22px; display: inline-flex; align-items: center;
  justify-content: center; border: 0; background: none; color: var(--ghost);
  border-radius: var(--r-sm); cursor: pointer; flex-shrink: 0; padding: 0; }
.ktw:hover { background: var(--panel2); color: var(--paper); }
.ktw.bare { cursor: default; }
.kcount { font-size: var(--t-caps); font-weight: 700; color: var(--mute); border: 1px solid var(--line);
  border-radius: var(--r-pill); padding: 1px 7px; flex-shrink: 0; }
.kidadd { padding-block: 4px; }
.kidadd .newbtn { display: inline-flex; align-items: center; gap: 6px; border: 0; background: none;
  color: var(--mute); font: inherit; font-size: var(--t-body); cursor: pointer; padding: 6px 0; }
.kidadd .newbtn:hover { color: var(--paper); }

/* THE DISCUSSION PANEL: a drawer against the inline end, over the
   peek and under every dialog and menu (the layer order at the top of this
   file; it used to sit above the dialogs and hid their close buttons). The body scrolls inside itself —
   constraint 4's shape — and the composer stays at hand at its foot.
   No scrim: the page beneath stays live so pages can be dragged
   in; the panel closes from its X and from Escape. */
.chatpanel { position: fixed; inset-block: 0; inset-inline-end: 0; inline-size: min(400px, 92vw);
  z-index: calc(var(--z-side) + 4); background: var(--panel); border-inline-start: 1px solid var(--line);
  display: flex; flex-direction: column; }
.chatpanel .phead { display: flex; align-items: center; gap: 8px; padding: 12px 14px;
  border-block-end: 1px solid var(--line); }
.chatbody { flex: 1; overflow-y: auto; padding: 4px 14px 14px;
  display: flex; flex-direction: column; }
/* THE MESSENGER SHAPE ("check whatsapp or discord or telegram"):
   the thread grows UP from the composer, so a short conversation sits at
   the bottom beside the field, not stranded at the top of an empty panel. */
.chatbody .thread, .chatbody .nothread { margin-block-start: auto; }
.chatfoot { border-block-start: 1px solid var(--line); padding: 10px 14px;
  background: var(--panel); flex-shrink: 0; }
.chatfoot .composer { margin: 0; }
/* The door's count: a real figure or nothing. */
[data-pgchat] { position: relative; }
.chatn { font-size: var(--t-micro); font-weight: 700; background: rgb(var(--c-wk) / .18); color: var(--wksoft);
  border-radius: var(--r-pill); padding: 1px 5px; margin-inline-start: 4px; }

/* The whole folder heading toggles (2026-09-03), so the whole heading says
   so with the hand — the name included. */
.navgroup[data-shelf] { cursor: pointer; }

/* THE CHAT CARRIES MORE THAN WORDS (2026-09-03): dropped page links,
   attached files, reactions. The drop mark is drawn INSIDE the panel's edge
   (outline-offset), because the panel is the whole target — nothing shifts
   when it appears (the reserve-space rule). */
.chatpanel.linkdrop, .chatpanel.filedrop { outline: 2px dashed var(--accent); outline-offset: -6px; }
/* What the next message will carry: chips above the composer, each with its
   own way out. An empty tray renders nothing at all. */
.chatdraft { display: flex; flex-wrap: wrap; gap: 6px; margin-block-end: 8px; }
.dchip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; max-inline-size: 100%;
  border: 1px solid var(--line); border-radius: var(--r-md); background: rgb(var(--c-panel2) / .6); font-size: var(--t-small); }
.dchip .gl { display: flex; color: var(--mute); flex-shrink: 0; }
.dchip > span:not(.gl) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dchip .dwait { color: var(--mute); font-size: var(--t-label); flex-shrink: 0; }
.dchip .dx { display: flex; border: 0; background: none; color: var(--mute); cursor: pointer; padding: 0; }
.dchip .dx:hover { color: var(--bad); }
.dchip .dx svg { width: 12px; height: 12px; }
.composer .attach { flex-shrink: 0; align-self: flex-end; }
/* A page linked in a message: the page's own face, or the honest grey when
   the reader cannot see it (the wire rule — no title without access). */
.mlinks, .mfiles { display: flex; flex-wrap: wrap; gap: 6px; margin-block-start: 6px; }
.chatpg { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; max-inline-size: 100%;
  border: 1px solid var(--line); border-radius: var(--r-md); background: rgb(var(--c-panel2) / .6);
  color: var(--paper); font: inherit; font-size: var(--t-small); cursor: pointer; }
.chatpg:hover { border-color: var(--accent); }
.chatpg .gl { display: flex; color: var(--mute); flex-shrink: 0; }
.chatpg > span:not(.gl) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chatpg.off { color: var(--mute); cursor: default; }
.chatpg.off:hover { border-color: var(--line); }
/* An attached image shows itself, capped so the thread stays a thread;
   everything else is the file-block card vocabulary, small. */
.chatimg { display: block; max-inline-size: 100%; max-block-size: 180px;
  border-radius: var(--r-lg); border: 1px solid var(--line); }
.chatfile { display: inline-flex; align-items: center; gap: 8px; padding: 6px 10px; max-inline-size: 100%;
  border: 1px solid var(--line); border-radius: var(--r-md); background: rgb(var(--c-panel2) / .6);
  color: var(--paper); font-size: var(--t-small); text-decoration: none; }
.chatfile:hover { border-color: var(--accent); }
.chatfile .linkicon { display: flex; color: var(--mute); flex-shrink: 0; }
.chatfile .linkmeta { min-width: 0; }
.chatfile .linkname { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Reactions: a pill per mark with its count — the count is the glyph's
   second voice, so a mark never speaks by colour alone. Mine is marked by
   the accent wash AND the heavier count. */
.mreacts { display: flex; flex-wrap: wrap; gap: 4px; margin-block-start: 6px; }
.rpill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: rgb(var(--c-panel2) / .55);
  color: var(--paper); font: inherit; font-size: var(--t-small); cursor: pointer; }
.rpill svg { width: 14px; height: 14px; }
.rpill .rn { font-size: var(--t-label); color: var(--mute); }
.rpill:hover { border-color: var(--accent); }
.rpill.on { border-color: rgb(var(--c-wk) / .6); background: rgb(var(--c-wk) / .15); }
.rpill.on .rn { color: var(--wksoft); font-weight: 700; }
.msg .radd { opacity: 0; flex-shrink: 0; }
.msg:hover .radd { opacity: 1; }
/* The picker: the quick marks in the icon picker's own grid. */
.reactpick .regrid { grid-template-columns: repeat(6, 1fr); }
.reactpick .remoji { font-size: var(--t-h3); }

/* THE PROPERTY LIST'S VALUES HOLD ONE SIZE (his report, 2026-09-03:
   "the font size is all over the place"). `.cellbtn` carries `font: inherit`
   for the table, where the ground is 13px — in a property row the ground is
   the PAGE's 16px, so a date stood three sizes above the person beside it.
   Chips keep their own 10px: a chip is a chip everywhere. */
.prow .cellbtn, .prow .duecell { font-size: var(--t-body); }
/* An inherited value, dimmed the way its table cell dims it (.inh at .55);
   the row's title names where it came from. */
.pvinh .chip, .pvinh .person, .pvinh input, .pvinh .cellbtn { opacity: .55; }
/* An inherited chip has nothing of its own to clear — the x would be a
   control that silently does nothing, the outcome this product treats as
   the worst one. Setting a value of your own is the chip itself. */
.pvinh .tagx { display: none; }
/* The company's member count on its rail row — the badge rule:
   a real positive figure or nothing. Pushed to the row's end. */
.setnavrow .kcount { margin-inline-start: auto; }

/* ---------------- the print view ----------------
 * The app drawn for headless Chrome to print: no chrome, one section per page,
 * each section its own CSS page (`page: prN`, sized by printSheets). Light
 * theme always — his answer, "keep it white" — through the tokens, not a
 * literal. Nothing here is reachable by a pointer, so hover states go too. */
.printing body { padding-inline-start: 0; background: var(--panel); }
.printing .appnav, .printing .navgrip, .printing .navscrim, .printing .topbar,
.printing #boardview, .printing #storagewarn, .printing .minebar, .printing .pagehead { display: none !important; }
.printing .wrap { padding: 0; }
/* Nothing but the sheets is in the flow: anything after the last named page
   would print as a blank letter-sized sheet of its own. */
.printing .wrap > :not(.pagecard), .printing .pagecard > :not(#pageview),
.printing body > :not(.wrap) { display: none !important; }
.printing .pagecard { border: 0; border-radius: 0; background: none; overflow: visible; }
.prsec { box-sizing: border-box; padding: 44px 52px 52px; break-before: page;
  background: var(--panel); color: var(--paper); }
.prsec:first-child { break-before: auto; }
.prsec .inner { max-width: none; }
.prsec .prevtable, .prsec .ttwrap { overflow: visible; }
.prtitle { font-size: var(--t-h1); font-weight: 700; margin: 0; letter-spacing: -.01em; line-height: 1.25; flex: 1; min-width: 0; }
.prico { pointer-events: none; }
.prsec button, .prsec [role="button"], .prsec .tw, .prsec .box { pointer-events: none; }
.prsec .bmark.tgl { transform: rotate(90deg); }
.prsec .blk:hover, .prsec .prow:hover { background: none; }
.prtoc { list-style: none; margin: 6px 0 0; padding: 0; }
.prindex .prtoc { margin-top: 18px; }
.prtoc li { padding-inline-start: calc(var(--d, 0) * 22px); }
.prlink { display: inline-flex; align-items: center; gap: 8px; min-height: 30px; color: var(--paper);
  text-decoration: none; font-size: var(--t-lead); }
.prlink .gl { display: flex; color: var(--mute); }
.prmedia .linkhost { word-break: break-all; }
.prsec a.gotolink, .prsec a.pagecardbtn, .prsec a.nlink { text-decoration: none; }
.prnote { color: var(--mute); font-size: var(--t-lead); }

/* A confirm dialog's CHECKLIST (`o.checks`): the rows it already
   draws for one tick, stacked in one scrolling box. */
.cfchecks { border: 1px solid var(--line); border-radius: var(--r-lg); padding: 6px; max-height: 320px;
  overflow-y: auto; margin: 0 0 16px; display: grid; gap: 4px; }
.cfchecks .confirmcheck { margin: 0; align-items: center; }
/* A mapping row (`o.maps`): who it was, and a select for who it becomes. */
.cfmap { justify-content: space-between; gap: 12px; }
.cfmap > span { display: flex; flex-direction: column; min-width: 0; }
.cfmap > span b { font-weight: 600; }
.cfmap > span small { color: var(--mute); font-size: var(--t-label); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cfmap select { border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel2); color: var(--paper);
  font: inherit; font-size: var(--t-body); padding: 7px 9px; max-width: 50%; }

/* ---------------- the dashboard ----------------
 * The door: a row at the portal row's altitude, lit when open. The grid:
 * twelve columns; a widget takes a third, half or all of them, and on a phone
 * every widget takes the whole width. A widget is a card in the panel's own
 * voice: a head with a grip, a name, its scope and its dots; a body. */
/* The dashboard's door is `navTopRow` (nav.js) — the same row as Back to
   portal, no rules of its own (his rule: unify, everything modular). */
/* A TILING GRID: twelve columns, rows of one fixed unit, every tile at
   `--wx`/`--wy` spanning `--wspan` columns and `--wh` rows. The gap and the
   row unit are the ones dash.js measures with (GRID_GAP, ROW_PX) — change one
   and the other. On a phone the tiles stack, full width, as tall as they need. */
.dashgrid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); grid-auto-rows: 80px; gap: 14px; margin-top: 8px;
  grid-template-rows: repeat(var(--wrows, 1), 80px); }
.widget { grid-column: calc(var(--wx, 0) + 1) / span var(--wspan, 12); grid-row: calc(var(--wy, 0) + 1) / span var(--wh, 4);
  min-width: 0; min-height: 0; border: 1px solid var(--line); border-radius: var(--r-xl);
  background: var(--panel2); overflow: hidden; display: flex; flex-direction: column; position: relative;
  transition: none; }
/* The body scrolls inside the tile: a tile is the size the person gave it. */
.widget .wbody { flex: 1 1 auto; min-height: 0; overflow: auto; container-type: size; display: flex; flex-direction: column; }
/* EVERY VISUAL FILLS ITS TILE (his resized tiles: a list stopped at
   twenty rows over a void, a calendar's days sat in the top third, a line
   chart kept its small fixed size, a donut did not grow). The body is a
   size container; each visual takes the space it has. */
.wbody > .wtile { flex: 1 1 auto; display: flex; flex-direction: column; justify-content: center; }
.wbody > .wtile .wnum { font-size: clamp(26px, 34cqh, 96px); }
.wbody > .wtiles { flex: 1 1 auto; }
.wbody > .wbars { flex: 1 1 auto; grid-auto-rows: minmax(0, 1fr); align-content: stretch; }
.wbody > .wbars .wbartrack { height: clamp(14px, 45%, 34px); }
.wbody > .wdonut { flex: 1 1 auto; min-height: 0; flex-wrap: nowrap; }
.wbody > .wdonut .wring { width: auto; height: 100%; max-height: 100%; aspect-ratio: 1 / 1; flex: 0 1 auto; max-width: 60%; }
.wbody > .wdonut .wlegend { flex: 1 1 120px; align-content: center; }
.wbody > .wtl { flex: 1 1 auto; grid-auto-rows: minmax(0, 1fr); align-content: stretch; }
.wbody > .wtl .wtltrack { height: clamp(18px, 60%, 32px); }
.wbody > .wcal { flex: 1 1 auto; grid-template-rows: auto; grid-auto-rows: minmax(0, 1fr); min-height: 0; }
.wbody > .wcal .wcalday { display: flex; flex-direction: column; min-height: 0; }
.wbody > .wcal .wcalitems { flex: 1 1 auto; }
.wbody > .wcal .wcalday { align-items: stretch; padding: 4px; }
.wbody > .wcal .wcalday .wcaln { text-align: start; }
.wbody > .wmeter { flex: 0 0 auto; }
.wbody > .wtable, .wbody > .wnotices, .wbody > .wpages { flex: 1 1 auto; min-height: 0; }
.wbody > .wline { flex: 1 1 auto; min-height: 0; width: 100%; height: 100%; }
.wbody > .wnote { flex: 1 1 auto; }
@media (max-width: 900px) {
  .dashgrid { grid-template-rows: none; grid-auto-rows: auto; }
  .dashgrid .widget { grid-column: 1 / -1; grid-row: auto; }
  .widget .wbody { overflow: visible; }
  /* A tile is content-tall here, so the number takes a phone's size (design
     audit C3): 96px of "0" owned half the screen for a single figure. */
  .wbody > .wtile .wnum { font-size: 44px; line-height: 1.1; }
  .show-number .wtile { padding: 10px 14px 10px; }
  .wresize { display: none; }
}
.wresize { position: absolute; inset-block: 0; inset-inline-end: 0; inline-size: 10px; border: 0; background: none;
  cursor: col-resize; padding: 0; z-index: 2; }
.wresize::after { content: ''; position: absolute; inset-block: 10px; inset-inline-end: 3px; inline-size: 3px; border-radius: var(--r-hair);
  background: var(--wk); opacity: 0; transition: opacity .12s; }
.wresize:hover::after, .wresize:focus-visible::after, .widget.dragging .wresize::after { opacity: .8; }
.wresize:focus-visible { outline: none; }
/* The bottom edge and the corner. */
.wresize-s { inset-block: auto 0; inset-inline: 0; inline-size: auto; block-size: 10px; cursor: row-resize; }
.wresize-s::after { inset-block: auto 3px; inset-inline: 10px; inline-size: auto; block-size: 3px; }
.wresize-se { inset-block: auto 0; inset-inline: auto 0; inline-size: 14px; block-size: 14px; cursor: nwse-resize; z-index: 3; }
[dir="rtl"] .wresize-se { cursor: nesw-resize; }
.wresize-se::after { display: none; }
/* MOVING A TILE: the tile floats with the cursor at full size; its own
   element stays in the grid as the PLACEHOLDER — a highlighted area of the
   tile's exact shape at the cell it will land in — and the others shift. */
.widget.wfloat { position: fixed; inset-inline-start: 0; inset-block-start: 0; z-index: var(--z-float); pointer-events: none;
  box-shadow: var(--shadow-card); opacity: .92; transform-origin: 0 0; }
/* Solid, not dotted (his word, 2026-09-05): a tinted area with a solid edge. */
.widget.wplace { background: rgb(var(--c-wk) / .14); border: 2px solid rgb(var(--c-wk) / .75); }
.widget.wplace > * { visibility: hidden; }
.widget.dragging { opacity: .55; }
body.wdragging { cursor: grabbing; user-select: none; }
body.wdragging .dashgrid::before { content: ''; position: absolute; inset: -6px 0; pointer-events: none; z-index: 0;
  background: repeating-linear-gradient(to right, rgb(var(--c-wk) / .06) 0, rgb(var(--c-wk) / .06) calc((100% - 11 * 14px) / 12), transparent calc((100% - 11 * 14px) / 12), transparent calc((100% - 11 * 14px) / 12 + 14px));
  background-size: calc((100% + 14px) / 12) 100%; }
.dashgrid .widget { transition: grid-row .12s, grid-column .12s; }
.wgrip { touch-action: none; }
/* THE GRID SHOWS WHILE A WIDTH IS BEING DRAGGED: twelve columns drawn in the
   surface, and the count in words at the top. */
.dashgrid { position: relative; }
.dashgrid.resizing::before { content: ''; position: absolute; inset: -6px 0; pointer-events: none; z-index: 1;
  background: repeating-linear-gradient(to right, rgb(var(--c-wk) / .10) 0, rgb(var(--c-wk) / .10) calc((100% - 11 * 14px) / 12), transparent calc((100% - 11 * 14px) / 12), transparent calc((100% - 11 * 14px) / 12 + 14px));
  background-size: calc((100% + 14px) / 12) 100%; }
.dashgrid.resizing::after { content: attr(data-resize-to); position: absolute; inset-block-start: -28px; inset-inline-end: 0; z-index: 3;
  font-size: var(--t-label); font-weight: 700; color: var(--paper); background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 2px 8px; }
/* THE LAYOUT BUTTON: a header control like every other, carrying the
   name of the layout in force. The name is the only part that can be any
   length, so it is the only part that ellipsises — the glyph and the dot keep
   their size, and the control cannot push the head sideways (constraint 4).
   The dot's SPACE IS ALWAYS THERE and only its opacity changes: a button that
   grows when a change is made would move under the pointer reaching for it. */
/* The layout control is a page-toolbar button like Share beside it (design
   audit B3): sentence case, not the header chrome's capitals. */
.dashlay { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 190px; flex-shrink: 1; }
.dashlay .dlname { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dashlay .dldot { flex: 0 0 6px; inline-size: 6px; block-size: 6px; border-radius: 50%;
  background: var(--wk); opacity: 0; transition: opacity .12s; }
.dashlay.dirty .dldot { opacity: 1; }
/* Naming a new layout happens IN the control: the same frame, the
   field where the name sits, no dot — nothing moves when Save New is picked. */
.dashlay.naming { cursor: text; }
.dashlay.naming .dlname { border: 0; background: transparent; color: var(--paper); font: inherit;
  letter-spacing: inherit; text-transform: inherit; padding: 0; min-width: 120px; max-width: 160px; outline: none; }
.dashlay.naming .dlname::placeholder { color: var(--mute); }
@media (max-width: 767px) { .dashlay { max-width: 130px; } }
.whead { display: flex; align-items: center; gap: 8px; padding: 8px 10px 8px 6px; border-bottom: 1px solid var(--line); min-height: 40px; }
/* THE TITLE WINS (design audit B7): it is what the reader is looking for, so it
   never shrinks while the meta beside it has room to give; the meta ellipsises
   first and disappears last. The cap keeps a very long title from pushing the
   grip and the menu off the head. */
.wtitle { margin: 0; font-size: var(--t-body); font-weight: 600; color: var(--paper); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  flex: 0 0 auto; max-width: calc(100% - 96px); }
.wscope { margin-inline-start: auto; font-size: var(--t-label); color: var(--mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 0 1 auto; text-align: end; }
.wgrip { border: 0; background: none; color: var(--ghost); cursor: grab; display: flex; padding: 4px; border-radius: var(--r-sm); flex-shrink: 0; }
.wgrip:hover { color: var(--paper); background: var(--line); }
.wbody { padding: 10px 12px 12px; min-width: 0; }
.wadd { align-items: center; justify-content: center; flex-direction: row; gap: 6px;
  border-style: dashed; background: none; color: var(--mute); font: inherit; font-size: var(--t-body); cursor: pointer; min-height: 52px; }
.wadd:hover { color: var(--paper); border-color: var(--wksoft); }
.wempty { margin: 6px 0; color: var(--mute); font-size: var(--t-body); }
/* The counts: three tiles, each a button whose number is the biggest thing on it. */
.wtiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.wtile { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--panel); padding: 12px 12px 10px;
  display: flex; flex-direction: column; gap: 2px; align-items: flex-start; cursor: pointer; font: inherit; color: var(--paper); text-align: start; }
.wtile:hover { border-color: var(--wksoft); }
.wnum { font-size: var(--t-h1); font-weight: 700; letter-spacing: -.02em; line-height: 1.1; font-variant-numeric: tabular-nums; }
.wword { font-size: var(--t-small); font-weight: 700; color: var(--mute); }
/* The task list: the table's own voice, four columns, no name column tricks.
   ITS OWN CLASS, not `.tt`: the board table's phone rules turn `.tt` into
   stacked blocks for the nine-column tree, and a four-column list wearing
   that class collapsed into a jumble at 390px. Here the phone loses the
   Board column and keeps the rows. */
.wtable { overflow-x: auto; }
.wtt { width: 100%; table-layout: auto; border-collapse: separate; border-spacing: 0; font-size: var(--t-body); }
@media (max-width: 600px) { .wtt .wboard, .wtt th:nth-child(2) { display: none; } .wtt td, .wtt th { padding-inline: 4px; } }
.wtt th { text-align: start; font-size: var(--t-small); color: var(--ghost); font-weight: 700; padding: 4px 8px; }
.wtt td { padding: 6px 8px; border-top: 1px solid var(--line); vertical-align: middle; font-size: var(--t-body); }
.wopen { border: 0; background: none; font: inherit; color: var(--paper); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; padding: 2px 4px; border-radius: var(--r-sm); text-align: start; }
.wopen:hover { background: var(--panel); }
.wopen .gl { display: flex; color: var(--mute); }
.wopen.wsub { color: var(--mute); }
.wdue { white-space: nowrap; }
.dashpanel .dashlist { padding: 10px 14px 14px; }

/* ---------------- widgets as five parts ----------------
 * A number is its whole widget body; bars are thin, rounded at the data end,
 * the value at the tip; the donut is one ring with a surface gap between
 * slices and its total in the centre; a legend always sits beside it. A
 * group with a colour of its own wears it (the option's chip class, used as
 * `currentColor`); one without takes the chart tokens in fixed order. */
.show-number .wbody { padding: 0; }
.show-number .wtile { border: 0; border-radius: 0; background: none; width: 100%; padding: 14px 14px 12px; }
.s1 { color: rgb(var(--c-chart-s1)); } .s2 { color: rgb(var(--c-chart-s2)); } .s3 { color: rgb(var(--c-chart-s3)); }
.s4 { color: rgb(var(--c-chart-s4)); } .s5 { color: rgb(var(--c-chart-s5)); } .s6 { color: rgb(var(--c-chart-s6)); }
.wswatch { display: inline-block; width: 10px; height: 10px; border-radius: var(--r-hair); background: currentColor; padding: 0; flex-shrink: 0; }
.wswatch.chip { border-radius: var(--r-hair); width: 10px; height: 10px; }
.wbars { display: grid; gap: 6px; }
.wbarrow { display: grid; grid-template-columns: minmax(80px, 30%) 1fr auto; align-items: center; gap: 10px;
  border: 0; background: none; font: inherit; color: var(--paper); cursor: pointer; padding: 4px 6px; border-radius: var(--r-md); text-align: start; }
.wbarrow:hover { background: var(--panel); }
.wbarlabel { font-size: var(--t-small); color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wbartrack { height: 14px; background: var(--panel); border-radius: var(--r-xs); overflow: hidden; }
/* A BAR IS ITS PAGES: the bar is a row of sections, one per page,
   the group's colour on every one, a hairline of the track between them.
   Each section is a page — hover for its card, press to open it. */
.wbar { display: flex; gap: 2px; block-size: 100%; border-start-end-radius: 4px; border-end-end-radius: 4px; background: none; min-inline-size: 2px; overflow: hidden; }
.wbar.chip { border-radius: 0 var(--r-xs) var(--r-xs) 0; padding: 0; font-size: 0; background: none; }
.wbar:empty { background: currentColor; }
.wseg { flex: 1 1 0; min-inline-size: 3px; block-size: 100%; background: currentColor; cursor: pointer; transition: filter .12s; }
.wseg:hover { filter: brightness(1.3); }
.wbarval { font-size: var(--t-body); font-weight: 600; font-variant-numeric: tabular-nums; min-width: 2ch; text-align: end; }
.wdonut { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.wring { width: 150px; height: 150px; flex-shrink: 0; transform: rotate(-90deg); }
.wring .wtrack { fill: none; stroke: var(--panel); stroke-width: 12; }
.wring .wslice { fill: none; stroke: currentColor; stroke-width: 12; cursor: pointer; }
.wring .wslice.chip { padding: 0; border-radius: 0; background: none; }
.wring .wslice:hover { stroke-width: 15; }
.wring .wslice.item { cursor: pointer; }
.wring .wcentre { transform: rotate(90deg); transform-origin: 50% 50%; fill: var(--paper); font-size: var(--t-h2); font-weight: 700; font-family: inherit; }
/* A ring on a phone keeps a size of its own; the tile has no height there. */
@media (max-width: 900px) { .wbody > .wdonut .wring { height: 150px; width: 150px; } .wbody > .wline { height: 160px; } .widget .wbody { container-type: normal; display: block; } }
.wlegend { display: grid; gap: 4px; min-width: 0; flex: 1 1 160px; }
.wlegrow { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px; border: 0; background: none;
  font: inherit; font-size: var(--t-body); color: var(--paper); cursor: pointer; padding: 4px 6px; border-radius: var(--r-md); text-align: start; }
.wlegrow:hover { background: var(--panel); }
.wlegname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--mute); }
.wlegval { font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---------------- every form from the research ---------------- */
/* Progress: a meter — one track, one fill, the percentage beside it. */
.wmeter { display: flex; align-items: center; gap: 14px; }
.wmeterbar { flex: 1; height: 14px; border-radius: var(--r-md); background: var(--panel); overflow: hidden; display: flex; gap: 2px; }
/* Each section a page: done in green, open in the track's own tone. */
.wmeterbar .wseg.done { background: var(--good); }
.wmeterbar .wseg.open { background: rgb(var(--c-mute) / .22); }
.wmeterfill { display: block; block-size: 100%; background: var(--good); border-radius: var(--r-md); min-inline-size: 2px; }
.wmeterpct { font-size: var(--t-h2); font-weight: 700; font-variant-numeric: tabular-nums; min-width: 3.2ch; text-align: end; }
.wmeterwords { display: flex; gap: 14px; margin-top: 8px; font-size: var(--t-small); color: var(--mute); }
.wmeterwords button { border: 0; background: none; font: inherit; color: var(--mute); cursor: pointer; padding: 2px 6px; border-radius: var(--r-sm); }
.wmeterwords button:hover { background: var(--panel); color: var(--paper); }
/* Timeline: one row per page, a bar from start to due across the window. */
.wtl { display: grid; gap: 4px; }
.wtlhead, .wtlrow { display: grid; grid-template-columns: minmax(110px, 26%) 1fr; gap: 10px; align-items: center; }
.wtlhead { font-size: var(--t-small); color: var(--ghost); font-weight: 700; }
.wtlscale { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; }
.wtlscale span { text-align: start; border-inline-start: 1px solid var(--line); padding-inline-start: 4px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.wtlname { border: 0; background: none; font: inherit; font-size: var(--t-small); color: var(--paper); text-align: start; cursor: pointer;
  padding: 2px 4px; border-radius: var(--r-sm); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; display: flex; align-items: center; gap: 6px; }
.wtlname:hover { background: var(--panel); }
.wtltrack { position: relative; height: 18px; background: var(--panel); border-radius: var(--r-xs); }
.wtlbar { position: absolute; inset-block: 3px; border-radius: var(--r-xs); background: rgb(var(--c-wk) / .75); min-inline-size: 6px; }
.wtlbar.late { background: rgb(var(--c-bad) / .8); }
.wtlbar.done { background: rgb(var(--c-good) / .7); }
.wtltoday { position: absolute; inset-block: 0; inline-size: 1px; background: var(--paper); opacity: .5; }
/* Calendar: a month grid; a day is a button carrying its count. */
.wcal { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 3px; }
.wcalhead { font-size: var(--t-caps); color: var(--ghost); text-align: center; font-weight: 700; padding: 2px 0; }
.wcalday { border: 1px solid var(--line); border-radius: var(--r-sm); background: none; font: inherit; color: var(--mute); cursor: pointer;
  min-height: 42px; padding: 4px; display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between; }
.wcalday:hover { background: var(--panel); }
.wcalday.today { border-color: var(--wksoft); }
.wcalday.other { opacity: .35; }
.wcalday .wcaln { font-size: var(--t-label); }
.wcalday .wcalc { font-size: var(--t-body); font-weight: 700; color: var(--paper); }
/* A day's pages, drawn as the board calendar draws them (restyled
   2026-09-05): `.cal-item` pills, stacked, clipped to the day. */
.wcalitems { display: flex; flex-direction: column; gap: 2px; inline-size: 100%; min-block-size: 0; overflow: hidden; padding: 2px 0 0; text-align: start; }
.wcalday .cal-item { font-size: var(--t-label); padding: 2px 5px; border-radius: var(--r-sm); flex: 0 0 auto; }
.wcalday .cal-item.done { color: var(--ghost); text-decoration: line-through; }
.wcalday .cal-item .opticon { display: inline-flex; }
.wcalmore { font-size: var(--t-caps); color: var(--mute); font-weight: 600; padding-inline-start: 5px; }
.wcalday.has { background: rgb(var(--c-wk) / .12); }
.wcalmonth { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: var(--t-body); font-weight: 600; }
/* Line: 2px line, 8px markers with a surface ring, hairline gridlines. */
.wline { width: 100%; height: 100%; display: block; }
.wline .grid { stroke: var(--line); stroke-width: 1; }
.wline .path { fill: none; stroke: rgb(var(--c-wk)); stroke-width: 2; stroke-linejoin: round; stroke-linecap: butt; }
/* One line, cut into its pages: a piece is a page; no points. */
.wline .seg { fill: none; stroke: rgb(var(--c-wk)); stroke-width: 2.5; stroke-linecap: butt; cursor: pointer; transition: stroke-width .12s; }
.wline .seg:hover { stroke-width: 5; }
.wline .lbl { fill: var(--mute); font-size: var(--t-label); font-family: inherit; }
.wline .val { fill: var(--paper); font-size: var(--t-small); font-weight: 600; font-family: inherit; }
/* Pivot: a cross-tabulation; every cell a button. */
.wpivot { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--t-small); }
.wpivot th { text-align: start; font-size: var(--t-small); color: var(--ghost); font-weight: 700; padding: 4px 6px; }
.wpivot td { padding: 2px; border-top: 1px solid var(--line); }
.wpivot td button { width: 100%; border: 0; background: none; font: inherit; color: var(--paper); cursor: pointer; padding: 5px 6px; border-radius: var(--r-sm); text-align: end; font-variant-numeric: tabular-nums; }
.wpivot td button:hover { background: var(--panel); }
.wpivot td.wrowhead { text-align: start; color: var(--mute); padding: 5px 6px; }
.wpivot td.zero button { color: var(--ghost); }
/* Note: plain words, the person's own. */
.wnote { white-space: pre-wrap; font-size: var(--t-lead); line-height: 1.55; color: var(--paper); margin: 0; }
.wnote.empty { color: var(--mute); }
/* Countdown: a stat tile whose number is days. */
.wtile .wsub { font-size: var(--t-small); color: var(--mute); }
/* Notice rows: who, what, where, when. */
.wnotice { display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: center; padding: 6px 4px; border-top: 1px solid var(--line); }
.wnotice:first-child { border-top: 0; }
.wnotice .wntext { font-size: var(--t-body); color: var(--paper); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wnotice .wntext .wnpage { color: var(--mute); }
.wnotice .wnwhen { font-size: var(--t-label); color: var(--ghost); white-space: nowrap; }
.wnotice .ndot { width: 7px; height: 7px; border-radius: 50%; background: var(--line); }
.wnotice .ndot.on { background: var(--wksoft); }
.wnotice.wopenrow { cursor: pointer; border-radius: var(--r-sm); }
.wnotice.wopenrow:hover { background: var(--panel); }
/* A page row for recent / favorites: icon, name, where it sits, when. */
.wpage { display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: center; padding: 2px 0; border-top: 1px solid var(--line); }
.wpage:first-child { border-top: 0; }
.wpage .wpwhere { font-size: var(--t-label); color: var(--ghost); white-space: nowrap; }
.wmore { margin: 8px 4px 0; font-size: var(--t-small); color: var(--mute); }
.wmore .wopen { padding: 0 4px; font-size: var(--t-small); }
