oxedyne/daimond/www/css/viewer.css
24.8 KiB, 1 run
created by r2519314175:1143, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | /* ============================================================ |
| 2 | viewer.css — how a file looks when it is shown as what it is. |
| 3 | ------------------------------------------------------------ |
| 4 | The markup is built by www/js/viewer.js. One root, `.fileview`, |
| 5 | carrying a `data-viewer` attribute that names the tier drawing |
| 6 | it, then a quiet meta line, any warnings, and a body. |
| 7 | |
| 8 | Everything here is spent from the tokens in variables.css, so |
| 9 | the eleven palettes and the two skins are covered by not having |
| 10 | an opinion about colour. Two things do have an opinion: |
| 11 | |
| 12 | * the hex dump keeps a monospace face and a fixed measure |
| 13 | whatever the skin says about de-teching telemetry, because |
| 14 | its columns ARE the information; and |
| 15 | * everything wide -- the dump, a table, a framed page -- |
| 16 | scrolls inside its own box. The panel is 380px on a phone |
| 17 | and the page itself must never scroll sideways. |
| 18 | ============================================================ */ |
| 19 | |
| 20 | /* The viewer owns whatever element it is handed, and fills it. */ |
| 21 | .fileview { |
| 22 | flex: 1; min-height: 0; |
| 23 | display: flex; flex-direction: column; |
| 24 | overflow: hidden; |
| 25 | background: var(--bg-primary); |
| 26 | } |
| 27 | |
| 28 | /* ── The line that says what this is ─────────────────────────── */ |
| 29 | .fv-meta { |
| 30 | flex: none; |
| 31 | display: flex; align-items: baseline; justify-content: space-between; gap: 8px; |
| 32 | padding: 6px 12px; |
| 33 | border-bottom: 1px solid var(--border); |
| 34 | font-size: var(--fs-2xs); |
| 35 | color: var(--text-secondary); |
| 36 | } |
| 37 | .fv-fmt { font-weight: 600; letter-spacing: 0.02em; } |
| 38 | .fv-size { font-family: var(--font-mono); white-space: nowrap; } |
| 39 | |
| 40 | /* A note explains; a warning says something is not as it seems. Both are |
| 41 | quiet -- neither is an error, and a file whose name and bytes disagree is |
| 42 | interesting rather than broken. */ |
| 43 | .fv-note, |
| 44 | .fv-warn { |
| 45 | flex: none; margin: 0; |
| 46 | padding: 6px 12px; |
| 47 | font-size: var(--fs-xs); |
| 48 | line-height: 1.45; |
| 49 | border-bottom: 1px solid var(--border); |
| 50 | } |
| 51 | .fv-note { color: var(--text-muted); } |
| 52 | .fv-warn { color: var(--warn); background: var(--warn-bg); } |
| 53 | |
| 54 | /* ── The body ────────────────────────────────────────────────── */ |
| 55 | .fv-body { |
| 56 | flex: 1; min-height: 0; |
| 57 | display: flex; flex-direction: column; |
| 58 | overflow: auto; |
| 59 | } |
| 60 | |
| 61 | /* ── Tier 1: what the browser decodes ────────────────────────── */ |
| 62 | |
| 63 | /* A soft chequer behind a picture, so a transparent PNG is a picture with |
| 64 | holes in it rather than a picture that half vanished into the panel. Built |
| 65 | from two surface tokens, so it follows every palette. */ |
| 66 | .fv-img { |
| 67 | flex: 1; min-height: 0; |
| 68 | max-width: 100%; |
| 69 | object-fit: contain; |
| 70 | align-self: center; |
| 71 | background-color: var(--bg-tertiary); |
| 72 | background-image: |
| 73 | linear-gradient(45deg, var(--bg-hover) 25%, transparent 25%, transparent 75%, var(--bg-hover) 75%), |
| 74 | linear-gradient(45deg, var(--bg-hover) 25%, transparent 25%, transparent 75%, var(--bg-hover) 75%); |
| 75 | background-size: 16px 16px; |
| 76 | background-position: 0 0, 8px 8px; |
| 77 | } |
| 78 | |
| 79 | .fv-audio { |
| 80 | flex: none; |
| 81 | width: calc(100% - 24px); |
| 82 | margin: 12px; |
| 83 | } |
| 84 | |
| 85 | .fv-video { |
| 86 | flex: 1; min-height: 0; |
| 87 | width: 100%; |
| 88 | background: #000; /* a letterbox is black in every palette */ |
| 89 | } |
| 90 | |
| 91 | /* A PDF, in the browser's own viewer. It is an <embed> and not a frame because |
| 92 | a sandbox attribute of any value stops Chrome instantiating that viewer; what |
| 93 | keeps the file from running as us is the blob's type, set in the JS. */ |
| 94 | .fv-doc { |
| 95 | flex: 1; min-height: 0; |
| 96 | width: 100%; |
| 97 | border: 0; |
| 98 | background: var(--bg-tertiary); |
| 99 | } |
| 100 | |
| 101 | /* A framed file runs in an opaque origin (sandbox="allow-scripts", set in the |
| 102 | JS and nowhere else). Nothing here may add a permission to it. */ |
| 103 | .fv-frame { |
| 104 | flex: 1; min-height: 0; |
| 105 | width: 100%; |
| 106 | border: 0; |
| 107 | background: var(--bg-tertiary); |
| 108 | } |
| 109 | |
| 110 | /* ── Tier 2: text-shaped structure ───────────────────────────── */ |
| 111 | |
| 112 | .fv-plain { |
| 113 | flex: 1; min-height: 0; margin: 0; |
| 114 | padding: 12px 14px; overflow: auto; |
| 115 | white-space: pre-wrap; word-break: break-word; |
| 116 | font-family: var(--font-mono); font-size: var(--fs-sm); |
| 117 | color: var(--text-primary); |
| 118 | } |
| 119 | |
| 120 | .fv-md { |
| 121 | flex: 1; min-height: 0; |
| 122 | padding: 12px 14px; overflow: auto; |
| 123 | font-size: var(--fs-base); line-height: 1.55; |
| 124 | color: var(--text-primary); |
| 125 | } |
| 126 | /* The renderer's own furniture -- code blocks, links, token colours -- comes |
| 127 | from render.css. What is left is the plain document flow, which the chat gets |
| 128 | from the bubble around it and a document has to be given. */ |
| 129 | .fv-md > :first-child { margin-top: 0; } |
| 130 | .fv-md h1, .fv-md h2, .fv-md h3, .fv-md h4 { margin: 1.1em 0 0.45em; line-height: 1.25; } |
| 131 | .fv-md h1 { font-size: var(--fs-3xl); } |
| 132 | .fv-md h2 { font-size: var(--fs-2xl); } |
| 133 | .fv-md h3 { font-size: var(--fs-xl); } |
| 134 | .fv-md h4 { font-size: var(--fs-lg); } |
| 135 | .fv-md p, .fv-md ul, .fv-md ol, .fv-md blockquote { margin: 0.55em 0; } |
| 136 | .fv-md ul, .fv-md ol { padding-left: 1.4em; } |
| 137 | .fv-md blockquote { |
| 138 | padding-left: 0.8em; border-left: 2px solid var(--border-2); |
| 139 | color: var(--text-secondary); |
| 140 | } |
| 141 | .fv-md hr { border: 0; border-top: 1px solid var(--border); margin: 1.2em 0; } |
| 142 | .fv-md img { max-width: 100%; height: auto; } |
| 143 | /* A wide table in a document scrolls in its own box, like every other wide |
| 144 | thing here. `display: block` on the table is what gives it one. */ |
| 145 | .fv-md table { |
| 146 | display: block; overflow-x: auto; max-width: 100%; |
| 147 | border-collapse: collapse; font-size: var(--fs-sm); |
| 148 | } |
| 149 | .fv-md th, .fv-md td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; } |
| 150 | .fv-md th { background: var(--bg-tertiary); color: var(--text-secondary); } |
| 151 | |
| 152 | /* A JSON tree. The disclosure marker is the browser's; the indent is one |
| 153 | ruled line per level, which is what makes a deep document readable at a |
| 154 | glance on a narrow panel. */ |
| 155 | .fv-jnode { padding: 0; } |
| 156 | .fv-body > .fv-jnode, |
| 157 | .fv-body > .fv-jrow { padding: 10px 12px; } |
| 158 | .fv-jsum { |
| 159 | cursor: pointer; list-style: none; |
| 160 | display: flex; gap: 8px; align-items: baseline; |
| 161 | padding: 1px 0; |
| 162 | font-family: var(--font-mono); font-size: var(--fs-xs); |
| 163 | } |
| 164 | .fv-jsum::-webkit-details-marker { display: none; } |
| 165 | .fv-jsum::before { |
| 166 | content: '▸'; color: var(--text-muted); |
| 167 | font-size: var(--fs-3xs); line-height: 1.6; width: 0.8em; |
| 168 | } |
| 169 | .fv-jnode[open] > .fv-jsum::before { content: '▾'; } |
| 170 | .fv-jsum:hover { background: var(--bg-hover); } |
| 171 | .fv-jsum:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } |
| 172 | .fv-jkids { |
| 173 | margin-left: 0.55em; padding-left: 0.75em; |
| 174 | border-left: 1px solid var(--border); |
| 175 | } |
| 176 | .fv-jrow { |
| 177 | display: flex; gap: 8px; align-items: baseline; |
| 178 | padding: 1px 0; |
| 179 | font-family: var(--font-mono); font-size: var(--fs-xs); |
| 180 | word-break: break-word; |
| 181 | } |
| 182 | .fv-jkey { color: var(--text-secondary); flex: none; } |
| 183 | .fv-jkey::after { content: ':'; opacity: 0.6; } |
| 184 | .fv-jshape { color: var(--text-muted); } |
| 185 | .fv-jval { color: var(--text-primary); min-width: 0; } |
| 186 | .fv-j-string { color: var(--accent-text); } |
| 187 | .fv-j-number { color: var(--ok); } |
| 188 | .fv-j-boolean { color: var(--warn); } |
| 189 | .fv-j-null { color: var(--text-muted); font-style: italic; } |
| 190 | |
| 191 | /* A table scrolls inside its own box, in both directions. Fifty columns of |
| 192 | CSV must not push the panel -- or the page behind it -- sideways. */ |
| 193 | .fv-tablewrap { |
| 194 | flex: 1; min-height: 0; |
| 195 | overflow: auto; |
| 196 | padding: 0 0 8px 0; |
| 197 | } |
| 198 | .fv-table { |
| 199 | border-collapse: collapse; |
| 200 | font-size: var(--fs-xs); |
| 201 | font-variant-numeric: tabular-nums; |
| 202 | } |
| 203 | .fv-table th, |
| 204 | .fv-table td { |
| 205 | border: 1px solid var(--border); |
| 206 | padding: 3px 8px; |
| 207 | text-align: left; vertical-align: top; |
| 208 | max-width: 28ch; overflow-wrap: anywhere; |
| 209 | color: var(--text-primary); |
| 210 | } |
| 211 | .fv-table th { |
| 212 | position: sticky; top: 0; z-index: 1; |
| 213 | background: var(--bg-tertiary); |
| 214 | color: var(--text-secondary); |
| 215 | font-weight: 600; white-space: nowrap; |
| 216 | } |
| 217 | .fv-table tr:nth-child(even) td { background: var(--bg-secondary); } |
| 218 | |
| 219 | /* ── What a reader may DO to the document ───────────────────── |
| 220 | `fv-bar` holds the controls, `fv-editrow` the fields one of them |
| 221 | opens. Both are rows of the panel and neither floats over the |
| 222 | document: the whole point of a reading view is that it stays |
| 223 | readable while somebody works on it, and a sheet over the top is |
| 224 | the same as taking it away. The typst live view above says this |
| 225 | about a rebuild and it is just as true of an edit. |
| 226 | |
| 227 | The buttons wear `.fv-btn`, which the hex bar already defined, so |
| 228 | a control added here looks like a control already in the panel |
| 229 | rather than like a control from somewhere else. */ |
| 230 | .fv-bar { |
| 231 | flex: none; |
| 232 | display: flex; align-items: center; gap: 8px; flex-wrap: wrap; |
| 233 | padding: 6px 12px; |
| 234 | border-bottom: 1px solid var(--border); |
| 235 | } |
| 236 | |
| 237 | .fv-editrow { |
| 238 | flex: none; |
| 239 | display: flex; align-items: center; gap: 8px; flex-wrap: wrap; |
| 240 | padding: 6px 12px; |
| 241 | border-bottom: 1px solid var(--border); |
| 242 | background: var(--bg-secondary); |
| 243 | font-size: var(--fs-xs); |
| 244 | } |
| 245 | .fv-editrow[hidden] { display: none; } |
| 246 | .fv-editrow label { |
| 247 | display: flex; align-items: center; gap: 5px; |
| 248 | color: var(--text-secondary); |
| 249 | } |
| 250 | /* The fields are TOOLBAR-SIZED, and the SELECTOR is the whole of how, which is a |
| 251 | bug fixed rather than a style. |
| 252 | |
| 253 | `skin-warm.css` gives every `input[type="text"]` room to type -- |
| 254 | `:root[data-skin="warm"] input[type="text"]`, padding of 12.6px and 18.2px at |
| 255 | that skin's scale. It is loaded AFTER this file and its specificity is (0,3,1), |
| 256 | so `.fileview .fv-editrow input` at (0,2,1) lost: measured, the boxes came out |
| 257 | 43px tall in a row of 22px buttons. That is the same rule that left `.tl-page` |
| 258 | above a tall empty pill with its digit clipped out, one step short of doing it |
| 259 | again -- a `width: 5ch` box under 18.2px of side padding has NO content width |
| 260 | at all. |
| 261 | |
| 262 | Naming the type here is not enough on its own: `[type="text"]` counts on BOTH |
| 263 | sides, so `.fileview .fv-editrow input[type="text"]` is also (0,3,1) and a tie |
| 264 | goes to whichever file loads last, which is the skin. `.fv-body` is the third |
| 265 | class -- the row is always inside the panel's body -- and (0,4,1) wins |
| 266 | outright rather than on load order. Measured under both skins at 640px and |
| 267 | 380px: 22px boxes in a row of 22px buttons. |
| 268 | |
| 269 | The skin's rule is right for a form and wrong for a toolbar; this says which |
| 270 | this is. */ |
| 271 | .fileview .fv-body .fv-editrow input[type="text"], |
| 272 | .fileview .fv-body .fv-editrow input[type="number"], |
| 273 | .fileview .fv-body .fv-editrow select, |
| 274 | .fileview .fv-body .fv-bar select { |
| 275 | background: var(--bg-tertiary); border: 1px solid var(--border-strong); |
| 276 | color: var(--text-primary); border-radius: var(--radius-sm); |
| 277 | padding: 2px 6px; height: auto; min-height: 0; |
| 278 | font-size: var(--fs-xs); line-height: 1.3; |
| 279 | } |
| 280 | .fileview .fv-body .fv-editrow input[type="text"] { width: 14ch; } |
| 281 | .fileview .fv-body .fv-editrow input[type="number"] { width: 5ch; text-align: right; } |
| 282 | .fv-editrow input:focus-visible, |
| 283 | .fv-editrow select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } |
| 284 | /* The row's own sentence takes the whole width under the fields, since it is a |
| 285 | sentence and not a field. It keeps `fv-note`'s colour and loses its border, so |
| 286 | the row reads as one thing. */ |
| 287 | .fv-editrow .fv-note { |
| 288 | flex: 1 0 100%; |
| 289 | padding: 2px 0 0 0; |
| 290 | border-bottom: 0; |
| 291 | } |
| 292 | |
| 293 | /* On a phone the fields stack rather than shrink: a 14ch box squeezed into a |
| 294 | 380px row is a box nobody can read what they typed in. */ |
| 295 | @media (max-width: 380px) { |
| 296 | .fv-bar { padding: 6px 10px; } |
| 297 | .fv-editrow { padding: 6px 10px; } |
| 298 | .fv-editrow label { flex: 1 0 100%; } |
| 299 | .fileview .fv-body .fv-editrow input[type="text"] { flex: 1 1 auto; width: auto; } |
| 300 | } |
| 301 | |
| 302 | /* ── Tier 3: the honest floor ────────────────────────────────── */ |
| 303 | |
| 304 | .fv-hexbar { |
| 305 | flex: none; |
| 306 | display: flex; align-items: center; gap: 8px; flex-wrap: wrap; |
| 307 | padding: 6px 12px; |
| 308 | border-bottom: 1px solid var(--border); |
| 309 | } |
| 310 | .fv-btn { |
| 311 | background: transparent; border: 1px solid var(--border-strong); |
| 312 | color: var(--text-secondary); border-radius: var(--radius-sm); |
| 313 | padding: 3px 8px; cursor: pointer; |
| 314 | font-size: var(--fs-xs); line-height: 1.2; |
| 315 | } |
| 316 | .fv-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--accent); } |
| 317 | .fv-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } |
| 318 | .fv-btn:disabled { opacity: 0.5; cursor: default; } |
| 319 | .fv-hexat { |
| 320 | font-family: var(--font-mono); font-size: var(--fs-2xs); |
| 321 | color: var(--text-muted); white-space: nowrap; |
| 322 | } |
| 323 | |
| 324 | /* The dump keeps its measure. `white-space: pre` and its own horizontal |
| 325 | scrollbar: sixteen bytes to a line is the format, and a line that wraps at |
| 326 | 380px is no longer a hex dump. The skin does not get to soften this face |
| 327 | either -- the columns only line up in a monospace. */ |
| 328 | .fv-hex { |
| 329 | flex: 1; min-height: 0; margin: 0; |
| 330 | padding: 10px 12px; |
| 331 | overflow: auto; |
| 332 | white-space: pre; |
| 333 | font-family: var(--font-mono); |
| 334 | font-size: var(--fs-2xs); |
| 335 | line-height: 1.45; |
| 336 | color: var(--text-primary); |
| 337 | tab-size: 4; |
| 338 | } |
| 339 | |
| 340 | /* On a phone the dump is the widest thing in the app. It still scrolls in its |
| 341 | own box rather than wrapping, and the type drops one rung so that more of a |
| 342 | line is on screen before anybody has to swipe. */ |
| 343 | @media (max-width: 380px) { |
| 344 | .fv-hex { font-size: var(--fs-3xs); padding: 8px 10px; } |
| 345 | .fv-meta { padding: 6px 10px; } |
| 346 | .fv-note, |
| 347 | .fv-warn { padding: 6px 10px; } |
| 348 | .fv-hexbar { padding: 6px 10px; } |
| 349 | .fv-hexat { white-space: normal; } |
| 350 | } |
| 351 | |
| 352 | /* ============================================================ |
| 353 | The watched live document — www/js/typstwatch.js |
| 354 | ------------------------------------------------------------ |
| 355 | The pages the author is looking at while he writes. It sits |
| 356 | INSIDE the Doc panel, over the <embed> the Compile button |
| 357 | filled, and takes nothing away: closing it puts the PDF back. |
| 358 | |
| 359 | Two rules here are load-bearing and neither is decoration. |
| 360 | |
| 361 | NOTHING MAY OVERLAY THE PAGES. The mark that a rebuild is |
| 362 | running is a dot and a few words in a bar ABOVE the document, |
| 363 | never a veil over it and never a spinner in the middle of it. |
| 364 | The whole point of the loop is that the last good pages stay |
| 365 | readable while the next ones are built, and dimming them is |
| 366 | the same as taking them away for anybody actually reading. |
| 367 | |
| 368 | AND THE PAGES ARE NOT SKINNED. They are the book: the paper is |
| 369 | white because the paper prints white, and the ink is the ink |
| 370 | the PDF carries. The app's palette stops at the bar. A dark |
| 371 | theme that tinted the page would be showing the author a book |
| 372 | that does not exist. |
| 373 | ============================================================ */ |
| 374 | |
| 375 | /* A ROW OF THE PANEL, NOT A SHEET OVER IT, and that is a bug fixed rather than a |
| 376 | style. Laid out first as `position: absolute; inset: 0`, which covered the |
| 377 | panel's own header — so the ✕ that closes the document was underneath it and the |
| 378 | reader could not put the document down. The live view is a third rendering of |
| 379 | the Doc panel beside the text view and the <embed>, so it takes its place in the |
| 380 | same column as the other two and leaves the header alone. */ |
| 381 | .typst-live { |
| 382 | flex: 1 1 auto; |
| 383 | display: flex; flex-direction: column; |
| 384 | min-height: 0; |
| 385 | background: var(--bg-tertiary); |
| 386 | } |
| 387 | |
| 388 | .tl-bar { |
| 389 | display: flex; align-items: center; gap: 8px; |
| 390 | padding: 5px 10px; |
| 391 | border-bottom: 1px solid var(--border-subtle); |
| 392 | background: var(--bg-secondary); |
| 393 | font-size: var(--fs-2xs); |
| 394 | color: var(--text-muted); |
| 395 | flex: 0 0 auto; |
| 396 | } |
| 397 | .tl-says { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| 398 | |
| 399 | /* THE RAIL'S OWN SWITCH, at the far left of the bar, which is where every |
| 400 | document viewer puts it and therefore where a reader looks for it. */ |
| 401 | .tl-toc { |
| 402 | flex: 0 0 auto; |
| 403 | background: transparent; border: 1px solid var(--border-subtle); |
| 404 | color: var(--text-secondary); border-radius: var(--radius-sm); |
| 405 | padding: 0 6px; cursor: pointer; |
| 406 | font-size: var(--fs-xs); line-height: 1.5; |
| 407 | } |
| 408 | .tl-toc:hover { color: var(--text-primary); border-color: var(--accent); } |
| 409 | .tl-toc:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } |
| 410 | .tl-toc[aria-pressed="true"] { color: var(--text-primary); border-color: var(--accent); } |
| 411 | |
| 412 | /* A dot, and what it means. Quiet on purpose: it reports, it does not ask. */ |
| 413 | .tl-mark { |
| 414 | width: 7px; height: 7px; border-radius: 50%; |
| 415 | flex: 0 0 auto; |
| 416 | background: var(--text-muted); |
| 417 | } |
| 418 | .tl-mark.tl-live { background: var(--ok, #3fa66a); } |
| 419 | .tl-mark.tl-building { background: var(--accent); animation: tl-pulse 1.1s ease-in-out infinite; } |
| 420 | .tl-mark.tl-stale, |
| 421 | .tl-mark.tl-held { background: var(--warn, #c9932b); } |
| 422 | .tl-mark.tl-dead { background: var(--danger, #c0453c); } |
| 423 | @keyframes tl-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } } |
| 424 | /* A reader who has asked for less movement gets a steady dot rather than none: |
| 425 | the state is still legible, it simply stops moving. */ |
| 426 | @media (prefers-reduced-motion: reduce) { |
| 427 | .tl-mark.tl-building { animation: none; } |
| 428 | } |
| 429 | |
| 430 | .tl-rebuild { |
| 431 | flex: 0 0 auto; |
| 432 | background: transparent; border: 1px solid var(--border-strong); |
| 433 | color: var(--text-secondary); border-radius: var(--radius-sm); |
| 434 | padding: 2px 8px; cursor: pointer; |
| 435 | font-size: var(--fs-2xs); line-height: 1.2; |
| 436 | } |
| 437 | .tl-rebuild:hover { color: var(--text-primary); border-color: var(--accent); } |
| 438 | .tl-rebuild:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } |
| 439 | |
| 440 | /* THE SETTINGS THE VIEW SHIPPED WITHOUT. A page box, a zoom and dark paper, in |
| 441 | the bar it already had rather than behind a menu: there are three of them, and |
| 442 | a menu holding three things is a place to hide them. They ride at the right so |
| 443 | the status keeps the left, which is where a reader looks first. */ |
| 444 | .tl-set { |
| 445 | flex: 0 0 auto; |
| 446 | display: flex; align-items: center; gap: 4px; |
| 447 | } |
| 448 | .tl-set button { |
| 449 | background: transparent; border: 1px solid var(--border-subtle); |
| 450 | color: var(--text-secondary); border-radius: var(--radius-sm); |
| 451 | padding: 1px 6px; cursor: pointer; |
| 452 | font-size: var(--fs-2xs); line-height: 1.3; |
| 453 | min-width: 22px; |
| 454 | } |
| 455 | .tl-set button:hover { color: var(--text-primary); border-color: var(--accent); } |
| 456 | .tl-set button:focus-visible, |
| 457 | .tl-page:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } |
| 458 | .tl-set .tl-fit { min-width: 42px; font-variant-numeric: tabular-nums; } |
| 459 | .tl-set .tl-night { margin-left: 4px; } |
| 460 | .tl-page { |
| 461 | width: 3.2em; |
| 462 | background: var(--bg-tertiary); border: 1px solid var(--border-subtle); |
| 463 | color: var(--text-primary); border-radius: var(--radius-sm); |
| 464 | padding: 1px 4px; text-align: right; |
| 465 | font-size: var(--fs-2xs); line-height: 1.3; |
| 466 | font-variant-numeric: tabular-nums; |
| 467 | } |
| 468 | /* AND THE PAGE NUMBER HAS TO FIT IN THE BOX, which it did not. |
| 469 | |
| 470 | `skin-warm.css` gives every `input[type="text"]` room to type -- |
| 471 | `padding: calc(9px * var(--space-scale)) calc(13px * var(--space-scale))`, |
| 472 | which at the warm skin's scale is 12.6px and 18.2px. That selector is |
| 473 | `:root[data-skin="warm"] input[type="text"]` and beats a bare `.tl-page`, |
| 474 | so on a box 36px wide with `box-sizing: border-box` the content width came |
| 475 | out at ZERO and `overflow: clip` took the digit away: the page box was a |
| 476 | tall empty pill beside "/ 4", in a bar of 20px controls, and had been since |
| 477 | it shipped. Measured, not guessed -- the computed padding is above and the |
| 478 | crop that found it is in the report. |
| 479 | |
| 480 | The id wins the specificity, and the height is stated rather than left to |
| 481 | the padding so that the box matches the buttons it sits between. The skin's |
| 482 | rule is right for a form and wrong for a toolbar; this says which this is. */ |
| 483 | #typst-live .tl-page { |
| 484 | padding: 1px 4px; |
| 485 | height: auto; |
| 486 | min-height: 0; |
| 487 | border-radius: var(--radius-sm); |
| 488 | } |
| 489 | .tl-of { color: var(--text-muted); font-variant-numeric: tabular-nums; margin-right: 4px; } |
| 490 | |
| 491 | /* THE RAIL AND THE PAGES SHARE A ROW. The rail takes width from the pages |
| 492 | rather than sitting over them, so nothing is ever hidden behind it and the |
| 493 | zoom keeps meaning "as wide as what is left". */ |
| 494 | .tl-body { |
| 495 | flex: 1 1 auto; min-height: 0; |
| 496 | display: flex; align-items: stretch; |
| 497 | } |
| 498 | |
| 499 | /* THE SECTIONS, HYPERLINKED. Its own scroller, because a long book's contents |
| 500 | is longer than the panel and the pages must not move when it is scrolled. |
| 501 | The page number rides right, as it does in a printed contents. */ |
| 502 | .tl-rail { |
| 503 | flex: 0 0 auto; |
| 504 | width: 210px; max-width: 45%; |
| 505 | overflow: auto; |
| 506 | border-right: 1px solid var(--border-subtle); |
| 507 | background: var(--bg-secondary); |
| 508 | padding: 6px 0; |
| 509 | } |
| 510 | .tl-rail[hidden] { display: none; } |
| 511 | .tl-toclist { list-style: none; margin: 0; padding: 0; } |
| 512 | .tl-toclist li { margin: 0; } |
| 513 | .tl-tocgo { |
| 514 | display: flex; align-items: baseline; gap: 6px; |
| 515 | width: 100%; |
| 516 | background: transparent; border: 0; |
| 517 | color: var(--text-secondary); |
| 518 | text-align: left; cursor: pointer; |
| 519 | padding: 3px 8px 3px 10px; |
| 520 | font-size: var(--fs-2xs); line-height: 1.35; |
| 521 | } |
| 522 | .tl-tocgo:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-tertiary); } |
| 523 | .tl-tocgo:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } |
| 524 | /* An entry whose page is not known yet still READS -- it is the document's own |
| 525 | contents -- it simply cannot be jumped to. Dimming it says which. */ |
| 526 | .tl-tocgo:disabled { cursor: default; opacity: 0.55; } |
| 527 | .tl-tocpage { |
| 528 | margin-left: auto; flex: 0 0 auto; |
| 529 | color: var(--text-muted); |
| 530 | font-variant-numeric: tabular-nums; |
| 531 | } |
| 532 | /* The level is the indent, and nothing else: a rail that also changed the size |
| 533 | of every level reads as six typefaces rather than one contents. */ |
| 534 | .tl-toclist li[data-level="2"] .tl-tocgo { padding-left: 22px; } |
| 535 | .tl-toclist li[data-level="3"] .tl-tocgo { padding-left: 34px; } |
| 536 | .tl-toclist li[data-level="4"] .tl-tocgo { padding-left: 46px; } |
| 537 | .tl-toclist li[data-level="5"] .tl-tocgo { padding-left: 58px; } |
| 538 | .tl-toclist li[data-level="6"] .tl-tocgo { padding-left: 70px; } |
| 539 | .tl-toclist li[data-level="1"] .tl-tocgo { color: var(--text-primary); } |
| 540 | /* Where the reader is, marked on the rail rather than announced anywhere. */ |
| 541 | .tl-toclist li[data-here="1"] .tl-tocgo { |
| 542 | color: var(--text-primary); |
| 543 | box-shadow: inset 2px 0 0 var(--accent); |
| 544 | } |
| 545 | .tl-tocnone { |
| 546 | margin: 0; padding: 8px 10px; |
| 547 | color: var(--text-muted); |
| 548 | font-size: var(--fs-2xs); line-height: 1.4; |
| 549 | } |
| 550 | |
| 551 | /* The scroller, and the reader's place in it. Nothing else scrolls: the pages |
| 552 | are one tall column, so the scroll position IS the place in the book. */ |
| 553 | .tl-scroll { |
| 554 | flex: 1 1 auto; min-height: 0; |
| 555 | overflow: auto; |
| 556 | background: var(--bg-tertiary); |
| 557 | padding: 8px; |
| 558 | } |
| 559 | /* THE STACK, WHICH IS NOT ITSELF PAPER. Each page is its own sheet inside the |
| 560 | shadow root and carries its own white and its own shadow (`SHEET_CSS` in |
| 561 | `www/js/typstwatch.js`, because the app's stylesheet does not reach into a |
| 562 | shadow root); what shows between two sheets is the panel's own ground, and |
| 563 | that gap is the whole of "distinct pages, not a continuous one". |
| 564 | |
| 565 | THE CONTAINER CARRIES THE HEIGHT AND EACH SHEET CARRIES ONE PAGE, which is |
| 566 | why this is `position: relative` with its height set from JavaScript. An SVG |
| 567 | as tall as a 251-page book is 192,000 pixels, and Chrome rasterises that far |
| 568 | from its origin at a reduced resolution: page 1 came out at 5.6% dark pixels |
| 569 | and page 40 at 0.00% -- the words still there in outline, the page a pale |
| 570 | wash. Measured, and it looks exactly like a colour bug. */ |
| 571 | .tl-pages { |
| 572 | display: block; |
| 573 | position: relative; |
| 574 | background: transparent; |
| 575 | margin: 0 auto; |
| 576 | /* The WIDTH IS SET FROM JAVASCRIPT, like the height, because the reader can |
| 577 | now ask for pages wider than the panel. A `max-width: 100%` here would have |
| 578 | clamped every zoom past "fit" back to the panel and made the control look |
| 579 | broken while doing exactly what it said. */ |
| 580 | max-width: none; |
| 581 | } |
| 582 | |
| 583 | /* DARK PAPER. The pages are inverted where they are drawn, so the panel around |
| 584 | them keeps the app's own ground and only the paper turns over; `hue-rotate` |
| 585 | after the inversion puts colours back roughly where they were, so a red figure |
| 586 | stays red instead of turning cyan. Nothing is recompiled and nothing about the |
| 587 | document changes: what is exported and printed is what it always was. */ |
| 588 | .typst-live[data-night="1"] .tl-pages { |
| 589 | filter: invert(1) hue-rotate(180deg); |
| 590 | } |
| 591 | |
| 592 | /* The compiler's own words, under the document rather than over it, so a broken |
| 593 | build costs the reader a strip at the bottom and not the page. */ |
| 594 | .tl-err { |
| 595 | flex: 0 0 auto; |
| 596 | max-height: 32%; |
| 597 | overflow: auto; |
| 598 | margin: 0; |
| 599 | padding: 8px 10px; |
| 600 | border-top: 1px solid var(--border-subtle); |
| 601 | background: var(--bg-secondary); |
| 602 | color: var(--danger, #c0453c); |
| 603 | font-family: var(--font-mono); |
| 604 | font-size: var(--fs-2xs); |
| 605 | line-height: 1.45; |
| 606 | white-space: pre-wrap; |
| 607 | overflow-wrap: anywhere; |
| 608 | } |
| 609 | |
| 610 | /* On a phone the bar loses the word and keeps the dot: the state is still |
| 611 | there, and the document gets the width. */ |
| 612 | @media (max-width: 380px) { |
| 613 | .tl-bar { padding: 4px 8px; } |
| 614 | .tl-scroll { padding: 4px; } |
| 615 | .tl-err { max-height: 40%; } |
| 616 | /* The page box and the percentage go; the zoom and the paper stay, because |
| 617 | those are the two a phone actually needs. The status keeps its dot. */ |
| 618 | .tl-page, .tl-of, .tl-set .tl-fit { display: none; } |
| 619 | /* And the rail takes the whole panel rather than a slice of it: 210px of a |
| 620 | 360px screen leaves a column of paper nobody can read. */ |
| 621 | .tl-rail { width: 100%; max-width: none; border-right: 0; } |
| 622 | .typst-live[data-rail="1"] .tl-scroll { display: none; } |
| 623 | } |