/*
 * Design system, deliberately small: a spacing scale, a handful of color
 * roles, one button base class with variants. This is a small church
 * spreadsheet tool, not a product with its own visual identity -- the aim
 * is "looks intentional and calm," not a from-scratch design language.
 * No build step: plain CSS, custom properties for the shared values.
 */
:root {
  /* Color roles */
  --color-bg: #f7f8fa;
  --color-surface: #ffffff;
  --color-border: #dde1e6;
  --color-border-strong: #c4cad3;
  --color-text: #1a202c;
  --color-text-muted: #667085;
  --color-accent: #2563eb;
  --color-accent-hover: #1d4ed8;
  --color-accent-bg: #eff6ff;
  --color-danger: #dc2626;
  --color-danger-hover: #b91c1c;

  /* Spacing scale (4px base) */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;

  /* Type scale */
  --font-size-xs: 0.75rem;
  --font-size-sm: 0.85rem;
  --font-size-base: 0.95rem;
  --font-size-lg: 1.15rem;
  --font-size-xl: 1.4rem;

  --radius: 6px;
  --radius-lg: 10px;
  --shadow-modal: 0 10px 30px rgba(16, 24, 40, 0.18);

  /* Kept for anything still referencing the old names */
  --border: var(--color-border);
  --accent: var(--color-accent);
  --bg: var(--color-surface);
  --muted: var(--color-text-muted);
}

* { box-sizing: border-box; }

/* html/body/#app need an explicit height chain so .sheet-page's
   height:100% (below) has something real to resolve against -- a
   percentage height is a no-op against an ancestor whose own height is
   just "auto" (content-driven). This is what lets the sheet view fill
   the actual viewport ("make the spreadsheet cells ui fill the window"). */
html, body { height: 100%; }
#app { height: 100%; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  font-size: var(--font-size-base);
  color: var(--color-text);
  background: var(--color-bg);
  line-height: 1.4;
}

h1, h2, h3 { margin: 0 0 var(--space-2); font-weight: 600; }
h1 { font-size: var(--font-size-xl); }
h2 { font-size: var(--font-size-lg); }
h3 { font-size: var(--font-size-base); color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.02em; font-weight: 600; }

.hidden { display: none !important; }
.muted { color: var(--color-text-muted); font-size: var(--font-size-sm); }
.error { color: var(--color-danger); font-size: var(--font-size-sm); }

/* --- Buttons -------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  height: 2.25rem;
  padding: 0 var(--space-3);
  margin: 0;
  font: inherit;
  font-size: var(--font-size-sm);
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  background: var(--color-accent);
  color: white;
  border: 1px solid var(--color-accent);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
.btn:hover { background: var(--color-accent-hover); border-color: var(--color-accent-hover); }
.btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.btn-secondary {
  background: var(--color-surface);
  color: var(--color-text);
  border-color: var(--color-border-strong);
}
.btn-secondary:hover { background: var(--color-bg); border-color: var(--color-text-muted); }

.btn-danger { background: var(--color-danger); border-color: var(--color-danger); }
.btn-danger:hover { background: var(--color-danger-hover); border-color: var(--color-danger-hover); }

.btn-small { height: 1.9rem; padding: 0 var(--space-2); font-size: var(--font-size-xs); }
.btn-icon { width: 2.25rem; padding: 0; font-family: Georgia, serif; }
.btn-block { width: 100%; }

/* Import CSV is a <label> wrapping a hidden file input, not a real
   <button> -- sharing .btn's exact box model (height/padding/line-height)
   is what keeps it the same size as the other toolbar buttons instead of
   inheriting a <label>'s own default sizing. */
.file-btn { position: relative; overflow: hidden; }
.file-btn input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }

.btn-color {
  height: 2.25rem;
  width: 2.25rem;
  padding: 2px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius);
  cursor: pointer;
  background: var(--color-surface);
}

.link {
  color: var(--color-accent);
  text-decoration: none;
  background: none;
  border: none;
  font: inherit;
  cursor: pointer;
  padding: 0;
}
.link:hover { text-decoration: underline; }

/* --- Layout ----------------------------------------------------------- */

.centered {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: var(--space-4);
}

.login-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: 100%;
  max-width: 340px;
  background: var(--color-surface);
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-border);
  box-shadow: 0 1px 3px rgba(16, 24, 40, 0.06);
}

input[type="text"], input[type="password"], select {
  height: 2.25rem;
  padding: 0 var(--space-3);
  font: inherit;
  font-size: var(--font-size-sm);
  border-radius: var(--radius);
  border: 1px solid var(--color-border-strong);
  background: var(--color-surface);
  color: var(--color-text);
}
input[type="text"]:focus, input[type="password"]:focus, select:focus {
  outline: 2px solid var(--color-accent);
  outline-offset: -1px;
  border-color: var(--color-accent);
}

.page { padding: var(--space-3) var(--space-4) var(--space-5); max-width: 1100px; margin: 0 auto; }

/* Sheet view fills the viewport instead of being width/height-capped
   like the list/login/etc. pages -- Fernando: "make the spreadsheet
   cells ui fill the window". Chrome (topbar/status-row/tab-nav/toolbar/
   formula-bar) keeps its natural height; .grid-scroll (below) is the
   only flexible child, taking flex:1 for whatever's left. overflow:
   hidden here so the page itself never scrolls -- only .grid-scroll
   does, internally. */
.page.sheet-page {
  max-width: none;
  margin: 0;
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.page.sheet-page > * { flex-shrink: 0; }
/* .grid-scroll's own flex-shrink:1 (below) has LOWER specificity (one
   class) than the blanket reset above (two classes, ".page.sheet-page >
   *"), so despite that rule's comment saying it "re-enables" shrink, the
   blanket reset was actually still winning -- .grid-scroll grew to fit
   all of its content instead of being clamped to the viewport, so it had
   nothing to shrink and therefore nothing to scroll (confirmed live:
   scrollHeight === clientHeight on a 60-row sheet in a real browser).
   This explicit override, matching the blanket rule's own specificity,
   is what actually makes the shrink take effect. */
.page.sheet-page > .grid-scroll { flex-shrink: 1; }

.topbar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border);
  margin-bottom: var(--space-2);
  flex-wrap: wrap;
}
.topbar h1 { margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* h1 no longer gets flex:1 itself -- .topbar-title (h1 + the saved
   indicator, moved here per Fernando: "the thing about how long ago the
   last save was to be next to the spreadsheet name") is the flexible
   element instead, so the two sit together as one unit next to the title
   rather than the saved indicator living in a separate row below.
   min-width was 0 -- allows the title to shrink all the way to nothing
   instead of ever triggering .topbar's flex-wrap:wrap, since flexbox only
   wraps once shrinking is actually blocked by a floor. At mobile widths
   this let the title vanish to 0px while .saved-indicator (flex-shrink:0,
   so it never shrinks) spilled out of the now-empty .topbar-title and
   visually overlapped .sheet-actions' buttons. A real floor here is what
   makes the row wrap onto two lines instead (title+saved above, actions
   below) once there's genuinely not enough room, matching what flex-wrap
   was already supposed to do. */
.topbar-title { display: flex; align-items: baseline; gap: var(--space-2); flex: 1; min-width: 160px; }
.saved-indicator { font-size: var(--font-size-xs); color: var(--color-text-muted); white-space: nowrap; flex-shrink: 0; }

/* flex-shrink:0 -- this shouldn't be squeezed by .topbar-title's own
   shrinking; once there's not enough room for both on one line, .topbar's
   flex-wrap:wrap should move this whole group to its own line instead,
   not partially compress it. */
.sheet-actions { display: flex; gap: var(--space-2); flex-wrap: wrap; flex-shrink: 0; }

/* Connection state (is the live socket up) and who's currently viewing --
   the "last saved" fact used to live in this row too, moved up next to
   the title above instead. */
.status-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
  flex-wrap: wrap;
}
.status-dot {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-size-xs);
  color: var(--color-text-muted);
}
.status-dot::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--color-border-strong);
}
.status-dot.status-live::before { background: #16a34a; }
.status-dot.status-offline::before { background: var(--color-text-muted); }
.status-dot.status-connecting::before { background: #d97706; }

/* [022] mitigation (BUGS_FOUND.md): shown only while this session's own
   WS connection is congestion-demoted to view-only (ws-server/session.py's
   TabSession active-editor cap) -- a session-level thing, unrelated to a
   genuine view-only permission (which never shows this, it has no toolbar
   to demote from in the first place). Same amber tone as the connecting
   status dot above -- "notice this, but it's not an error" register, not
   --color-danger. */
.congestion-banner {
  background: #fef3e2;
  border: 1px solid #d97706;
  color: #7c4a03;
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  font-size: var(--font-size-sm);
  margin-bottom: var(--space-3);
}
.congestion-banner[hidden] { display: none; }

/* Presence: who's viewing this spreadsheet right now (any tab, not just
   this one -- see app.js's renderPresence). Each viewer gets a stable
   per-session color (server-assigned, ws-server/presence.py), reused here
   for their name text AND for their cell-selection highlight in the grid
   below, so the same color visually ties a name to a highlighted cell.
   The activity dot's color is unrelated to that per-person color -- it's
   just green/yellow for active/idle, same meaning for everyone. */
.presence-list {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.presence-viewer {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-size-xs);
}
.presence-viewer.is-anonymous .presence-name { font-style: italic; }
.presence-activity-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
}
/* Green = active, or idle for less than the 2-minute grace period;
   yellow = idle for 2+ minutes but still connected (hasn't closed the
   page) -- see computeViewerActivity() in app.js. No elapsed-time text
   anywhere, per Fernando: "don't report last active time, just show the
   yellow dot." */
.presence-activity-dot.is-active { background: #16a34a; }
.presence-activity-dot.is-idle { background: #d97706; }

.inline-form { display: flex; gap: var(--space-2); margin-bottom: var(--space-4); }
.inline-form input { flex: 1; }

/* Grid's onNeedUserInfo dialog -- one labeled input per USERINFO field,
   stacked (unlike .inline-form's single row, this can hold any number of
   fields depending on what an ACTIONGROUP references). */
.userinfo-prompt-form { display: flex; flex-direction: column; gap: var(--space-3); }
.userinfo-prompt-field { display: flex; flex-direction: column; gap: var(--space-1); font-weight: 600; }
.userinfo-prompt-field input { font-weight: normal; }

.sheet-list, .tab-list, .history-list, .access-list { list-style: none; padding: 0; margin: 0; }
.sheet-list li, .tab-list li {
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}
.sheet-list li:hover, .tab-list li:hover { background: var(--color-bg); }
.sheet-list a, .tab-list a { text-decoration: none; color: var(--color-text); display: block; }
/* Duplicate/"Make a copy" button sits beside the link, same row -- matches
   the .access-list li layout rather than introducing a new pattern. */
.sheet-list li { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); }
/* min-width:0 -- without it, a flex item won't shrink below its own
   unwrapped content width, so a title with no spaces to wrap at (one long
   unbroken token) forced the whole row (and the Duplicate/"Make a copy"
   button next to it, and the whole page) into horizontal overflow instead
   of wrapping. overflow-wrap/word-break:break-word preserves the existing
   multi-line-wrap-at-spaces behavior for normal titles unchanged, while
   also allowing a mid-word break specifically for the unbreakable-token
   case that was overflowing. */
.sheet-list a { flex: 1; min-width: 0; overflow-wrap: break-word; word-break: break-word; }

.history-list li, .access-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
  font-size: var(--font-size-sm);
}
.history-list li:last-child, .access-list li:last-child { border-bottom: none; }
/* min-width:0 + wrapping so a long "name (username) -- level" line wraps
   instead of pushing Revoke/the row's other button off the edge of a
   narrow modal (same overflow-instead-of-wrap issue as the Share form
   above -- .access-list li's own flex row otherwise lets this span refuse
   to shrink below its full unwrapped width). */
.access-list li > span:first-child { min-width: 0; white-space: normal; word-break: break-word; }

.tab-nav {
  display: flex;
  gap: var(--space-1);
  overflow-x: auto;
  margin-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-border);
}
.tab-nav a {
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius) var(--radius) 0 0;
  background: transparent;
  color: var(--color-text-muted);
  text-decoration: none;
  white-space: nowrap;
  font-size: var(--font-size-sm);
  border: 1px solid transparent;
  border-bottom: none;
  margin-bottom: -1px;
}
.tab-nav a:hover { color: var(--color-text); }
.tab-nav a.active {
  background: var(--color-surface);
  color: var(--color-accent);
  font-weight: 600;
  border-color: var(--color-border);
}

/* Cross-tab presence: "if that user is viewing another tab, they should
   still appear, but add their color dot to the tab name display they are
   on" -- rendered on every tab (including the active one, for consistent
   treatment) since the roster is spreadsheet-wide, not just for the tab
   currently displayed. */
.tab-nav a { display: inline-flex; align-items: center; gap: var(--space-1); }
.tab-presence-dots { display: inline-flex; gap: 2px; }
.tab-presence-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }

.toolbar {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
  flex-wrap: wrap;
  align-items: center;
}
.toolbar-spacer { flex: 1; }
.toolbar-select {
  height: 2rem;
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  font: inherit;
  font-size: var(--font-size-sm);
  color: var(--color-text);
}

/* Formula bar: classic spreadsheet UX, sits between the toolbar and the
   grid -- shows/edits the raw value of the current selection. */
.formula-bar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.formula-ref {
  min-width: 3.5rem;
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  text-align: right;
}
.formula-input {
  flex: 1;
  height: 2rem;
  padding: 0 var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font: inherit;
  font-size: var(--font-size-sm);
}
.formula-input:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }

/* Floating quick-actions menu for the currently-viewed tab (Export/Import
   CSV, History, Rename) -- positioned in JS relative to its anchor button. */
.tab-menu {
  position: absolute;
  z-index: 10;
  display: flex;
  flex-direction: column;
  min-width: 160px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  padding: var(--space-1);
}
.tab-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: var(--font-size-sm);
  background: none;
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
  color: var(--color-text);
}
.tab-menu-item:hover { background: var(--color-bg); }

/* Transient toast for the context menu's "Show browser menu" hint
   ("Right-click again for the browser menu") -- not interactive, just a
   brief pointer near the cursor, auto-removed by showTransientHint(). */
.context-hint {
  position: absolute;
  z-index: 10;
  background: var(--color-text);
  color: var(--color-surface);
  font-size: var(--font-size-xs);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius);
  box-shadow: var(--shadow-modal);
  pointer-events: none;
}
/* .file-btn (already applied alongside .tab-menu-item on the Import CSV
   item) already provides the position/overflow/hidden-input handling. */

.manage-tabs-list { margin-bottom: var(--space-3); }
.manage-tabs-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
  font-size: var(--font-size-sm);
}
.manage-tabs-list li:last-child { border-bottom: none; }
.manage-tabs-current { font-weight: 600; color: var(--color-accent); }
.manage-tabs-controls { display: flex; gap: var(--space-1); flex-shrink: 0; }

/* --- Grid --------------------------------------------------------- */

.grid-scroll {
  overflow: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  /* flex:1 + min-height:0 (not max-height:70vh, the old fixed cap) is
     what makes this fill exactly the space .sheet-page's other children
     don't use. min-height:0 overrides the flex-item default of
     min-height:auto, which would otherwise refuse to let this shrink
     below its content's natural size and break the fill -- classic
     flexbox gotcha. flex-shrink is re-enabled here specifically,
     overriding the blanket flex-shrink:0 set on all of .sheet-page's
     other (fixed-height chrome) children. */
  flex: 1 1 auto;
  min-height: 0;
  max-width: 100%;
  background: var(--color-surface);
}

table.grid {
  border-collapse: collapse;
  /* Copying is handled explicitly (Ctrl+C, see grid.js) -- the grid
     doesn't need native text selection, and disabling it is what stops
     drag-to-select-a-range from also highlighting text as a side effect. */
  user-select: none;
  -webkit-user-select: none;
  /* table-layout:fixed makes <colgroup><col style="width:Npx"> a hard
     constraint instead of a hint. Without this (the previous state --
     table-layout defaults to "auto"), the browser's auto-layout algorithm
     redistributes column widths to fit whatever space is available in
     the containing .grid-scroll instead of respecting the literal pixel
     values grid.js sets -- that's why growing one column visibly shrank
     the others ("resizes all other cols to fit in the current view"):
     the browser was fighting the explicit widths to make everything fit,
     not honoring them. auto-layout is also inherently more expensive
     (content-aware sizing across every column on relevant changes),
     compounding the resize-drag performance issue on top of the
     per-mousemove DOM-write-count fix from an earlier pass.
     grid.js keeps this element's own `width` in sync with the sum of all
     column widths (see _syncTableWidth) so the table can legitimately
     become wider than .grid-scroll and trigger its existing
     overflow:auto for horizontal scroll, instead of being squeezed to
     fit -- table-layout:fixed alone isn't enough without that pairing:
     with no explicit table width, "fixed" layout still falls back to
     fitting available space when the intrinsic column-width sum isn't
     otherwise pinned down. */
  table-layout: fixed;
}
/* No fixed min-width/height here -- column widths come from the
   <colgroup> and row heights from each <tr>'s inline style, both set in
   grid.js (default 96px/28px, user-resizable via the drag handles below).
   A CSS min-width/height here would fight that: table layout treats it as
   a floor, so a column resized narrower than a leftover CSS min-width
   would silently refuse to actually shrink in the browser. */
table.grid th, table.grid td {
  border: 1px solid var(--color-border);
  padding: 2px 6px;
  font-size: var(--font-size-sm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Browsers' default table-cell vertical-align is "middle", not "top" --
     Fernando: "the text should be cut off top left centered" (read as:
     anchored top-left, clipped rather than growing the cell). Scoped to
     both th/td here since top-aligned header labels are harmless too;
     text-align:left is td-only below so column-letter/row-number header
     labels keep their own (centered) look. */
  vertical-align: top;
}
table.grid td { text-align: left; }
table.grid td.wrap { white-space: normal; word-break: break-word; }
/* Stand-ins for a whole range of currently-unmounted rows/columns under
   grid virtualization (see grid.js's _renderWindow/_makeSpacerRow) --
   pure layout filler, not a real cell, so none of the normal cell chrome
   (border, padding, edit-affordance cursor) applies. */
table.grid td.grid-spacer-cell { border: none; padding: 0; cursor: default; }
/* Cell text/buttons render inside this wrapper, not the <td> directly --
   see grid.js _renderCell()'s comment: a <td>'s own explicit height is
   only a floor in table layout (same as a <tr height>), so overflow:hidden
   on the <td> itself doesn't reliably clip a large font's natural line
   box. A normal block child with its own explicit height (set inline per
   row, see grid.js) has no such table-layout floor semantics -- clips
   correctly regardless of font size. white-space/word-break/text-overflow
   are inherited from the td rules above, no need to repeat them here. */
table.grid .cell-content { display: block; }
table.grid thead th, table.grid tbody th {
  background: var(--color-bg);
  position: sticky;
  font-weight: 500;
  color: var(--color-text-muted);
}
table.grid thead th { top: 0; z-index: 2; }
table.grid tbody th { left: 0; z-index: 1; }
table.grid thead th:first-child { left: 0; z-index: 3; }

table.grid td { cursor: cell; position: relative; }
/* cursor:cell (a small pencil-adjacent glyph in most browsers) is itself
   an edit affordance -- a read-only grid still needs to support
   selecting and copying (see Grid._build()'s .grid-readonly class), just
   without implying you can type into it. Selection highlighting
   (td.selected below) stays identical either way. */
.grid-readonly table.grid td { cursor: default; }
table.grid td.selected { outline: 2px solid var(--color-accent); outline-offset: -2px; background: var(--color-accent-bg); }
/* Another viewer's selection -- --remote-color is set inline per cell
   (app.js's renderPresence(), since each viewer's color is server-
   assigned per session, not something a stylesheet can know ahead of
   time). box-shadow (inset), not outline, deliberately: outline is
   already used by .selected above, and an element only gets one outline
   -- box-shadow can coexist with it if the local user and a remote viewer
   both land on the same cell. If more than one remote viewer selects the
   same cell, the later one in the roster wins the highlight color rather
   than layering multiple shadows -- acceptable at this app's scale (see
   CELL_SCHEMA.md / ws-server presence docs), not attempting to render N
   simultaneous colors on one cell. */
table.grid td.remote-selected { box-shadow: inset 0 0 0 2px var(--remote-color); }
table.grid td.bold { font-weight: 700; }
table.grid td.italic { font-style: italic; }
table.grid td.underline { text-decoration: underline; }

/* Column/row resize drag handles -- thin invisible strips on the trailing
   edge of each header cell, widened visually only on hover so they don't
   clutter a 30-column header by default.
   These used to be centered ON the boundary line (right/bottom: -3px,
   width/height: 6px), straddling half into the next header cell's own
   box. That half was never actually hit-testable: table.grid thead th /
   tbody th are `position: sticky` with explicit, non-auto z-index values
   (own stacking contexts), and sibling header cells share the same
   z-index, so ties break by DOM order -- the next column/row's entire
   stacking context (its whole box, not just its content) paints over
   anything from the previous header cell that overflows into its
   rect, regardless of this handle's own z-index. Keeping the strip
   flush with (not straddling) the trailing edge -- right/bottom: 0
   instead of -3px -- keeps the whole hit area inside the current header
   cell's own box, where it's reliably on top, while still visually
   ending exactly on the boundary line. */
table.grid th { position: relative; }
.col-resize-handle {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 6px;
  cursor: col-resize;
  z-index: 4;
}
.row-resize-handle {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 6px;
  cursor: row-resize;
  z-index: 4;
}
/* Only a device with genuine hover capability (a mouse) gets the
   thicker-border affordance -- touch resize was never implemented (see
   _onTouchStart's deliberate touch-lands-on-a-resize-handle fallthrough
   to normal drag-select), so showing this same "you can drag this"
   affordance on a touch tap would be misleading: real iPhone testing
   found a tap on the handle lights up this hover style, then the
   fallthrough drag-select takes over and grows the selection instead of
   resizing anything, with no visual explanation of why. `:hover`
   ordinarily wouldn't even apply on a touchscreen, except mobile Safari/
   Chrome simulate a "sticky" :hover on tap for compatibility with
   hover-authored sites -- scoping these rules to `(hover: hover)` opts
   back out of that simulation without touching the resize-arms-
   selection behavior itself, which lives entirely in JS drag-start
   handling and is untouched here. */
@media (hover: hover) {
  .col-resize-handle:hover { background: var(--color-accent); opacity: 0.5; }
  .row-resize-handle:hover { background: var(--color-accent); opacity: 0.5; }
}

.cell-input {
  width: 100%;
  height: 100%;
  border: none;
  outline: none;
  font: inherit;
  padding: 0;
  /* Editing a cell is the one place inside the grid where normal text
     selection should work again (selecting text to retype part of it). */
  user-select: text;
  -webkit-user-select: text;
}

/* An ACTIONGROUP(buttonText, ...) cell -- see grid.js
   _renderActionGroupCell() / CELL_SCHEMA.md. Sized to fill the cell like
   .cell-input above, not the standalone .btn's fixed height -- a table
   cell is usually shorter than a normal toolbar button. */
.actiongroup-btn {
  width: 100%;
  height: 100%;
  padding: 0 var(--space-1);
  border: 1px solid var(--color-accent);
  border-radius: 3px;
  background: var(--color-accent-bg);
  color: var(--color-accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.actiongroup-btn:hover:not(:disabled) { background: var(--color-accent); color: #fff; }
.actiongroup-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* --- Modals --------------------------------------------------------- */

.modal {
  position: fixed;
  inset: 0;
  background: rgba(16, 24, 40, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  padding: var(--space-4);
}
.modal-content {
  background: var(--color-surface);
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-modal);
  width: 100%;
  max-width: 480px;
  max-height: 80vh;
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.modal-content > .btn { align-self: flex-start; }

.share-section { padding-bottom: var(--space-3); border-bottom: 1px solid var(--color-border); }
.share-section:last-of-type { border-bottom: none; padding-bottom: 0; }
.share-section select { margin-top: var(--space-2); }
/* flex-wrap:wrap -- the global .inline-form is a single non-wrapping row
   (fine for its other uses, e.g. the 2-element create-spreadsheet form),
   but this one has three items (username input + access-level select +
   Share button) that don't all fit at mobile widths. .modal-content's own
   overflow:auto silently let that overflow become invisible horizontal
   scroll instead -- Share (and Revoke on any already-listed access row,
   pushed by the same modal-wide overflow) ended up completely
   unreachable off-screen with no visible scroll affordance. min-width:0
   on the input lets it actually shrink/wrap instead of refusing to go
   below its unshrunk content width. */
.share-section .inline-form { margin-top: var(--space-2); margin-bottom: 0; flex-wrap: wrap; }
/* min-width:0 (no floor) let this shrink to ~4-5 visible characters on
   mobile instead of ever actually wrapping -- same min-width-blocks-wrap
   mechanism as .topbar-title (line 236), same fix: a real floor forces
   the row to wrap once the other items (select + button) don't fit
   alongside it anymore, instead of squeezing the input first. */
.share-section .inline-form input { min-width: 120px; flex: 1 1 auto; }

.share-link-row { display: flex; gap: var(--space-2); margin-top: var(--space-2); }
.share-link-input {
  flex: 1 1 auto;
  min-width: 0;
  font-family: inherit;
  color: var(--color-text-muted, inherit);
}
.share-link-row .btn { flex: 0 0 auto; }

/* Formula help dialog -- reuses .modal/.modal-content, just needs more
   width than the default 480px for a legible function list. */
.modal-content-wide { max-width: 640px; }
/* Darker/thicker than .formula-help-item's own 1px border below -- that
   one separates individual entries within a section, this one separates
   whole sections (intro/references, Functions, Actions) and needs to read
   as a stronger break at a glance, not just another item boundary. */
.formula-help-divider { border: none; border-top: 2px solid var(--color-border-strong); margin: var(--space-4) 0; }
.formula-help-list { display: flex; flex-direction: column; gap: var(--space-3); }
.formula-help-item {
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}
.formula-help-item:last-child { border-bottom: none; padding-bottom: 0; }
.formula-help-item code { font-weight: 600; }
.formula-help-item p { margin: var(--space-1) 0; color: var(--color-text-muted); }
.formula-help-example {
  display: block;
  background: var(--color-bg);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius);
  font-size: var(--font-size-sm);
}

/* Hidden by default (desktop): click-outside-to-close is a familiar
   pattern there. Must come before the mobile media query below --
   equal-specificity rules resolve by source order regardless of which
   one is inside a matching @media block, so this base rule has to be
   first or it would always win and the mobile override below would
   never take effect. */
.modal-close-hint { display: none; margin: 0; font-size: var(--font-size-xs); color: var(--color-text-muted); text-align: center; }

/* Mobile: bigger tap targets, stacked toolbar. */
@media (max-width: 767px) {
  .page { padding: var(--space-2) var(--space-3) var(--space-4); }
  /* Deliberately overrides .btn-small's own 1.9rem too (same specificity,
     wins by appearing later in the stylesheet) -- reviewed and confirmed
     intentional, not an oversight: the whole point of this rule is
     uniformly bigger tap targets on mobile, and a "small" button is
     exactly the kind of target most likely to be mis-tapped at finger
     size if left at its smaller desktop height. Not changing this. */
  .btn, .btn-color { height: 2.5rem; }
  .topbar h1 { font-size: var(--font-size-lg); }
  .modal-content { padding: var(--space-4); }
  .modal-close-hint { display: block; }
  /* Tab quick-actions menu items (Export/Import CSV, History, Rename) were
     left out of the mobile bigger-tap-target treatment above -- at
     ~150x35px they sit below the ~44x44pt (iOS) / 48x48dp (Android)
     platform-recommended minimum. Bumping to a 44px min-height (with
     flex/align-items to vertically center the label regardless of its
     line-height) brings them in line with the same tap-target goal the
     .btn/.btn-color rule above already applies elsewhere. */
  .tab-menu-item {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: var(--space-2) var(--space-3);
  }
}
