Recently Written · git

scan-subread

PDF reader that follows an audiobook with the SubRead suite

git clone https://github.com/equwal/scan-subread

Log | Files | Refs


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 }