/* DBOT QS — the finish layer.
   ===========================================================================
   Everything here is *additive*. It loads after `style.css` and overrides it;
   nothing in the app reads a class defined here, and no screen script depends
   on it. Take the two lines out of `index.html` -- the <link> below and the
   motion module -- and the platform is exactly as it was, to the pixel.
   That is the point: a look you can put back.

   The direction is the one the app was already heading in and did not finish:
   Swiss, dense, quiet. A QS reads figures here for eight hours; the interface
   has to hold still, tell the eye where it is, and get out of the way. So no
   gradients, no glass, no colour that does not mean something. What changes
   is the things a dense tool actually needs and this one lacked -- a real
   elevation scale, motion that says what just happened, focus you can find
   from a keyboard, and a loading state that is not the word "Loading".

   Two things asked for are not here, and this is where to say so:

   * **anime.js** is unreachable from this machine -- every CDN (cdnjs,
     jsdelivr, unpkg) is refused by the egress proxy, and the project has no
     npm step to vendor it with (`make run`, one process, no build). The
     motion is written on the Web Animations API instead, which is the same
     capability natively: `motion.js` keeps an anime-shaped helper so the
     library can be dropped in later without touching a call site.
   * **Kokonut UI** is a React + Tailwind + shadcn registry. There is no
     React, no Tailwind and no build step in this codebase, so it cannot be
     installed without changing what this platform *is*. What is taken from
     it is the language -- soft elevation, generous radii, motion that
     responds to the pointer -- written in plain CSS below.
   =========================================================================== */

:root {
  /* ---- elevation ---------------------------------------------------------
     Four steps, and each one means something: a card sits on the page, a
     menu sits over it, a panel sits over the screen, a dialog over
     everything. One shadow used for all of them, which is what was here
     before, flattens that order into nothing. */
  --e1: 0 1px 2px rgba(16, 24, 40, .04);
  --e2: 0 1px 3px rgba(16, 24, 40, .07), 0 1px 2px -1px rgba(16, 24, 40, .04);
  --e3: 0 6px 16px -4px rgba(16, 24, 40, .10), 0 2px 6px -2px rgba(16, 24, 40, .05);
  --e4: 0 18px 40px -12px rgba(16, 24, 40, .22), 0 6px 12px -6px rgba(16, 24, 40, .08);
  --shadow: var(--e2);

  /* ---- motion ------------------------------------------------------------
     Chosen by distance, not by taste. A hover travels no distance, so it is
     fast; a panel crosses 400px, so it is not. Exits run at ~60% of their
     entrance, because leaving should feel like the interface getting out of
     your way rather than a second event to wait through. */
  --t-fast:  110ms;   /* a tint, a border: no distance at all */
  --t-base:  170ms;   /* hover, press, a cell changing state */
  --t-enter: 240ms;   /* something arriving from off-screen */
  --t-exit:  150ms;   /* the same thing leaving */
  --ease:     cubic-bezier(.22, .61, .36, 1);   /* decelerate: arriving */
  /* Leaving is fast out of the gate too: an accelerating curve starts slow,
     which is exactly when the eye is on it. And nothing overshoots -- a
     settle, not a bounce. (`premium.css` has always overridden both; these
     now say what actually runs.) */
  --ease-in:  cubic-bezier(.23, 1, .32, 1);
  --ease-spring: cubic-bezier(.32, .72, 0, 1);
  --tempo: var(--t-base);

  /* ---- surface -----------------------------------------------------------
     A touch cooler and a touch lighter, so the white of an editable cell
     reads as white against it. That contrast is the whole legend in the
     sidebar: white is yours, grey is the system's. */
  --bg: #f4f6f9;
  --line: #e4e8ee;
  --radius-1: 6px;
  --radius-2: 10px;
  --radius-3: 14px;
}

/* ===========================================================================
   Shell
   =========================================================================== */

/* The bar is the one element on screen at every moment, so it is the one
   that must not wobble. It gets its own hairline plus a shadow that only
   appears once the page beneath it has scrolled -- set by `motion.js`, so
   the bar looks flat against a page that has not moved and lifted over one
   that has. */
#topbar {
  transition: box-shadow var(--t-base) var(--ease);
  backdrop-filter: saturate(1.1);
}
#topbar.lifted { box-shadow: 0 1px 0 var(--line), 0 6px 16px -12px rgba(16, 24, 40, .5); }

/* --- the bar's composition -------------------------------------------------
   It carries three unrelated things -- which project this is, what it comes
   to, and what you can do -- and they were a single run of items at one
   weight, so 278 / 32 / 2,47,170 read as one wall of digits and your own
   name sat at the same volume as Sign out.

   Three groups now, divided by rules that are actually visible: a full-height
   one between the groups, a short centred one between figures inside a
   group. Nothing was added and nothing removed -- the same elements, given
   room and an order to be read in. */
#topbar { gap: 0; padding: 0 14px 0 16px; }

.brand {
  gap: 11px; min-width: 0; padding-right: 18px; align-self: stretch;
  align-items: center; position: relative;
}
/* As wide as the rail below it, less the bar's own 16px of left padding, so
   the hairline drawn at its right edge lands on the sidebar's border for
   every project rather than wherever that project's name happened to end.
   A name that needs more than that is cut with an ellipsis -- the full
   name is in the sidebar's own box and the Projects list. Off a project the
   sidebar is hidden and there is nothing to line up with, so the block is
   free to take its own width there. */
#app:not(.no-project) .brand {
  width: calc(var(--rail) - 16px); box-sizing: border-box; flex: none;
}
.brand > div { min-width: 0; }
.project-name, .project-sub {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.brand::before {
  content: ""; position: absolute; right: 0; top: 12px; bottom: 12px;
  width: 1px; background: var(--line-strong);
}
.logo { height: 24px; }
.project-name { font-size: 13.5px; letter-spacing: -.012em; line-height: 1.25; }
.project-sub { font-size: 10.5px; letter-spacing: .005em; }

/* The figures. A shorter rule between them than between the groups, so they
   read as one set of readings rather than five separate facts -- and enough
   air that the eye lands on a number instead of sliding off a wall. */
.headline { padding-left: 18px; align-items: stretch; }
.stat {
  display: flex; flex-direction: column; justify-content: center; gap: 1px;
  padding: 0 20px; position: relative; border-left: 0;
}
.stat::before {
  content: ""; position: absolute; left: 0; top: 17px; bottom: 17px;
  width: 1px; background: var(--line-strong);
}
.stat:first-child { padding-left: 0; }
.stat:first-child::before { display: none; }
.stat .v { font-size: 14.5px; font-weight: 640; letter-spacing: -.015em; }
.stat .k { font-size: 10.5px; letter-spacing: .06em; font-weight: 600; }

/* What you can do, behind its own rule and in its own order: what is
   happening to this project, then what you can do about it, then who you
   are, then which build. The rule belongs to the group, not to the first
   thing that happens to be in it -- it used to be drawn by the health chip,
   so it vanished on the Projects screen, where that chip is hidden. */
#topbar .health { padding-left: 0; margin-left: 0; border-left: 0; }
.bar-tools::before {
  content: ""; position: absolute; left: 0; top: 12px; bottom: 12px;
  width: 1px; background: var(--line-strong);
}
/* A rhythm rather than a single spacing: things that belong together sit
   closer than things that merely follow one another. The project's state,
   then a step, then what you can do about it, then a step, then you. */
.bar-tools > #paletteBtn { margin-left: 6px; }
.bar-tools > #whoami { margin-left: 6px; }
/* Off a project there are no figures to divide from. */
#app.no-project .bar-tools::before { display: none; }
#app.no-project .bar-tools { padding-left: 0; }
/* The group sits hard right on every screen, by the one auto margin in the
   bar, which is on `.bar-tools` itself. One, deliberately: an auto margin
   on more than one item does not "claim what is left", it splits it --
   flexbox shares free space equally between every auto margin it finds --
   and the toggle used to sit in the middle of the Projects screen with the
   profile at the edge. */
#whoami { display: flex; align-items: center; }
/* Scoped. `.who` is also the class on a colleague's initials in the presence
   chip, and this rule's 6px of right padding was pushing those initials off
   the centre of their circle -- two features sharing a class name, one of
   them silently misdrawn. */
#whoami .who { font-size: 12px; color: var(--ink-2); padding-right: 6px; }

/* The build is a stamp, not a control: it was a pill of the same shape and
   weight as the health chip, which is the one thing in this group somebody
   should actually click. */
#buildStamp .chip.mute {
  background: none; border: 0; color: var(--ink-3); font-family: var(--mono);
  font-size: 11px; padding: 4px 0; letter-spacing: -.01em;
}

/* Where you are, said by position as well as by colour. The tick is the fast
   read -- you find it in peripheral vision without parsing the label -- and
   the size, weight and colour stay for everyone who cannot see the blue.

   It sits inside the link's own padding, at its left edge. It used to hang
   10px outside it, and the folding group's `overflow: hidden` clipped it
   away: the mark had been drawn and never seen.

   The move is the link growing from its left edge while the one you left
   shrinks back -- both on `transform`, so the compositor runs them and a
   slow screen render cannot stall them, and both on the strong ease-out:
   it answers the click at once and settles. 180ms: this is a navigation
   done dozens of times an hour. */
#sidebar a {
  position: relative;
  transition: color var(--t-fast) var(--ease), transform var(--t-base) var(--ease);
}
#sidebar a::before {
  content: ""; position: absolute; left: 0; top: 50%;
  width: 3px; height: 16px; border-radius: 3px;
  background: var(--accent);
  /* Grown from the middle by a scale rather than a height: a height
     transition is a layout on every frame. Scaled to .88 at rest so that,
     inside a link grown by 1.14, it is the 3 x 16 it was drawn as. */
  transform: translateY(-50%) scale(.88, 0);
  transition: transform var(--t-base) var(--ease);
}
#sidebar a.active::before { transform: translateY(-50%) scale(.88); }
/* The rail belongs to the screens, not to the group headings or the way out,
   both of which are `#sidebar a` or close enough to inherit it. */
.nav-sec-head::before, .nav-out::before { content: none !important; }

/* --- the groups ------------------------------------------------------------
   Four labels in 10px type were a list with captions in it rather than a
   structure. A heading is a control now: a chevron that turns and the
   stage's name, lit when the screen you are on is behind it. */
.nav-sec-head {
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.nav-sec-head:hover { background: var(--row-hover); color: var(--ink-2); }
.nav-sec-head:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px; color: var(--ink-2);
}
.nav-chev {
  transform: rotate(0deg);
  transition: transform var(--t-base) var(--ease-spring);
}
.nav-sec.is-open .nav-chev { transform: rotate(90deg); }

/* The same caret, on a project row that folds its towers out. One rule and one
   token, so the two carets on screen turn at the same speed and with the same
   curve -- and, being here rather than in `style.css`, the motion comes off
   with this stylesheet exactly as every other animation does. */
.proj-fold svg { transition: transform var(--t-fast) var(--ease); }

/* The fold. `grid-template-rows` from 0fr to 1fr is how a list of unknown
   height opens without its height being measured in JavaScript, and the row
   inside it clips. It is a layout transition, which this stylesheet avoids
   on anything that runs during a navigation -- this one runs on a click, on
   one column of seven links, and on nothing else at the same time. */
.nav-sec-body { transition: grid-template-rows var(--t-base) var(--ease); }
/* On the first paint, and whenever a group is put right rather than folded
   by somebody: opened, not unfolded. */
.nav-sec.no-fold .nav-sec-body, .nav-sec.no-fold .nav-chev { transition: none; }

.nav-out { transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .nav-sec-body, .nav-chev { transition: none; }
}

/* A screen arrives rather than appearing. 6px and 200ms: enough for the eye
   to register that the content changed, not enough to wait for.

   `backwards`, never `both` -- see the note in `motion.js`. A fill that
   outlives the animation leaves a transform on the element for ever, and any
   transform makes it the containing block for `position: fixed` children,
   which is what sent the row menu on Projects to the wrong place. */
#main > .screen > * { animation: screen-in var(--t-enter) var(--ease) backwards; }
/* Not on a redraw in place. The router marks the container it rebuilds after
   a committed cell; that screen has arrived already, and rising six pixels
   again on every value typed is the page moving under somebody's hands. */
#main > .screen.redraw > * { animation: none; }
@keyframes screen-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* A room's heading inside a unit-type block, in the workbook's own header
   blue. The block above it is the sheet title (`--wb-title`, navy) and the
   tables below are its rows; the room is the band between them, and on a
   white card it read as another row rather than as the head of one. Same
   token the column headers use, so the sheet has one blue and not two. */
.card-body.room-head {
  background: var(--wb-header); color: var(--wb-header-ink);
  border-top: 1px solid var(--wb-title-line);
}
.card-body.room-head .muted,
.card-body.room-head .blk-figs .k { color: var(--wb-header-ink); opacity: .78; }
.card-body.room-head .blk-figs .v,
.card-body.room-head .blk-figs .k.tot,
.card-body.room-head .blk-figs .v.tot { color: var(--wb-header-ink); opacity: 1; }

/* A figure that lands after its screen has -- the dashboard's estimate,
   which waits on the summary -- fades in where the ellipsis was rather than
   swapping under the reader. Opacity only: a rise on a line of text three
   tiles in would move the eye off the figure it is trying to read. */
.tile .arrived { animation: figure-in var(--t-enter) var(--ease) backwards; }
@keyframes figure-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .tile .arrived { animation: none; }
}

/* ===========================================================================
   Cards, tiles, tables
   =========================================================================== */

.card {
  border-radius: var(--radius-2); box-shadow: var(--e2);
  border-color: var(--line);
  transition: box-shadow var(--t-base) var(--ease);
}
/* The workbook's navy sheet title. This layer is the one that paints a card
   header -- `style.css` sets its box and this sets its colour -- so the
   colour code goes here or it is overruled by a gradient. */
.card > h2 {
  background: linear-gradient(var(--wb-title), var(--wb-title));
  letter-spacing: -.005em;
}

.tile {
  border-radius: var(--radius-2); box-shadow: var(--e1);
  transition: box-shadow var(--t-base) var(--ease),
              border-color var(--t-base) var(--ease),
              transform var(--t-base) var(--ease);
}
/* A figure you can question lifts under the pointer -- 1px, the smallest
   movement that still reads as "this responds". Transform only: nothing
   around it moves (`layout-shift-avoid`). A mouse only: on a phone a tap
   left the tile lifted until the next tap landed somewhere else. */
@media (hover: hover) and (pointer: fine) {
  .tile.clickable:hover { transform: translateY(-1px); box-shadow: var(--e3); }
}
.tile.clickable:active { transform: translateY(0); box-shadow: var(--e1); }
.tile .v { letter-spacing: -.02em; }

/* Every figure in the app is read in a column against the figure above it,
   so every figure in the app is tabular. Proportional digits shift the
   decimal point by a pixel a row and the column stops reading as a column. */
.stat .v, .tile .v, td.cell, td.derived, .list-table .num,
.deriv-value, .deriv-input .v, .status .why {
  font-variant-numeric: tabular-nums;
}

/* The header of a scrolled table sat on the same hairline as its rows, so a
   long take-off scrolled its first row *under* the header with nothing to
   say it had gone. */
table.grid th { box-shadow: inset 0 -1px 0 var(--line-strong); }
table.grid tbody tr { transition: background var(--t-fast) linear; }

/* A cell that has just been written to says so, once, and stops. This is the
   only confirmation a QS gets that a number reached the server -- the row
   redraws with the same value it showed while being typed, and without this
   nothing on screen distinguishes "saved" from "not sent yet". */
@keyframes cell-saved {
  0%   { background: var(--flash-ok); }
  100% { background: transparent; }
}
td.saved { animation: cell-saved 900ms var(--ease); }

/* The unit under a column heading -- "sq.m", "m", "Rs" -- was set at 10px,
   below the 10.5px floor the stylesheet's own `--ink-3` note claims for the
   smallest text in the app, and there are 67 of them on Room Configuration
   alone. It is the label that says what every figure in the column *is*, so
   it is the last thing that should be hard to read. */
.col-sub { font-size: 10.5px; letter-spacing: .01em; }

/* ===========================================================================
   Controls
   =========================================================================== */

button.btn {
  border-radius: var(--radius-1); font-weight: 520;
  transition: background var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
  box-shadow: var(--e1);
}
button.btn:hover:not(:disabled) { box-shadow: var(--e2); }
/* Pressed is felt, not read: the button goes *down*, the way a key does. */
button.btn:active:not(:disabled) { transform: translateY(1px); box-shadow: none; }
button.btn.primary { box-shadow: var(--e2); }
button.btn.primary:hover:not(:disabled) { box-shadow: var(--e3); }
button.btn:disabled { box-shadow: none; }

select.sel-input, select.text-input, input.text-input, .field textarea.text-input {
  border-radius: var(--radius-1);
  transition: border-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease);
}
select.sel-input:hover, select.text-input:hover, input.text-input:hover {
  border-color: var(--ink-3);
}
/* The ring is on the field, not around the whole row, and it is the accent
   rather than a browser default -- so it is the same shape as every other
   focus in the app and a keyboard can never lose itself. */
select.sel-input:focus, select.text-input:focus, input.text-input:focus,
.field textarea.text-input:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.chip.act { transition: background var(--t-fast) var(--ease),
                        border-color var(--t-fast) var(--ease),
                        box-shadow var(--t-fast) var(--ease); }

/* ===========================================================================
   Panel, dialog, toast
   =========================================================================== */

/* The panel is over the screen, so it carries the third step of elevation,
   and it comes in from the edge it lives on rather than materialising. */
#panel { box-shadow: var(--e4); border-left: 1px solid var(--line); }
.panel-head { position: sticky; top: 0; z-index: 2; background: var(--surface); }

/* Rising from the bottom edge it is anchored to, and leaving faster than it
   arrived.
   
   Both directions in CSS, and deliberately so. The first attempt drove the
   exit from `motion.js` by watching the `hidden` attribute -- and the write
   that held the element open re-triggered the observer that had just made
   it, so the panel animated out and then never hid at all. `allow-discrete`
   is the feature for this: it defers `display: none` until the transition
   has finished, with no script, no observer and nothing to re-enter.
   
   A browser without it simply snaps, which is what happened before. */
#toast, #panel {
  transition: opacity var(--t-exit) var(--ease-in),
              transform var(--t-exit) var(--ease-in),
              display var(--t-exit) allow-discrete;
  opacity: 1;
}
#toast[hidden] { display: none; opacity: 0; transform: translateY(8px) scale(.98); }
#panel[hidden] { display: none; opacity: 0; transform: translateX(16px); }
@starting-style {
  #toast:not([hidden]) { opacity: 0; transform: translateY(12px) scale(.97); }
  #panel:not([hidden]) { opacity: 0; transform: translateX(24px); }
}
#toast { border-radius: var(--radius-2); box-shadow: var(--e4); }

/* ===========================================================================
   Waiting
   =========================================================================== */

/* A centred "Loading…" is a word where a shape should be. The words this app
   uses are better than that -- "Measuring every room and pricing it…" says
   what the wait is for -- so they stay, moved to where the reading starts,
   with the shape of the table underneath them. The skeleton holds the space
   the content is about to take, so the screen does not jump when it lands
   (`content-jumping`), and the shimmer says the wait is progressing rather
   than stuck. Under reduced motion the shimmer stops and the block stays:
   the message is the shape, not the movement. */
.loading {
  padding: 2px 0 0; text-align: left; display: grid; gap: 12px;
  justify-items: stretch;
}
.loading::after {
  content: ""; display: block; height: 220px; border-radius: var(--radius-2);
  background: linear-gradient(90deg, var(--skeleton-a) 25%, var(--skeleton-b) 37%, var(--skeleton-a) 63%);
  background-size: 400% 100%;
  animation: shimmer 1.4s ease-in-out infinite;
}
@keyframes shimmer {
  from { background-position: 100% 0; }
  to   { background-position: 0 0; }
}

/* ===========================================================================
   The explanations, shown properly
   =========================================================================== */

/* Positioned by transform from the top-left, so showing one never moves
   anything else on the page and the browser can put it on its own layer. */
.tip {
  position: fixed; top: 0; left: 0; z-index: 60; max-width: 300px;
  padding: 7px 10px; border-radius: var(--radius-1);
  background: var(--tip-bg); color: var(--tip-ink); box-shadow: var(--e4);
  font-size: 11.5px; line-height: 1.45; letter-spacing: .002em;
  pointer-events: none; white-space: normal;
}
.tip::after {
  content: ""; position: absolute; left: 50%; margin-left: -4px;
  border: 4px solid transparent; border-bottom-color: var(--tip-bg); top: -8px;
}
.tip.above::after { top: auto; bottom: -8px; border-bottom-color: transparent;
                    border-top-color: var(--tip-bg); }

/* ===========================================================================
   The cost bands, as lengths
   =========================================================================== */

/* A grid rather than a flex row, so the nine names share one column width and
   the nine bars share one track -- the bars all start at the same x, which is
   the whole point of drawing them. `minmax(0, 1fr)` on the track: without the
   0 the track refuses to shrink below its content and a long section name
   pushes the chart off the card. */
.bands {
  display: grid;
  grid-template-columns: minmax(96px, 168px) minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 2px 14px;
}

/* Each band is a button, because it opens its own working -- so it has to be
   reachable by keyboard, and a div with a click handler is not. Everything
   that makes a button look like a button is taken back off. */
.band {
  display: grid; grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
  appearance: none; background: none; border: 0; padding: 5px 6px;
  margin: 0 -6px; border-radius: var(--radius-1);
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.band:hover { background: var(--row-hover); }
.band:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.band-name {
  font-size: 12.5px; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.band-track {
  position: relative; height: 14px; border-radius: 3px;
  background: var(--chart-track);
}
/* The data end is rounded and the baseline end is square, so the bar reads as
   growing out of the axis rather than floating beside it.
   `--bar` arrives as the fraction the engine computed; the multiply into a
   width is CSS's, not JavaScript's. */
.band-fill {
  position: absolute; inset: 0 auto 0 0;
  width: calc(var(--bar, 0) * 100%);
  min-width: 3px;                       /* a band under half a per cent is
                                           still a band, and must be visible */
  background: var(--chart-bar);
  border-radius: 0 4px 4px 0;
  transform-origin: left center;
}
.band-amt   { font-size: 12.5px; text-align: right; min-width: 92px; }
.band-share { font-size: 12px; color: var(--ink-3); text-align: right;
              min-width: 48px; }

/* M8. The bars grow from nothing once, on entry. `scaleX` and nothing else,
   so there is no layout to redo per frame; the stagger is nine custom
   `transition-delay`s rather than nine animations. `.drawn` is the
   reduced-motion state and the resting state both -- no transition, full
   length -- so a screen that never animates still looks right. */
.bands .band-fill { transform: scaleX(1); }
.bands.drawing .band-fill {
  transform: scaleX(0);
  animation: band-draw 400ms cubic-bezier(.22, .61, .36, 1) forwards;
}
.bands.drawing .band:nth-child(1)  .band-fill { animation-delay: 0ms; }
.bands.drawing .band:nth-child(2)  .band-fill { animation-delay: 30ms; }
.bands.drawing .band:nth-child(3)  .band-fill { animation-delay: 60ms; }
.bands.drawing .band:nth-child(4)  .band-fill { animation-delay: 90ms; }
.bands.drawing .band:nth-child(5)  .band-fill { animation-delay: 120ms; }
.bands.drawing .band:nth-child(6)  .band-fill { animation-delay: 150ms; }
.bands.drawing .band:nth-child(7)  .band-fill { animation-delay: 180ms; }
.bands.drawing .band:nth-child(8)  .band-fill { animation-delay: 210ms; }
.bands.drawing .band:nth-child(9)  .band-fill { animation-delay: 240ms; }
/* Past nine they all draw together rather than trailing off the end of a
   list of delays -- the platform may not assume nine bands (non-negotiable
   5), and a tenth must not arrive undrawn. */
@keyframes band-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }

@media (prefers-reduced-motion: reduce) {
  .bands.drawing .band-fill { animation: none; transform: scaleX(1); }
}

@media (max-width: 720px) {
  .bands { grid-template-columns: minmax(0, 1fr) auto; }
  .band  { grid-template-columns: minmax(0, 1fr) auto; row-gap: 4px; }
  .band-track { grid-column: 1 / -1; }
  .band-share { min-width: 0; }
}

/* ===========================================================================
   Narrowing a grid: the filter row and the column choice
   =========================================================================== */

/* A second header row of plain boxes. They sit under the headings they
   filter and take the heading's width, so what you type lines up with what
   it narrows. */
.grid thead tr.filter-row th { padding: 3px 4px; background: var(--surface-2); }
.grid thead tr.filter-row input {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 12px;
  padding: 4px 6px; border: 1px solid var(--line); border-radius: 4px;
  background: var(--surface); color: var(--ink);
}
.grid thead tr.filter-row input::placeholder { color: var(--ink-3); }
.grid thead tr.filter-row input:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -1px;
}
/* Pressed while the row is showing. Outranks the quiet-button rule further
   down, which would otherwise wash it back to plain. */
.grid-actions .btn.quiet[data-filter-toggle][aria-pressed="true"] {
  background: var(--accent-soft); color: var(--accent); border-color: var(--accent);
}
.grid-count { font-size: 12px; margin-left: 4px; }

/* The column choice, a `<details>` in the actions bar. Positioned against
   the bar rather than the viewport: the bar sits under the grid and is not
   clipped, unlike the row menus on the Projects screen. */
.col-pick { position: relative; display: inline-block; }
.col-pick > summary { list-style: none; cursor: pointer; }
.col-pick > summary::-webkit-details-marker { display: none; }
.col-pick-body {
  position: absolute; left: 0; bottom: calc(100% + 6px); z-index: 30;
  min-width: 220px; max-height: 320px; overflow: auto;
  padding: 8px 10px; display: grid; gap: 4px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius-1); box-shadow: var(--e4);
}
.col-pick-body label {
  display: flex; align-items: center; gap: 8px; font-size: 12.5px;
  white-space: nowrap;
}
.col-pick-body input[disabled] + span, .col-pick-body label:has(input[disabled]) { color: var(--ink-3); }

/* ===========================================================================
   Anywhere by name: the palette
   =========================================================================== */

/* The way in, drawn as what it is. A magnifier beside a bare `K` said
   neither "search" nor which key to hold, and at 28px tall beside a 38px
   profile it read as a stray fragment. It is a field-shaped control now:
   icon, the word, the shortcut for this machine, on the same ground the
   grid's own inputs use. Wide enough to click at any width, and the word
   steps out first when the bar runs short. */
.palette-btn {
  display: inline-flex; align-items: center; gap: 7px;
  height: 32px; min-width: 32px; padding: 0 8px 0 9px; justify-content: center;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface-2); color: var(--ink-3); cursor: pointer;
  font: inherit;
  transition: background var(--t-base) var(--ease),
              border-color var(--t-base) var(--ease), color var(--t-base) var(--ease);
}
.palette-btn svg { width: 15px; height: 15px; flex: none; }
.palette-word { font-size: 12.5px; letter-spacing: -.01em; }
.palette-btn kbd {
  font: 600 10px/1 var(--mono); color: var(--ink-3);
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: 4px; padding: 3px 5px; white-space: nowrap;
}
.palette-btn:hover {
  color: var(--ink); border-color: var(--line-strong); background: var(--surface);
}
.palette-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Short bar: the word goes, then the shortcut, and the icon alone is still a
   32px target. Nothing is ever cut off mid-word. */
@media (max-width: 1360px) { .palette-word { display: none; } }
@media (max-width: 1180px) { .palette-btn kbd { display: none; } }

/* The overlay is a plain fixed layer, the box is a card, and the list under
   the box is the combobox's own `.cs-list` -- one control, everywhere it
   appears. The box does not move while its list changes height. */
.palette-veil {
  position: fixed; inset: 0; z-index: 70;
  background: color-mix(in srgb, var(--ink) 28%, transparent);
  display: grid; justify-items: center; align-items: start;
  padding-top: 12vh;
  animation: veil-in 120ms var(--ease);
}
.palette {
  width: min(560px, calc(100vw - 32px));
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: var(--e4);
  padding: 10px;
  animation: palette-in 120ms var(--ease);
}
.palette input {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 15px;
  padding: 10px 12px; border: 1px solid var(--line-strong);
  border-radius: var(--radius-1); background: var(--surface); color: var(--ink);
}
.palette input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.palette .palette-hint {
  display: flex; justify-content: space-between; gap: 12px;
  margin: 8px 4px 0; font-size: 11.5px; color: var(--ink-3);
}
.palette .palette-hint kbd {
  font: 600 10px/1 ui-monospace, monospace; border: 1px solid var(--line-strong);
  border-radius: 3px; padding: 2px 4px; color: var(--ink-3);
}
@keyframes veil-in    { from { opacity: 0; } }
@keyframes palette-in { from { opacity: 0; transform: translateY(-6px) scale(.98); } }

@media (prefers-reduced-motion: reduce) {
  .palette-veil, .palette { animation: none; }
}

/* ===========================================================================
   Who else is here
   =========================================================================== */

/* A person's colour is a hue, chosen from their name and the same every
   time; the lightness and chroma are the theme's, so the same hue reads on
   both grounds. 38% lightness keeps white initials over 4.5:1 for every
   hue, yellows included, which a brighter swatch would not. */
.presence { display: flex; align-items: center; }
/* Everything the circle needs is set here, `padding` and `box-sizing`
   included. It is not the only `.who` in the stylesheet -- the profile block
   has one too -- and inheriting that one's right padding is what pushed a
   colleague's initials a third of the way off their own circle. */
.presence .who {
  box-sizing: border-box; width: 26px; height: 26px; padding: 0;
  border-radius: 50%; overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10.5px; font-weight: 650; letter-spacing: .01em;
  text-indent: .01em;                    /* the letter-spacing's trailing gap */
  color: var(--on-ink);
  background: hsl(var(--who-hue, 220) 55% 38%);
  border: 2px solid var(--surface);
  animation: who-in 150ms var(--ease);
}
.presence .who + .who { margin-left: -6px; }
@keyframes who-in { from { opacity: 0; transform: scale(.6); } to { opacity: 1; transform: none; } }

/* A row somebody else just changed. A background wash, as the landing flash
   is: paint, not layout, once, on one row. It is a person's colour and not
   the accent, so it cannot be mistaken for the row you were sent to. */
@keyframes touched {
  from { background-color: hsl(var(--who-hue, 220) 70% 88%); }
  to   { background-color: transparent; }
}
.grid tbody tr.touched > td { animation: touched 2.4s var(--ease) both; }
:root[data-theme="dark"] .grid tbody tr.touched > td {
  animation-name: touched-dark;
}
@keyframes touched-dark {
  from { background-color: hsl(var(--who-hue, 220) 40% 24%); }
  to   { background-color: transparent; }
}

@media (prefers-reduced-motion: reduce) {
  .presence .who { animation: none; }
  /* Still says which row, without fading; the script takes it off again. */
  .grid tbody tr.touched > td { animation: none; background: hsl(var(--who-hue, 220) 70% 88%); }
  :root[data-theme="dark"] .grid tbody tr.touched > td { background: hsl(var(--who-hue, 220) 40% 24%); }
}

/* ===========================================================================
   One cell's history, and the way back
   =========================================================================== */

/* M4. The cell says which cell the panel is about -- the panel opens at the
   right-hand edge and a grid has two hundred rows, so without this the answer
   to "which one did I press?" is on the far side of the screen.
   A ring on a pseudo-element, so nothing on the cell itself moves and no
   neighbour is relaid out. */
td.cell.pulse { position: relative; }
td.cell.pulse::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  border: 2px solid var(--accent); border-radius: 3px;
  animation: cell-ring 300ms cubic-bezier(.2, .8, .3, 1);
}
@keyframes cell-ring {
  from { transform: scale(1.15); opacity: 0; }
  40%  { opacity: 1; }
  to   { transform: scale(1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  td.cell.pulse::after { animation: none; display: none; }
}

/* The change log's own filters. Links, not buttons: each one is an address,
   so it can be shared, bookmarked and reached with the back button. */
.log-filters { display: flex; gap: 6px; flex-wrap: wrap; padding-bottom: 10px; }
.log-chip {
  font-size: 12px; padding: 4px 10px; border-radius: 999px;
  border: 1px solid var(--line-strong); color: var(--ink-2);
  text-decoration: none; white-space: nowrap;
}
.log-chip:hover { background: var(--row-hover); color: var(--ink); }
.log-chip.on {
  background: var(--accent-soft); border-color: var(--accent);
  color: var(--accent); font-weight: 560;
}
.log-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.hist { display: grid; gap: 2px; }
.hist-row {
  display: grid; grid-template-columns: 96px 1fr auto;
  gap: 10px; align-items: baseline;
  padding: 7px 0; border-top: 1px solid var(--line); font-size: 12.5px;
}
.hist-row:first-child { border-top: 0; }
.hist-when { color: var(--ink-3); font-size: 11.5px; line-height: 1.35; }
.hist-move { display: flex; gap: 7px; align-items: baseline; flex-wrap: wrap; }
.hist-old { color: var(--ink-3); text-decoration: line-through; }
.hist-arrow { color: var(--ink-3); }
.hist-new { color: var(--ink); font-weight: 560; }
.hist-why { flex-basis: 100%; font-size: 11.5px; }

@media (max-width: 720px) {
  .hist-row { grid-template-columns: 1fr; }
}

/* ===========================================================================
   Two revisions, side by side
   =========================================================================== */

.compare-pick { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.compare-pick .picker {
  font: inherit; font-size: 13px; padding: 6px 9px; min-width: 200px;
  border: 1px solid var(--line-strong); border-radius: var(--radius-1);
  background: var(--surface); color: var(--ink);
}
.compare-pick .picker:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}

/* The sign is part of the value, not decoration on it: the direction has to
   survive being printed, and being read by somebody who cannot tell these two
   apart by hue. */
.delta { font-variant-numeric: tabular-nums; font-weight: 560; }
.delta.up   { color: var(--chart-up); }
.delta.down { color: var(--chart-down); }

/* A delta band is a div, not a button -- there is no working behind a
   difference to open, so it must not look like there is. */
.band.static { cursor: default; }
.band.static:hover { background: none; }
.band.up   .band-fill { background: var(--chart-up); }
.band.down .band-fill { background: var(--chart-down); }
/* A band that shrank grows its bar from the right, so the two directions are
   apart before any sign is read. */
.band.down .band-fill {
  inset: 0 0 0 auto; border-radius: 4px 0 0 4px;
  transform-origin: right center;
}
.band.static .band-share { min-width: 170px; }
/* A band that did not move draws no bar. The 3px floor on .band-fill is there
   so a real but tiny band stays visible; applied to a zero it puts a stub
   against every unchanged band and reads as a small movement that is not
   there. */
.band.zero .band-fill { display: none; }

/* What changed, collection by collection. */
.cmp-group { border-top: 1px solid var(--line); padding: 8px 0; }
.cmp-group:first-child { border-top: 0; padding-top: 0; }
.cmp-group > summary {
  cursor: pointer; list-style: none; display: flex; gap: 10px;
  align-items: baseline; padding: 4px 0; font-size: 13px;
}
.cmp-group > summary::-webkit-details-marker { display: none; }
.cmp-group > summary::before {
  content: "›"; display: inline-block; width: 12px; color: var(--ink-3);
  transition: transform 120ms ease;
}
.cmp-group[open] > summary::before { transform: rotate(90deg); }
.cmp-group > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.cmp-rows { padding: 4px 0 8px 22px; }
.cmp-row {
  display: grid; grid-template-columns: 74px minmax(120px, 220px) 1fr;
  gap: 4px 12px; align-items: baseline;
  padding: 5px 0; border-top: 1px solid var(--line);
}
.cmp-row:first-child { border-top: 0; }
.cmp-name { font-size: 12.5px; }
.cmp-detail { display: grid; gap: 2px; }
.cmp-field {
  display: grid; grid-template-columns: minmax(0, 150px) 1fr 14px 1fr;
  gap: 8px; align-items: baseline; font-size: 12px;
}
.cmp-what  { color: var(--ink-3); }
.cmp-a     { color: var(--ink-2); text-align: right; }
.cmp-arrow { color: var(--ink-3); text-align: center; }
.cmp-b     { color: var(--ink); font-weight: 540; }

ul.plain { margin: 6px 0 0; padding-left: 18px; }
ul.plain li { margin: 3px 0; }

@media (max-width: 720px) {
  .cmp-row { grid-template-columns: 74px 1fr; }
  .cmp-detail { grid-column: 1 / -1; }
  .cmp-field { grid-template-columns: 1fr; gap: 0; }
  .cmp-a, .cmp-arrow, .cmp-b { text-align: left; }
  .band.static .band-share { min-width: 0; }
}

/* ===========================================================================
   The small things
   =========================================================================== */

/* Scrollbars. Every screen here is a table taller than the window, so these
   are on screen constantly -- at the OS default width they are a grey slab
   down the side of every card. Thin, and only as dark as they need to be.

   The two ways of asking are not additive, and that is why this had stopped
   working. Chrome 121 shipped the standard `scrollbar-width` / `scrollbar-color`
   and gave them precedence: where both are written, the whole
   `::-webkit-scrollbar` block below is ignored. So the rounded 10px thumb this
   app draws was never reaching the screen, and what showed instead was Chrome's
   own scrollbar -- a wide track with a stepper arrow at each end, in the middle
   of a page where nothing else is the operating system's. Nothing here changed;
   the browser did.

   So the standard properties are handed only to an engine that has no
   `::-webkit-scrollbar` to style -- Firefox -- and everything else uses the
   rules written for it. The colour is the token rather than a literal, because
   a literal stayed light grey in dark mode. */
@supports not selector(::-webkit-scrollbar) {
  * { scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) transparent; }
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb); border-radius: 6px;
  border: 3px solid transparent; background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); background-clip: content-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* Selected text in the app's own blue rather than the browser's, and dark
   enough to read through. */
::selection { background: var(--select-bg); color: var(--ink); }

/* A select had the operating system's arrow, which is a different size and a
   different grey on every machine this runs on. One chevron, the app's. */
select.sel-input, select.text-input, .compare-pick .picker {
  appearance: none; -webkit-appearance: none;
  padding-right: 26px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23636c79' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 9px center;
}

/* The focus ring on a cell arrived instantly and left instantly, which on a
   grid you are arrowing across reads as a flicker rather than as a cursor. */
td.cell, td.derived { transition: background var(--t-fast) linear,
                                  box-shadow var(--t-fast) var(--ease); }

/* Deleting a row is the one irreversible thing in a table, so its control
   stays quiet until the row is under the pointer, and then says clearly what
   it is. It was at full strength on all 1,480 rows at once. */
button.row-del {
  opacity: .35; transition: opacity var(--t-fast) var(--ease),
                            background var(--t-fast) var(--ease),
                            color var(--t-fast) var(--ease);
}
tr:hover button.row-del, button.row-del:focus-visible { opacity: 1; }

/* One focus ring, everywhere, in the accent -- so a keyboard never has to
   work out which of three shapes means "you are here". */
a:focus-visible, button:focus-visible, summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px;
}
td.cell:focus, td.cell:focus-visible { outline-offset: -2px; border-radius: 0; }

/* ===========================================================================
   Reduced motion
   =========================================================================== */

/* Not "less motion": none. Every animation above is decoration on top of a
   final state that is already correct, so removing them costs nothing and
   showing them to somebody who gets sick from them costs a great deal. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .tile.clickable:hover { transform: none; }
}

/* ===========================================================================
   The change log
   =========================================================================== */

/* A day at a time. Eighty rows each repeating "15 Sep 2026" is eighty
   readings of one fact; the heading says it once and the rows under it carry
   only the clock. Sticky, so the day you are reading stays named while you
   scroll through it. */
.grid.log tr.log-day th {
  position: sticky; top: 0; z-index: 2;
  text-align: left; font-size: 11px; font-weight: 650; letter-spacing: .02em;
  color: var(--ink-2); background: var(--surface-2);
  padding: 7px 10px; border-bottom: 1px solid var(--line);
}
.grid.log .log-time { font-variant-numeric: tabular-nums; color: var(--ink-2); }

/* What changed, then what it changed on, then why -- in that order, because
   that is the order the question is asked in. The field is the answer; the
   record is where to look; the reason is there when something other than a
   person typing put the row here. */
.grid.log .log-what { font-weight: 560; }
.grid.log .log-of { color: var(--ink-3); margin-left: 6px; }
/* On the same line as the rest, not under it: a row is one line tall, and a
   reason that dropped to a second line made that row eight pixels taller
   than its neighbours. */
.grid.log .log-why {
  color: var(--ink-3); font-size: 11px; font-style: italic; margin-left: 8px;
}

/* ===========================================================================
   The dropdown, as a sheet draws one
   =========================================================================== */

/* It was a native <select> dropped into the cell: rounded, bordered, arrowed
   and taller than the row, so opening one made the line jump and the grid
   stopped looking like a grid. A sheet draws it as part of the cell -- the
   box stays exactly where it was, a small square button appears at its right
   edge, and the list comes down underneath it. */

td.sel-cell.editing { padding: 0; }
.cs {
  display: flex; align-items: center; gap: 6px;
  height: 100%; width: 100%; padding: 0 0 0 8px;
  outline: 2px solid var(--accent); outline-offset: -2px;
  background: var(--surface); cursor: default;
}
.cs-value {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap;
}
/* The button Excel puts against the cell's edge: square, flat, attached --
   not a control floating inside the box with air around it. */
.cs-btn {
  flex: none; width: 19px; align-self: stretch;
  background: var(--accent); position: relative;
}
.cs-btn::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 0; height: 0; border: 4px solid transparent;
  border-top-color: var(--surface); transform: translateY(2px);
}

/* The list. Square corners, one hairline, no shadow theatre: it is a panel
   attached to a cell, not a card floating over a page. Anchored in script,
   so nothing here positions it. */
.cs-list {
  position: fixed; z-index: 120; margin: 0; padding: 2px 0; list-style: none;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: 2px;
  box-shadow: var(--e2); overflow-y: auto; overscroll-behavior: contain;
  font-size: 12px; max-width: min(460px, 92vw);
}
.cs-list li {
  padding: 4px 10px; cursor: pointer; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
/* The one under the cursor or the keyboard, in the app's own blue -- and the
   one the cell already holds, ticked, so "where am I" and "what is set" are
   two different questions with two different answers. */
.cs-list li.on { background: var(--accent); color: var(--on-ink); }
.cs-list li[aria-selected="true"]::before {
  content: "✓"; margin-right: 6px; margin-left: -6px; font-size: 10px;
}
.cs-list li:not([aria-selected="true"]) { padding-left: 20px; }
.cs-list li.off { color: var(--ink-3); cursor: default; }
.cs-list li.off.on { background: none; color: var(--ink-3); }

@media (prefers-reduced-motion: no-preference) {
  .cs-list { animation: cs-drop 110ms var(--ease) backwards; }
}
@keyframes cs-drop {
  from { opacity: 0; transform: translateY(-3px); }
}

/* The column that holds the rest of the screen, so the five that matter sit
   together at the left where they are read as one sentence. */
.grid.log th.log-rest, .grid.log td.log-rest {
  width: auto; background: none; border-left: 0;
}

/* What tells two rows of the same family apart -- `Tiles · kota · 03` beside
   `Tiles · wooden · 02`. Quiet, because the name is the answer and this is
   the detail that settles which one. */
.cs-list .cs-hint {
  color: var(--ink-3); margin-left: 8px; font-size: 11px;
}
.cs-list li.on .cs-hint { color: rgba(255,255,255,.8); }

/* Which material a room's finish actually is. Two rooms both say "Flooring"
   and the whole difference is the tile: `Tiles · kota` under the balcony,
   `Tiles · wooden` under the bedroom. Under the finish name, because it
   answers "which one" about the line above it. */
.mat-of { color: var(--ink-2); }
.mat-of .mono { color: var(--ink-3); }

/* The figures on a block heading, arranged rather than run together.
   =========================================================================
   Internal finishes puts three or four money figures on every heading -- what
   a unit standing on each height pays, and what the whole room or the whole
   type comes to. Strung along one line with middots between them they read as
   a sentence nobody can scan: "4 units · Office · Rs 1.46 L on 2.90 m ·
   Rs 1.67 L on 4.20 m · Rs 6.27 L for all 4". "There are too many values,
   arrange them in a better way."

   So: a label and a figure per line, the figures right-aligned in a column of
   their own and in tabular figures, so they line up under each other and the
   total reads as the total. Floated, so the heading's own words keep the left
   of the line. */
.blk-figs {
  display: grid; grid-template-columns: auto auto;
  gap: 2px 14px; align-items: baseline; font-size: 11.5px;
  /* Pushed to the end of its row rather than floated: both headings it sits on
     are flex rows, and a float inside one is not a float at all -- it became
     the first item on the line, which put the money where the name goes. */
  margin-left: auto; flex: none;
}
.blk-figs .k { color: var(--ink-3); text-align: right; white-space: nowrap; }
.blk-figs .v {
  color: var(--ink-2); text-align: right; white-space: nowrap;
  font-variant-numeric: tabular-nums; font-weight: 550;
}
/* The one figure the block is about. */
.blk-figs .tot { color: var(--ink); font-weight: 650; }
.blk-figs .tot.v { font-size: 12.5px; }
/* On a card's own title the ground is the workbook's navy, so the block reads
   in the title's ink instead of the greys it wears on paper: `--ink-3` measures
   2.3:1 on that navy and `--ink` 1.3:1, which is the total -- the one figure
   the block is about -- gone invisible on its own heading. The same block on a
   room heading sits on white and keeps the greys, which is why this is bound
   to the title and not to `.blk-figs`. */
.card > h2 .blk-figs .k { color: var(--wb-title-ink); opacity: .72; }
.card > h2 .blk-figs .v, .card > h2 .blk-figs .tot { color: var(--wb-title-ink); }

/* ===========================================================================
   The dashboard tiles, after Kokonut UI's spotlight-cards
   =========================================================================== */

/* Ported rather than installed: that component is React 19 + Tailwind 4 +
   motion/react, and its registry host is refused by this machine. The source
   was read and the parts that matter rebuilt here — the accent tint, the
   hover glow, the shimmer sweep, the icon badge and the growing underline.
   The tilt is in `spotlight.js`, because it needs the pointer. The dimming of
   the other cards was too, and is gone -- see `.tile.lit` below.

   Each tile carries its own `--accent`. Every colour below is derived from
   that one value with `color-mix`, so a tile is one line to add and there is
   no second place to keep in step. */

.tile-row.spotlit {
  position: relative;
  padding: 18px;
  border-radius: var(--radius-3);
  background: var(--surface);
  border: 1px solid var(--line);
  /* Kokonut's dot grid: 1px dots on a 22px pitch, barely there. It is what
     stops six pale cards on a pale page reading as a gap in the layout. */
  background-image: radial-gradient(circle, color-mix(in srgb, var(--ink) 5%, transparent) 1px,
                                    transparent 1px);
  background-size: 22px 22px;
}

.tile-row.spotlit .tile {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  border-radius: var(--radius-3);
  background: var(--surface);
  transform-style: preserve-3d;
  will-change: transform;
  /* The transform is written by script every frame, so it is deliberately
     not transitioned: two things easing one property fight each other. */
  transition: opacity var(--t-base) var(--ease), scale var(--t-base) var(--ease),
              border-color 300ms var(--ease), box-shadow var(--t-base) var(--ease);
}

/* Kokonut dims the siblings of the card being read -- 0.96 scale, 0.5 opacity.
   Taken out: "don't fade the rest which is not having the cursor." Moving the
   mouse across a dashboard put every other figure behind a veil, and a figure
   you are not pointing at is not less true. The tile under the cursor still
   lights, tilts, sweeps and underlines, so it is still the one being read. */
.tile-row.spotlit .tile.lit { border-color: color-mix(in srgb, var(--accent) 38%, var(--line)); }

/* Always-on tint, and the stronger one that comes up on hover. Both are the
   same gradient from the same corner; only the alpha differs — 8% resting,
   18% lit, which is Kokonut's 14 and 2e in hex. */
.tile-row.spotlit .tile::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(ellipse at 20% 20%,
    color-mix(in srgb, var(--accent) 8%, transparent), transparent 65%);
}
.tile-glow {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; opacity: 0;
  transition: opacity 260ms var(--ease);
  background: radial-gradient(ellipse at 20% 20%,
    color-mix(in srgb, var(--accent) 18%, transparent), transparent 65%);
}
.tile.lit .tile-glow { opacity: 1; }

/* The shimmer: a skewed band that crosses the card once. A glint, not a
   show -- a tile is pointed at tens of times a day, and at 700ms the sweep
   was still crossing after the eye had read the figure. */
.tile-sweep {
  position: absolute; inset-block: 0; left: 0; width: 55%; z-index: 0;
  pointer-events: none; transform: translateX(-100%) skewX(-12deg);
  transition: transform 300ms var(--ease);
  background: linear-gradient(to right, transparent,
    color-mix(in srgb, var(--ink) 4%, transparent), transparent);
}
.tile.lit .tile-sweep { transform: translateX(280%) skewX(-12deg); }

/* The underline, growing from nothing to the full width.

   Grown by `transform`, not `width`. Kokonut animates the width, and so did
   this -- and width is a layout property, so every frame of the 500ms was a
   layout pass, with nothing on the stack to say so. Measured: sweeping the
   mouse across the row restarted it on every crossing, 122 layouts in one
   pass. A scale runs on the compositor and lays nothing out. */
.tile-line {
  position: absolute; left: 0; bottom: 0; height: 2px; width: 100%; z-index: 1;
  pointer-events: none; border-radius: 999px;
  transform: scaleX(0); transform-origin: left center;
  transition: transform var(--t-enter) var(--ease);
  background: linear-gradient(to right,
    color-mix(in srgb, var(--accent) 60%, transparent), transparent);
}
.tile.lit .tile-line { transform: scaleX(1); }

/* The badge. 40px square in Kokonut; 30px here, because these tiles carry a
   figure at 21px and the icon is the label's companion, not the subject. */
.tile-icon {
  position: relative; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; margin-bottom: 9px;
  border-radius: 9px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 26%, transparent);
  color: var(--accent);
}
.tile-icon svg { width: 16px; height: 16px; }

/* Everything with words in it sits above the gradients. */
.tile-row.spotlit .tile .k,
.tile-row.spotlit .tile .v,
.tile-row.spotlit .tile .s { position: relative; z-index: 1; }

/* The lift on hover was the old tile's whole hover state; with a tilt and a
   glow doing the work it is one effect too many. */
.tile-row.spotlit .tile.clickable:hover { transform: none; box-shadow: var(--e2); }

@media (prefers-reduced-motion: reduce) {
  .tile-row.spotlit .tile,
  .tile-glow, .tile-sweep, .tile-line { transition: none; }
  .tile-sweep { display: none; }
}

/* ===========================================================================
   The sidebar's active mark
   =========================================================================== */

/* It was a pill behind the link, slid from one to the next on a spring
   (`nav-pill.js`, after Kokonut UI's smooth-tab). "Not a tab": the mark is
   the link itself now -- coloured, larger, ticked, in `#sidebar a` above --
   so there is nothing behind it to slide, nothing to measure, and nothing
   to follow when a group folds. Under reduced motion the size and the tick
   are simply there; the colour still fades. */
@media (prefers-reduced-motion: reduce) {
  #sidebar a { transition: color var(--t-fast) linear; }
  #sidebar a::before { transition: none; }
}

/* ===========================================================================
   Sign in, after a sign-in-card-2
   =========================================================================== */

/* The page is the whole viewport. `no-project` already takes the sidebar and
   the figures; `signed-out` takes the bar as well, because a white bar over
   this ground is not a header. The ground is set on `#app` too, so there is
   no flash of the light shell before the screen draws. */
#app.signed-out { grid-template-rows: 1fr; grid-template-areas: "main"; background: #01030e; }
#app.signed-out #topbar { display: none; }
#app.signed-out #main { padding: 0; max-width: none; margin: 0; }
/* The router draws every screen into a `.screen` of its own. The sign-in
   stage fills its parent with `min-height: 100%`, which a container of
   automatic height gives it nothing to fill: the card sat in a band at the
   top with the shell showing under it. Signed out, that container is the
   whole of `#main`. */
#app.signed-out #main > .screen { height: 100%; }
/* Before `login.js` has drawn, `#main` holds the screen placeholder -- and
   its skeleton is a 220px slab of #edf0f4, which on this ground is a white
   box sitting on black. There is one card coming and it arrives at once, so
   the wait is announced in words the colour of the page, not in a shape. */
#app.signed-out .loading { color: rgba(255, 255, 255, .5); padding: 40px; text-align: center; }
#app.signed-out .loading::after { display: none; }

.signin-stage {
  position: relative; min-height: 100%; overflow: hidden; box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  padding: 32px 16px; background: #01030e; color: #fff; font-family: var(--sans);
}

/* --- the ground ------------------------------------------------------------
   DBOT's own blue, sampled off `dbot-logo.png` rather than guessed: #001860
   is 67% of the mark and #006cc0 its bright accent. Near-black at the top,
   through that navy, with the blue glowing up from the bottom edge.

   Gradients, not blurred boxes. The card this is ported from blurs a 120vh
   box by 80px and animates it; a filter on a moving layer is re-run every
   frame it moves, which is the kind of thing "laggy" was about (Y1). A
   radial gradient is the same shape painted once, and its opacity and scale
   are the compositor's to animate. */
.signin-ground { position: absolute; inset: 0; pointer-events: none; }
.signin-ground::before {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to bottom, #01030e 0%, #030a33 38%, #001860 72%, #0a1f7a 100%);
}
.signin-ground > span { position: absolute; display: block; }
.sg-noise {
  inset: 0; opacity: .035; mix-blend-mode: soft-light; background-size: 200px 200px;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* The light at the top: the logo's bright blue, wide and faint. */
.sg-top {
  top: -34vh; left: 50%; width: 130vh; height: 96vh; margin-left: -65vh;
  background: radial-gradient(ellipse at center, rgba(0, 108, 192, .26), transparent 62%);
}
.sg-breathe {
  top: -34vh; left: 50%; width: 108vh; height: 96vh; margin-left: -54vh;
  background: radial-gradient(ellipse at center, rgba(56, 132, 235, .24), transparent 60%);
  animation: sg-breathe 8s ease-in-out infinite alternate;
}
/* The bottom, in two: the broad navy swell, and the vivid core rising
   through it from below the edge. */
.sg-bottom {
  bottom: -52vh; left: 50%; width: 130vh; height: 104vh; margin-left: -65vh;
  background: radial-gradient(circle at center, rgba(28, 58, 214, .45), transparent 62%);
  animation: sg-swell 6s ease-in-out 1s infinite alternate;
}
.sg-rise {
  bottom: -30vh; left: 50%; width: 78vh; height: 62vh; margin-left: -39vh;
  background: radial-gradient(circle at center, rgba(62, 78, 240, .62), rgba(44, 58, 224, .22) 42%, transparent 66%);
  animation: sg-swell 7s ease-in-out .5s infinite alternate;
}
.sg-spot {
  width: 384px; height: 384px; border-radius: 50%; opacity: .4;
  background: radial-gradient(circle, rgba(255, 255, 255, .06), transparent 65%);
  animation: sg-pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;
}
.sg-spot-a { left: 24%; top: 22%; margin: -192px 0 0 -192px; }
.sg-spot-b { right: 24%; bottom: 20%; margin: 0 -192px -192px 0; animation-delay: 1s; }
@keyframes sg-breathe { from { opacity: .5; transform: scale(.98); } to { opacity: 1; transform: scale(1.02); } }
@keyframes sg-swell   { from { opacity: .6; transform: scale(1); }   to { opacity: 1; transform: scale(1.08); } }
@keyframes sg-pulse   { 0%, 100% { opacity: .4; } 50% { opacity: .2; } }

/* --- the card --------------------------------------------------------------
   Two panels: the brand stands on its own to the left, the form to the
   right, and neither of them moves.

   1040px across and a little over 400 down. This is the one screen with
   nothing else on it -- no sidebar, no figures, no bar -- so the card is
   the page rather than an object sitting on it, and it is sized to be read
   from across a desk. */
/* No perspective and no entrance: the card is put where it goes and stays
   there. It used to tilt up to 10 degrees under the pointer and float up
   20px on arrival, and a sign-in box that leans when you reach for the
   email field reads as a fault rather than an effect. */
.signin-3d { position: relative; z-index: 1; width: 100%; max-width: 1040px; }
.signin-tilt { position: relative; }

/* The halo. The original animates a box-shadow between two sizes, which
   repaints the card every frame; the shadow is painted once here and its
   opacity breathes. Fuller on hover. */
.signin-halo {
  position: absolute; inset: -1px; border-radius: 24px; pointer-events: none;
  box-shadow: 0 0 18px 6px rgba(120, 160, 255, .07);
  animation: halo 4s ease-in-out infinite alternate; transition: opacity 700ms var(--ease);
}
.signin-tilt:hover .signin-halo { opacity: .7; animation: none; }
@keyframes halo { from { opacity: .2; } to { opacity: .45; } }

/* Four beams, each crossing one edge in 2.5s and resting a second before it
   goes again, 0.6s apart; four corner points that pulse. The beams move by
   transform, not by `left` and `top`, and their blur is fixed at 1.5px
   rather than animated. */
.signin-beams { position: absolute; inset: -1px; border-radius: 24px; overflow: hidden; pointer-events: none; }
.beam {
  position: absolute; display: block; filter: blur(1.5px);
  animation: beam-x 3.5s ease-in-out infinite, beam-fade 1.2s ease-in-out infinite alternate;
}
.beam-top    { top: 0; left: 0; height: 3px; width: 50%;
               background: linear-gradient(to right, transparent, #fff, transparent); }
.beam-right  { top: 0; right: 0; width: 3px; height: 50%; animation-name: beam-y, beam-fade;
               animation-delay: .6s; background: linear-gradient(to bottom, transparent, #fff, transparent); }
.beam-bottom { bottom: 0; right: 0; height: 3px; width: 50%; animation-name: beam-x-back, beam-fade;
               animation-delay: 1.2s; background: linear-gradient(to right, transparent, #fff, transparent); }
.beam-left   { bottom: 0; left: 0; width: 3px; height: 50%; animation-name: beam-y-back, beam-fade;
               animation-delay: 1.8s; background: linear-gradient(to bottom, transparent, #fff, transparent); }
/* 2.5s of travel, then a rest: 71.4% of a 3.5s cycle. */
@keyframes beam-x      { 0% { transform: translateX(-100%); } 71.4%, 100% { transform: translateX(300%); } }
@keyframes beam-x-back { 0% { transform: translateX(100%); }  71.4%, 100% { transform: translateX(-300%); } }
@keyframes beam-y      { 0% { transform: translateY(-100%); } 71.4%, 100% { transform: translateY(300%); } }
@keyframes beam-y-back { 0% { transform: translateY(100%); }  71.4%, 100% { transform: translateY(-300%); } }
@keyframes beam-fade   { from { opacity: .3; } to { opacity: .7; } }
.corner {
  position: absolute; display: block; border-radius: 50%; background: rgba(255, 255, 255, .5);
  filter: blur(1px); animation: corner 2s ease-in-out infinite alternate;
}
.corner-tl { top: 0; left: 0; width: 5px; height: 5px; }
.corner-tr { top: 0; right: 0; width: 8px; height: 8px; animation-duration: 2.4s; animation-delay: .5s; }
.corner-br { bottom: 0; right: 0; width: 8px; height: 8px; animation-duration: 2.2s; animation-delay: 1s; }
.corner-bl { bottom: 0; left: 0; width: 5px; height: 5px; animation-duration: 2.3s; animation-delay: 1.5s; }
@keyframes corner { from { opacity: .2; } to { opacity: .4; } }

/* The edge that lights on hover. */
.signin-edge {
  position: absolute; inset: -.5px; border-radius: 24px; pointer-events: none; opacity: 0;
  background: linear-gradient(to right, rgba(255, 255, 255, .03), rgba(255, 255, 255, .07), rgba(255, 255, 255, .03));
  transition: opacity 500ms var(--ease);
}
.signin-tilt:hover .signin-edge { opacity: .7; }

/* The glass: near-black at 42% over a 24px backdrop blur, a hairline, and
   the original's faint diagonal pattern inside. */
.signin-card {
  position: relative; border-radius: 24px; overflow: hidden;
  display: grid; grid-template-columns: 1fr 1.12fr; align-items: stretch;
  background: rgba(2, 5, 22, .42); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
  border: 1px solid rgba(255, 255, 255, .07);
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, .45);
}
.signin-card::before {
  content: ""; position: absolute; inset: 0; opacity: .03; pointer-events: none;
  background-image: linear-gradient(135deg, #fff .5px, transparent .5px),
                    linear-gradient(45deg, #fff .5px, transparent .5px);
  background-size: 30px 30px;
}

/* --- the brand panel -------------------------------------------------------
   The mark at a size you can read it at, and what the letters stand for
   under it. On its own ground, a shade deeper than the form's, with a
   hairline between the two. */
.signin-brand {
  position: relative; display: flex; flex-direction: column; gap: 22px;
  padding: 46px 42px; box-sizing: border-box;
  background: linear-gradient(160deg, rgba(0, 24, 96, .5), rgba(2, 5, 22, .2));
  border-right: 1px solid rgba(255, 255, 255, .07);
}
/* The mark is dark navy ink, so it would disappear into its own ground: it
   gets a white plate, which is what it sits on everywhere else.

   The plate spans the panel rather than shrinking to the wordmark. This is
   the only place in the platform where the mark is the subject instead of a
   label in the corner of a bar, so it is given the room -- it was a 34px
   logo on an 81px plate in a 414px column, which left the top third of the
   panel empty. */
.signin-logo {
  align-self: stretch;
  display: flex; align-items: center; justify-content: center;
  padding: 26px 24px; border-radius: 18px;
  background: rgba(255, 255, 255, .96); border: 1px solid rgba(255, 255, 255, .1);
  box-shadow: 0 14px 38px -14px rgba(0, 24, 96, .9);
}
.signin-logo img { height: 66px; width: auto; max-width: 100%; display: block; }
.signin-logo .wordmark { font-weight: 700; color: var(--accent); font-size: 52px; line-height: 66px; }

/* The tail of the `g` in "Together" was sliced flat, and it was never being
   drawn rather than being cut off: this text is not painted by `color` but by
   a gradient clipped to the glyphs, and a `background-clip: text` gradient
   exists only inside the element's own box. At 34px on a line-height of 1.14
   the line box was 38.8px against a font content area of about 40.8px, so the
   half-leading ran about a pixel negative at each end -- and on the last line
   that missing pixel is exactly where a descender lives.

   So the box is given the descender's room and takes it back in margin: the
   paint area grows, the layout does not move. In `em` so the 25px mobile size
   below scales with it. */
.brand-tag {
  margin: auto 0 -.18em; padding-bottom: .18em;
  font-size: 34px; font-weight: 700; line-height: 1.2;
  letter-spacing: -.022em;
  color: #fff;
}
.brand-foot {
  margin: 0; font-size: 11.5px; letter-spacing: .06em; text-transform: uppercase;
  color: rgba(255, 255, 255, .38); font-weight: 550;
}

/* --- the form panel ------------------------------------------------------ */
.signin-form { position: relative; padding: 46px 44px; box-sizing: border-box; }
.signin-head { margin-bottom: 26px; }
/* Clipped to its glyphs the same way, so it carries the same descender room.
   Nothing it says today ends in one -- "Welcome back", "Set up the first
   account" -- which is the only reason it has not shown the fault the brand
   line did. */
.signin-head h1 {
  margin: 0 0 2px; padding-bottom: .16em;
  font-size: 27px; font-weight: 700; letter-spacing: -.018em;
  color: #fff;
}
.signin-head p {
  margin: 0; font-size: 13px; line-height: 1.55; color: rgba(255, 255, 255, .6);
}

/* Fields. An icon at the left that turns white when the field has focus;
   the box lifts 1% under the pointer and 2% under focus. */
/* The field is marked by its border and its ground, not by growing: the
   whole card is static now, and a box that swells under the cursor is the
   same complaint at a smaller size. */
.sf { position: relative; margin-bottom: 15px; }
.sf:focus-within { z-index: 1; }
.sf-icon {
  position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px; color: rgba(255, 255, 255, .4); pointer-events: none;
  transition: color 300ms var(--ease);
}
.sf-icon svg, .sf-eye svg { width: 100%; height: 100%; display: block; }
.sf:focus-within .sf-icon { color: #fff; }
.sf-input {
  display: block; width: 100%; height: 52px; box-sizing: border-box;
  padding: 0 16px 0 50px; border-radius: 11px;
  border: 1px solid transparent; background: rgba(255, 255, 255, .06);
  color: #fff; font: inherit; font-size: 14.5px; outline: none;
  transition: background 300ms var(--ease), border-color 300ms var(--ease);
}
.sf-input::placeholder { color: rgba(255, 255, 255, .3); }
.sf-input:focus { background: rgba(255, 255, 255, .1); border-color: rgba(255, 255, 255, .22); }
.sf-input.has-eye { padding-right: 50px; }
/* The browser's autofill paints a pale yellow box; on this ground, not. */
.sf-input:-webkit-autofill { -webkit-text-fill-color: #fff; box-shadow: 0 0 0 1000px #061033 inset; }
.sf-eye {
  position: absolute; right: 16px; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px; padding: 0; border: 0; background: none;
  color: rgba(255, 255, 255, .4); cursor: pointer; transition: color 300ms var(--ease);
}
.sf-eye:hover, .sf-eye:focus-visible { color: #fff; outline: none; }
.signin-note { margin: -7px 0 15px 2px; font-size: 12px; color: rgba(255, 255, 255, .45); }
.signin-stage .signin-error {
  margin: 0 0 15px; padding: 10px 14px; border-radius: 11px; font-size: 13px;
  background: rgba(239, 68, 68, .12); color: #fca5a5; border: 1px solid rgba(239, 68, 68, .25);
}

/* The button: white on the dark ground, a glow behind it on hover, the arrow
   nudging right; while it waits, the words give way to a ring and a band of
   light crosses it. */
.signin-go {
  position: relative; display: block; width: 100%; margin-top: 26px; padding: 0;
  border: 0; background: none; cursor: pointer; font: inherit;
}
.signin-go:disabled { cursor: progress; }
.signin-go:focus-visible { outline: none; }
.signin-go:focus-visible .go-face { box-shadow: 0 0 0 2px #01030e, 0 0 0 4px #fff; }
.go-glow {
  position: absolute; inset: 0; border-radius: 11px; background: rgba(160, 190, 255, .18);
  filter: blur(16px); opacity: 0; transition: opacity 300ms var(--ease);
}
.signin-go:hover:not(:disabled) .go-glow { opacity: .8; }
.go-face {
  position: relative; display: flex; align-items: center; justify-content: center;
  height: 52px; border-radius: 11px; overflow: hidden;
  background: #fff; color: #001860; font-size: 14.5px; font-weight: 600;
  letter-spacing: -.01em;
}
.go-text { display: inline-flex; align-items: center; gap: 5px; transition: opacity 200ms var(--ease); }
.go-text svg { width: 14px; height: 14px; transition: transform 300ms var(--ease); }
.signin-go:hover:not(:disabled) .go-text svg { transform: translateX(4px); }
.go-shimmer {
  position: absolute; inset: 0; opacity: 0; transition: opacity 300ms ease;
  background: linear-gradient(to right, transparent, rgba(0, 24, 96, .18), transparent);
}
.go-spin {
  position: absolute; width: 19px; height: 19px; border-radius: 50%; opacity: 0;
  border: 2px solid rgba(0, 24, 96, .75); border-top-color: transparent;
  transition: opacity 200ms var(--ease);
}
/* Keyed off `aria-busy`, never a class: `loading` is this stylesheet's own
   name for a screen placeholder a few hundred lines up, and a button wearing
   it grew that placeholder's 220px skeleton block. */
.signin-go[aria-busy="true"] .go-text { opacity: 0; }
.signin-go[aria-busy="true"] .go-spin { opacity: 1; animation: go-spin 1s linear infinite; }
.signin-go[aria-busy="true"] .go-shimmer { opacity: 1; animation: go-shimmer 2.5s ease-in-out infinite; }
/* Both named for this button. `shimmer` was already taken by the screen
   placeholder above, and a second `@keyframes shimmer` does not coexist with
   the first -- the later one replaces it everywhere, so the placeholder's
   sweep would have become this button's. */
@keyframes go-spin { to { transform: rotate(360deg); } }
/* 1.5s across, then a second's rest. */
@keyframes go-shimmer { 0% { transform: translateX(-100%); } 60%, 100% { transform: translateX(100%); } }

.signin-foot { margin: 20px 0 0; font-size: 13px; color: rgba(255, 255, 255, .6); }
.signin-foot strong { color: #fff; font-weight: 550; }

/* Two panels need two panels' worth of room. Under that they stack, brand
   first, with the tagline down to a line that still fits one. */
@media (max-width: 860px) {
  .signin-card { grid-template-columns: 1fr; }
  .signin-brand {
    border-right: 0; border-bottom: 1px solid rgba(255, 255, 255, .07);
    gap: 16px; padding: 30px 28px;
  }
  /* The plate stops spanning: stretched across a full-width panel it reads
     as a banner rather than a mark. */
  .signin-logo { align-self: flex-start; padding: 16px 22px; }
  .signin-logo img { height: 42px; }
  .signin-logo .wordmark { font-size: 33px; line-height: 42px; }
  /* The break stays. Hiding it ran the two words together --
     "DeliveringBetter Outcomes" -- because a <br> carries no space with it. */
  .brand-tag { margin-top: 0; font-size: 25px; }
  .brand-foot { display: none; }
  .signin-form { padding: 30px 28px; }
}

/* Still ground, no beams, no tilt, no lift. The ring that says "working"
   still turns: it is the one motion here that carries information. */
@media (prefers-reduced-motion: reduce) {
  .signin-stage *:not(.go-spin), .signin-stage *::before { animation: none !important; transition: none !important; }
  .beam { display: none; }
}

/* ===========================================================================
   The profile dropdown, after Kokonut UI's profile-dropdown
   =========================================================================== */

/* The bar sits above the derivation panel now. They never overlap -- the
   panel starts where the bar ends -- but the menu that drops out of the bar
   lands on the panel's ground, and it has to be the thing on top. */
#topbar { z-index: 35; }

.profile { position: relative; display: flex; align-items: center; }

/* The trigger: name over email, avatar in its ring. Kokonut's is a 16px
   radius on a 12px pad round a 40px avatar; the bar here is 56px tall, so
   it is the same shape at a size that fits in it. */
.profile-btn {
  display: flex; align-items: center; gap: 10px;
  padding: 4px 5px 4px 10px; border-radius: 12px;
  border: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  background: var(--surface); color: inherit; font: inherit; text-align: left;
  cursor: pointer;
  transition: border-color var(--t-base) var(--ease),
              background var(--t-base) var(--ease),
              box-shadow var(--t-base) var(--ease);
}
.profile-btn:hover {
  border-color: var(--line-strong);
  background: color-mix(in srgb, var(--surface-2) 80%, transparent);
  box-shadow: var(--e1);
}
.profile-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--ink);
}
.profile-text { display: flex; flex-direction: column; line-height: 1.2; letter-spacing: -.01em; }
.profile-name { font-size: 12.5px; font-weight: 550; color: var(--ink); white-space: nowrap; }
/* The name only, in the bar. The address is in the menu, where there is room
   for it to be read rather than squinted at. */
.profile-head {
  display: flex; flex-direction: column; gap: 1px;
  padding: 6px 10px 10px; margin-bottom: 4px;
  border-bottom: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
}
.profile-head-name { font-size: 13px; font-weight: 600; color: var(--ink); }
.profile-head-mail {
  font-size: 11.5px; color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Kokonut's ring: purple through pink to orange, 2px, round a white disc. */
.profile-ring {
  flex: none; width: 28px; height: 28px; border-radius: 50%; padding: 2px;
  box-sizing: border-box;
  background: linear-gradient(135deg, #a855f7, #ec4899, #fb923c);
}
.profile-avatar {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; border-radius: 50%; background: var(--surface);
  font-size: 10.5px; font-weight: 650; letter-spacing: .02em; color: var(--ink);
}

/* The bending line to the right: dim, brighter on hover, accent-coloured
   and a tenth larger while the menu is open. */
.profile-bend {
  flex: none; margin-left: -1px; color: var(--ink-3); opacity: .6;
  transition: opacity var(--t-base) var(--ease), color var(--t-base) var(--ease),
              transform var(--t-base) var(--ease);
}
.profile:hover .profile-bend { opacity: 1; color: var(--ink-2); }
.profile.is-open .profile-bend { opacity: 1; color: var(--accent); transform: scale(1.1); }

/* The menu. Zooms in from 95% and slides down 8px from its top-right
   corner, and leaves the same way, faster -- both by transform and opacity,
   so the compositor runs it. */
.profile-menu {
  position: absolute; right: 0; top: calc(100% + 4px); width: 256px; z-index: 5;
  padding: 8px; border-radius: 16px; box-sizing: border-box;
  border: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  background: color-mix(in srgb, var(--surface) 95%, transparent);
  backdrop-filter: blur(4px);
  box-shadow: var(--e4);
  transform-origin: top right;
  opacity: 0; transform: scale(.95) translateY(-8px);
  transition: opacity var(--t-exit) var(--ease-in), transform var(--t-exit) var(--ease-in);
}
.profile-menu.open {
  opacity: 1; transform: none;
  transition: opacity var(--t-base) var(--ease), transform var(--t-base) var(--ease);
}
.profile-menu[hidden] { display: none; }

.profile-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 10px 12px; border-radius: 12px; border: 1px solid transparent;
  background: none; color: var(--ink); font: inherit; font-size: 12.5px;
  font-weight: 550; letter-spacing: -.01em; text-align: left;
  text-decoration: none; cursor: pointer; box-sizing: border-box;
  transition: background var(--t-base) var(--ease), border-color var(--t-base) var(--ease),
              box-shadow var(--t-base) var(--ease);
}
.profile-item + .profile-item { margin-top: 4px; }
.profile-item svg { width: 16px; height: 16px; flex: none; color: var(--ink-2); }
.profile-item:hover, .profile-item:focus-visible {
  outline: none;
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  border-color: color-mix(in srgb, var(--line) 50%, transparent);
  box-shadow: var(--e1);
}
/* A row that reports rather than acts: no hover, no hand. */
.profile-item.static, .profile-item.static:hover {
  cursor: default; background: none; border-color: transparent; box-shadow: none;
}
.profile-badge {
  margin-left: auto; padding: 3px 8px; border-radius: 6px;
  font-size: 11px; font-weight: 550; letter-spacing: -.01em;
  background: var(--accent-soft); color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 10%, transparent);
}
.profile-sep {
  height: 1px; margin: 12px 0;
  background: linear-gradient(to right, transparent, var(--line), transparent);
}
/* Signing out ends the session, so it is not dressed as a peer of the rest:
   the sample's red-tinted row, in this platform's own red. */
.profile-item.out { background: var(--bad-soft); color: var(--bad); }
.profile-item.out svg { color: var(--bad); }
.profile-item.out:hover, .profile-item.out:focus-visible {
  background: color-mix(in srgb, var(--bad) 18%, transparent);
  border-color: color-mix(in srgb, var(--bad) 30%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .profile-menu, .profile-menu.open, .profile-bend, .profile-btn, .profile-item { transition: none; }
}

/* ===========================================================================
   A folding note -- the prose that explains a table
   ===========================================================================

   Quiet by default and quieter still when shut: a card's heading should be
   followed by its table, not by six lines a reader has read fifty times.
   Built on `<details>`, like the grid's column picker and a project's row
   menu, so the click, the keyboard and the open state are the browser's. */
.fold-note {
  border-top: 1px solid var(--line);
  background: var(--surface);
}
/* A note directly under the card's heading wants no line above it -- the
   heading is already one -- and a line below, parting it from the figures. */
.card > h2 + .fold-note { border-top: 0; border-bottom: 1px solid var(--line); }
.fold-note > summary {
  display: flex; align-items: center; gap: 7px;
  padding: 7px 14px; cursor: pointer; list-style: none;
  font-size: 11.5px; font-weight: 550; color: var(--ink-3);
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
/* Safari draws its own triangle from a pseudo-element rather than from
   `list-style`, and two disclosure marks is one too many. */
.fold-note > summary::-webkit-details-marker { display: none; }
.fold-note > summary:hover { background: var(--row-hover); color: var(--ink-2); }
.fold-note > summary:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px; color: var(--ink-2);
}
.fold-note[open] > summary { color: var(--ink-2); }
.fold-chev {
  width: 13px; height: 13px; flex: none;
  transform: rotate(0deg);
  transition: transform var(--t-base) var(--ease-spring);
}
.fold-note[open] .fold-chev { transform: rotate(90deg); }
.fold-note-body {
  padding: 2px 14px 12px 34px; color: var(--ink-2);
  font-size: 12px; line-height: 1.55; max-width: 92ch;
}
.fold-note-body p { margin: 0 0 8px; }
.fold-note-body p:last-child { margin-bottom: 0; }
/* The body arrives rather than appearing. `<details>` transitions nothing of
   its own, so this is on the content: it is in the layout the moment it
   opens, and only its ink and its position move. `backwards`, never `both`:
   a fill that outlives the animation keeps the final keyframe's transform
   applied, and any transform -- even the identity -- makes this element the
   containing block for a `position: fixed` menu opened from inside it. */
/* On `.opening`, which the toggle puts on for one animation, and not on
   `[open]`: an open note is rendered open on every redraw of its screen, and
   bound to the attribute it faded in again on every committed cell. */
@media (prefers-reduced-motion: no-preference) {
  .fold-note.opening .fold-note-body {
    animation: fold-note-in var(--t-base) var(--ease) backwards;
  }
}
@keyframes fold-note-in {
  from { opacity: 0; transform: translateY(-3px); }
  to   { opacity: 1; transform: none; }
}

/* ===========================================================================
   Dark
   =========================================================================== */

/* Every token the light palette defines, answered. Nothing here is a filter
   or an inversion: a QS screen is read as columns of figures against a
   surface, and `invert()` turns a considered set of greys into a set nobody
   chose -- it would also flip the company's own navy to orange.

   One selector, not two. The obvious shape is a `prefers-color-scheme` block
   *and* an attribute block, which means this palette written out twice and
   drifting the first time somebody edits one of them. Instead the system
   preference is resolved to the attribute before the first paint, by the
   script in `index.html`, so there is exactly one place a dark value lives
   and `[data-theme]` is always set to one thing or the other.

   `color-scheme` is what makes the browser's own furniture follow: the
   scrollbars it draws itself, the caret, `<select>` popups, the autofill
   panel, and the space past the end of the page when it over-scrolls.
   Without it those stay light and the window looks half-converted. */

:root[data-theme="dark"] {
  color-scheme: dark;

  /* --- ground and ink ----------------------------------------------------
     Not black. #0d1117 reads as a hole cut in the screen; these are the
     light palette's own cool greys turned over, so a card still sits *on*
     the page rather than dissolving into it. */
  --bg: #12161d;
  --surface: #1a1f28;
  --surface-2: #1f242e;
  --line: #2a313d;
  --line-strong: #3a4250;
  --ink: #e6eaf0;
  --ink-2: #a7b1bf;
  /* #8d97a5 came to 4.35:1 on a hovered computed cell, the one place the
     quietest text meets the second-lightest surface; this clears 4.5 there
     and everywhere lighter. */
  --ink-3: #949eac;

  /* The accent has to carry text on this ground, and #3050c8 does not --
     it comes to about 2:1 against #12161d. Lightened until it clears 4.5,
     with the soft tint rebuilt as a deep wash rather than the pale blue it
     is in light, which would glow here. */
  --accent: #7f9cff;
  --accent-soft: #1e2740;

  /* And this is where the bar fill has to part company with the accent.
     #7f9cff was chosen above to clear 4.5:1 as text on #12161d, which puts
     it at OKLCH lightness 0.714 -- outside the 0.48-0.67 band a chart fill
     must sit in against this surface, so a bar painted in it glares and
     stops reading as a quantity. #3987e5 is inside the band, over the
     chroma floor, and over 3:1 against #1a1f28. Both fills are validated:
     node scripts/validate_palette.js "#3987e5" --mode dark --surface "#1a1f28"
     Change either one and run that again -- do not reason about it. */
  --chart-bar: #3987e5;
  --chart-track: #232935;
  /* The diverging pair, regraded for this ground:
     node scripts/validate_palette.js "#d4703a,#3987e5" --mode dark --surface "#1a1f28"
     passes all five, and both come to better than 4.5:1 as text on #1a1f28
     (4.86 and 4.54), so the signed figures wear them too. */
  --chart-up: #d4703a;
  --chart-down: #3987e5;

  /* The sidebar legend is the reason these two must not simply swap: white
     is yours, grey is the system's. --surface stays the lighter of the two
     here exactly as it is in light, so an editable cell still reads as the
     brighter one. */
  --derived-bg: #232935;
  --derived-ink: #aeb8c6;

  --ok: #58c896;
  --ok-soft: #14291f;
  --ok-line: #2c5c45;
  --warn: #e2a648;
  --warn-soft: #2a2115;
  --warn-line: #5c4a25;
  --bad: #f27272;
  --bad-soft: #2d1a1c;
  --bad-line: #653436;
  --notice-ink: #e2a648;

  /* A shadow on a dark ground is black, not grey. The light palette's
     rgba(20,30,50,.06) is invisible here, and a card with no lift on a
     near-flat ground loses the one thing that says it is a card. */
  --shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 4px 16px rgba(0, 0, 0, .4);
  --e1: 0 1px 2px rgba(0, 0, 0, .4);
  --e2: 0 1px 3px rgba(0, 0, 0, .5), 0 1px 2px -1px rgba(0, 0, 0, .4);
  --e3: 0 6px 16px -4px rgba(0, 0, 0, .55), 0 2px 6px -2px rgba(0, 0, 0, .4);
  --e4: 0 18px 40px -12px rgba(0, 0, 0, .7), 0 6px 12px -6px rgba(0, 0, 0, .5);

  /* --- the named surfaces ----------------------------------------------- */
  --on-ink: #12161d;              /* --ink is light here, so its text is dark */
  --ink-hover: #cfd6e0;
  --row-hover: #232935;
  --row-hover-derived: #2b3240;
  --derived-hover: #2b3240;
  --total-bg: #232a36;
  /* The workbook's four roles, at dark-mode weight. Same hues, because the
     colour is the message -- a QS looking for the amber row has to find it
     here too -- and the pale fills would be four glares on a dark page, so
     each is the same hue taken deep with its own light ink over it. */
  --wb-title: #0b1d3f;
  --wb-title-ink: #dbe6f8;
  --wb-title-line: #1e4586;
  --wb-header: #1c2d41;
  --wb-header-ink: #c3d7ed;
  --wb-subtotal: #1f3222;
  --wb-subtotal-ink: #bcdcad;
  --wb-total: #3c3117;
  --wb-total-ink: #ecd190;
  --flash-warn: #4a3d12;
  --flash-ok: #17362a;
  --skeleton-a: #212734;
  --skeleton-b: #2a313f;
  /* The tooltip is the one thing that inverts, and for the same reason it
     was dark in the first place: it has to read as sitting above the page. */
  --tip-bg: #eef1f5;
  --tip-ink: #171c24;
  --scroll-thumb: #3a4250;
  --scroll-thumb-hover: #4c5665;
  --select-bg: #2d3f70;
}

/* Two places the light palette is spelled out rather than tokenised, and
   both need a word here. The wordmark is the company's navy, which is
   unreadable on this ground -- the mark itself is a PNG with its own colours
   and stays as it is, so only the type fallback moves. And a hover on the
   sidebar was a literal grey before this change; it is `--row-hover` now and
   follows on its own. */
:root[data-theme="dark"] .wordmark { color: #8ea6ff; }
/* The primary button is `--ink` on a light page and therefore light here.
   That is correct -- it stays the most emphatic thing on the screen -- but
   its hover must go lighter, not darker. */
:root[data-theme="dark"] button.btn.primary:hover { background: var(--ink-hover); }

/* The control itself, and the wipe it starts.
   ---------------------------------------------------------------------------
   A quiet icon button at the right-hand end, with the rest of what you can
   do. It carries the bar's one auto margin -- see `#whoami` above -- so the
   group it begins is pushed to the edge and the profile follows it there. */
#themeToggle { display: flex; align-items: center; margin-left: auto; }
.theme-btn {
  display: grid; place-items: center; width: 30px; height: 30px; padding: 0;
  border: 1px solid transparent; border-radius: 8px;
  background: none; color: var(--ink-3); cursor: pointer;
  transition: background var(--t-base) var(--ease), color var(--t-base) var(--ease),
              border-color var(--t-base) var(--ease);
}
.theme-btn svg { width: 16px; height: 16px; display: block; }
.theme-btn:hover { background: var(--surface-2); color: var(--ink); border-color: var(--line); }
.theme-btn:focus-visible {
  outline: none; box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--ink);
}

/* The wipe. Without these the browser cross-fades the two snapshots as well,
   which muddies the circle into a grey blur halfway through; what is wanted
   is the new page clipped over the old one, hard-edged. */
/* Scoped to the toggle's own transition, and this scoping is load-bearing.
   Unqualified, these rules take the animation off *every* view transition on
   the page -- which was fine while the toggle was the only one, and became
   wrong the moment navigation started crossfading (M2). The theme wipe drives
   its own clip through element.animate(), so it needs the default animation
   out of the way; a screen change needs exactly that default. The class goes
   on the root for the length of the wipe and comes off after. */
:root.vt-wipe::view-transition-old(root),
:root.vt-wipe::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
:root.vt-wipe::view-transition-old(root) { z-index: 1; }
:root.vt-wipe::view-transition-new(root) { z-index: 2; }

@media (prefers-reduced-motion: reduce) {
  .theme-btn { transition: none; }
}

/* The mark is dark navy ink on transparent, so on a dark bar it all but
   disappears -- and it is a PNG, so the colours cannot be swapped. It gets
   the same answer the sign-in card already gives it: a light plate, which
   keeps the company's own colours rather than flattening the mark to a white
   silhouette with `invert()`. */
:root[data-theme="dark"] .brand .logo {
  background: rgba(255, 255, 255, .93);
  padding: 3px 7px; border-radius: 7px; box-sizing: content-box;
}

/* A row arrives, a row leaves.
   ---------------------------------------------------------------------------
   Adding a row, deleting one and taking a delete back all used to redraw the
   table with no sign of which row had changed -- on a grid of 164 rates the
   eye has nothing to follow. These say which one, and only which one: the
   rows below do not move to make room, because animating forty rows to
   announce one is both the expensive way (Y1) and the wrong thing to look
   at. Transform and opacity only. */
@keyframes row-came {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
.grid tbody tr.row-came > td { animation: row-came var(--t-enter) var(--ease) backwards; }
.grid tbody tr.row-going > td {
  opacity: 0; transform: translateY(-4px);
  transition: opacity var(--t-exit) var(--ease-in),
              transform var(--t-exit) var(--ease-in);
}

@media (prefers-reduced-motion: reduce) {
  /* The row still goes and still comes; it simply does not travel. */
  .grid tbody tr.row-came > td { animation: none; }
  .grid tbody tr.row-going > td { transition: none; opacity: 1; transform: none; }
}

/* The row a finding sent you to.
   ---------------------------------------------------------------------------
   Long enough to find in a table of 163, gone by the time it has been read.
   A background fade is a paint rather than a layout, which is affordable
   once, on one row -- and the alternative, a transform, would move the row
   out of line with the rest of the table, which on a grid is worse than the
   paint. */
@keyframes landed {
  from { background-color: var(--accent-soft); }
  to   { background-color: transparent; }
}
.grid tbody tr.landed > td { animation: landed 1.2s var(--ease); }

/* Except the first column, which is sticky. A fade that ends at
   `transparent` is a hole in it: the columns scrolled underneath show
   straight through, and "Architrave" reads as "Architravet used yet". A
   row background cannot fix that -- the scrolled-under cell's text paints
   above the row and below the sticky cell -- so the fade ends where that
   cell actually rests, which is the surface. */
@keyframes landed-sticky {
  from { background-color: var(--accent-soft); }
  to   { background-color: var(--surface); }
}
.grid tbody tr.landed > td.sticky-1 { animation-name: landed-sticky; }

/* And the one cell the finding is actually about, held a little longer than
   the row's wash and ringed, because by the time the row has faded you are
   still reading across it. Outline rather than border: it is drawn outside
   the cell's box, so nothing on the row moves by a pixel when it appears. */
@keyframes landed-cell {
  0%   { outline-color: var(--accent); background-color: var(--accent-soft); }
  70%  { outline-color: var(--accent); background-color: var(--accent-soft); }
  100% { outline-color: transparent; background-color: transparent; }
}
/* No `both`: a fill that outlives its animation holds the last keyframe for
   ever, which is the rule `test_no_entrance_animation_outlives_itself`
   exists to keep. Nothing needs holding here -- the ring is transparent
   when the animation is not running, so the cell is an ordinary cell before
   it starts and again after it ends. */
.grid tbody tr > td.landed-cell {
  outline: 2px solid transparent; outline-offset: -2px;
  animation: landed-cell 2.4s var(--ease);
  position: relative; z-index: 1;
}
/* Same reason as the row above: it may be the sticky column. */
@keyframes landed-cell-sticky {
  0%, 70% { outline-color: var(--accent); background-color: var(--accent-soft); }
  100%    { outline-color: transparent; background-color: var(--surface); }
}
.grid tbody tr > td.sticky-1.landed-cell { animation-name: landed-cell-sticky; }

@media (prefers-reduced-motion: reduce) {
  /* It still says which row and which cell, it simply does not fade away on
     its own. The script takes the class off either way. */
  .grid tbody tr.landed > td { animation: none; background: var(--accent-soft); }
  .grid tbody tr > td.landed-cell {
    animation: none; background: var(--accent-soft);
    outline-color: var(--accent);
  }
}

/* A finding, and what can be done about it.
   ---------------------------------------------------------------------------
   A fourth column for the actions, sized to hold them and no wider, so the
   message keeps the room it had. The actions stay out of the way until the
   row is under the pointer or holds focus -- a wall of 54 findings each with
   two buttons reads as a wall of buttons -- but they are always reachable by
   keyboard, which `:focus-within` is there for. */
.finding { grid-template-columns: 72px 168px 1fr auto; }
.finding-act {
  display: flex; gap: 6px; align-items: center; white-space: nowrap;
  opacity: 0; transition: opacity var(--t-base) var(--ease);
}
.finding:hover .finding-act,
.finding:focus-within .finding-act { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .finding-act { transition: none; }
}
/* No pointer on a touch screen, so nothing would ever reveal them. */
@media (hover: none) {
  .finding-act { opacity: 1; }
}

/* A finding somebody has answered.
   ---------------------------------------------------------------------------
   Quieter than a live one, and still fully readable -- it is a record of a
   decision, not a thing to act on. The reason sits under the message, which
   is where the eye already is after reading it. */
.finding.is-aside .msg { color: var(--ink-2); }
.finding.is-aside .chip { opacity: .75; }
.finding-why {
  display: block; margin-top: 3px; font-size: 11.5px; color: var(--ink-3);
  line-height: 1.45;
}
/* Set aside and brought back are both writes, so the row leaves its group
   rather than vanishing: M13. Only this row moves; the ones below it close
   the gap on the next render, which is one reflow rather than forty. */
@keyframes finding-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: translateX(10px); }
}
.finding.going { animation: finding-out var(--t-exit) var(--ease-in) forwards; }

@media (prefers-reduced-motion: reduce) {
  .finding.going { animation: none; opacity: 1; transform: none; }
}

/* Saving, saved, or not saved.
   ---------------------------------------------------------------------------
   The toast says "Saved" once and goes. Whether a write is still on its way,
   or failed a minute ago after its toast had gone, nothing on screen said --
   and on a platform where every edit recomputes the building, that is the
   question worth answering without being asked.

   It sits between the profile and the build stamp, quiet enough to ignore
   and specific enough to trust. Empty until the first write, so a session
   that has only read says nothing rather than claiming a save. */
.save-state {
  display: flex; align-items: center; gap: 6px; padding-left: 10px;
  font-size: 11.5px; color: var(--ink-3); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.save-state:empty { display: none; }
.save-state.failed { color: var(--warn); font-weight: 550; }

.save-dot, .save-tick { flex: none; display: block; }
.save-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--ink-3);
  animation: save-breathe 900ms ease-in-out infinite alternate;
}
/* A tick drawn rather than fetched: two strokes of a box's border, turned. */
.save-tick {
  width: 5px; height: 9px; margin-top: -2px;
  border: 2px solid var(--ok); border-top: 0; border-left: 0;
  transform: rotate(42deg);
  animation: save-tick var(--t-base) var(--ease-spring) backwards;
}
@keyframes save-breathe { from { opacity: .35; } to { opacity: 1; } }
@keyframes save-tick {
  from { opacity: 0; transform: rotate(42deg) scale(.6); }
  to   { opacity: 1; transform: rotate(42deg) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  /* It still says which of the three it is; it simply says it without moving. */
  .save-dot { animation: none; opacity: .7; }
  .save-tick { animation: none; }
}

/* Copy table, beside Add row.
   ---------------------------------------------------------------------------
   Quieter than the add: adding a row is what somebody came to this screen to
   do, and taking the table to Excel is what they do on the way out. */
.grid-actions .btn.quiet {
  background: none; border-color: transparent; color: var(--ink-3);
}
.grid-actions .btn.quiet:hover {
  background: var(--surface-2); border-color: var(--line); color: var(--ink);
}

/* A figure that changed (M1).
   ---------------------------------------------------------------------------
   The old value leaves upward and the new one arrives from below, in a box
   that clips. Not a count-up: a count-up from 2,45,603 to 2,45,891 puts
   2,45,714 on the screen on the way, and the engine never computed it --
   on a screen a QS reads figures off, that is a small lie sixty times a
   second. Only the two real values are ever shown.

   The box keeps the line's height so the bar does not shuffle while
   somebody is reading it, and both halves move by transform alone. */
.stat .v.rolling {
  position: relative; display: block; overflow: hidden;
  height: 1.25em;
}
.stat .v.rolling > span {
  display: block; position: absolute; left: 0; right: 0; top: 0;
  animation-duration: 240ms; animation-timing-function: var(--ease);
  animation-fill-mode: both;
}
.stat .v.rolling .roll-out { animation-name: roll-out; }
.stat .v.rolling .roll-in { animation-name: roll-in; }
@keyframes roll-out { from { transform: none; opacity: 1; }
                      to   { transform: translateY(-100%); opacity: 0; } }
@keyframes roll-in  { from { transform: translateY(100%); opacity: 0; }
                      to   { transform: none; opacity: 1; } }

/* The blocking count, when it moves (M9). One beat, not a loop. */
@keyframes chip-beat {
  0%, 100% { transform: none; }
  50%      { transform: scale(1.08); }
}
.health .chip.beat { animation: chip-beat 200ms var(--ease); }

@media (prefers-reduced-motion: reduce) {
  /* `roll()` and the beat are both skipped in script; these are the belt to
     that brace, for a stylesheet loaded without it. */
  .stat .v.rolling > span { animation: none; }
  .health .chip.beat { animation: none; }
}

/* ===========================================================================
   Reading through a share link (W4.6)
   ===========================================================================
   One project, read only, for somebody without an account. The chip in the
   bar says so, and what is not the project is not offered: no change log,
   no comparing with other revisions, nobody else "here", nothing to save. */
.share-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 570;
  background: var(--accent-soft); color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  white-space: nowrap;
}
.share-chip .muted { font-weight: 450; }
#app.shared #presence, #app.shared #saveState,
#app.shared #sidebar a[href="#/audit"], #app.shared #sidebar a[href="#/compare"],
#app.shared #sidebar .legend { display: none; }

/* A list that belongs to one cell rather than to a whole column.
 *
 * The Cost Sheet's lift is paid in named steps while every other line on the
 * same column is a percentage, so the chevron appears on the one row that has
 * steps. That cell keeps the column's own right alignment -- a percentage
 * among percentages still reads right to left -- and the chevron sits after
 * the figure rather than pushing it out of line.
 */
td.sel-cell.sel-inline {
  text-align: right; padding-right: 6px;
  background: var(--surface);
}
td.sel-cell.sel-inline::after {
  /* Closer to the figure than the column-wide version, and in the accent so
     it reads as "this one can be picked" against a column that cannot. */
  content: " ⌄"; color: var(--accent); font-size: 12px;
  margin-left: 2px; font-weight: 700;
}
td.sel-cell.sel-inline:hover { background: var(--accent-soft); }
/* The focus ring the rest of the platform draws. A cell that opens a menu is
   a control, and a control with no visible focus is one a keyboard cannot
   find (WCAG 2.2: an indicator of at least 2px at 3:1). */
td.sel-cell.sel-inline:focus,
td.sel-cell.sel-inline:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
/* Open, it is the same combobox every other list cell uses, so there is one
   control to learn and one to maintain. */
td.sel-cell.sel-inline.editing { padding: 0; }
td.sel-cell.sel-inline.editing::after { content: none; }
