-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshell.css
More file actions
982 lines (852 loc) · 37.6 KB
/
Copy pathshell.css
File metadata and controls
982 lines (852 loc) · 37.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
/* ==========================================================================
vivaocean-theme / shell
--------------------------------------------------------------------------
The machine: one window, two rails, one menu, and a desk to put the window
down on. The water is next door in scene/scene.css; this file is everything
the reader can touch.
THE SHELL HAS TWO STATES AND THEY ARE BOTH REAL. Window-open is the site.
Desk-clear is the window closed: the scene with nothing in front of it, the
rails still in the water at 1024 and up, and one launcher at the top left.
Neither is a mode and neither is a dialog; they are the two things a window
on a desk can be doing. Every rule below that is scoped under `.vo-desk`
belongs to the second one.
HOW THE PROGRESSIVE ENHANCEMENT WORKS. Every page in this theme is a real
static HTML document. There is no app, no router, and no client-rendered
anything, and the enhancement is not a fallback story bolted onto one:
1. The base styles below assume no JavaScript. A page is a masthead,
two navigations as ordinary link lists, and the window as a card in
the document flow, on the ocean. It scrolls like a page. Every link
is a link and every URL is a URL.
2. shell.js puts `vo-live` on the <html> element as its first act.
Every rule scoped under .vo-live upgrades that same markup: the page
stops scrolling, the window pins to the middle of the screen, the
two navigations become rails down the left and right edges, and
clicking one swaps the window's contents instead of reloading the
document. No markup is duplicated and nothing is re-rendered.
THE SWAP IS AN OPTIMISATION, NOT AN ARCHITECTURE. shell.js fetches the
target page, lifts its window body out, and puts it in this one. If the
fetch cannot run, and over file:// it cannot, the click falls through to
an ordinary navigation and the reader gets the same page by the slow
road. That is why every page is a real document: the fallback is not a
degraded mode, it is the site.
There is not one color literal in this file. Every color, every duration
and every piece of shell geometry is a var() reference to tokens.css.
What is a bare length here, and why. Three kinds, and nothing else:
1. Control and target minimums that exist to satisfy a standard rather
than a design: min-width and min-height 44px, WCAG 2.5.8. These stay
literal deliberately. A token invites a re-skin to lower an
accessibility floor, which is the one move this theme should make
awkward rather than easy.
2. Injected-glyph boxes: the 18px, 20px and 26px that size the SVGs
shell.js draws. Those are intrinsic to the drawings, not design
values.
3. The 1023px breakpoint in the media queries, which cannot be a custom
property because media query conditions do not read them.
Requires tokens.css, and scene.css if you want the water. Load them, then
this, then the script:
<link rel="stylesheet" href="tokens/tokens.css" />
<link rel="stylesheet" href="scene/scene.css" />
<link rel="stylesheet" href="components/components.css" />
<link rel="stylesheet" href="shell/shell.css" />
<script src="shell/shell.js" defer></script>
========================================================================== */
/* --------------------------------------------------------------------------
THE DOCUMENT
Live, the page does not scroll; the window does. Without the script the
page is a page, and pages scroll.
-------------------------------------------------------------------------- */
.vo-live,
.vo-live body {
overflow: hidden;
height: 100vh;
height: 100dvh;
}
/* --------------------------------------------------------------------------
SKIP LINK
Above everything, including the menu panel.
-------------------------------------------------------------------------- */
.vo-skip {
position: absolute;
left: -9999px;
}
.vo-skip:focus {
left: var(--vo-space-4);
top: var(--vo-space-4);
z-index: var(--vo-z-skip);
padding: var(--vo-space-3) var(--vo-space-4);
background: var(--vo-surface);
color: var(--vo-ink);
border: var(--vo-border-width) solid var(--vo-border-strong);
border-radius: var(--vo-radius);
box-shadow: var(--vo-shadow-raised);
font-weight: var(--vo-weight-bold);
}
/* --------------------------------------------------------------------------
THE PAGE MASTHEAD
Only ever seen without the script. Live, the brand moves into the title
bar, where an application keeps its name, and this element goes away
rather than being duplicated up there.
-------------------------------------------------------------------------- */
.vo-masthead {
padding: var(--vo-space-8) var(--vo-space-4) 0;
text-align: center;
color: var(--vo-ground-ink);
}
.vo-masthead-name {
margin: 0;
font-size: var(--vo-text-h1);
font-weight: var(--vo-weight-display);
letter-spacing: var(--vo-tracking-tight);
}
.vo-masthead-tagline {
max-width: var(--vo-measure);
margin: var(--vo-space-2) auto 0;
}
.vo-live .vo-masthead {
display: none;
}
/* --------------------------------------------------------------------------
THE RAILS
Two navigations, split by what they are for rather than by where they
fit: the left one is the product, the right one is the company. That is
also why they survive the collapse to one menu without needing new
labels, and why a screen reader hears two named navigations rather than
one navigation cut in half.
As a page: two ordinary link lists under the masthead. Live on a wide
screen: two vertical rails pinned to the left and right edges. Live on a
narrow screen: both of them inside one menu panel, further down.
THE RAILS ARE DRAWN STRAIGHT ONTO THE WATER, with no plate behind them,
and that is only allowable because the scene has a measured ceiling.
Ground ink on the lightest paint the scene can produce is 4.94:1, so a
rail label clears AA over any frame of the animation, including the one
where a sunbeam is crossing it. tokens.css carries the arithmetic.
-------------------------------------------------------------------------- */
.vo-rails {
display: block;
}
.vo-rail {
padding: var(--vo-space-6) var(--vo-space-4) 0;
}
.vo-rail-title {
margin: 0 0 var(--vo-space-2);
font-size: var(--vo-text-xs);
font-weight: var(--vo-weight-bold);
letter-spacing: var(--vo-tracking-wide);
text-transform: uppercase;
color: var(--vo-ground-ink);
text-align: center;
}
.vo-rail-list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--vo-rail-gap);
}
.vo-rail-item {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--vo-space-1);
width: var(--vo-rail-cell);
padding: var(--vo-space-2) var(--vo-space-1);
pointer-events: auto;
color: var(--vo-ground-ink);
text-decoration: none;
text-align: center;
border-radius: var(--vo-radius);
}
.vo-rail-item:hover {
background: var(--vo-rail-hover);
}
.vo-rail-item:focus-visible {
outline: var(--vo-ring-width) solid var(--vo-ring);
outline-offset: var(--vo-ring-offset);
box-shadow: 0 0 0 calc(var(--vo-ring-width) + var(--vo-ring-offset) + 1px)
var(--vo-ring-halo);
}
.vo-rail-glyph {
display: grid;
place-items: center;
width: var(--vo-rail-icon);
height: var(--vo-rail-icon);
}
.vo-rail-glyph > svg {
width: 100%;
height: 100%;
}
.vo-rail-label {
font-size: var(--vo-text-xs);
font-weight: var(--vo-weight-medium);
line-height: var(--vo-leading-snug);
}
/* Where you are. A warm bar under the glyph in the same light as the focus
ring and the sunbeams, because on this theme warmth is what marks a
thing. It measures 4.17:1 against the lightest water, past the 3:1 floor
for a non-text indicator, and the weight change carries it a second time
for anyone who cannot see the bar at all. */
.vo-rail-item[aria-current="page"] {
font-weight: var(--vo-weight-bold);
}
.vo-rail-item[aria-current="page"] .vo-rail-glyph {
border-bottom: 2px solid var(--vo-ring);
}
/* Live, on a wide screen. Each rail is a full-height column with its items
centred in it, so the two rails balance whatever their item counts are.
The rail zone is 116px and a rail cell is 88px, which leaves 14px of
clearance at the screen edge. The focus ring needs 5px of that
(--vo-ring-width plus --vo-ring-offset), so the ring on the outermost
item is never shaved by the viewport. That is class decision 36's third
mechanic, applied at the only edge on this shell that has chrome on it. */
/* A RAIL IS 116px WIDE AND 100% TALL AND DRAWS NOTHING, which makes it an
invisible click target down the whole edge of the screen. That was harmless
for as long as the only thing in that column was water, and it stopped being
harmless the moment a second control stood at the top of it: the launcher
was painted under the rail and, because hit-testing follows the same order,
was dead across its first ninety-two pixels.
Giving the launcher its own layer fixes the launcher. This fixes the class,
which is the difference class decision 36 is about: the column takes pointer
events only where it actually draws something, so the next element to share
this space does not have to know the rails are there. The items opt back in
on their own rule; the narrow block opts the whole rail back in, because
inside the menu panel it is an ordinary list on a surface and the surface
should stop clicks rather than pass them to the window behind it. */
.vo-live .vo-rail {
position: fixed;
top: 0;
bottom: 0;
width: var(--vo-rail-zone);
z-index: var(--vo-z-rail);
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: center;
padding: 0;
}
.vo-live .vo-rail-start {
left: 0;
}
.vo-live .vo-rail-end {
right: 0;
}
.vo-live .vo-rail-list {
flex-direction: column;
align-items: center;
gap: var(--vo-rail-gap);
}
/* The group headings are page furniture. Live, the rails are two short
columns of named icons at opposite edges of a screen and a heading over
each one is a label on a label. */
.vo-live .vo-rail-title {
display: none;
}
/* --------------------------------------------------------------------------
THE WINDOW
One window. There is no pile, no cascade, no z-order, no minimize and no
maximize, and the class-decision log in the pull request argues each of
those deletions rather than assuming them. There IS a close, and it is
argued in the same place: a window you cannot put down is a picture of a
window, and this shell's whole subject is the thing behind it.
As a page it is a card in the flow. Live it is pinned to the middle of
the screen with the water showing around it, which is the composition
this whole repository is built around. Closed, it is not there at all, by
`display: none` rather than by `visibility: hidden`: closing is not
covering, and the difference the reader can actually observe is that a
reopened window is at the top of its page rather than wherever it was
scrolled to. That is the honest reading of a close control, and it is the
second half of why there is no minimize.
overflow: hidden is on the base rule and the live rules never take it
away. That is class decision 36 honoured at the only rounded container on
this shell: one clip, not a list of elements each remembering to round
their own corner.
-------------------------------------------------------------------------- */
.vo-window {
max-width: var(--vo-window-width);
margin: var(--vo-space-8) auto;
display: flex;
flex-direction: column;
background: var(--vo-surface);
color: var(--vo-ink);
border-radius: var(--vo-radius-window);
box-shadow: var(--vo-shadow-window);
overflow: hidden;
}
.vo-titlebar {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--vo-space-4);
min-height: var(--vo-titlebar-height);
padding: 0 var(--vo-space-2) 0 var(--vo-space-4);
background: var(--vo-chrome);
border-bottom: var(--vo-border-width) solid var(--vo-border);
box-shadow: inset 0 2px 0 0 var(--vo-accent);
}
/* The window's name is the brand and the page, in that order, the way an
application window is titled. The brand half is a link home; the page
half is the only text in the chrome that a swap rewrites. */
.vo-window-name {
display: flex;
align-items: center;
gap: var(--vo-space-2);
margin: 0;
min-width: 0;
font-size: var(--vo-text-sm);
overflow: hidden;
}
.vo-brand {
font-weight: var(--vo-weight-display);
letter-spacing: var(--vo-tracking-tight);
color: var(--vo-ink);
text-decoration: none;
white-space: nowrap;
}
.vo-brand:focus-visible {
outline: var(--vo-ring-width) solid var(--vo-accent);
outline-offset: var(--vo-ring-offset);
border-radius: var(--vo-radius-tight);
}
.vo-window-sep {
color: var(--vo-ink-muted);
}
.vo-window-title {
color: var(--vo-ink-muted);
font-weight: var(--vo-weight-medium);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* The tray. Empty in the markup, filled by shell.js, because a control that
does nothing must not exist. There are no decorative dots up here either:
a row of coloured circles that imitates window controls without being any
is the same lie as a dead button, one step further from the reader. */
.vo-tray {
display: flex;
align-items: center;
gap: var(--vo-space-1);
flex: none;
}
.vo-tray-btn {
display: grid;
place-items: center;
width: var(--vo-control-size);
height: var(--vo-control-size);
padding: 0;
color: var(--vo-ink);
background: transparent;
border: var(--vo-border-width) solid transparent;
border-radius: var(--vo-radius-tight);
cursor: pointer;
}
.vo-tray-btn:hover {
background: var(--vo-surface-muted);
border-color: var(--vo-border);
}
.vo-tray-btn:focus-visible {
outline: var(--vo-ring-width) solid var(--vo-accent);
outline-offset: 0;
}
.vo-tray-btn > svg {
width: 18px;
height: 18px;
}
/* The menu button only means something live and only fits on a narrow
screen. shell.js injects it; the media query at the bottom shows it. */
.vo-menu-btn {
display: none;
min-width: 44px;
min-height: 44px;
}
.vo-menu-btn > svg {
width: 20px;
height: 20px;
}
/* The close control. Last in the tray, where a window has kept it for as
long as windows have had one, and 44px square at every width rather than
only on a phone: it is the control with the largest consequence in the
chrome and it should not be the smallest target in it.
THERE IS STILL NO MINIMIZE AND NO MAXIMIZE, and the close button is the
reason to say so again rather than the reason to stop. Minimize means "keep
this window, put it somewhere I can get it back from, in the state I left
it", and that sentence needs a restore surface to be true. This shell has
no taskbar and no window list, because it has one window and a list of one
is a label. Without that surface, minimize and close would leave the reader
on the same screen and be undone by the same gesture: two controls, one
behavior, which is the dead control of class decision 3 wearing a second
icon. Inventing a dock to justify the button would be adding chrome to
justify a control, which is the argument running backwards. */
.vo-close-btn {
min-width: 44px;
min-height: 44px;
}
/* --------------------------------------------------------------------------
THE MENU BAR
Every page in the site as a plain text link, in the window chrome directly
under the title bar, which is where an application window has carried one
for forty years. It is deliberately NOT a site navbar above the scene: that
would be a second, non-diegetic layer floating over the water, and the
whole composition is one window held in it.
WHY IT EXISTS. The rails are the metaphor's navigation and they are lovely,
and a reader who does not read them as navigation has, without this, no
conventional path through the site. So the rails are the delight layer and
this is the guarantee layer, which is WCAG 2.4.5 satisfied on purpose
rather than by luck.
Nine links, where the rails carry seven. The rails are curated into two
named groups and reach the two documentation sub-pages from inside the docs
page; a guarantee that skipped two of nine pages would not be one.
THAT ASYMMETRY IS A CURATION CHOICE AT 1280 AND LOAD-BEARING AT 390. On a
wide screen the rails are ambient and leaving the two documentation
sub-pages to be found inside the docs page costs nothing. On a phone this
strip is the ONLY surface in the repository that carries all nine: the rails
carry seven at every width, in both modes, and docs-dives.html and
docs-shoal.html are on no rail anywhere. This strip used to be hidden below
1024, and that is what the hiding actually did.
IT IS NOT SCOPED UNDER .vo-live, AND THAT IS THE POINT. These are plain
anchors to real documents, so they behave identically with the script and
without it, and there is nothing for an enhancement to add. The script's
only involvement is moving aria-current after a swap, which is the same
thing it already does for the rails.
ONE HEIGHT, ONE LINE, SCROLLING SIDEWAYS, which is class decision 45's shape
for a chrome strip carrying a list that grows with the site. Nine links are
529px of content in a 390px window, so wrapping here is not a risk that
might arrive with a tenth page, it is the default behaviour today, and it
costs three rows of a phone's chrome. One scrolling row costs 35px and stays
35px.
TWO THINGS THIS REPOSITORY OWES THE LOG ABOUT THAT RULE.
Part 3, the focus-ring reservation, is not owed here, for the same reason
retro-desktop-theme is the log's stated exception: this ring is drawn INSIDE
the control, at a negative outline-offset, so there is nothing outside the
padding edge for an overflow container to clip. Retro arrived at that
geometry because its era's ring is dotted and inset; this one arrived at it
because an outline drawn outside a chrome item would collide with the
chrome's own borders. Two routes, one exemption. The reservation is owed by
ring geometry, not by the existence of a scroll box, and this is the second
shell to demonstrate it.
Part 2 says a strip may wrap as a page and should, because a plain document
should not scroll sideways. THIS STRIP DEPARTS FROM THAT, and the departure
is the design of the element rather than a shortcut: it is the one piece of
chrome here that is identical in both modes, and splitting its layout
between them to satisfy a rule about documents would trade the property the
guarantee layer is built on for one that nothing here needs. The exchange
part 2 is protecting does not arise: the strip scrolls inside itself, so the
document does not scroll sideways in either mode.
Contrast, measured against the chrome it sits on, and the same at 390
because the strip takes no new tokens there:
link ink on chrome 13.97:1, on the hover fill 13.32:1
the current-page underline and the focus ring, accent on chrome, 5.01:1,
past the 3:1 floor for a non-text indicator
Muted ink was the first instinct for a chrome strip and measures 5.67:1,
which clears AA and still reads as a row of disabled menus. Full ink.
-------------------------------------------------------------------------- */
.vo-menubar {
flex: none;
background: var(--vo-chrome);
border-bottom: var(--vo-border-width) solid var(--vo-border);
}
/* The scroll box. `scrollbar-width: none` hides the bar and does not disable
the scrolling, which is the right trade in a 35px strip: a classic scrollbar
is 15px of a 35px chrome row, it would be the tallest thing in the strip
after the text, and a horizontal bar under nine words reads as a defect
rather than as an affordance. What replaces it as the affordance is the
content: the strip is deliberately cut mid-item at 390 rather than at a
tidy boundary, so the row visibly continues. */
.vo-menubar-list {
display: flex;
flex-wrap: nowrap;
align-items: center;
margin: 0;
padding: 0 var(--vo-space-2);
list-style: none;
overflow-x: auto;
scrollbar-width: none;
}
.vo-menubar-list::-webkit-scrollbar {
display: none;
}
.vo-menu-item {
display: inline-block;
padding: var(--vo-space-2) var(--vo-space-3);
font-size: var(--vo-text-sm);
line-height: var(--vo-leading-snug);
color: var(--vo-ink);
text-decoration: none;
white-space: nowrap;
}
.vo-menu-item:hover {
background: var(--vo-surface-muted);
}
.vo-menu-item:focus-visible {
outline: var(--vo-ring-width) solid var(--vo-accent);
outline-offset: calc(-1 * var(--vo-ring-width));
}
/* Where you are, marked twice: an accent rule under the item and the weight
change, so it does not depend on seeing a two-pixel line. */
.vo-menu-item[aria-current="page"] {
font-weight: var(--vo-weight-bold);
box-shadow: inset 0 -2px 0 0 var(--vo-accent);
}
.vo-window-body {
padding: var(--vo-space-8);
overflow-wrap: break-word;
}
.vo-window-body > :first-child {
margin-top: 0;
}
/* Live: the window is the screen's centrepiece and its body is the only
thing that scrolls.
Centred with auto margins rather than a transform, deliberately. A
transform on this element would make it the containing block for anything
fixed inside it, which is the trap class decision 36 warns about, and the
window is exactly the element a future addition would try to escape. */
.vo-live .vo-window {
position: fixed;
top: var(--vo-window-inset);
bottom: var(--vo-window-inset);
left: 0;
right: 0;
margin: 0 auto;
max-width: min(
var(--vo-window-width),
calc(100vw - 2 * var(--vo-rail-zone) - 2 * var(--vo-space-6))
);
z-index: var(--vo-z-window);
}
/* The scroll region, and the element a swap moves focus to. shell.js gives
it tabindex="0", so it is genuinely user-focusable and the ring is
genuinely earned: class decision 29 exempts a programmatic-only target
from drawing one, and this element is not that. :focus-visible is what
keeps a mouse click on a rail item from outlining the whole page while a
keyboard activation still shows where focus went. */
.vo-live .vo-window-body {
flex: 1;
overflow-y: auto;
}
.vo-live .vo-window-body:focus-visible {
outline: var(--vo-ring-width) solid var(--vo-accent);
outline-offset: calc(-1 * var(--vo-ring-width));
}
/* The swap's one animation. The outgoing content is never shown fading;
only the arriving content fades up, over a distance small enough to read
as settling rather than sliding. */
.vo-live .vo-window-body.vo-swapped {
animation: vo-settle var(--vo-swap-duration) var(--vo-ease);
}
@keyframes vo-settle {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* --------------------------------------------------------------------------
DESK-CLEAR
The window closed, and the scene with nothing in front of it. At 1024 and
up the rails stay exactly where they were, because they are drawn on the
water rather than on the window and closing a window does not take the
desk with it; below 1024 there are no rails to keep, and the launcher is
the whole surface. That asymmetry is not a compromise, it is what the
metaphor transform already decided at class decision 8, showing up in a
second state.
`.vo-desk` is written without `.vo-live` in front of it on purpose. The
class is only ever set by shell.js, which sets `vo-live` as its first act,
so the two are inseparable by construction rather than by selector, and
the rules read as what they are.
THE MENU PANEL CANNOT BE OPEN HERE, and it is worth saying how that is
guaranteed rather than checked. The only control that opens it is the menu
button, the menu button lives in the title bar, and the title bar is
inside the element this rule hides. There is no path to the panel that does
not go through a control that is not on the screen. shell.js also closes
the menu on its way out, which is belt to that braces.
The skip link goes too. It is an anchor to `#main`, `#main` is inside the
closed window, and a control whose destination is not on the screen is a
control that does nothing. Hiding it is also what puts the launcher first
in the tab order, which the launcher owes to being the only way back.
-------------------------------------------------------------------------- */
.vo-desk .vo-window {
display: none;
}
.vo-desk .vo-skip {
display: none;
}
/* The window's return settles rather than snapping. This is the same
`vo-settle` keyframe the swap and the menu panel already use, at the
interface duration, so it adds a moment rather than a vocabulary.
Closing is deliberately NOT animated, and the asymmetry is the argument: a
reader who closes the window is asking for the water, and the water should
already be there. A reader who opens it is being handed something, and
180ms is the difference between arriving and appearing. */
.vo-desk-open .vo-window {
animation: vo-settle var(--vo-duration) var(--vo-ease);
}
/* --------------------------------------------------------------------------
THE LAUNCHER
The one control on a clear desk, and the only way back to the site at the
widths where the rails are not on the screen. It is therefore a first-class
control rather than an affordance: first in the tab order, 44px of target,
a visible ring, and a label that says what will happen.
IT IS A PIECE OF THE WINDOW, WHICH IS THE WHOLE IDEA. Not a button drawn on
the water in the rails' language — ground ink over a glyph over a caption,
which is what the seven navigation items already look like and would make
the one control on the desk that is not navigation the eighth thing that
looks like it. This is a plate in the window's own `--vo-surface`, carrying
the title bar's own accent hairline, so the thing that brings the window
back is visibly made of the window. A bubble mark rather than an arrow or a
chevron: bubbles are what this ocean would use for something on its way up,
and they appear nowhere else in the scene, so the mark collides with
neither the fish nor the beams.
CONTRAST, MEASURED THE WAY CLASS DECISION 38 REQUIRES: against the ceiling
the scene can produce rather than against the paint that happens to be
there. The plate is at the top of the frame, where the water is the surface
stop, and it measures 7.45:1 against it; against #2e7095, the lightest
paint the scene is capable of anywhere, it measures 5.22:1. Both clear the
3:1 floor for a non-text boundary with room.
The label is the number that matters and it is the number that cannot move:
ink on surface, 15.63:1, both tokens belonging to the window rather than to
the water. That is the payoff of the plate over the first concept. Under
the lagoon re-skin this repository ships as a failure, ground ink on the
lightest water falls to 1.76:1 and the rail labels are gone; this label is
still at 15.63:1, because an opaque plate does not care what is behind it.
The plate's own edge is the part the re-skin does take: 1.85:1 at that
theme's ceiling, which is the same coverage story the focus ring tells, and
the ring's halo is at 9.06:1 there, so a keyboard reader still finds it.
-------------------------------------------------------------------------- */
.vo-launch {
display: none;
position: fixed;
top: var(--vo-launch-inset);
left: var(--vo-launch-inset);
z-index: var(--vo-z-launch);
align-items: center;
gap: var(--vo-space-3);
min-height: 44px;
padding: var(--vo-space-2) var(--vo-space-4);
font-family: inherit;
font-size: var(--vo-text-sm);
font-weight: var(--vo-weight-bold);
line-height: var(--vo-leading-snug);
text-align: left;
color: var(--vo-ink);
background: var(--vo-surface);
border: var(--vo-border-width) solid var(--vo-border);
border-radius: var(--vo-radius-window);
box-shadow: inset 0 2px 0 0 var(--vo-accent), var(--vo-shadow-window);
cursor: pointer;
}
.vo-desk .vo-launch {
display: inline-flex;
}
.vo-launch:hover {
background: var(--vo-chrome);
}
/* The rails' ring, not the window's. This control stands on the water, and
the water is what class decision 37's two-colour ring exists for: the
accent-on-chrome ring the tray buttons use is a promise about a background
that is not behind this one. The window shadow is restated in the focus
rule because a `box-shadow` list replaces rather than adds, and dropping it
would make the plate lift off the water only while it was NOT focused. */
.vo-launch:focus-visible {
outline: var(--vo-ring-width) solid var(--vo-ring);
outline-offset: var(--vo-ring-offset);
box-shadow: 0 0 0 calc(var(--vo-ring-width) + var(--vo-ring-offset) + 1px)
var(--vo-ring-halo),
inset 0 2px 0 0 var(--vo-accent), var(--vo-shadow-window);
}
/* An intrinsic glyph box, like the tray's 18px and 20px: the number belongs
to the drawing rather than to the design. The accent is spent here because
this is a light surface, which tokens.css says is the only side of the
theme the accent lives on. */
.vo-launch-mark {
display: grid;
place-items: center;
width: 26px;
height: 26px;
flex: none;
color: var(--vo-accent);
}
.vo-launch-mark > svg {
width: 100%;
height: 100%;
}
/* --------------------------------------------------------------------------
THE LIVE REGION
Injected by shell.js. A swap changes the page without a page load, so
nothing announces it; this is what does. Visually hidden by the standard
clip rectangle rather than by display:none, which would take it out of
the accessibility tree and defeat the point.
-------------------------------------------------------------------------- */
.vo-announce {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
/* --------------------------------------------------------------------------
THE METAPHOR TRANSFORM
Below 1024px there is not enough width for a window with water on both
sides of it AND two rails in that water, and the honest response is to
change the arrangement rather than shrink it until it is a diorama. The
window goes full-bleed under a title bar that becomes the top of the
screen, and both rails move inside one menu panel that drops from that
title bar, above the window's content.
The number is this repository's; the rule that a shell transforms rather
than scales is class decision 8. The breakpoint is set by arithmetic
rather than by convention: 2 rail zones (232px) plus two gutters (48px)
plus a window narrow enough to still hold a four-column pricing table is
the last width where the wide arrangement is telling the truth.
The water does not transform. It is still the ocean, there is just less
of it showing, and what shows is the lit part near the surface.
-------------------------------------------------------------------------- */
@media (max-width: 1023px) {
/* THE MENU BAR STAYS. It used to be hidden here, and the sentence that
justified it said the menu button was "the same guarantee at that width".
That sentence was wrong in two ways at once, and a per-state audit is what
found it: the button opens a panel of seven icon-and-label links, which
class decision 44 counts as an icon surface and which is missing two of
the nine pages. So below 1024 this shell had no text-link navigation
surface at all, in either mode, and its guarantee-layer compliance was
assessed at 1280 and reported for the repository.
The strip is unchanged here apart from tightening its own padding: same
markup, same links, same nowrap-and-scroll it uses at every width. There
is nothing to hide and nothing to substitute, which is what makes this
block three lines instead of a second design. */
.vo-menubar-list {
padding: 0 var(--vo-space-1);
}
/* Full-bleed across, not full-screen. The window loses its side margins
and its corners and takes the whole width, and it keeps a band of water
above it, because the alternative was measured and it is worse: with the
window at top: 0 the render matrix produced identical hashes for the
ocean, calm and prefers-contrast cells at 390, which is what it looks
like when the ground is not on screen at all. */
.vo-live .vo-window {
top: var(--vo-window-band);
bottom: 0;
max-width: none;
border-radius: 0;
}
.vo-live .vo-menu-btn {
display: grid;
}
/* THE LAUNCHER MOVES DOWN, and the number is not a nudge. A render of the
390 desk found it parked on the one living thing a phone can see: the
surface shoal draws at x 152-219, y 37-64, and a plate at the shipped
inset occupies x 24-204, y 24-68, so it covered fifty-two of the shoal's
sixty-seven pixels. Under motion that is a fish swimming behind a button;
in the composed still life, which is the reduced-motion state and every
capture in this repository, it is permanent, and on a theme whose whole
subject is the ground it is the ground being sat on. Class decision 41
was filed for the version of this where the ground goes off the screen
entirely; this is the same mistake at a smaller radius, and the same
thing caught it.
The offset is --vo-window-band, which is the token that already means
"how much ground a phone keeps", plus the inset the launcher already has.
64 + 24 = 88, and the plate's 44px puts its bottom at 132, clear of the
shoal by 24px and clear of the next shoal down by 34. Deriving it from
that token rather than picking 88 means a re-skin that changes how much
water a phone keeps takes the launcher with it. */
.vo-launch {
top: calc(var(--vo-window-band) + var(--vo-launch-inset));
}
/* Both rails, in one panel, under the title bar. The panel is a surface
rather than a wash: it is holding the whole navigation and it has to be
readable over whatever the water is doing behind it. */
/* THE BORDER IS PART OF THE CHROME'S HEIGHT AND WAS MISSING FROM BOTH OF
THESE SUMS. `--vo-titlebar-height` is a min-height on a border-box
element, so the bar is that tall UNTIL something 44px tall goes in it,
and then it is 44 of content plus its 1px bottom border. A 44px control
is exactly what WCAG 2.5.8 puts in there. Measured on a long-titled page:
the title bar is 45px at 390 and 320, the panel was landing at y=108
against a bar whose bottom edge is at y=109, and the panel was covering
the border it is supposed to hang from.
One pixel, invisible, and the reason to fix it rather than round it off
is that this is precisely the promise class decision 45 says to check —
a calc() that subtracts a chrome token from a viewport unit is a claim
that the chrome is that tall. It was not, on merged main, before this
branch existed. The log records it as verified at 44px and that sentence
is corrected in the same pull request as this line.
NOTE WHAT IS DELIBERATELY NOT HERE: no menu-bar-height token. The panel
hangs from the title bar and covers the strip, the way a dropdown covers
what is under it, and the window body is `flex: 1` inside a flex column
so layout carries the strip's height rather than arithmetic. Item 45's
own preference is layout over the token where a shell can have it, and
inventing a second chrome height to measure from is the trap rather than
the fix. */
.vo-live .vo-rails {
display: none;
position: fixed;
top: calc(
var(--vo-window-band) + var(--vo-titlebar-height) + var(--vo-border-width)
);
left: 0;
right: 0;
z-index: var(--vo-z-menu);
max-height: calc(
100dvh - var(--vo-window-band) - var(--vo-titlebar-height) -
var(--vo-border-width)
);
overflow-y: auto;
padding: var(--vo-space-4);
background: var(--vo-surface);
border-bottom: var(--vo-border-width) solid var(--vo-border-strong);
box-shadow: var(--vo-shadow-window);
}
.vo-live .vo-rails.vo-open {
display: block;
animation: vo-settle var(--vo-duration) var(--vo-ease);
}
/* Inside the panel the rails are lists on a light surface, so they take
page ink rather than ground ink. This is the second half of class
decision 20: the two inks are not interchangeable, and the place that
proves it is the one element that appears on both grounds. */
.vo-live .vo-rail {
position: static;
width: auto;
padding: 0;
pointer-events: auto;
}
.vo-live .vo-rail + .vo-rail {
margin-top: var(--vo-space-4);
padding-top: var(--vo-space-4);
border-top: var(--vo-border-width) solid var(--vo-border);
}
.vo-live .vo-rail-title {
display: block;
text-align: left;
color: var(--vo-ink-muted);
}
.vo-live .vo-rail-list {
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
}
.vo-live .vo-rail-item {
color: var(--vo-ink);
}
.vo-live .vo-rail-item:hover {
background: var(--vo-surface-muted);
}
.vo-live .vo-rail-item:focus-visible {
outline-color: var(--vo-accent);
box-shadow: none;
}
/* And the where-you-are mark swaps with them. The warm light that marks
the current page over water measures 1.25:1 on the panel's surface,
which a render caught as a barely-there tan smudge under one glyph. The
mark is not the color, it is the role; on a light ground the accent is
what carries the role. Third instance of class decision 20 in this
repository, and the one that was nearly missed. */
.vo-live .vo-rail-item[aria-current="page"] .vo-rail-glyph {
border-bottom-color: var(--vo-accent);
}
.vo-live .vo-window-body {
padding: var(--vo-space-6) var(--vo-space-4);
}
}