src/style.css (11012 bytes)
1 :root { 2 color-scheme: light dark; 3 font-family: system-ui, sans-serif; 4 --highlight: rgba(255, 200, 0, 0.45); 5 --highlight-solid: rgb(255, 200, 0); 6 --highlight-weak: rgba(120, 160, 255, 0.35); 7 --topbar: 3.25rem; 8 /* The height of the status strip. The panel of its full texts starts under it. */ 9 --strip: 2rem; 10 /* The size of a button of the top bar: 48 CSS px, the Android guideline for a touch target. */ 11 --target: 3rem; 12 --panel-width: 22rem; 13 } 14 15 /* The hidden attribute wins over each `display` rule below, for example `.row`. */ 16 [hidden] { 17 display: none !important; 18 } 19 20 /* Phone layout: top bar, page below, controls in a drawer. */ 21 body { 22 margin: 0; 23 display: flex; 24 flex-direction: column; 25 height: 100vh; 26 height: 100dvh; 27 overflow: hidden; 28 } 29 30 /* 31 * The top bar fits a phone of 360 CSS pixels in one row: five icon buttons 32 * of 48 px, and the page label in the free space. 33 */ 34 #topbar { 35 flex: none; 36 box-sizing: border-box; 37 height: var(--topbar); 38 display: flex; 39 gap: 4px; 40 align-items: center; 41 padding: 0 4px; 42 border-bottom: 1px solid #8884; 43 } 44 45 #topbar button { 46 flex: none; 47 box-sizing: border-box; 48 height: var(--target); 49 min-width: var(--target); 50 padding: 0 0.3rem; 51 display: inline-flex; 52 align-items: center; 53 justify-content: center; 54 font: inherit; 55 font-size: 0.9rem; 56 white-space: nowrap; 57 color: inherit; 58 background: none; 59 border: 1px solid transparent; 60 border-radius: 6px; 61 cursor: pointer; 62 } 63 64 /* A touch screen keeps the hover state on the last button that was tapped. */ 65 @media (hover: hover) { 66 #topbar button:hover:not(:disabled) { 67 background: #8882; 68 } 69 } 70 71 #topbar button:active:not(:disabled) { 72 background: #8884; 73 } 74 75 #topbar button:focus-visible { 76 outline: 2px solid Highlight; 77 outline-offset: -2px; 78 } 79 80 #topbar button:disabled { 81 opacity: 0.35; 82 cursor: default; 83 } 84 85 #topbar svg { 86 width: 1.5rem; 87 height: 1.5rem; 88 fill: none; 89 stroke: currentColor; 90 stroke-width: 2; 91 stroke-linecap: round; 92 stroke-linejoin: round; 93 } 94 95 #topbar svg .solid { 96 fill: currentColor; 97 stroke: none; 98 } 99 100 #play .i-pause, 101 #play.playing .i-play { 102 display: none; 103 } 104 105 #play.playing .i-pause { 106 display: block; 107 } 108 109 /* The page label takes the free space, and it opens the page jump. */ 110 #topbar #page-label { 111 flex: 1 1 auto; 112 min-width: 0; 113 overflow: hidden; 114 text-overflow: ellipsis; 115 font-variant-numeric: tabular-nums; 116 } 117 118 /* 119 * The Follow button shows while a page turn by the user holds the follow. 120 * It floats over the lower right corner of the page, so the buttons of the 121 * top bar stay where they are when it shows. 122 */ 123 #follow-now { 124 position: absolute; 125 right: 1rem; 126 bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); 127 z-index: 5; 128 min-height: 48px; 129 padding: 0 1.2rem; 130 font: inherit; 131 font-weight: 600; 132 color: #1a1a1a; 133 background: var(--highlight-solid); 134 border: none; 135 border-radius: 24px; 136 box-shadow: 0 2px 8px #0006; 137 cursor: pointer; 138 } 139 140 #follow-note { 141 font-weight: 600; 142 } 143 144 /* The page jump takes the place of the page label. */ 145 #page-input { 146 flex: 1 1 auto; 147 min-width: 0; 148 width: 4rem; 149 box-sizing: border-box; 150 height: var(--target); 151 padding: 0 0.4rem; 152 font: inherit; 153 font-size: 1rem; 154 text-align: center; 155 } 156 157 /* 158 * The status strip: the player, the reading of the pages, the last event. 159 * One line of a fixed height, so the page under it never moves: a long 160 * message wrapped and pushed the page down, and the page went up again 161 * when the message faded. A long text ends with an ellipsis. Its title 162 * attribute holds the full text, and a tap on the strip shows the full 163 * texts in #strip-full (status.ts). 164 */ 165 #strip { 166 flex: none; 167 box-sizing: border-box; 168 height: var(--strip); 169 cursor: pointer; 170 display: flex; 171 flex-wrap: nowrap; 172 align-items: stretch; 173 column-gap: 0.75rem; 174 padding: 0 0.5rem; 175 font-size: 0.8rem; 176 line-height: 1.3; 177 white-space: nowrap; 178 overflow: hidden; 179 border-bottom: 1px solid #8884; 180 } 181 182 #strip > * { 183 display: flex; 184 align-items: center; 185 gap: 0.3rem; 186 min-width: 0; 187 flex: 0 1 auto; 188 } 189 190 /* The player text is short and always there: the other parts shrink first. */ 191 #player-status { 192 flex-shrink: 0.3; 193 } 194 195 #strip .text { 196 min-width: 0; 197 overflow: hidden; 198 text-overflow: ellipsis; 199 } 200 201 /* A link, a button and "Follow paused" do not shrink: a cut button cannot be read. */ 202 #strip a, 203 #strip button.action, 204 #follow-note { 205 flex: none; 206 } 207 208 #strip button.action { 209 margin: 3px 0; 210 align-self: stretch; 211 } 212 213 /* A strip with no text takes no space. */ 214 #strip:not(:has(> :not([hidden]):not(:empty))) { 215 display: none; 216 } 217 218 #strip a { 219 color: inherit; 220 } 221 222 /* A button after a message, for example Retry. */ 223 button.action { 224 font: inherit; 225 padding: 0 0.5rem; 226 color: inherit; 227 background: none; 228 border: 1px solid currentColor; 229 border-radius: 4px; 230 cursor: pointer; 231 } 232 233 #pages-status { 234 opacity: 0.8; 235 } 236 237 #status { 238 transition: opacity 600ms ease-out; 239 } 240 241 #status.faded { 242 opacity: 0; 243 } 244 245 /* 246 * While an event shows, it takes the place of the player text: on a phone 247 * the parts do not fit in one line. The link or the button of the player 248 * stays. 249 */ 250 #strip.event #player-status .text, 251 #strip.event #player-status:not(:has(a, button)) { 252 display: none; 253 } 254 255 /* 256 * The full texts of the strip, after a tap on it: a phone cannot hover to 257 * read the title attribute. The panel floats over the top of the page, so 258 * the page does not move. The next tap closes it. 259 */ 260 #strip-full { 261 position: fixed; 262 top: calc(var(--topbar) + var(--strip)); 263 left: 0; 264 right: 0; 265 z-index: 6; 266 box-sizing: border-box; 267 max-height: 50vh; 268 overflow-y: auto; 269 padding: 0.5rem 0.75rem; 270 display: flex; 271 flex-direction: column; 272 gap: 0.4rem; 273 font-size: 0.85rem; 274 line-height: 1.35; 275 background: Canvas; 276 border-bottom: 1px solid #8884; 277 box-shadow: 0 4px 12px #0004; 278 } 279 280 #strip-full p { 281 margin: 0; 282 overflow-wrap: anywhere; 283 } 284 285 #panel { 286 position: fixed; 287 top: var(--topbar); 288 left: 0; 289 bottom: 0; 290 width: min(var(--panel-width), 88vw); 291 box-sizing: border-box; 292 padding: 1rem; 293 overflow-y: auto; 294 display: flex; 295 flex-direction: column; 296 gap: 0.6rem; 297 background: Canvas; 298 border-right: 1px solid #8884; 299 box-shadow: 0 0 12px #0004; 300 transform: translateX(-100%); 301 transition: transform 150ms ease-out; 302 z-index: 10; 303 } 304 305 body.menu-open #panel { 306 transform: none; 307 } 308 309 #panel h1 { 310 font-size: 1.2rem; 311 margin: 0; 312 } 313 314 .hint { 315 margin: 0; 316 font-size: 0.85rem; 317 opacity: 0.8; 318 } 319 320 #panel label { 321 display: flex; 322 flex-direction: column; 323 font-size: 0.85rem; 324 gap: 0.2rem; 325 } 326 327 #panel input[type='file'] { 328 max-width: 100%; 329 } 330 331 .row { 332 display: flex; 333 flex-wrap: wrap; 334 align-items: center; 335 gap: 0.5rem; 336 font-size: 0.85rem; 337 } 338 339 #panel label.check, 340 #follow label { 341 flex-direction: row; 342 align-items: center; 343 gap: 0.4rem; 344 } 345 346 #follow { 347 display: flex; 348 flex-direction: column; 349 gap: 0.3rem; 350 margin: 0; 351 padding: 0.4rem 0.6rem; 352 border: 1px solid #8884; 353 border-radius: 4px; 354 font-size: 0.85rem; 355 } 356 357 #follow legend { 358 padding: 0 0.2rem; 359 } 360 361 #make, 362 #audio-row { 363 display: flex; 364 flex-direction: column; 365 gap: 0.4rem; 366 font-size: 0.85rem; 367 } 368 369 #make-status { 370 word-break: break-word; 371 } 372 373 #make-status a { 374 color: inherit; 375 } 376 377 #audio { 378 width: 100%; 379 } 380 381 /* 382 * The cue list has its own scroll box: it takes the free height of the 383 * menu, at least 12rem. It scrolls to the cue of now, and the settings 384 * above it stay. 385 */ 386 #cues { 387 position: relative; 388 flex: 1 0 12rem; 389 min-height: 12rem; 390 overflow-y: auto; 391 padding-left: 1.5rem; 392 margin: 0; 393 font-size: 0.9rem; 394 border-top: 1px solid #8884; 395 } 396 397 #cues li { 398 cursor: pointer; 399 padding: 0.15rem 0.3rem; 400 border-radius: 3px; 401 white-space: pre-line; 402 } 403 404 #cues li.unmatched { 405 opacity: 0.55; 406 } 407 408 #cues li.active { 409 background: var(--highlight); 410 } 411 412 /* The frame of the Follow button. */ 413 #main { 414 position: relative; 415 flex: 1; 416 min-height: 0; 417 display: flex; 418 flex-direction: column; 419 } 420 421 #viewer { 422 flex: 1; 423 min-height: 0; 424 overflow: auto; 425 /* A scroll bar that comes and goes does not change the width of the page. */ 426 scrollbar-gutter: stable; 427 padding: 0.5rem; 428 /* The browser pans vertically only, so a sideways swipe turns the page (nav.ts). */ 429 touch-action: pan-y pinch-zoom; 430 } 431 432 /* A zoomed page pans in each direction, and a swipe does not turn it. */ 433 body.zoomed #viewer { 434 touch-action: auto; 435 } 436 437 /* The start card: what the app does, and how to start. */ 438 .card { 439 box-sizing: border-box; 440 max-width: 28rem; 441 margin: 1.5rem auto; 442 padding: 1.25rem; 443 display: flex; 444 flex-direction: column; 445 align-items: flex-start; 446 gap: 0.75rem; 447 border: 1px solid #8884; 448 border-radius: 10px; 449 line-height: 1.4; 450 } 451 452 .card h2 { 453 margin: 0; 454 font-size: 1.3rem; 455 } 456 457 .card p { 458 margin: 0; 459 } 460 461 .card button.primary { 462 font: inherit; 463 font-weight: 600; 464 padding: 0.6rem 1.2rem; 465 border-radius: 8px; 466 border: none; 467 color: #1a1a1a; 468 background: var(--highlight-solid); 469 cursor: pointer; 470 } 471 472 #suite-checklist:empty { 473 display: none; 474 } 475 476 #suite-checklist h3, 477 #suite h2 { 478 margin: 0 0 0.3rem; 479 font-size: 0.95rem; 480 } 481 482 /* The checklist of the SubRead suite: a mark, the text, then a link or a button. */ 483 .suite { 484 list-style: none; 485 margin: 0; 486 padding: 0; 487 display: flex; 488 flex-direction: column; 489 gap: 0.35rem; 490 font-size: 0.85rem; 491 } 492 493 .suite li::before { 494 content: '✓'; 495 display: inline-block; 496 width: 1.3em; 497 font-weight: 700; 498 } 499 500 .suite li.missing::before { 501 content: '✗'; 502 } 503 504 .suite a { 505 color: inherit; 506 } 507 508 /* 509 * The page fits the viewer width in each orientation, up to 1000 CSS 510 * pixels on a wide screen. The viewer scrolls vertically. 511 */ 512 #page { 513 position: relative; 514 margin: 0 auto; 515 width: 100%; 516 max-width: 1000px; 517 /* A long press makes an Anki card: no selection, no image menu. */ 518 -webkit-user-select: none; 519 user-select: none; 520 -webkit-touch-callout: none; 521 } 522 523 #page canvas { 524 display: block; 525 width: 100%; 526 height: auto; 527 box-shadow: 0 0 6px #0006; 528 } 529 530 /* The padding of a box can go past the edge of the page. The page clips it. */ 531 #overlay { 532 position: absolute; 533 inset: 0; 534 pointer-events: none; 535 overflow: hidden; 536 } 537 538 .box { 539 position: absolute; 540 background: var(--highlight); 541 border-radius: 2px; 542 transition: all 120ms ease-out; 543 } 544 545 .box.unmatched { 546 background: var(--highlight-weak); 547 } 548 549 /* Desktop layout: panel and page side by side. */ 550 @media (min-width: 900px) { 551 body { 552 display: grid; 553 grid-template-columns: var(--panel-width) 1fr; 554 grid-template-rows: var(--topbar) auto 1fr; 555 } 556 557 #topbar #menu { 558 display: none; 559 } 560 561 #topbar { 562 grid-column: 2; 563 grid-row: 1; 564 } 565 566 #strip { 567 grid-column: 2; 568 grid-row: 2; 569 min-width: 0; 570 } 571 572 #strip-full { 573 left: var(--panel-width); 574 } 575 576 #panel { 577 grid-column: 1; 578 grid-row: 1 / 4; 579 position: static; 580 width: auto; 581 transform: none; 582 box-shadow: none; 583 } 584 585 #main { 586 grid-column: 2; 587 grid-row: 3; 588 min-width: 0; 589 } 590 591 #viewer { 592 padding: 1rem; 593 } 594 }