/* Sheets' own chrome, on top of rastrillo's tokens.
 *
 * Every colour here is a token, never a literal — a literal does not
 * follow the person's light/dark choice, and cell fills resolve against
 * the theme, so one hard-coded grey stays wrong in one of the two
 * schemes forever.
 *
 * Token names are checked mechanically by
 * internal/guardstest/csstokens_test.go. Five invented ones shipped
 * here first (--rst-border, --rst-text-mut, --rst-warning,
 * --rst-font-mono, --rst-shell-bar-h): CSS resolves an undefined custom
 * property to its fallback in silence, so the page looked plausible and
 * was simply not themed. The real names are --rst-line,
 * --rst-text-muted, --rst-tone-warning-fg and --rst-font.
 */

/* The grid surface fills what is left of the viewport under the topbar.
 * It is why this app has its own layout shell rather than rastrillo's:
 * a spreadsheet is the one screen that should not sit in a padded,
 * max-width page.
 *
 * The shell becomes a viewport-height flex column ONLY on a page that
 * holds a sheet, so every other page still scrolls normally. :has() is
 * what scopes it without a marker class the template would have to
 * remember to set.
 *
 * The surface takes the remaining space with flex rather than
 * subtracting a topbar height. There is no token for that height, and a
 * guessed constant is a scrolling document at every size it was not
 * guessed for — which is exactly what `height: 100dvh` here produced:
 * a 771px document in a 720px viewport, off by the topbar. */
[rst-shell-topbar]:has([data-sheet-surface]) {
  display: flex;
  flex-direction: column;
  height: 100dvh;
}

[data-sheet-surface] {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

/* ── The chrome ──────────────────────────────────────────────────────
 *
 * Three strips around the grid, and a budget: the whole lot is about
 * 7rem above the grid and 2rem below, against Google's ~9.5rem, because
 * the document title sits in the app's own top bar rather than in a row
 * of its own.
 *
 * Row B — the menu bar and the toolbar, sharing one row. Six short
 * words are about 300px, so a row holding only the menus wastes
 * two-thirds of itself. They wrap onto separate rows when they no
 * longer fit, which is also what keeps this inside a 320px viewport
 * with no horizontal scroll. */
[data-chrome] {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--rst-sp-1) var(--rst-sp-3);
  padding: 0 var(--rst-sp-3);
  border-bottom: 1px solid var(--rst-line);
  background: var(--rst-surface);
  color: var(--rst-text);
}

/* The document title, in the top bar beside the brand. It truncates
 * rather than wrapping: the bar is a fixed height, and a two-line title
 * would push the account menu out of it. */
[data-doc-title] {
  color: var(--rst-text);
  font-weight: 600;
  max-inline-size: 22rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

[data-menubar] {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
}

[data-menubar] > [rst-dropdown] > summary {
  border-radius: var(--rst-radius-sm);
  font-size: var(--rst-fs-sm);
  padding: var(--rst-sp-1) var(--rst-sp-2);
}

[data-menubar] > [rst-dropdown][open] > summary {
  background: var(--rst-accent-soft);
  color: var(--rst-accent);
}

/* rastrillo's dropdown panel is right-aligned, which is right for the
 * account menu it was written for and wrong for a menu bar: a File menu
 * hanging off the right edge of the word "File" opens away from the
 * item you clicked.
 *
 * This is the fallback branch, for an engine with no anchor
 * positioning. There rastrillo's panel is `position: absolute;
 * top: 100%; inset-inline-end: 0` and flipping the two insets is the
 * whole fix. The @supports block below is the branch that actually
 * runs in Chromium, and it must not be folded into this rule — see
 * its own comment for what happens when it is. */
[data-menubar] [rst-dropdown-menu] {
  inset-inline-start: 0;
  inset-inline-end: auto;
  /* 17rem, not the 15rem this started at, and the extra 2rem is spent
   * on the two resize rows: label, field and button need 236px side by
   * side, and 15rem leaves 224. Widening the panel is the honest fix —
   * squeezing the field instead makes a number box you cannot read a
   * three-digit value in. */
  min-inline-size: 17rem;
  /* 15rem is 240px, and the menus nearest the right edge open from
   * there — so on a 320px viewport an open panel ran off the screen
   * even though the closed chrome fits. max wins over min in CSS, so
   * this bounds it without the panel becoming narrow on a wide one. */
  max-inline-size: calc(100vw - 1rem);
  /* rastrillo caps a panel at 20rem and scrolls past it, which is right
   * for a list nobody can predict the length of — a locale menu, a row
   * kebab. A menu bar's menus are a FIXED list we wrote, and Format's
   * closed height is 394px: the cap put "Clear formatting" behind a
   * scrollbar on every viewport, including a 900px-tall one with room
   * to spare. The dvh term is rastrillo's own and stays, so a short
   * window still scrolls rather than overflowing. */
  max-block-size: min(30rem, max(calc(100dvh - 6rem), 8rem));
}

/* Where anchor positioning exists, the two insets above are not a
 * refinement of rastrillo's placement — they DEFEAT it, and this is
 * the bug that put every menu against the right edge of the viewport.
 *
 * rastrillo v0.23.0 anchors its panel: `position: fixed`,
 * `position-anchor: --rst-menu`, `position-area: block-end
 * span-inline-start`, and `inset: auto` so the area does the placing.
 * That `inset: auto` is (0,1,0). Ours above is (0,2,0), so it wins —
 * and an anchored, fixed panel with inset-inline-start resolved to 0
 * takes the VIEWPORT as its containing block. Measured 2026-09-03 on a
 * 1280px viewport: all six panels landed at x=1015, flush right,
 * whichever word you clicked. The menu bar looked broken and the CSS
 * looked correct.
 *
 * So the fix is to hand the placement back and change the AREA, not
 * the insets. `span-inline-end` is the one word that differs from
 * rastrillo's: its account menu sits at the right edge and opens
 * leftward, and a menu bar sits at the left and opens rightward.
 *
 * rastrillo's `position-try-fallbacks` still applies on top, so a menu
 * near the right edge of a narrow viewport still flips rather than
 * running off — which is why this is a change of base area and not of
 * `position-try-fallbacks` too. Restating the @supports condition
 * rather than nesting: the two must test the same thing, and an engine
 * that supports one half of it would otherwise get neither branch. */
@supports (position-area: block-end) and (position-try-fallbacks: flip-block) {
  [data-menubar] [rst-dropdown-menu] {
    inset: auto;
    position-area: block-end span-inline-end;
  }
}

/* An item is a row with its shortcut pushed to the far end, which is
 * what makes a menu readable as a table of keys as well as a list of
 * actions. The form wrapper around a scriptless POST is display:contents
 * so its button lays out as though the form were not there.
 *
 * :not([data-size-form]) is load-bearing, and its absence is what made
 * the Format menu ragged. A resize form is not a wrapper around one
 * menu item — it is a label, a number field and a button that read as
 * ONE control, and [data-size-form] lays them out in a flex row to say
 * so. This rule is (0,3,0) and that one is (0,1,0), so display:contents
 * won: the form stopped generating a box, its three children became
 * menu items in their own right, and the panel rendered "Column width"
 * and "px" on one line with a full-width "Set" under them. Both halves
 * looked correct in isolation. */
[data-menubar] [rst-dropdown-menu] > form:not([data-size-form]) {
  display: contents;
}

[data-menubar] [rst-dropdown-menu] a,
[data-menubar] [rst-dropdown-menu] button {
  align-items: baseline;
  display: flex;
  gap: var(--rst-sp-4);
  justify-content: space-between;
}

/* A submenu summary is an item too — the same row, the same hover, plus
 * the one thing that distinguishes it: a chevron saying it opens rather
 * than does. rastrillo styles [rst-menu-group] > summary with padding
 * and a muted colour and stops there, so Alignment and Bold were the
 * same shape and only one of them expanded.
 *
 * The chevron is real markup, not a ::after character, so rastrillo's
 * own `details[open] > summary > [rst-caret]` rotates it on open and
 * the state is visible rather than remembered. */
[data-menubar] [rst-dropdown-menu] [rst-menu-group] > summary {
  align-items: center;
  border-radius: var(--rst-radius-sm);
  display: flex;
  gap: var(--rst-sp-4);
  justify-content: space-between;
}

[data-menubar] [rst-dropdown-menu] [rst-menu-group] > summary:hover {
  background: var(--rst-accent-soft);
}

[data-menubar] kbd {
  color: var(--rst-text-faint);
  font-family: var(--rst-font);
  font-size: var(--rst-fs-xs);
  white-space: nowrap;
}

[data-menubar] [data-sample] {
  color: var(--rst-text-faint);
  font-size: var(--rst-fs-xs);
}

[data-menubar] hr {
  border: 0;
  border-top: 1px solid var(--rst-line);
  margin: var(--rst-sp-1) 0;
}

/* The Help panel is the shortcut list itself rather than a link to a
 * page that does not exist, so it is a definition list and not a stack
 * of menu items. */
[data-shortcuts] dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--rst-sp-1) var(--rst-sp-4);
  margin: 0;
  padding: var(--rst-sp-2);
}

[data-shortcuts] dt {
  text-align: end;
}

[data-shortcuts] dd {
  color: var(--rst-text-muted);
  font-size: var(--rst-fs-sm);
  margin: 0;
}

/* ── The toolbar ─────────────────────────────────────────────────────
 *
 * `rst-btn="compact"` is not a rastrillo variant — the word appears in
 * tokens.css only inside comments — so a toolbar built on it would be
 * silently unstyled. These buttons are ours.
 *
 * 1.75rem square, clear of the 24px minimum target, and the pressed
 * state is a box-shadow ring rather than a border: a border costs
 * layout, and eleven of them would widen the strip by 22px against a
 * hard 320px reflow budget. A full ring rather than an underline,
 * because a one-sided inset shadow follows the border radius round the
 * corners and reads as a tab stem. */
[data-toolbar] {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--rst-sp-1);
  padding: var(--rst-sp-1) 0;
}

[data-tool] {
  align-items: center;
  background: transparent;
  border: 0;
  border-radius: var(--rst-radius-sm);
  color: var(--rst-text-muted);
  cursor: pointer;
  display: inline-flex;
  font: inherit;
  font-size: var(--rst-fs-sm);
  gap: 0.1rem;
  justify-content: center;
  min-block-size: 1.75rem;
  min-inline-size: 1.75rem;
  padding: 0 var(--rst-sp-1);
  /* [rst-tip]::after is absolutely positioned and rastrillo does not
   * position the host, so the tooltip would anchor to the page. */
  position: relative;
}

[data-tool]:hover {
  background: var(--rst-surface-2);
  color: var(--rst-text);
}

[data-tool][aria-pressed="true"] {
  background: var(--rst-accent-soft);
  box-shadow: inset 0 0 0 1px currentColor;
  color: var(--rst-accent);
}

[data-tool]:focus-visible {
  outline: 2px solid var(--rst-accent);
  outline-offset: 2px;
}

[data-tool] svg {
  block-size: 1rem;
  fill: none;
  inline-size: 1rem;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-width: 1.5;
}

[data-tool-rule] {
  align-self: stretch;
  border-inline-start: 1px solid var(--rst-line);
  margin: var(--rst-sp-1) var(--rst-sp-1);
}

[data-tool-menu] > summary {
  padding: 0;
}

[data-tool-menu] > summary::-webkit-details-marker {
  display: none;
}

/* The same pair as the menu bar's, for the same reason and with the
 * same two branches. A toolbar's colour picker opens under its own
 * button, not off the right edge of the window. */
[data-tool-menu] [rst-dropdown-menu] {
  inset-inline-start: 0;
  inset-inline-end: auto;
}

@supports (position-area: block-end) and (position-try-fallbacks: flip-block) {
  [data-tool-menu] [rst-dropdown-menu] {
    inset: auto;
    position-area: block-end span-inline-end;
  }
}

[data-tool] [rst-caret] svg {
  block-size: 0.75rem;
  inline-size: 0.75rem;
}

/* The two colour chips on the toolbar show what the anchor cell has,
 * so the button says what it will do rather than only what it is.
 * --sheet-h and --sheet-c are set by commands.js from the selection. */
[data-fill-chip],
[data-ink-chip] {
  border-radius: 2px;
  display: block;
  inline-size: 1rem;
  line-height: 1;
  text-align: center;
}

[data-fill-chip] {
  block-size: 1rem;
  border: 1px solid var(--rst-line);
  background: var(--rst-surface-2);
}

[data-fill-chip][data-set] {
  background: oklch(var(--sheet-fill-l) var(--sheet-c) var(--sheet-h));
}

[data-ink-chip] {
  border-bottom: 3px solid var(--rst-text-muted);
  font-weight: 700;
}

[data-ink-chip][data-set] {
  border-bottom-color: oklch(var(--sheet-ink-l) var(--sheet-kc) var(--sheet-kh));
}

/* A swatch button is its colour with its name beside it. Named, not
 * colour alone: a picker that only shows colour is unusable to somebody
 * who cannot tell two of them apart, and the name is also what the
 * server is sent. */
[data-swatches] {
  min-inline-size: 11rem;
}

/* A swatch button is its colour and then its name, reading left to
 * right as one thing. The menu's general rule pushes an item's trailing
 * content to the far end so a keyboard shortcut lines up in a column —
 * right for "Bold … Ctrl B", wrong here, where it stranded each name
 * against the opposite edge from its own colour.
 *
 * Written at the full depth on purpose: that general rule is
 * [data-menubar] [rst-dropdown-menu] button, which is (0,2,1), and the
 * obvious [data-swatches] button is (0,1,1) and loses. */
[data-menubar] [rst-dropdown-menu] [data-swatches] button {
  gap: 0;
  justify-content: flex-start;
}

[data-swatch]::before {
  border: 1px solid var(--rst-line);
  border-radius: 2px;
  content: "";
  display: inline-block;
  block-size: 0.85rem;
  inline-size: 0.85rem;
  margin-inline-end: var(--rst-sp-2);
  vertical-align: -2px;
  background: oklch(var(--sheet-fill-l) var(--sheet-c) var(--sheet-h));
}

[data-ink-swatch]::before {
  background: oklch(var(--sheet-ink-l) var(--sheet-kc) var(--sheet-kh));
}

/* ── Row C: the name box and the formula bar ─────────────────────────
 *
 * The formula bar gets the whole row and all the slack. It used to
 * share a strip with five links and was capped at 40rem so they had
 * somewhere to sit; on its own row a cap only truncates long formulas
 * for no gain. */
[data-formula-row] {
  align-items: center;
  background: var(--rst-surface);
  border-bottom: 1px solid var(--rst-line);
  display: flex;
  gap: var(--rst-sp-2);
  padding: var(--rst-sp-1) var(--rst-sp-3);
}

[data-name-box] {
  flex: 0 0 auto;
  inline-size: 6rem;
  text-align: center;
}

[data-fx] {
  color: var(--rst-text-faint);
  flex: 0 0 auto;
  font-size: var(--rst-fs-sm);
}

[data-formula-input] {
  flex: 1;
  min-inline-size: 0;
}

/* ── Row D: the tab strip ────────────────────────────────────────────
 *
 * One tab today, and deliberately no "+": nothing in this build
 * creates, names or switches a tab, and spec §14 makes "a second tab is
 * reachable in the UI" the trigger for multi-worksheet import. */
[data-tabstrip] {
  align-items: stretch;
  background: var(--rst-surface-2);
  border-top: 1px solid var(--rst-line);
  display: flex;
  gap: var(--rst-sp-3);
  min-block-size: 2rem;
  padding-inline: var(--rst-sp-3);
}

[data-sheet-tabs] {
  display: flex;
}

[data-sheet-tabs] a {
  align-items: center;
  border-bottom: 2px solid transparent;
  color: var(--rst-text-muted);
  display: flex;
  font-size: var(--rst-fs-sm);
  padding-inline: var(--rst-sp-3);
  text-decoration: none;
}

[data-sheet-tabs] a:hover {
  color: var(--rst-text);
}

[data-sheet-tabs] a[aria-current] {
  background: var(--rst-surface);
  border-bottom-color: var(--rst-accent);
  color: var(--rst-text);
  font-weight: 600;
}

/* The size of the selection, not a sum of it. A total would be the
 * client doing arithmetic on strings the server rendered, which is the
 * one thing this design keeps out of JavaScript. */
[data-selection-size] {
  align-self: center;
  color: var(--rst-text-faint);
  font-size: var(--rst-fs-xs);
}

/* The transient note. Empty almost always, and the app's only way of
 * saying something happened or did not: today, that a browser refused
 * the menu's clipboard read, or that a paste is larger than one write
 * can carry.
 *
 * It takes the slack rather than the live indicator, which is why the
 * auto margin moved here — otherwise a note appearing shoves the
 * connection state sideways every time one is shown. Truncates rather
 * than wrapping: this strip is 2rem and a two-line message would push
 * the grid. */
/* A note the person has to act on. Muted grey in a strip under the
 * grid is the right weight for "12 cells were not pasted" and the
 * wrong one for "the menu could not paste, use the keyboard" — the
 * second went unread on the live instance while the menu item read as
 * simply broken. */
[data-sheet-note][data-tone="warn"] {
  color: var(--rst-tone-warning-fg);
  font-weight: 550;
}

[data-sheet-note] {
  align-self: center;
  color: var(--rst-text-muted);
  font-size: var(--rst-fs-xs);
  margin-inline-start: auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The live indicator. It says connected or reconnecting and nothing
 * else — presence is a later slice, and a dot that claims more than it
 * knows is worse than no dot. */
[data-sheet-live] {
  align-self: center;
  font-size: var(--rst-fs-xs);
  color: var(--rst-tone-positive-fg);
}

[data-sheet-live][data-state="off"] {
  color: var(--rst-tone-warning-fg);
}

[data-sheet-grid] {
  flex: 1;
  min-height: 0;
  overflow: auto;
  background: var(--rst-bg);
}

/* ── The vendored grid ───────────────────────────────────────────────
 *
 * Jspreadsheet paints its own colours, hard-coded: #fff for the table,
 * #f3f3f3 for the headers, #ccc for every border. None of them follow a
 * theme, so all of them are restated here in tokens.
 *
 * The selectors are `.jss_*`. Jspreadsheet v5 renamed everything from
 * v4's `.jexcel`, and this block was first written against `.jexcel` —
 * so it matched nothing at all, and the grid stayed white inside dark
 * chrome. Check the vendored stylesheet, not a memory of the last major
 * version, when these stop working.
 *
 * Every rule that sets a background on something carrying text also
 * sets a color. The library sets background-color on the headers and no
 * color, so the text is inherited from the page: in dark mode that put
 * light grey text on a light grey header and the row and column names
 * became invisible. A background without a foreground beside it is the
 * bug, not the colour. (The exceptions are the things with no text in
 * them — the fill handle is a 1px knob.) */
[data-sheet-grid] .jss_worksheet {
  background-color: var(--rst-bg);
  color: var(--rst-text);
  border-right-color: var(--rst-line);
  border-bottom-color: var(--rst-line);
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td {
  background-color: var(--rst-bg);
  color: var(--rst-text);
  border-top-color: var(--rst-line);
  border-left-color: var(--rst-line);
}

/* The row and column names. Both headers, one look. */
[data-sheet-grid] .jss_worksheet > thead > tr > td,
[data-sheet-grid] .jss_worksheet > tbody > tr > td:first-child {
  background-color: var(--rst-surface-2);
  color: var(--rst-text-muted);
  border-top-color: var(--rst-line);
  border-left-color: var(--rst-line);
  font-weight: 600;
}

/* The names of the selected column and row. The library puts .selected
 * on the thead td but on the tbody TR — not on its first cell — so the
 * two halves need different selectors. Written as `td:first-child.selected`
 * the second half matched nothing at all, and a rule that matches nothing
 * fails exactly like a rule nobody wrote: the vendored #dcdcdc stood, and
 * a dark grid grew a light-grey slab down its left edge carrying
 * --rst-text-muted (#a8b0ba) at 1.60:1. */
[data-sheet-grid] .jss_worksheet > thead > tr > td.selected,
[data-sheet-grid] .jss_worksheet > tbody > tr.selected > td:first-child {
  background-color: var(--rst-accent-soft);
  color: var(--rst-text);
}

/* ── The selection ───────────────────────────────────────────────────
 *
 * A range is a fill plus an outline. This file used to restate only the
 * fill, and the outline is where every hard-coded colour in the vendored
 * stylesheet lives:
 *
 *   .highlight-right/-bottom   border: 1px solid #000
 *   .highlight-top/-left       border: 1px solid #000, plus a decorative
 *                              box-shadow of #ccc just outside it
 *
 * In dark that renders backwards. The black borders disappear into the
 * grid; the two that do not are overwritten by our own
 * `td { border-top-color: var(--rst-line) }` above, which is one
 * specificity step higher than the library's `.jss_worksheet .highlight-top`
 * — so they become ordinary grid lines, while the #ccc shadow, which
 * exists only to mask the grid line the border sits on, survives as the
 * one bright thing on screen. What you get is a navy slab lit down its
 * top and left and nowhere else, with no way to see where it ends.
 *
 * ── Why every rule below is scoped `> tbody > tr > td` ────────────────
 * Two reasons, and the second is the one that bites.
 *
 * 1. The grid-line rule above is (0,2,3). A restatement written at the
 *    library's own `.jss_worksheet .highlight-top` is (0,2,0) and loses
 *    to it — a rule that parses, uses a token, and paints nothing.
 * 2. sheet.html links the vendored stylesheets in the head block, which
 *    layout.html renders AFTER sheets.css. The library is later in
 *    source order, so equal specificity loses too. Ours must be
 *    strictly higher, never merely equal. That is also the real reason
 *    the vendored #dcdcdc beat the header rule above: both are (0,3,3),
 *    and the vendor came last.
 *
 * So: one outline colour, --rst-accent, on all four sides, above both.
 * Measured on the fill it sits on, 6.59:1 in dark and 4.75:1 in light —
 * both past 1.4.11's 3:1 for a component boundary. The shadow goes:
 * with the outline in a colour that reads, masking the line underneath
 * it buys nothing and costs the selection its symmetry.
 *
 * The library gives each of its three range states a different border
 * STYLE — solid for the selection, dashed for the copy marquee, dotted
 * for the fill-handle drag preview — and the style survives our
 * colour-only override. That is the distinction doing the work, so all
 * three take the same accent and stay told apart by shape. */
[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight {
  background-color: var(--rst-accent-soft);
  color: var(--rst-text);
}

/* A link inside a selected cell. Vendored as the browser's own `blue`,
 * which on the fill in dark is 2.4:1. */
[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight > a {
  color: var(--rst-accent);
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-top,
[data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-top,
[data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-top {
  border-top-color: var(--rst-accent);
  box-shadow: none;
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-left,
[data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-left,
[data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-left {
  border-left-color: var(--rst-accent);
  box-shadow: none;
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-right,
[data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-right,
[data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-right {
  border-right-color: var(--rst-accent);
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-bottom,
[data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-bottom,
[data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-bottom {
  border-bottom-color: var(--rst-accent);
}

/* The rows the fill handle is being dragged over — the preview of what
 * a release would fill, NOT the range a formula is pointing at. There is
 * no formula chrome in this app: grid.js sets `parseFormulas: false`,
 * because our engine computes and the client only paints. The library
 * adds these classes from updateCopySelection, off `selectedCorner`.
 * Vendored as rgba(0,0,0,0.05) — a darkening, which on a dark grid is
 * nothing at all. */
[data-sheet-grid] .jss_worksheet > tbody > tr > td.selection {
  background-color: var(--rst-accent-soft);
  color: var(--rst-text);
}

/* The anchor — the one cell the keyboard is on. The library paints it
 * transparent on purpose so it reads out of the fill; our .highlight
 * rule was opaque and took that back, leaving a range with no visible
 * cursor in either scheme.
 *
 * It comes LAST among the SELECTION rules on purpose. It ties
 * .highlight and .selection at (0,3,3), and the anchor is inside both
 * while you drag the fill handle — so on a tie it has to be the later
 * rule or the cursor disappears at exactly the moment you are aiming
 * with it.
 *
 * **The ring is what actually marks it, and the background no longer
 * can.** Cell fills are painted further down this file, at the same
 * (0,3,3) and therefore later, so on a filled range a background-only
 * anchor is indistinguishable from the rest of the selection — the
 * exact failure the paragraph above guards against, arriving by a new
 * route. An inset shadow survives any fill because it is drawn over
 * one, and it costs no layout, so it cannot shift a cell by a pixel
 * when the cursor lands on it.
 *
 * The background stays for the unfilled case, where it is what makes
 * the anchor read as "not part of the tint". */
[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-selected {
  background-color: var(--rst-bg);
  color: var(--rst-text);
  box-shadow: inset 0 0 0 2px var(--rst-accent);
}

/* The fill handle at the corner of the range: a black knob ringed in
 * white, which in dark reads as a white dot with a dark centre — the
 * inverse of the thing it is meant to be. It is 1px plus its ring, so
 * it is the accent or it is invisible. */
[data-sheet-grid] .jss_corner {
  background-color: var(--rst-accent);
  border-color: var(--rst-bg);
}

/* ── The right-click menu ────────────────────────────────────────────
 *
 * Not Jspreadsheet's: jsuites', which it binds to `contextmenu` whether
 * or not the host app configures one. So it exists here, it is reachable
 * on every cell, and vendored it is #fff with #555 text — a white card
 * dropped on a dark grid, and the last unthemed surface on this screen.
 *
 * It is `position: fixed` and mounted inside the grid, so it is scoped
 * the same way everything else here is. */
[data-sheet-grid] .jcontextmenu {
  background: var(--rst-surface);
  color: var(--rst-text);
  border-color: var(--rst-line-strong);
  box-shadow: var(--rst-shadow-pop);
}

[data-sheet-grid] .jcontextmenu > div a {
  color: var(--rst-text);
}

[data-sheet-grid] .jcontextmenu > div span {
  color: var(--rst-text-muted);
}

[data-sheet-grid] .jcontextmenu > div:hover {
  background: var(--rst-accent-soft);
  color: var(--rst-text);
}

[data-sheet-grid] .jcontextmenu .jcontextmenu-disabled a,
[data-sheet-grid] .jcontextmenu .jcontextmenu-disabled::before {
  color: var(--rst-text-faint);
}

[data-sheet-grid] .jcontextmenu hr {
  border-color: var(--rst-line);
}

/* The cell editor is an input the library drops into the td. Left
 * alone it inherits the browser's own white. */
[data-sheet-grid] .jss_worksheet td input,
[data-sheet-grid] .jss_worksheet td textarea {
  background-color: var(--rst-bg);
  color: var(--rst-text);
}

[data-sheet-grid] .jss_container,
[data-sheet-grid] .jss_content {
  background-color: var(--rst-bg);
}

/* A cell whose imported fill fails WCAG AA against its ink. Spec §11.1
 * ruled that we retain such a fill rather than silently correcting it,
 * so the fill stays and this marks it instead: a corner flag that costs
 * nothing to ignore and is impossible to miss once you look. */
[data-sheet-grid] td[data-contrast="low"] {
  position: relative;
}

[data-sheet-grid] td[data-contrast="low"]::after {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline-end: 0;
  border-block-start: 0.5rem solid var(--rst-tone-warning-fg);
  border-inline-start: 0.5rem solid transparent;
  /* A halo in the page's own surface colour, so the flag survives the
   * fill it is flagging.
   *
   * The flag marks a cell whose IMPORTED fill and ink fail AA, and the
   * fill is an arbitrary colour out of somebody's file — including, in
   * the case this app cites by name, Excel's #FFEB9C, against which an
   * amber triangle is invisible in dark mode. A warning that disappears
   * on exactly the cells it is about is not a warning.
   *
   * drop-shadow rather than a border or an outline: those two follow
   * the box, and this triangle is drawn out of borders, so there is no
   * box to put an edge on. filter traces the rendered shape. */
  filter: drop-shadow(0 0 1px var(--rst-surface))
          drop-shadow(0 0 1px var(--rst-surface));
}

/* ── The scriptless table ────────────────────────────────────────────
 *
 * Not a fallback in the apologetic sense: it is the only view that
 * works everywhere, and every affordance the grid has exists here too. */
[data-cell-table] {
  border-collapse: collapse;
  width: 100%;
}

[data-cell-table] th,
[data-cell-table] td {
  border: 1px solid var(--rst-line);
  padding: var(--rst-sp-1) var(--rst-sp-2);
  text-align: start;
  vertical-align: top;
}

[data-cell-table] th {
  background: var(--rst-surface-2);
  color: var(--rst-text-muted);
  font-weight: 600;
}

/* The cell link, which is every cell.
 *
 * Each cell is an anchor, because the link IS the edit affordance in a
 * view with no script. Left at the browser's default that meant roughly
 * 650 blue underlined links per page — data and emptiness in the same
 * colour and the same weight, so `94200` and a blank cell differed only
 * in glyph width. A spreadsheet you can only read cell by cell is not a
 * spreadsheet, and this is the view the spec calls the product.
 *
 * So the cell reads as a cell and the affordance shows on approach. The
 * anchor takes the cell's full width and a line of height, so the
 * target is the whole line rather than the few pixels of the glyph —
 * which is also what lets an empty cell be clickable without a &nbsp;
 * standing in for content. It does not stretch to a row made taller by
 * a wrapped neighbour; that was true of the &nbsp; it replaces too.
 *
 * :focus-visible carries the same treatment as :hover deliberately.
 * Keyboard is the primary way through this view, and a focus ring alone
 * on a 26-column grid is easy to lose. */
[data-cell-table] td > a {
  color: inherit;
  text-decoration: none;
  display: block;
  min-block-size: 1.25rem;
  border-radius: var(--rst-radius-sm);
}

[data-cell-table] td > a:hover,
[data-cell-table] td > a:focus-visible {
  background: var(--rst-accent-soft);
  color: var(--rst-accent);
}

/* Numbers right, text left — the alignment IS the type signal in a
 * spreadsheet, and it is the one the grid gives for free and this view
 * had to be told.
 *
 * Both spellings, because there are two of these tables. The editable
 * one wraps every cell in an anchor, so the alignment has to reach the
 * block anchor rather than the cell around it; the anonymous link view
 * has no anchors, because there is nothing on it to edit. One rule for
 * each, rather than one selector that quietly covers only the surface
 * that happened to get tested. */
[data-cell-table] td[data-kind="number"],
[data-cell-table] td[data-kind="number"] > a {
  text-align: end;
}

[data-cell-table] td form {
  display: flex;
  gap: var(--rst-sp-1);
}

[data-cell-table] td input[type="text"] {
  inline-size: 100%;
  min-inline-size: 6ch;
}

/* [data-cell-value] used to be styled here and is now gone. Nothing in
 * any template or module has ever written that attribute, so the rule
 * matched no element for the life of the branch — the same defect as
 * the contrast flag two sections up, which had a rule and no attribute
 * and therefore warned about cells it never marked.
 *
 * A selector that matches nothing is invisible to every test we have
 * and to every screenshot, which is why it is worth saying it was here
 * rather than deleting it silently: the next person to want a secondary
 * line under a table cell should write the markup and the rule
 * together, not find this and assume it works. */


/* Import drop zone ───────────────────────────────────────────────────
 *
 * The dashed outline is the whole visual: it says "this box takes a
 * file" without becoming a second, competing button beside the one that
 * is already there. The field and its button inside it are untouched
 * rastrillo, because with scripting off they are the entire feature and
 * a drop zone that restyles them would be decorating something that
 * cannot happen.
 *
 * The highlight is an ATTRIBUTE toggled by drop.js, not a class: it is
 * transient state on one element, and the attribute spelling is what
 * the rest of this app's own hooks use (data-sheet-surface,
 * data-cell-table).
 *
 * transition on border-color only — a person dragging a file across the
 * page crosses this box repeatedly, and animating the outline is the
 * difference between a box that acknowledges the drag and one that
 * flashes. */
[data-drop] {
  border: 1px dashed var(--rst-line);
  border-radius: var(--rst-radius);
  padding: var(--rst-sp-3);
  transition: border-color 120ms ease, background-color 120ms ease;
}

[data-drop][data-drop-over] {
  border-color: var(--rst-accent);
  background: var(--rst-accent-soft);
}

/* Somebody who has asked for less motion still needs to see the box
   respond — so the change stays and only the animation goes. */
@media (prefers-reduced-motion: reduce) {
  [data-drop] {
    transition: none;
  }
}

/* ── Cell formatting ─────────────────────────────────────────────────
 *
 * The paint for a cell's stored Format: bold, italic, alignment, fill
 * and ink. Every rule is here, in the file the guards read, rather than
 * in a stylesheet JavaScript generates — a generated sheet is invisible
 * to TestOurStylesheetHasNoColourLiterals, TestEveryTokenWeUseExists
 * and TestVendorPaintIsRestated alike, which is three reasons not to
 * have one. paint.js only sets attributes and two custom properties.
 *
 * Specificity: [data-sheet-grid] .jss_worksheet > tbody > tr > td[attr]
 * is (0,3,3) — one step above the grid-line rule at (0,2,3), as the
 * restatement rule requires, and equal to the selection rules. So these
 * are placed AFTER them: a filled cell keeps its fill while selected,
 * and the selection outline does the work of showing the selection,
 * which is what every spreadsheet does.
 *
 * A colour a person picks HERE is a hue and a chroma with no lightness
 * — see store/models.go — and the lightness comes from the theme below.
 * A colour an imported FILE authored is painted exactly as authored,
 * in both themes, because spec §11.1 says we render a document as
 * written and warn rather than silently correcting it. */

/* light-dark() is a colour function and cannot hold a number, so these
 * are written the long way. They still resolve before first paint:
 * data-theme is server-rendered into the first bytes of the response
 * (layout.html), so there is no flash of the wrong lightness. */
:root {
  --sheet-fill-l: 0.92;
  --sheet-ink-l: 0.42;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --sheet-fill-l: 0.30;
    --sheet-ink-l: 0.80;
  }
}

:root[data-theme="dark"] {
  --sheet-fill-l: 0.30;
  --sheet-ink-l: 0.80;
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td[data-b] {
  font-weight: 600;
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td[data-i] {
  font-style: italic;
}

/* ── Alignment, and why these three rules carry !important ───────────
 *
 * Jspreadsheet writes `text-align` as an INLINE style on every data
 * cell, from the column's `align` or from `defaultColAlign`. An inline
 * declaration outranks any selector, at any specificity, so the
 * ordinary version of these rules set the attribute, reflected
 * aria-pressed on the button, and changed nothing on screen. The
 * toolbar looked like it worked. Only a browser could see it, and the
 * branch's own drive did not check alignment — bold and fill, which
 * both paint from properties the library does not touch, passed.
 *
 * !important is the correct tool here and not a shortcut: it is what
 * outranks an inline style, and there is no other. These are new paint
 * rather than a restatement of a vendored rule, so the restatement
 * guard is unaffected.
 *
 * The numeric rule is important for the same reason and comes FIRST,
 * so an explicit alignment beats it on source order — both are
 * important and (0,3,3), so nothing else separates them. */
[data-sheet-grid] .jss_worksheet > tbody > tr > td[data-n] {
  text-align: end !important;
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td[data-a="left"] {
  text-align: start !important;
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td[data-a="center"] {
  text-align: center !important;
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td[data-a="right"] {
  text-align: end !important;
}

/* Ours: hue and chroma from the cell, lightness from the theme. */
[data-sheet-grid] .jss_worksheet > tbody > tr > td[data-fill] {
  background-color: oklch(var(--sheet-fill-l) var(--sheet-c) var(--sheet-h));
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td[data-ink] {
  color: oklch(var(--sheet-ink-l) var(--sheet-kc) var(--sheet-kh));
}

/* The author's: painted as the file wrote it. --sheet-fx and --sheet-k
 * carry a hex that came from a document, which is why a literal is
 * acceptable here and nowhere else in this file — it is data, set by
 * script from the document, not a colour this stylesheet chose. */
[data-sheet-grid] .jss_worksheet > tbody > tr > td[data-fx] {
  background-color: var(--sheet-fx);
}

[data-sheet-grid] .jss_worksheet > tbody > tr > td[data-k] {
  color: var(--sheet-k);
}

/* ── The table view paints what the grid paints ──────────────────────
 *
 * The same attributes, because the server writes the same attributes
 * (tablepaint.go). PRODUCT.md's second principle is why: the scriptless
 * path is the product, and a screen that can SET bold but cannot SHOW
 * it is the worse half of the feature.
 *
 * Written as its own block rather than by widening the grid's selectors
 * to cover both. Those carry `.jss_worksheet` and `!important` for
 * reasons that belong entirely to the vendored library — an inline
 * text-align the library writes on every data cell — and none of that
 * is true here. A shared selector would carry the library's problem
 * onto a table that does not have it, and would make the restatement
 * guard's job ambiguous.
 *
 * No !important, and that is the tell that these are new paint rather
 * than a restatement: nothing writes an inline text-align on this
 * table. The one inline style a cell here carries is the custom
 * properties, which set values rather than win a cascade. */
[data-cell-table] td[data-b],
[data-cell-table] td[data-b] > a {
  font-weight: 600;
}

[data-cell-table] td[data-i],
[data-cell-table] td[data-i] > a {
  font-style: italic;
}

/* The numeric rule comes first so an explicit alignment beats it on
 * source order, the same ordering the grid's block relies on. Both are
 * (0,2,1) here, so nothing else separates them.
 *
 * The existing [data-kind="number"] rule above already right-aligns a
 * numeric cell; these three are the explicit override, and they have to
 * come after it in the file for the same reason. */
[data-cell-table] td[data-a="left"],
[data-cell-table] td[data-a="left"] > a {
  text-align: start;
}

[data-cell-table] td[data-a="center"],
[data-cell-table] td[data-a="center"] > a {
  text-align: center;
}

[data-cell-table] td[data-a="right"],
[data-cell-table] td[data-a="right"] > a {
  text-align: end;
}

/* Ours: hue and chroma from the cell, lightness from the theme, so a
 * fill somebody picked follows their light/dark choice. Identical to
 * the grid's rule and deliberately so — a fill must not be one colour
 * on one screen and another on the other. */
[data-cell-table] td[data-fill] {
  background-color: oklch(var(--sheet-fill-l) var(--sheet-c) var(--sheet-h));
}

[data-cell-table] td[data-ink],
[data-cell-table] td[data-ink] > a {
  color: oklch(var(--sheet-ink-l) var(--sheet-kc) var(--sheet-kh));
}

/* The author's, painted as the file wrote it (spec §11.1). --sheet-fx
 * and --sheet-k carry a hex that came out of a document — data, checked
 * by previewsvg.go's safeHex before it was ever written into the
 * attribute, and not a colour this stylesheet chose. */
[data-cell-table] td[data-fx] {
  background-color: var(--sheet-fx);
}

[data-cell-table] td[data-k],
[data-cell-table] td[data-k] > a {
  color: var(--sheet-k);
}

/* The cell edit form's formatting row. Six selects and a button, on
 * their own line under the value field: side by side they would push
 * the table's column wider than the page. */
[data-cell-format] {
  display: flex;
  flex-wrap: wrap;
  gap: var(--rst-sp-1);
  margin-block-start: var(--rst-sp-1);
}

[data-cell-format] select {
  font-size: var(--rst-fs-xs);
  padding-block: 0.15rem;
}

/* ── The list's thumbnails ───────────────────────────────────────────
 *
 * A 26x34 slot at the row's left edge, the same box in the same place
 * as the Docs app's, so the two file lists have the same row height and
 * read as one product rather than two apps that both grew thumbnails.
 *
 * The slot is reserved whether or not there is anything to draw: an
 * empty sheet renders the empty bordered box, so a row never changes
 * height when a sheet gains its first cell. */
/* The list card is a <ul> and rastrillo's [rst-list] is written for a
 * <div> — its comment says "a list card holds rows", and every rule it
 * declares is background, border and radius. It resets nothing, because
 * a <div> has nothing to reset.
 *
 * A <ul> does. The UA stylesheet gives it `padding-inline-start: 40px`
 * and `list-style: disc`, and both survived: the 40px is the gap Paul
 * saw to the left of the thumbnail on 2026-09-03, and it is 40px of
 * card that every row is indented inside. The markers were invisible
 * only because they render in that padding, outside the row's own box.
 *
 * The element stays a <ul>. A list of sheets is a list, and the fix for
 * a framework rule that assumed a <div> is to reset what the <div>
 * never had — not to drop the semantics that make the list announce as
 * one to a screen reader.
 *
 * The block margin is REPLACED rather than removed. Nothing in
 * rastrillo stacks the children of [rst-page] — there is no
 * `> * + *` rule — so what separated this card from the import form
 * above it was the <ul>'s own UA `margin-block: 1em`. Zeroing the
 * shorthand would have closed that gap as a side effect of fixing the
 * padding, so the margin is restated from the token scale, which is
 * where every other gap on the page comes from. */
[data-sheet-list] {
  list-style: none;
  margin-block: var(--rst-sp-4);
  margin-inline: 0;
  padding: 0;
}

/* [rst-lrow] is a grid, and --rst-cols is rastrillo's own hook for
 * saying what its columns are. Setting it is how a row gains a
 * thumbnail without overriding anything: without it the row has one
 * column and the picture stacks above the title. */
[data-sheet-list] [rst-lrow] {
  --rst-cols: 26px 1fr;
}

/* Below 800px rastrillo sets grid-template-columns on [rst-lrow]
 * DIRECTLY rather than through --rst-cols, so the hook above stops
 * being read and the thumbnail lands in the title's column with an
 * empty one trailing. This is (0,2,0) against the framework's (0,1,0),
 * so it wins wherever it sits in the file. */
@media (max-width: 800px) {
  [data-sheet-list] [rst-lrow] {
    grid-template-columns: 26px minmax(0, 1fr);
  }
}

[data-preview-slot] {
  display: block;
  block-size: 34px;
  inline-size: 26px;
}

[data-preview] {
  display: block;
}

/* The Format menu's two resize forms. A row rather than a stack: the
 * label, the number and the button read as one control, and the panel
 * is already 15rem wide enough to hold them. */
[data-size-form] {
  align-items: center;
  display: flex;
  gap: var(--rst-sp-2);
  padding: var(--rst-sp-1) var(--rst-sp-2);
}

/* nowrap is the point of the three rules below, and they are sized
 * backwards from it. The panel is 15rem, so a row has 224px of content
 * box; at 4rem of field and a button padded to its word, the label is
 * left about 100px and "Column width" sets on one line. At the numbers
 * this started with — a 4.5rem field and a default-padded button — it
 * had 48px and broke over two lines, and the two forms then disagreed
 * about their own height. */
/* A fixed label column, not flex: 1. The two forms are siblings and
 * cannot share a grid, so the only thing that lines their fields and
 * their two Set buttons up is both labels reserving the same width —
 * with flex: 1 each label took its own text's width and the rows sat
 * 33px out of step with each other. */
[data-size-form] label {
  color: var(--rst-text-muted);
  flex: 0 0 7rem;
  font-size: var(--rst-fs-sm);
  white-space: nowrap;
}

[data-size-form] input {
  inline-size: 4rem;
}

[data-size-form] button {
  padding-inline: var(--rst-sp-2);
}
