| 1 | /* Sheets' own chrome, on top of rastrillo's tokens. |
| 2 | * |
| 3 | * Every colour here is a token, never a literal — a literal does not |
| 4 | * follow the person's light/dark choice, and cell fills resolve against |
| 5 | * the theme, so one hard-coded grey stays wrong in one of the two |
| 6 | * schemes forever. |
| 7 | * |
| 8 | * Token names are checked mechanically by |
| 9 | * internal/guardstest/csstokens_test.go. Five invented ones shipped |
| 10 | * here first (--rst-border, --rst-text-mut, --rst-warning, |
| 11 | * --rst-font-mono, --rst-shell-bar-h): CSS resolves an undefined custom |
| 12 | * property to its fallback in silence, so the page looked plausible and |
| 13 | * was simply not themed. The real names are --rst-line, |
| 14 | * --rst-text-muted, --rst-tone-warning-fg and --rst-font. |
| 15 | */ |
| 16 | |
| 17 | /* The grid surface fills what is left of the viewport under the bar. |
| 18 | * |
| 19 | * The shell does that part now: the grid page renders |
| 20 | * <div estilo-shell="fill">, and estilo's chrome.css makes that shell |
| 21 | * exactly the viewport, holds the bar at its 52px and gives the grid's |
| 22 | * <main> the rest (flex: 1, min-block-size: 0). This rule used to be |
| 23 | * [rst-shell-topbar]:has([data-sheet-surface]) { height: 100dvh } here, |
| 24 | * keyed on rastrillo's wrapper, and before that a guessed topbar height |
| 25 | * that made a 771px document in a 720px viewport. What is left is the |
| 26 | * surface's own column: the chrome strips and the grid. */ |
| 27 | [data-sheet-surface] { |
| 28 | display: flex; |
| 29 | flex-direction: column; |
| 30 | } |
| 31 | |
| 32 | /* ── The chrome ────────────────────────────────────────────────────── |
| 33 | * |
| 34 | * Three strips around the grid, and a budget: the whole lot is about |
| 35 | * 7rem above the grid and 2rem below, against Google's ~9.5rem, because |
| 36 | * the document title sits in the app's own top bar rather than in a row |
| 37 | * of its own. |
| 38 | * |
| 39 | * Row B — the menu bar and the toolbar, sharing one row. Six short |
| 40 | * words are about 300px, so a row holding only the menus wastes |
| 41 | * two-thirds of itself. They wrap onto separate rows when they no |
| 42 | * longer fit, which is also what keeps this inside a 320px viewport |
| 43 | * with no horizontal scroll. */ |
| 44 | [data-chrome] { |
| 45 | display: flex; |
| 46 | align-items: center; |
| 47 | flex-wrap: wrap; |
| 48 | gap: var(--rst-sp-1) var(--rst-sp-3); |
| 49 | padding: 0 var(--rst-sp-3); |
| 50 | border-bottom: 1px solid var(--rst-line); |
| 51 | background: var(--rst-surface); |
| 52 | color: var(--rst-text); |
| 53 | } |
| 54 | |
| 55 | /* The menu bar is estilo's ([estilo-menubar], [estilo-menu], estilo |
| 56 | * menu.css): square titles in the page flow, 9px floating panels, inset |
| 57 | * rows and a shortcut column. What was here — rastrillo dropdowns |
| 58 | * repositioned with an anchor-positioning override, 6px rounded titles |
| 59 | * with no hover, a 17rem panel and inline accordion submenus — went with |
| 60 | * it. Only what is Sheets' own is left below. */ |
| 61 | |
| 62 | /* An item that needs commands.js is rendered hidden until it can run. |
| 63 | * estilo's rows are display: flex, which is an author rule and beats the |
| 64 | * user agent's [hidden] { display: none } however weak its selector |
| 65 | * looks, so hidden is restated. */ |
| 66 | [data-menubar] [estilo-menu] > [hidden] { |
| 67 | display: none; |
| 68 | } |
| 69 | |
| 70 | /* The Format menu's two resize forms sit in the panel as rows: the same |
| 71 | * inline padding as estilo's items, so their labels line up with the |
| 72 | * item text above and below. */ |
| 73 | [data-menubar] [estilo-menu] > [data-size-form] { |
| 74 | padding: var(--rst-sp-1) var(--estilo-item-padding-inline, 0.65rem); |
| 75 | } |
| 76 | |
| 77 | /* ── The keyboard shortcuts dialog ────────────────────────────────── |
| 78 | * |
| 79 | * Opened from the account menu's Help. A native modal <dialog>, so it is |
| 80 | * in the top layer above the bar; a float, so it takes estilo's float |
| 81 | * radius and shadow. */ |
| 82 | [data-shortcuts] { |
| 83 | inline-size: min(26rem, calc(100vw - 2rem)); |
| 84 | padding: var(--rst-sp-4); |
| 85 | border: 1px solid var(--rst-line); |
| 86 | border-radius: var(--estilo-float-radius, 9px); |
| 87 | background: var(--rst-surface); |
| 88 | color: var(--rst-text); |
| 89 | box-shadow: var(--rst-shadow-pop); |
| 90 | } |
| 91 | |
| 92 | [data-shortcuts]::backdrop { |
| 93 | background: color-mix(in oklab, var(--rst-text) 30%, transparent); |
| 94 | } |
| 95 | |
| 96 | [data-shortcuts] h2 { |
| 97 | margin: 0 0 var(--rst-sp-3); |
| 98 | font-size: 1.125rem; |
| 99 | } |
| 100 | |
| 101 | [data-shortcuts] dl { |
| 102 | align-items: baseline; |
| 103 | display: grid; |
| 104 | grid-template-columns: auto 1fr; |
| 105 | gap: var(--rst-sp-1) var(--rst-sp-4); |
| 106 | margin: 0; |
| 107 | } |
| 108 | |
| 109 | [data-shortcuts] dt { |
| 110 | text-align: end; |
| 111 | } |
| 112 | |
| 113 | [data-shortcuts] dt kbd { |
| 114 | font-family: var(--rst-font); |
| 115 | font-size: var(--rst-fs-sm); |
| 116 | font-variant-numeric: tabular-nums; |
| 117 | } |
| 118 | |
| 119 | [data-shortcuts] dd { |
| 120 | color: var(--rst-text-muted); |
| 121 | font-size: var(--rst-fs-sm); |
| 122 | margin: 0; |
| 123 | } |
| 124 | |
| 125 | [data-shortcuts-foot] { |
| 126 | display: flex; |
| 127 | justify-content: flex-end; |
| 128 | margin-block-start: var(--rst-sp-4); |
| 129 | } |
| 130 | |
| 131 | /* ── The toolbar ───────────────────────────────────────────────────── |
| 132 | * |
| 133 | * `rst-btn="compact"` is not a rastrillo variant — the word appears in |
| 134 | * tokens.css only inside comments — so a toolbar built on it would be |
| 135 | * silently unstyled. These buttons are ours. |
| 136 | * |
| 137 | * 1.75rem square, clear of the 24px minimum target, and the pressed |
| 138 | * state is a box-shadow ring rather than a border: a border costs |
| 139 | * layout, and eleven of them would widen the strip by 22px against a |
| 140 | * hard 320px reflow budget. A full ring rather than an underline, |
| 141 | * because a one-sided inset shadow follows the border radius round the |
| 142 | * corners and reads as a tab stem. */ |
| 143 | [data-toolbar] { |
| 144 | align-items: center; |
| 145 | display: flex; |
| 146 | flex-wrap: wrap; |
| 147 | gap: var(--rst-sp-1); |
| 148 | padding: var(--rst-sp-1) 0; |
| 149 | } |
| 150 | |
| 151 | /* Square (the suite's square rule, spec §11 and Paul's §13 answer 4): |
| 152 | * these buttons are borderless, so their hover and pressed fill is an |
| 153 | * in-flow highlight, not a bordered control. They were 6px. */ |
| 154 | [data-tool] { |
| 155 | align-items: center; |
| 156 | background: transparent; |
| 157 | border: 0; |
| 158 | border-radius: 0; |
| 159 | color: var(--rst-text-muted); |
| 160 | cursor: pointer; |
| 161 | display: inline-flex; |
| 162 | font: inherit; |
| 163 | font-size: var(--rst-fs-sm); |
| 164 | gap: 0.1rem; |
| 165 | justify-content: center; |
| 166 | min-block-size: 1.75rem; |
| 167 | min-inline-size: 1.75rem; |
| 168 | padding: 0 var(--rst-sp-1); |
| 169 | /* [rst-tip]::after is absolutely positioned and rastrillo does not |
| 170 | * position the host, so the tooltip would anchor to the page. */ |
| 171 | position: relative; |
| 172 | } |
| 173 | |
| 174 | [data-tool]:hover { |
| 175 | background: var(--rst-surface-2); |
| 176 | color: var(--rst-text); |
| 177 | } |
| 178 | |
| 179 | [data-tool][aria-pressed="true"] { |
| 180 | background: var(--rst-accent-soft); |
| 181 | box-shadow: inset 0 0 0 1px currentColor; |
| 182 | color: var(--rst-accent); |
| 183 | } |
| 184 | |
| 185 | [data-tool]:focus-visible { |
| 186 | outline: 2px solid var(--rst-accent); |
| 187 | outline-offset: 2px; |
| 188 | } |
| 189 | |
| 190 | [data-tool] svg { |
| 191 | block-size: 1rem; |
| 192 | fill: none; |
| 193 | inline-size: 1rem; |
| 194 | stroke: currentColor; |
| 195 | stroke-linecap: round; |
| 196 | stroke-width: 1.5; |
| 197 | } |
| 198 | |
| 199 | [data-tool-rule] { |
| 200 | align-self: stretch; |
| 201 | border-inline-start: 1px solid var(--rst-line); |
| 202 | margin: var(--rst-sp-1) var(--rst-sp-1); |
| 203 | } |
| 204 | |
| 205 | [data-tool-menu] > summary { |
| 206 | padding: 0; |
| 207 | } |
| 208 | |
| 209 | [data-tool-menu] > summary::-webkit-details-marker { |
| 210 | display: none; |
| 211 | } |
| 212 | |
| 213 | /* The same pair as the menu bar's, for the same reason and with the |
| 214 | * same two branches. A toolbar's colour picker opens under its own |
| 215 | * button, not off the right edge of the window. */ |
| 216 | [data-tool-menu] [rst-dropdown-menu] { |
| 217 | inset-inline-start: 0; |
| 218 | inset-inline-end: auto; |
| 219 | } |
| 220 | |
| 221 | @supports (position-area: block-end) and (position-try-fallbacks: flip-block) { |
| 222 | [data-tool-menu] [rst-dropdown-menu] { |
| 223 | inset: auto; |
| 224 | position-area: block-end span-inline-end; |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | /* A picker's tooltip names the button; once its panel is open it only |
| 229 | * covers the panel's first row ("No fill"), so it steps aside. */ |
| 230 | [data-tool-menu][open] > [rst-tip]::after { |
| 231 | opacity: 0; |
| 232 | } |
| 233 | |
| 234 | [data-tool] [rst-caret] svg { |
| 235 | block-size: 0.75rem; |
| 236 | inline-size: 0.75rem; |
| 237 | } |
| 238 | |
| 239 | /* The two colour chips on the toolbar show what the anchor cell has, |
| 240 | * so the button says what it will do rather than only what it is. |
| 241 | * --sheet-h and --sheet-c are set by commands.js from the selection. */ |
| 242 | [data-fill-chip], |
| 243 | [data-ink-chip] { |
| 244 | border-radius: 2px; |
| 245 | display: block; |
| 246 | inline-size: 1rem; |
| 247 | line-height: 1; |
| 248 | text-align: center; |
| 249 | } |
| 250 | |
| 251 | [data-fill-chip] { |
| 252 | block-size: 1rem; |
| 253 | border: 1px solid var(--rst-line); |
| 254 | background: var(--rst-surface-2); |
| 255 | } |
| 256 | |
| 257 | [data-fill-chip][data-set] { |
| 258 | background: oklch(var(--sheet-fill-l) var(--sheet-c) var(--sheet-h)); |
| 259 | } |
| 260 | |
| 261 | [data-ink-chip] { |
| 262 | border-bottom: 3px solid var(--rst-text-muted); |
| 263 | font-weight: 700; |
| 264 | } |
| 265 | |
| 266 | [data-ink-chip][data-set] { |
| 267 | border-bottom-color: oklch(var(--sheet-ink-l) var(--sheet-kc) var(--sheet-kh)); |
| 268 | } |
| 269 | |
| 270 | /* A swatch button is its colour with its name beside it. Named, not |
| 271 | * colour alone: a picker that only shows colour is unusable to somebody |
| 272 | * who cannot tell two of them apart, and the name is also what the |
| 273 | * server is sent. */ |
| 274 | [data-swatches] { |
| 275 | min-inline-size: 11rem; |
| 276 | } |
| 277 | |
| 278 | /* A swatch button is its colour and then its name, reading left to |
| 279 | * right as one thing. The menu's general rule pushes an item's trailing |
| 280 | * content to the far end so a keyboard shortcut lines up in a column — |
| 281 | * right for "Bold … Ctrl B", wrong here, where it stranded each name |
| 282 | * against the opposite edge from its own colour. |
| 283 | * |
| 284 | * Written at the full depth on purpose: that general rule is |
| 285 | * [data-menubar] [rst-dropdown-menu] button, which is (0,2,1), and the |
| 286 | * obvious [data-swatches] button is (0,1,1) and loses. */ |
| 287 | [data-menubar] [rst-dropdown-menu] [data-swatches] button { |
| 288 | gap: 0; |
| 289 | justify-content: flex-start; |
| 290 | } |
| 291 | |
| 292 | [data-swatch]::before { |
| 293 | border: 1px solid var(--rst-line); |
| 294 | border-radius: 2px; |
| 295 | content: ""; |
| 296 | display: inline-block; |
| 297 | block-size: 0.85rem; |
| 298 | inline-size: 0.85rem; |
| 299 | margin-inline-end: var(--rst-sp-2); |
| 300 | vertical-align: -2px; |
| 301 | background: oklch(var(--sheet-fill-l) var(--sheet-c) var(--sheet-h)); |
| 302 | } |
| 303 | |
| 304 | [data-ink-swatch]::before { |
| 305 | background: oklch(var(--sheet-ink-l) var(--sheet-kc) var(--sheet-kh)); |
| 306 | } |
| 307 | |
| 308 | /* ── Row C: the name box and the formula bar ───────────────────────── |
| 309 | * |
| 310 | * The formula bar gets the whole row and all the slack. It used to |
| 311 | * share a strip with five links and was capped at 40rem so they had |
| 312 | * somewhere to sit; on its own row a cap only truncates long formulas |
| 313 | * for no gain. */ |
| 314 | [data-formula-row] { |
| 315 | align-items: center; |
| 316 | background: var(--rst-surface); |
| 317 | border-bottom: 1px solid var(--rst-line); |
| 318 | display: flex; |
| 319 | gap: var(--rst-sp-2); |
| 320 | padding: var(--rst-sp-1) var(--rst-sp-3); |
| 321 | } |
| 322 | |
| 323 | [data-name-box] { |
| 324 | flex: 0 0 auto; |
| 325 | inline-size: 6rem; |
| 326 | text-align: center; |
| 327 | } |
| 328 | |
| 329 | [data-fx] { |
| 330 | color: var(--rst-text-faint); |
| 331 | flex: 0 0 auto; |
| 332 | font-size: var(--rst-fs-sm); |
| 333 | } |
| 334 | |
| 335 | [data-formula-input] { |
| 336 | flex: 1; |
| 337 | min-inline-size: 0; |
| 338 | } |
| 339 | |
| 340 | /* ── Row D: the tab strip ──────────────────────────────────────────── |
| 341 | * |
| 342 | * One tab today, and deliberately no "+": nothing in this build |
| 343 | * creates, names or switches a tab, and spec §14 makes "a second tab is |
| 344 | * reachable in the UI" the trigger for multi-worksheet import. */ |
| 345 | [data-tabstrip] { |
| 346 | align-items: stretch; |
| 347 | background: var(--rst-surface-2); |
| 348 | border-top: 1px solid var(--rst-line); |
| 349 | display: flex; |
| 350 | gap: var(--rst-sp-3); |
| 351 | min-block-size: 2rem; |
| 352 | padding-inline: var(--rst-sp-3); |
| 353 | } |
| 354 | |
| 355 | [data-sheet-tabs] { |
| 356 | display: flex; |
| 357 | } |
| 358 | |
| 359 | [data-sheet-tabs] a { |
| 360 | align-items: center; |
| 361 | border-bottom: 2px solid transparent; |
| 362 | color: var(--rst-text-muted); |
| 363 | display: flex; |
| 364 | font-size: var(--rst-fs-sm); |
| 365 | padding-inline: var(--rst-sp-3); |
| 366 | text-decoration: none; |
| 367 | } |
| 368 | |
| 369 | [data-sheet-tabs] a:hover { |
| 370 | color: var(--rst-text); |
| 371 | } |
| 372 | |
| 373 | [data-sheet-tabs] a[aria-current] { |
| 374 | background: var(--rst-surface); |
| 375 | border-bottom-color: var(--rst-accent); |
| 376 | color: var(--rst-text); |
| 377 | font-weight: 600; |
| 378 | } |
| 379 | |
| 380 | /* The size of the selection, not a sum of it. A total would be the |
| 381 | * client doing arithmetic on strings the server rendered, which is the |
| 382 | * one thing this design keeps out of JavaScript. */ |
| 383 | [data-selection-size] { |
| 384 | align-self: center; |
| 385 | color: var(--rst-text-faint); |
| 386 | font-size: var(--rst-fs-xs); |
| 387 | } |
| 388 | |
| 389 | /* The transient note. Empty almost always, and the app's only way of |
| 390 | * saying something happened or did not: today, that a browser refused |
| 391 | * the menu's clipboard read, or that a paste is larger than one write |
| 392 | * can carry. |
| 393 | * |
| 394 | * It takes the slack rather than the live indicator, which is why the |
| 395 | * auto margin moved here — otherwise a note appearing shoves the |
| 396 | * connection state sideways every time one is shown. Truncates rather |
| 397 | * than wrapping: this strip is 2rem and a two-line message would push |
| 398 | * the grid. */ |
| 399 | /* A note the person has to act on. Muted grey in a strip under the |
| 400 | * grid is the right weight for "12 cells were not pasted" and the |
| 401 | * wrong one for "the menu could not paste, use the keyboard" — the |
| 402 | * second went unread on the live instance while the menu item read as |
| 403 | * simply broken. */ |
| 404 | [data-sheet-note][data-tone="warn"] { |
| 405 | color: var(--rst-tone-warning-fg); |
| 406 | font-weight: 550; |
| 407 | } |
| 408 | |
| 409 | [data-sheet-note] { |
| 410 | align-self: center; |
| 411 | color: var(--rst-text-muted); |
| 412 | font-size: var(--rst-fs-xs); |
| 413 | margin-inline-start: auto; |
| 414 | overflow: hidden; |
| 415 | text-overflow: ellipsis; |
| 416 | white-space: nowrap; |
| 417 | } |
| 418 | |
| 419 | /* The live indicator. It says connected or reconnecting and nothing |
| 420 | * else — presence is a later slice, and a dot that claims more than it |
| 421 | * knows is worse than no dot. */ |
| 422 | [data-sheet-live] { |
| 423 | align-self: center; |
| 424 | font-size: var(--rst-fs-xs); |
| 425 | color: var(--rst-tone-positive-fg); |
| 426 | } |
| 427 | |
| 428 | [data-sheet-live][data-state="off"] { |
| 429 | color: var(--rst-tone-warning-fg); |
| 430 | } |
| 431 | |
| 432 | /* isolation: the vendored grid's stylesheets stack with z-index values |
| 433 | * up to 10001. Contained here they compete only with each other, under |
| 434 | * the bar's 45 and the menus that float over the grid (spec §5.3). */ |
| 435 | [data-sheet-grid] { |
| 436 | flex: 1; |
| 437 | min-height: 0; |
| 438 | overflow: auto; |
| 439 | background: var(--rst-bg); |
| 440 | isolation: isolate; |
| 441 | } |
| 442 | |
| 443 | /* ── The vendored grid ─────────────────────────────────────────────── |
| 444 | * |
| 445 | * Jspreadsheet paints its own colours, hard-coded: #fff for the table, |
| 446 | * #f3f3f3 for the headers, #ccc for every border. None of them follow a |
| 447 | * theme, so all of them are restated here in tokens. |
| 448 | * |
| 449 | * The selectors are `.jss_*`. Jspreadsheet v5 renamed everything from |
| 450 | * v4's `.jexcel`, and this block was first written against `.jexcel` — |
| 451 | * so it matched nothing at all, and the grid stayed white inside dark |
| 452 | * chrome. Check the vendored stylesheet, not a memory of the last major |
| 453 | * version, when these stop working. |
| 454 | * |
| 455 | * Every rule that sets a background on something carrying text also |
| 456 | * sets a color. The library sets background-color on the headers and no |
| 457 | * color, so the text is inherited from the page: in dark mode that put |
| 458 | * light grey text on a light grey header and the row and column names |
| 459 | * became invisible. A background without a foreground beside it is the |
| 460 | * bug, not the colour. (The exceptions are the things with no text in |
| 461 | * them — the fill handle is a 1px knob.) */ |
| 462 | [data-sheet-grid] .jss_worksheet { |
| 463 | background-color: var(--rst-bg); |
| 464 | color: var(--rst-text); |
| 465 | border-right-color: var(--rst-line); |
| 466 | border-bottom-color: var(--rst-line); |
| 467 | } |
| 468 | |
| 469 | [data-sheet-grid] .jss_worksheet > tbody > tr > td { |
| 470 | background-color: var(--rst-bg); |
| 471 | color: var(--rst-text); |
| 472 | border-top-color: var(--rst-line); |
| 473 | border-left-color: var(--rst-line); |
| 474 | } |
| 475 | |
| 476 | /* The row and column names. Both headers, one look. */ |
| 477 | [data-sheet-grid] .jss_worksheet > thead > tr > td, |
| 478 | [data-sheet-grid] .jss_worksheet > tbody > tr > td:first-child { |
| 479 | background-color: var(--rst-surface-2); |
| 480 | color: var(--rst-text-muted); |
| 481 | border-top-color: var(--rst-line); |
| 482 | border-left-color: var(--rst-line); |
| 483 | font-weight: 600; |
| 484 | } |
| 485 | |
| 486 | /* The names of the selected column and row. The library puts .selected |
| 487 | * on the thead td but on the tbody TR — not on its first cell — so the |
| 488 | * two halves need different selectors. Written as `td:first-child.selected` |
| 489 | * the second half matched nothing at all, and a rule that matches nothing |
| 490 | * fails exactly like a rule nobody wrote: the vendored #dcdcdc stood, and |
| 491 | * a dark grid grew a light-grey slab down its left edge carrying |
| 492 | * --rst-text-muted (#a8b0ba) at 1.60:1. */ |
| 493 | [data-sheet-grid] .jss_worksheet > thead > tr > td.selected, |
| 494 | [data-sheet-grid] .jss_worksheet > tbody > tr.selected > td:first-child { |
| 495 | background-color: var(--rst-accent-soft); |
| 496 | color: var(--rst-text); |
| 497 | } |
| 498 | |
| 499 | /* ── The selection ─────────────────────────────────────────────────── |
| 500 | * |
| 501 | * A range is a fill plus an outline. This file used to restate only the |
| 502 | * fill, and the outline is where every hard-coded colour in the vendored |
| 503 | * stylesheet lives: |
| 504 | * |
| 505 | * .highlight-right/-bottom border: 1px solid #000 |
| 506 | * .highlight-top/-left border: 1px solid #000, plus a decorative |
| 507 | * box-shadow of #ccc just outside it |
| 508 | * |
| 509 | * In dark that renders backwards. The black borders disappear into the |
| 510 | * grid; the two that do not are overwritten by our own |
| 511 | * `td { border-top-color: var(--rst-line) }` above, which is one |
| 512 | * specificity step higher than the library's `.jss_worksheet .highlight-top` |
| 513 | * — so they become ordinary grid lines, while the #ccc shadow, which |
| 514 | * exists only to mask the grid line the border sits on, survives as the |
| 515 | * one bright thing on screen. What you get is a navy slab lit down its |
| 516 | * top and left and nowhere else, with no way to see where it ends. |
| 517 | * |
| 518 | * ── Why every rule below is scoped `> tbody > tr > td` ──────────────── |
| 519 | * Two reasons, and the second is the one that bites. |
| 520 | * |
| 521 | * 1. The grid-line rule above is (0,2,3). A restatement written at the |
| 522 | * library's own `.jss_worksheet .highlight-top` is (0,2,0) and loses |
| 523 | * to it — a rule that parses, uses a token, and paints nothing. |
| 524 | * 2. sheet.html links the vendored stylesheets in the head block, which |
| 525 | * layout.html renders AFTER sheets.css. The library is later in |
| 526 | * source order, so equal specificity loses too. Ours must be |
| 527 | * strictly higher, never merely equal. That is also the real reason |
| 528 | * the vendored #dcdcdc beat the header rule above: both are (0,3,3), |
| 529 | * and the vendor came last. |
| 530 | * |
| 531 | * So: one outline colour, --rst-accent, on all four sides, above both. |
| 532 | * Measured on the fill it sits on, 6.59:1 in dark and 4.75:1 in light — |
| 533 | * both past 1.4.11's 3:1 for a component boundary. The shadow goes: |
| 534 | * with the outline in a colour that reads, masking the line underneath |
| 535 | * it buys nothing and costs the selection its symmetry. |
| 536 | * |
| 537 | * The library gives each of its three range states a different border |
| 538 | * STYLE — solid for the selection, dashed for the copy marquee, dotted |
| 539 | * for the fill-handle drag preview — and the style survives our |
| 540 | * colour-only override. That is the distinction doing the work, so all |
| 541 | * three take the same accent and stay told apart by shape. */ |
| 542 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight { |
| 543 | background-color: var(--rst-accent-soft); |
| 544 | color: var(--rst-text); |
| 545 | } |
| 546 | |
| 547 | /* A link inside a selected cell. Vendored as the browser's own `blue`, |
| 548 | * which on the fill in dark is 2.4:1. */ |
| 549 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight > a { |
| 550 | color: var(--rst-accent); |
| 551 | } |
| 552 | |
| 553 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-top, |
| 554 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-top, |
| 555 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-top { |
| 556 | border-top-color: var(--rst-accent); |
| 557 | box-shadow: none; |
| 558 | } |
| 559 | |
| 560 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-left, |
| 561 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-left, |
| 562 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-left { |
| 563 | border-left-color: var(--rst-accent); |
| 564 | box-shadow: none; |
| 565 | } |
| 566 | |
| 567 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-right, |
| 568 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-right, |
| 569 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-right { |
| 570 | border-right-color: var(--rst-accent); |
| 571 | } |
| 572 | |
| 573 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-bottom, |
| 574 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-bottom, |
| 575 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-bottom { |
| 576 | border-bottom-color: var(--rst-accent); |
| 577 | } |
| 578 | |
| 579 | /* The rows the fill handle is being dragged over — the preview of what |
| 580 | * a release would fill, NOT the range a formula is pointing at. There is |
| 581 | * separate formula reference chrome below. `parseFormulas: false` keeps |
| 582 | * calculation on the server. The library |
| 583 | * adds these classes from updateCopySelection, off `selectedCorner`. |
| 584 | * Vendored as rgba(0,0,0,0.05) — a darkening, which on a dark grid is |
| 585 | * nothing at all. */ |
| 586 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.selection { |
| 587 | background-color: var(--rst-accent-soft); |
| 588 | color: var(--rst-text); |
| 589 | } |
| 590 | |
| 591 | /* The anchor — the one cell the keyboard is on. The library paints it |
| 592 | * transparent on purpose so it reads out of the fill; our .highlight |
| 593 | * rule was opaque and took that back, leaving a range with no visible |
| 594 | * cursor in either scheme. |
| 595 | * |
| 596 | * It comes LAST among the SELECTION rules on purpose. It ties |
| 597 | * .highlight and .selection at (0,3,3), and the anchor is inside both |
| 598 | * while you drag the fill handle — so on a tie it has to be the later |
| 599 | * rule or the cursor disappears at exactly the moment you are aiming |
| 600 | * with it. |
| 601 | * |
| 602 | * **The ring is what actually marks it, and the background no longer |
| 603 | * can.** Cell fills are painted further down this file, at the same |
| 604 | * (0,3,3) and therefore later, so on a filled range a background-only |
| 605 | * anchor is indistinguishable from the rest of the selection — the |
| 606 | * exact failure the paragraph above guards against, arriving by a new |
| 607 | * route. An inset shadow survives any fill because it is drawn over |
| 608 | * one, and it costs no layout, so it cannot shift a cell by a pixel |
| 609 | * when the cursor lands on it. |
| 610 | * |
| 611 | * The background stays for the unfilled case, where it is what makes |
| 612 | * the anchor read as "not part of the tint". */ |
| 613 | [data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-selected { |
| 614 | background-color: var(--rst-bg); |
| 615 | color: var(--rst-text); |
| 616 | box-shadow: inset 0 0 0 2px var(--rst-accent); |
| 617 | } |
| 618 | |
| 619 | /* The fill handle at the corner of the range: a black knob ringed in |
| 620 | * white, which in dark reads as a white dot with a dark centre — the |
| 621 | * inverse of the thing it is meant to be. It is 1px plus its ring, so |
| 622 | * it is the accent or it is invisible. */ |
| 623 | [data-sheet-grid] .jss_corner { |
| 624 | background-color: var(--rst-accent); |
| 625 | border-color: var(--rst-bg); |
| 626 | } |
| 627 | |
| 628 | /* ── The right-click menu ──────────────────────────────────────────── |
| 629 | * |
| 630 | * Not Jspreadsheet's: jsuites', which it binds to `contextmenu` whether |
| 631 | * or not the host app configures one. grid.js turns it off (its stock |
| 632 | * items bypass our write paths), so today it never opens. The restatement |
| 633 | * stays because the element is still mounted, and TestVendorPaintIsRestated |
| 634 | * lists it: vendored it is #fff with #555 text — a white card on a dark |
| 635 | * grid — should anything ever open it again. |
| 636 | * |
| 637 | * It is `position: fixed` and mounted inside the grid, so it is scoped |
| 638 | * the same way everything else here is. */ |
| 639 | [data-sheet-grid] .jcontextmenu { |
| 640 | background: var(--rst-surface); |
| 641 | color: var(--rst-text); |
| 642 | border-color: var(--rst-line-strong); |
| 643 | box-shadow: var(--rst-shadow-pop); |
| 644 | } |
| 645 | |
| 646 | [data-sheet-grid] .jcontextmenu > div a { |
| 647 | color: var(--rst-text); |
| 648 | } |
| 649 | |
| 650 | [data-sheet-grid] .jcontextmenu > div span { |
| 651 | color: var(--rst-text-muted); |
| 652 | } |
| 653 | |
| 654 | [data-sheet-grid] .jcontextmenu > div:hover { |
| 655 | background: var(--rst-accent-soft); |
| 656 | color: var(--rst-text); |
| 657 | } |
| 658 | |
| 659 | [data-sheet-grid] .jcontextmenu .jcontextmenu-disabled a, |
| 660 | [data-sheet-grid] .jcontextmenu .jcontextmenu-disabled::before { |
| 661 | color: var(--rst-text-faint); |
| 662 | } |
| 663 | |
| 664 | [data-sheet-grid] .jcontextmenu hr { |
| 665 | border-color: var(--rst-line); |
| 666 | } |
| 667 | |
| 668 | /* The cell editor is an input the library drops into the td. Left |
| 669 | * alone it inherits the browser's own white. */ |
| 670 | [data-sheet-grid] .jss_worksheet td input, |
| 671 | [data-sheet-grid] .jss_worksheet td textarea { |
| 672 | font: inherit; |
| 673 | letter-spacing: inherit; |
| 674 | text-align: inherit; |
| 675 | background-color: inherit; |
| 676 | color: inherit; |
| 677 | } |
| 678 | |
| 679 | /* iOS zooms the page into any text control under 16px, and tokens.css |
| 680 | * brings controls up to 16px where the pointer is coarse. Two of ours |
| 681 | * set `font: inherit` with a selector that outweighs tokens.css's bare |
| 682 | * `input`, and fell back to 14px: the cell editor above, and estilo's |
| 683 | * title field in the bar. Same rule, same max(), restated here at their |
| 684 | * weight. TestNoZoomOnTap (make test-browser) measures both. */ |
| 685 | @media (pointer: coarse) { |
| 686 | [data-sheet-grid] .jss_worksheet td input, |
| 687 | [data-sheet-grid] .jss_worksheet td textarea, |
| 688 | [estilo-bar-title-input] { |
| 689 | font-size: max(1rem, 1em); |
| 690 | } |
| 691 | } |
| 692 | |
| 693 | [data-sheet-grid] .jss_container, |
| 694 | [data-sheet-grid] .jss_content { |
| 695 | background-color: var(--rst-bg); |
| 696 | } |
| 697 | |
| 698 | /* A cell whose imported fill fails WCAG AA against its ink. Spec §11.1 |
| 699 | * ruled that we retain such a fill rather than silently correcting it, |
| 700 | * so the fill stays and this marks it instead: a corner flag that costs |
| 701 | * nothing to ignore and is impossible to miss once you look. */ |
| 702 | [data-sheet-grid] td[data-contrast="low"] { |
| 703 | position: relative; |
| 704 | } |
| 705 | |
| 706 | [data-sheet-grid] td[data-contrast="low"]::after { |
| 707 | content: ""; |
| 708 | position: absolute; |
| 709 | inset-block-start: 0; |
| 710 | inset-inline-end: 0; |
| 711 | border-block-start: 0.5rem solid var(--rst-tone-warning-fg); |
| 712 | border-inline-start: 0.5rem solid transparent; |
| 713 | /* A halo in the page's own surface colour, so the flag survives the |
| 714 | * fill it is flagging. |
| 715 | * |
| 716 | * The flag marks a cell whose IMPORTED fill and ink fail AA, and the |
| 717 | * fill is an arbitrary colour out of somebody's file — including, in |
| 718 | * the case this app cites by name, Excel's #FFEB9C, against which an |
| 719 | * amber triangle is invisible in dark mode. A warning that disappears |
| 720 | * on exactly the cells it is about is not a warning. |
| 721 | * |
| 722 | * drop-shadow rather than a border or an outline: those two follow |
| 723 | * the box, and this triangle is drawn out of borders, so there is no |
| 724 | * box to put an edge on. filter traces the rendered shape. */ |
| 725 | filter: drop-shadow(0 0 1px var(--rst-surface)) |
| 726 | drop-shadow(0 0 1px var(--rst-surface)); |
| 727 | } |
| 728 | |
| 729 | /* ── The scriptless table ──────────────────────────────────────────── |
| 730 | * |
| 731 | * Not a fallback in the apologetic sense: it is the only view that |
| 732 | * works everywhere, and every affordance the grid has exists here too. */ |
| 733 | [data-cell-table] { |
| 734 | border-collapse: collapse; |
| 735 | width: 100%; |
| 736 | } |
| 737 | |
| 738 | [data-cell-table] th, |
| 739 | [data-cell-table] td { |
| 740 | border: 1px solid var(--rst-line); |
| 741 | padding: var(--rst-sp-1) var(--rst-sp-2); |
| 742 | text-align: start; |
| 743 | vertical-align: top; |
| 744 | } |
| 745 | |
| 746 | [data-cell-table] th { |
| 747 | background: var(--rst-surface-2); |
| 748 | color: var(--rst-text-muted); |
| 749 | font-weight: 600; |
| 750 | } |
| 751 | |
| 752 | /* The cell link, which is every cell. |
| 753 | * |
| 754 | * Each cell is an anchor, because the link IS the edit affordance in a |
| 755 | * view with no script. Left at the browser's default that meant roughly |
| 756 | * 650 blue underlined links per page — data and emptiness in the same |
| 757 | * colour and the same weight, so `94200` and a blank cell differed only |
| 758 | * in glyph width. A spreadsheet you can only read cell by cell is not a |
| 759 | * spreadsheet, and this is the view the spec calls the product. |
| 760 | * |
| 761 | * So the cell reads as a cell and the affordance shows on approach. The |
| 762 | * anchor takes the cell's full width and a line of height, so the |
| 763 | * target is the whole line rather than the few pixels of the glyph — |
| 764 | * which is also what lets an empty cell be clickable without a |
| 765 | * standing in for content. It does not stretch to a row made taller by |
| 766 | * a wrapped neighbour; that was true of the it replaces too. |
| 767 | * |
| 768 | * :focus-visible carries the same treatment as :hover deliberately. |
| 769 | * Keyboard is the primary way through this view, and a focus ring alone |
| 770 | * on a 26-column grid is easy to lose. */ |
| 771 | [data-cell-table] td > a { |
| 772 | color: inherit; |
| 773 | text-decoration: none; |
| 774 | display: block; |
| 775 | min-block-size: 1.25rem; |
| 776 | /* Square, and the whole cell: the link reaches out over the cell's own |
| 777 | * padding and pads itself back in, so its hover fills the cell edge to |
| 778 | * edge. It was a 6px rounded pill round the text inside a square cell |
| 779 | * — on an empty cell a small floating blob (the square rule, spec §11: |
| 780 | * an in-flow highlight is square). */ |
| 781 | margin: calc(-1 * var(--rst-sp-1)) calc(-1 * var(--rst-sp-2)); |
| 782 | padding: var(--rst-sp-1) var(--rst-sp-2); |
| 783 | } |
| 784 | |
| 785 | [data-cell-table] td > a:hover, |
| 786 | [data-cell-table] td > a:focus-visible { |
| 787 | background: var(--rst-accent-soft); |
| 788 | color: var(--rst-accent); |
| 789 | } |
| 790 | |
| 791 | /* Numbers right, text left — the alignment IS the type signal in a |
| 792 | * spreadsheet, and it is the one the grid gives for free and this view |
| 793 | * had to be told. |
| 794 | * |
| 795 | * Both spellings, because there are two of these tables. The editable |
| 796 | * one wraps every cell in an anchor, so the alignment has to reach the |
| 797 | * block anchor rather than the cell around it; the anonymous link view |
| 798 | * has no anchors, because there is nothing on it to edit. One rule for |
| 799 | * each, rather than one selector that quietly covers only the surface |
| 800 | * that happened to get tested. */ |
| 801 | [data-cell-table] td[data-kind="number"], |
| 802 | [data-cell-table] td[data-kind="number"] > a { |
| 803 | text-align: end; |
| 804 | } |
| 805 | |
| 806 | [data-cell-table] td form { |
| 807 | display: flex; |
| 808 | gap: var(--rst-sp-1); |
| 809 | } |
| 810 | |
| 811 | [data-cell-table] td input[type="text"] { |
| 812 | inline-size: 100%; |
| 813 | min-inline-size: 6ch; |
| 814 | } |
| 815 | |
| 816 | /* [data-cell-value] used to be styled here and is now gone. Nothing in |
| 817 | * any template or module has ever written that attribute, so the rule |
| 818 | * matched no element for the life of the branch — the same defect as |
| 819 | * the contrast flag two sections up, which had a rule and no attribute |
| 820 | * and therefore warned about cells it never marked. |
| 821 | * |
| 822 | * A selector that matches nothing is invisible to every test we have |
| 823 | * and to every screenshot, which is why it is worth saying it was here |
| 824 | * rather than deleting it silently: the next person to want a secondary |
| 825 | * line under a table cell should write the markup and the rule |
| 826 | * together, not find this and assume it works. */ |
| 827 | |
| 828 | |
| 829 | /* Import drop zone ─────────────────────────────────────────────────── |
| 830 | * |
| 831 | * The dashed outline is the whole visual: it says "this box takes a |
| 832 | * file" without becoming a second, competing button beside the one that |
| 833 | * is already there. The field and its button inside it are untouched |
| 834 | * rastrillo, because with scripting off they are the entire feature and |
| 835 | * a drop zone that restyles them would be decorating something that |
| 836 | * cannot happen. |
| 837 | * |
| 838 | * The highlight is an ATTRIBUTE toggled by drop.js, not a class: it is |
| 839 | * transient state on one element, and the attribute spelling is what |
| 840 | * the rest of this app's own hooks use (data-sheet-surface, |
| 841 | * data-cell-table). |
| 842 | * |
| 843 | * transition on border-color only — a person dragging a file across the |
| 844 | * page crosses this box repeatedly, and animating the outline is the |
| 845 | * difference between a box that acknowledges the drag and one that |
| 846 | * flashes. */ |
| 847 | /* :not([data-drop-page]) on both, because there are two zones now and |
| 848 | only one of them is a box. The overlay on the sheet list carries |
| 849 | data-drop too — it has to, it is what drop.js looks for — and these |
| 850 | two selectors are 0-2-0 and 0-3-0, so without the exclusion they beat |
| 851 | [rst-modal-overlay]'s 0-1-0 and paint the sheet of glass over the |
| 852 | whole page in accent-soft. It renders as a flat blue screen with the |
| 853 | list gone: a defect a browser found and no test could. */ |
| 854 | [data-drop]:not([data-drop-page]) { |
| 855 | border: 1px dashed var(--rst-line); |
| 856 | border-radius: var(--rst-radius); |
| 857 | padding: var(--rst-sp-3); |
| 858 | transition: border-color 120ms ease, background-color 120ms ease; |
| 859 | } |
| 860 | |
| 861 | [data-drop][data-drop-over]:not([data-drop-page]) { |
| 862 | border-color: var(--rst-accent); |
| 863 | background: var(--rst-accent-soft); |
| 864 | } |
| 865 | |
| 866 | /* Somebody who has asked for less motion still needs to see the box |
| 867 | respond — so the change stays and only the animation goes. */ |
| 868 | @media (prefers-reduced-motion: reduce) { |
| 869 | [data-drop] { |
| 870 | transition: none; |
| 871 | } |
| 872 | } |
| 873 | |
| 874 | /* ── The sheet list's drop overlay ─────────────────────────────────── |
| 875 | * |
| 876 | * The list has no drop box. It has a full-window target that does not |
| 877 | * exist until a file is over the window, and drop.js is what reveals |
| 878 | * it — see the comment on [data-drop-page] in sheets.html. |
| 879 | * |
| 880 | * It borrows rastrillo's modal paint rather than growing its own, so |
| 881 | * the layer that appears over the list looks like every other layer in |
| 882 | * the suite. Only the four rules below are ours. |
| 883 | * |
| 884 | * THIS FIRST RULE IS LOAD-BEARING. [rst-modal-overlay] sets |
| 885 | * display: flex, and an author declaration beats the user agent's |
| 886 | * [hidden] { display: none } no matter how weak its selector looks. |
| 887 | * Without it the overlay covers the sheet list permanently and the page |
| 888 | * is unusable — with scripting off, where nothing would ever come along |
| 889 | * to hide it again. TestTheDropOverlayIsHiddenAtRest pins the markup; |
| 890 | * this pins the paint. */ |
| 891 | [data-drop-page][hidden] { |
| 892 | display: none; |
| 893 | } |
| 894 | |
| 895 | /* A full-window overlay sits above the bar (spec §5.3): rastrillo's |
| 896 | * modal overlay is z-index 10, under the bar's 45, so without this the |
| 897 | * bar would paint over the top of the drop target. */ |
| 898 | [data-drop-page] { |
| 899 | z-index: 50; |
| 900 | } |
| 901 | |
| 902 | [data-drop-page] [rst-modal-panel] { |
| 903 | max-width: 24rem; |
| 904 | transition: border-color 120ms ease; |
| 905 | } |
| 906 | |
| 907 | /* The panel answers the drag, the same way the box on /sheets/import |
| 908 | does and with the same token, so the two read as one gesture. */ |
| 909 | [data-drop-page][data-drop-over] [rst-modal-panel] { |
| 910 | border-color: var(--rst-accent); |
| 911 | } |
| 912 | |
| 913 | [data-drop-page-body] { |
| 914 | display: grid; |
| 915 | gap: var(--rst-sp-3); |
| 916 | justify-items: center; |
| 917 | padding: var(--rst-sp-5); |
| 918 | text-align: center; |
| 919 | width: 100%; |
| 920 | } |
| 921 | |
| 922 | [data-drop-page-title] { |
| 923 | font-size: var(--rst-fs-base); |
| 924 | font-weight: 600; |
| 925 | margin: 0; |
| 926 | } |
| 927 | |
| 928 | /* The chooser is for afterwards. |
| 929 | * |
| 930 | * While a file is over the window the panel has one thing to say, and a |
| 931 | * "Choose File" button beside "Drop it to import" offers a second way to |
| 932 | * do the thing the person is already doing. It appears when the overlay |
| 933 | * is up WITHOUT a drag over it, which is exactly the state a refused |
| 934 | * drop leaves behind — and then it is the way out of that refusal. */ |
| 935 | [data-drop-page-recover] { |
| 936 | display: grid; |
| 937 | gap: var(--rst-sp-3); |
| 938 | justify-items: center; |
| 939 | width: 100%; |
| 940 | } |
| 941 | |
| 942 | [data-drop-page][data-drop-over] [data-drop-page-recover] { |
| 943 | display: none; |
| 944 | } |
| 945 | |
| 946 | @media (prefers-reduced-motion: reduce) { |
| 947 | [data-drop-page] [rst-modal-panel] { |
| 948 | transition: none; |
| 949 | } |
| 950 | } |
| 951 | |
| 952 | /* ── Cell formatting ───────────────────────────────────────────────── |
| 953 | * |
| 954 | * The paint for a cell's stored Format: bold, italic, alignment, fill |
| 955 | * and ink. Every rule is here, in the file the guards read, rather than |
| 956 | * in a stylesheet JavaScript generates — a generated sheet is invisible |
| 957 | * to TestOurStylesheetHasNoColourLiterals, TestEveryTokenWeUseExists |
| 958 | * and TestVendorPaintIsRestated alike, which is three reasons not to |
| 959 | * have one. paint.js only sets attributes and two custom properties. |
| 960 | * |
| 961 | * Specificity: [data-sheet-grid] .jss_worksheet > tbody > tr > td[attr] |
| 962 | * is (0,3,3) — one step above the grid-line rule at (0,2,3), as the |
| 963 | * restatement rule requires, and equal to the selection rules. So these |
| 964 | * are placed AFTER them: a filled cell keeps its fill while selected, |
| 965 | * and the selection outline does the work of showing the selection, |
| 966 | * which is what every spreadsheet does. |
| 967 | * |
| 968 | * A colour a person picks HERE is a hue and a chroma with no lightness |
| 969 | * — see store/models.go — and the lightness comes from the theme below. |
| 970 | * A colour an imported FILE authored is painted exactly as authored, |
| 971 | * in both themes, because spec §11.1 says we render a document as |
| 972 | * written and warn rather than silently correcting it. */ |
| 973 | |
| 974 | /* light-dark() is a colour function and cannot hold a number, so these |
| 975 | * are written the long way. They still resolve before first paint: |
| 976 | * data-theme is server-rendered into the first bytes of the response |
| 977 | * (layout.html), so there is no flash of the wrong lightness. */ |
| 978 | :root { |
| 979 | --sheet-fill-l: 0.92; |
| 980 | --sheet-ink-l: 0.42; |
| 981 | } |
| 982 | |
| 983 | @media (prefers-color-scheme: dark) { |
| 984 | :root:not([data-theme="light"]) { |
| 985 | --sheet-fill-l: 0.30; |
| 986 | --sheet-ink-l: 0.80; |
| 987 | } |
| 988 | } |
| 989 | |
| 990 | :root[data-theme="dark"] { |
| 991 | --sheet-fill-l: 0.30; |
| 992 | --sheet-ink-l: 0.80; |
| 993 | } |
| 994 | |
| 995 | [data-sheet-grid] .jss_worksheet > tbody > tr > td[data-b] { |
| 996 | font-weight: 600; |
| 997 | } |
| 998 | |
| 999 | [data-sheet-grid] .jss_worksheet > tbody > tr > td[data-i] { |
| 1000 | font-style: italic; |
| 1001 | } |
| 1002 | |
| 1003 | /* ── Alignment, and why these three rules carry !important ─────────── |
| 1004 | * |
| 1005 | * Jspreadsheet writes `text-align` as an INLINE style on every data |
| 1006 | * cell, from the column's `align` or from `defaultColAlign`. An inline |
| 1007 | * declaration outranks any selector, at any specificity, so the |
| 1008 | * ordinary version of these rules set the attribute, reflected |
| 1009 | * aria-pressed on the button, and changed nothing on screen. The |
| 1010 | * toolbar looked like it worked. Only a browser could see it, and the |
| 1011 | * branch's own drive did not check alignment — bold and fill, which |
| 1012 | * both paint from properties the library does not touch, passed. |
| 1013 | * |
| 1014 | * !important is the correct tool here and not a shortcut: it is what |
| 1015 | * outranks an inline style, and there is no other. These are new paint |
| 1016 | * rather than a restatement of a vendored rule, so the restatement |
| 1017 | * guard is unaffected. |
| 1018 | * |
| 1019 | * The numeric rule is important for the same reason and comes FIRST, |
| 1020 | * so an explicit alignment beats it on source order — both are |
| 1021 | * important and (0,3,3), so nothing else separates them. */ |
| 1022 | [data-sheet-grid] .jss_worksheet > tbody > tr > td[data-n] { |
| 1023 | text-align: end !important; |
| 1024 | } |
| 1025 | |
| 1026 | [data-sheet-grid] .jss_worksheet > tbody > tr > td[data-a="left"] { |
| 1027 | text-align: start !important; |
| 1028 | } |
| 1029 | |
| 1030 | [data-sheet-grid] .jss_worksheet > tbody > tr > td[data-a="center"] { |
| 1031 | text-align: center !important; |
| 1032 | } |
| 1033 | |
| 1034 | [data-sheet-grid] .jss_worksheet > tbody > tr > td[data-a="right"] { |
| 1035 | text-align: end !important; |
| 1036 | } |
| 1037 | |
| 1038 | /* Ours: hue and chroma from the cell, lightness from the theme. */ |
| 1039 | [data-sheet-grid] .jss_worksheet > tbody > tr > td[data-fill] { |
| 1040 | background-color: oklch(var(--sheet-fill-l) var(--sheet-c) var(--sheet-h)); |
| 1041 | } |
| 1042 | |
| 1043 | [data-sheet-grid] .jss_worksheet > tbody > tr > td[data-ink] { |
| 1044 | color: oklch(var(--sheet-ink-l) var(--sheet-kc) var(--sheet-kh)); |
| 1045 | } |
| 1046 | |
| 1047 | /* The author's: painted as the file wrote it. --sheet-fx and --sheet-k |
| 1048 | * carry a hex that came from a document, which is why a literal is |
| 1049 | * acceptable here and nowhere else in this file — it is data, set by |
| 1050 | * script from the document, not a colour this stylesheet chose. */ |
| 1051 | [data-sheet-grid] .jss_worksheet > tbody > tr > td[data-fx] { |
| 1052 | background-color: var(--sheet-fx); |
| 1053 | } |
| 1054 | |
| 1055 | [data-sheet-grid] .jss_worksheet > tbody > tr > td[data-k] { |
| 1056 | color: var(--sheet-k); |
| 1057 | } |
| 1058 | |
| 1059 | /* ── The table view paints what the grid paints ────────────────────── |
| 1060 | * |
| 1061 | * The same attributes, because the server writes the same attributes |
| 1062 | * (tablepaint.go). PRODUCT.md's second principle is why: the scriptless |
| 1063 | * path is the product, and a screen that can SET bold but cannot SHOW |
| 1064 | * it is the worse half of the feature. |
| 1065 | * |
| 1066 | * Written as its own block rather than by widening the grid's selectors |
| 1067 | * to cover both. Those carry `.jss_worksheet` and `!important` for |
| 1068 | * reasons that belong entirely to the vendored library — an inline |
| 1069 | * text-align the library writes on every data cell — and none of that |
| 1070 | * is true here. A shared selector would carry the library's problem |
| 1071 | * onto a table that does not have it, and would make the restatement |
| 1072 | * guard's job ambiguous. |
| 1073 | * |
| 1074 | * No !important, and that is the tell that these are new paint rather |
| 1075 | * than a restatement: nothing writes an inline text-align on this |
| 1076 | * table. The one inline style a cell here carries is the custom |
| 1077 | * properties, which set values rather than win a cascade. */ |
| 1078 | [data-cell-table] td[data-b], |
| 1079 | [data-cell-table] td[data-b] > a { |
| 1080 | font-weight: 600; |
| 1081 | } |
| 1082 | |
| 1083 | [data-cell-table] td[data-i], |
| 1084 | [data-cell-table] td[data-i] > a { |
| 1085 | font-style: italic; |
| 1086 | } |
| 1087 | |
| 1088 | /* The numeric rule comes first so an explicit alignment beats it on |
| 1089 | * source order, the same ordering the grid's block relies on. Both are |
| 1090 | * (0,2,1) here, so nothing else separates them. |
| 1091 | * |
| 1092 | * The existing [data-kind="number"] rule above already right-aligns a |
| 1093 | * numeric cell; these three are the explicit override, and they have to |
| 1094 | * come after it in the file for the same reason. */ |
| 1095 | [data-cell-table] td[data-a="left"], |
| 1096 | [data-cell-table] td[data-a="left"] > a { |
| 1097 | text-align: start; |
| 1098 | } |
| 1099 | |
| 1100 | [data-cell-table] td[data-a="center"], |
| 1101 | [data-cell-table] td[data-a="center"] > a { |
| 1102 | text-align: center; |
| 1103 | } |
| 1104 | |
| 1105 | [data-cell-table] td[data-a="right"], |
| 1106 | [data-cell-table] td[data-a="right"] > a { |
| 1107 | text-align: end; |
| 1108 | } |
| 1109 | |
| 1110 | /* Ours: hue and chroma from the cell, lightness from the theme, so a |
| 1111 | * fill somebody picked follows their light/dark choice. Identical to |
| 1112 | * the grid's rule and deliberately so — a fill must not be one colour |
| 1113 | * on one screen and another on the other. */ |
| 1114 | [data-cell-table] td[data-fill] { |
| 1115 | background-color: oklch(var(--sheet-fill-l) var(--sheet-c) var(--sheet-h)); |
| 1116 | } |
| 1117 | |
| 1118 | [data-cell-table] td[data-ink], |
| 1119 | [data-cell-table] td[data-ink] > a { |
| 1120 | color: oklch(var(--sheet-ink-l) var(--sheet-kc) var(--sheet-kh)); |
| 1121 | } |
| 1122 | |
| 1123 | /* The author's, painted as the file wrote it (spec §11.1). --sheet-fx |
| 1124 | * and --sheet-k carry a hex that came out of a document — data, checked |
| 1125 | * by previewsvg.go's safeHex before it was ever written into the |
| 1126 | * attribute, and not a colour this stylesheet chose. */ |
| 1127 | [data-cell-table] td[data-fx] { |
| 1128 | background-color: var(--sheet-fx); |
| 1129 | } |
| 1130 | |
| 1131 | [data-cell-table] td[data-k], |
| 1132 | [data-cell-table] td[data-k] > a { |
| 1133 | color: var(--sheet-k); |
| 1134 | } |
| 1135 | |
| 1136 | /* The cell edit form's formatting row. Six selects and a button, on |
| 1137 | * their own line under the value field: side by side they would push |
| 1138 | * the table's column wider than the page. */ |
| 1139 | [data-cell-format] { |
| 1140 | display: flex; |
| 1141 | flex-wrap: wrap; |
| 1142 | gap: var(--rst-sp-1); |
| 1143 | margin-block-start: var(--rst-sp-1); |
| 1144 | } |
| 1145 | |
| 1146 | [data-cell-format] select { |
| 1147 | font-size: var(--rst-fs-xs); |
| 1148 | padding-block: 0.15rem; |
| 1149 | } |
| 1150 | |
| 1151 | /* ── The list's thumbnails ─────────────────────────────────────────── |
| 1152 | * |
| 1153 | * A 26x34 slot at the row's left edge, the same box in the same place |
| 1154 | * as the Docs app's, so the two file lists have the same row height and |
| 1155 | * read as one product rather than two apps that both grew thumbnails. |
| 1156 | * |
| 1157 | * The slot is reserved whether or not there is anything to draw: an |
| 1158 | * empty sheet renders the empty bordered box, so a row never changes |
| 1159 | * height when a sheet gains its first cell. */ |
| 1160 | /* The list card is a <ul> and rastrillo's [rst-list] is written for a |
| 1161 | * <div> — its comment says "a list card holds rows", and every rule it |
| 1162 | * declares is background, border and radius. It resets nothing, because |
| 1163 | * a <div> has nothing to reset. |
| 1164 | * |
| 1165 | * A <ul> does. The UA stylesheet gives it `padding-inline-start: 40px` |
| 1166 | * and `list-style: disc`, and both survived: the 40px is the gap Paul |
| 1167 | * saw to the left of the thumbnail on 2026-09-03, and it is 40px of |
| 1168 | * card that every row is indented inside. The markers were invisible |
| 1169 | * only because they render in that padding, outside the row's own box. |
| 1170 | * |
| 1171 | * The element stays a <ul>. A list of sheets is a list, and the fix for |
| 1172 | * a framework rule that assumed a <div> is to reset what the <div> |
| 1173 | * never had — not to drop the semantics that make the list announce as |
| 1174 | * one to a screen reader. |
| 1175 | * |
| 1176 | * The block margin is REPLACED rather than removed. Nothing in |
| 1177 | * rastrillo stacks the children of [rst-page] — there is no |
| 1178 | * `> * + *` rule — so what separated this card from the import form |
| 1179 | * above it was the <ul>'s own UA `margin-block: 1em`. Zeroing the |
| 1180 | * shorthand would have closed that gap as a side effect of fixing the |
| 1181 | * padding, so the margin is restated from the token scale, which is |
| 1182 | * where every other gap on the page comes from. */ |
| 1183 | [data-sheet-list] { |
| 1184 | list-style: none; |
| 1185 | margin-block: var(--rst-sp-4); |
| 1186 | margin-inline: 0; |
| 1187 | padding: 0; |
| 1188 | } |
| 1189 | |
| 1190 | /* [rst-lrow] is a grid, and --rst-cols is rastrillo's own hook for |
| 1191 | * saying what its columns are. Setting it is how a row gains a |
| 1192 | * thumbnail without overriding anything: without it the row has one |
| 1193 | * column and the picture stacks above the title. */ |
| 1194 | [data-sheet-list] [rst-lrow] { |
| 1195 | --rst-cols: 26px 1fr; |
| 1196 | } |
| 1197 | |
| 1198 | /* Below 800px rastrillo sets grid-template-columns on [rst-lrow] |
| 1199 | * DIRECTLY rather than through --rst-cols, so the hook above stops |
| 1200 | * being read and the thumbnail lands in the title's column with an |
| 1201 | * empty one trailing. This is (0,2,0) against the framework's (0,1,0), |
| 1202 | * so it wins wherever it sits in the file. */ |
| 1203 | @media (max-width: 800px) { |
| 1204 | [data-sheet-list] [rst-lrow] { |
| 1205 | grid-template-columns: 26px minmax(0, 1fr); |
| 1206 | } |
| 1207 | } |
| 1208 | |
| 1209 | /* The share page's two lists: links and people. The same reset the |
| 1210 | * sheet list needs — rastrillo's [rst-list] keeps the browser's 40px |
| 1211 | * list indent — and two columns, so "Remove" and "Turn off" sit beside |
| 1212 | * the row instead of dropping under its name: [rst-lrow] is a one-column |
| 1213 | * grid until --rst-cols says otherwise. Below 800px rastrillo sets the |
| 1214 | * columns directly, so the narrow rule restates them. */ |
| 1215 | [data-share-list] { |
| 1216 | list-style: none; |
| 1217 | margin-block: var(--rst-sp-3) var(--rst-sp-4); |
| 1218 | margin-inline: 0; |
| 1219 | padding: 0; |
| 1220 | } |
| 1221 | |
| 1222 | [data-share-list] > [rst-lrow] { |
| 1223 | --rst-cols: minmax(0, 1fr) auto; |
| 1224 | } |
| 1225 | |
| 1226 | @media (max-width: 800px) { |
| 1227 | [data-share-list] > [rst-lrow] { |
| 1228 | grid-template-columns: minmax(0, 1fr) auto; |
| 1229 | } |
| 1230 | } |
| 1231 | |
| 1232 | [data-share-list] form { |
| 1233 | margin: 0; |
| 1234 | } |
| 1235 | |
| 1236 | [data-preview-slot] { |
| 1237 | display: block; |
| 1238 | block-size: 34px; |
| 1239 | inline-size: 26px; |
| 1240 | } |
| 1241 | |
| 1242 | [data-preview] { |
| 1243 | display: block; |
| 1244 | } |
| 1245 | |
| 1246 | /* The Format menu's two resize forms. A row rather than a stack: the |
| 1247 | * label, the number and the button read as one control, and the panel |
| 1248 | * is already 15rem wide enough to hold them. */ |
| 1249 | [data-size-form] { |
| 1250 | align-items: center; |
| 1251 | display: flex; |
| 1252 | gap: var(--rst-sp-2); |
| 1253 | padding: var(--rst-sp-1) var(--rst-sp-2); |
| 1254 | } |
| 1255 | |
| 1256 | /* nowrap is the point of the three rules below, and they are sized |
| 1257 | * backwards from it. The panel is 15rem, so a row has 224px of content |
| 1258 | * box; at 4rem of field and a button padded to its word, the label is |
| 1259 | * left about 100px and "Column width" sets on one line. At the numbers |
| 1260 | * this started with — a 4.5rem field and a default-padded button — it |
| 1261 | * had 48px and broke over two lines, and the two forms then disagreed |
| 1262 | * about their own height. */ |
| 1263 | /* A fixed label column, not flex: 1. The two forms are siblings and |
| 1264 | * cannot share a grid, so the only thing that lines their fields and |
| 1265 | * their two Set buttons up is both labels reserving the same width — |
| 1266 | * with flex: 1 each label took its own text's width and the rows sat |
| 1267 | * 33px out of step with each other. */ |
| 1268 | [data-size-form] label { |
| 1269 | color: var(--rst-text-muted); |
| 1270 | flex: 0 0 7rem; |
| 1271 | font-size: var(--rst-fs-sm); |
| 1272 | white-space: nowrap; |
| 1273 | } |
| 1274 | |
| 1275 | [data-size-form] input { |
| 1276 | inline-size: 4rem; |
| 1277 | } |
| 1278 | |
| 1279 | [data-size-form] button { |
| 1280 | padding-inline: var(--rst-sp-2); |
| 1281 | } |
| 1282 | |
| 1283 | /* On a touch screen the field is 16px type (no zoom on tap) and the |
| 1284 | * button is padded for a finger, and the row no longer fits: Set hung |
| 1285 | * 11-21px out of the panel's right edge. The label column gives the room |
| 1286 | * back; "Column width" still sets on one line in 5.5rem, and both rows |
| 1287 | * keep the same column, so they stay in step. */ |
| 1288 | @media (pointer: coarse) { |
| 1289 | [data-size-form] label { |
| 1290 | flex-basis: 5.5rem; |
| 1291 | } |
| 1292 | } |
| 1293 | |
| 1294 | /* The referenced range is separate from the cell receiving the formula. */ |
| 1295 | [data-sheet-grid] .jss_worksheet > tbody > tr > td[data-formula-reference] { |
| 1296 | outline: 2px solid var(--rst-accent); |
| 1297 | outline-offset: -2px; |
| 1298 | } |
| 1299 | |
| 1300 | /* One entry in a comment thread (comments.html): author and time, then |
| 1301 | the text, always shown. Entries after the first get a rule between. */ |
| 1302 | [data-comment] > p { |
| 1303 | margin: 0; |
| 1304 | } |
| 1305 | |
| 1306 | [data-comment] > p + p { |
| 1307 | margin-block-start: var(--rst-sp-1); |
| 1308 | white-space: pre-line; |
| 1309 | } |
| 1310 | |
| 1311 | [data-comment] + [data-comment] { |
| 1312 | border-block-start: 1px solid var(--rst-line); |
| 1313 | margin-block-start: var(--rst-sp-3); |
| 1314 | padding-block-start: var(--rst-sp-3); |
| 1315 | } |
| 1316 | |
| 1317 | [data-comment] [rst-row-sub] { |
| 1318 | display: inline; |
| 1319 | margin-inline-start: var(--rst-sp-2); |
| 1320 | } |
| 1321 | |