[OCaml] High Intensity Training Online
style warm high-contrast palette, flat surfaces, workout table
Rework the visual language: pure black on a warm white with an oxblood primary, warm bronze secondary, and evergreen tertiary (all WCAG AA+); remove the page-layout card metaphor in favour of flat surfaces; move the oxblood accent to the masthead; a golden-ratio heading scale; and the workout logging styles (golden-ratio set table, centered Courier timer, single-line ending block, crossed-off completed slots, outline Save correction). Remove the toast styles.
lib/web/assets/hito.css
@@ -2,30 +2,42 @@
2
2
3
3
:root {
4
4
color-scheme: light;
5
Removed:
--paper: #f7f2e8;
6
Removed:
--paper-raised: #fffdf8;
7
Removed:
--on-primary: #fffdf8;
8
Removed:
--on-brass: #fffdf8;
9
Removed:
--ink: #1a1a16;
10
Removed:
--muted-ink: #5b584f;
11
Removed:
--rule: #b9b1a2;
12
Removed:
--rule-strong: #756f63;
5
Added:
/* Uncompromising legibility: pure black text on a very slightly warm white.
6
Added:
The palette stays warm throughout — oxblood primary and a warm bronze
7
Added:
secondary carry the blood-and-effort register — with evergreen reserved for
8
Added:
success. Contrast is the constraint the warmth must respect, not the other
9
Added:
way round. */
10
Added:
--paper: #fdfcfa;
11
Added:
--paper-raised: #ffffff;
12
Added:
--on-primary: #ffffff;
13
Added:
--on-brass: #ffffff;
14
Added:
--ink: #000000;
15
Added:
--muted-ink: #3f3f3f;
16
Added:
--rule: #c9c6c0;
17
Added:
--rule-strong: #6f6c66;
13
18
--oxblood: #8c201b;
14
Removed:
--oxblood-dark: #65130f;
15
Removed:
--oxblood-hover: #65130f;
16
Removed:
--link-hover: #8c201b;
17
Removed:
--brass: #7b5714;
18
Removed:
--brass-wash: #f4e7c5;
19
Removed:
--hover-surface: #ece4d5;
20
Removed:
--disabled-surface: #ded8cc;
19
Added:
--oxblood-dark: #6d1713;
20
Added:
--oxblood-hover: #6d1713;
21
Added:
--link-hover: #6d1713;
22
Added:
--brass: #8a5a12;
23
Added:
--brass-wash: #f3e6c8;
24
Added:
--hover-surface: #eeeae3;
25
Added:
--disabled-surface: #ddd9d1;
21
26
--evergreen: #1f5b45;
22
27
--evergreen-wash: #dcebe2;
23
Removed:
--focus: #65130f;
28
Added:
--focus: #8c201b;
24
29
--error: #8c201b;
25
30
--font-size-small: 0.875rem;
26
31
--font-size-base: 1rem;
27
32
--font-size-lead: 1.125rem;
28
Removed:
--font-size-heading: 2rem;
33
Added:
/* Heading scale anchored on the golden ratio. Body is 1rem; each heading
34
Added:
tier steps by √φ (≈1.272), so h2 sits a full φ (≈1.618) above body and h1
35
Added:
a further √φ above h2. A ratio-based hierarchy a reader perceives, in place
36
Added:
of ad-hoc sizes. */
37
Added:
--font-size-h3: 1.272rem;
38
Added:
--font-size-h2: 1.618rem;
39
Added:
--font-size-h1: 2.058rem;
40
Added:
--font-size-heading: 2.058rem;
29
41
--radius: 0.4em;
30
42
--sans: "Work Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
31
43
--mono: "Courier Prime", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
@@ -35,25 +47,28 @@
35
47
36
48
html[data-theme="dark"] {
37
49
color-scheme: dark;
38
Removed:
--paper: #171614;
39
Removed:
--paper-raised: #24211d;
40
Removed:
--on-primary: #f3eee3;
41
Removed:
--on-brass: #171614;
42
Removed:
--ink: #f3eee3;
43
Removed:
--muted-ink: #c2b9aa;
44
Removed:
--rule: #655e54;
45
Removed:
--rule-strong: #918778;
46
Removed:
--oxblood: #ae453e;
50
Added:
/* The dark inversion of the same warm system: near-white text on a warm
51
Added:
near-black, with the oxblood primary and a warm amber secondary lightened
52
Added:
for AA+ contrast on the dark surface. */
53
Added:
--paper: #141312;
54
Added:
--paper-raised: #201e1b;
55
Added:
--on-primary: #ffffff;
56
Added:
--on-brass: #201607;
57
Added:
--ink: #f5f2ec;
58
Added:
--muted-ink: #c6bfb4;
59
Added:
--rule: #5f584f;
60
Added:
--rule-strong: #938a7c;
61
Added:
--oxblood: #d8564d;
47
62
--oxblood-dark: #f08073;
48
Removed:
--oxblood-hover: #96332f;
49
Removed:
--link-hover: #d4a94f;
50
Removed:
--brass: #d4a94f;
51
Removed:
--brass-wash: #40351f;
52
Removed:
--hover-surface: #40351f;
53
Removed:
--disabled-surface: #4a443c;
63
Added:
--oxblood-hover: #b8433b;
64
Added:
--link-hover: #e0b256;
65
Added:
--brass: #e0b256;
66
Added:
--brass-wash: #3a2c12;
67
Added:
--hover-surface: #2b2824;
68
Added:
--disabled-surface: #423d36;
54
69
--evergreen: #74c49a;
55
Removed:
--evergreen-wash: #244334;
56
Removed:
--focus: #f08073;
70
Added:
--evergreen-wash: #17362a;
71
Added:
--focus: #d8564d;
57
72
--error: #f08073;
58
73
}
59
74
@@ -105,22 +120,22 @@
105
120
padding: 0 1rem 2.5rem;
106
121
}
107
122
123
Added:
/* The oxblood bar is the header's base edge — the brand marker — so it belongs
124
Added:
to the masthead, not the content. */
108
125
.masthead {
109
126
display: flex;
110
127
align-items: center;
111
128
gap: 1rem;
112
129
padding: 0.75rem 0;
130
Added:
border-bottom: 6px solid var(--oxblood);
113
131
}
114
132
115
Removed:
/* The footer is a small card beneath the page surface: same raised paper,
116
Removed:
border, and radius, without the surface's oxblood accent. */
133
Added:
/* The footer is a flat divider: a quiet line of text separated by a hairline
134
Added:
rule, at every width. */
117
135
.site-footer {
118
136
margin: 1.25rem 0 0;
119
Removed:
padding: 0.85rem 1rem;
120
Removed:
border: 1px solid var(--rule-strong);
121
Removed:
border-radius: var(--radius);
122
Removed:
background: var(--paper-raised);
123
Removed:
box-shadow: 0 0.4rem 1.5rem rgba(26, 26, 22, 0.09);
137
Added:
padding: 0.85rem 0;
138
Added:
border-top: 1px solid var(--rule);
124
139
color: var(--muted-ink);
125
140
font-size: var(--font-size-small);
126
141
text-align: center;
@@ -139,7 +154,7 @@
139
154
display: inline-flex;
140
155
align-items: center;
141
156
color: var(--ink);
142
Removed:
font: 800 2rem/1 var(--sans);
157
Added:
font: 800 var(--font-size-h1)/1 var(--sans);
143
158
letter-spacing: -0.03em;
144
159
text-decoration: none;
145
160
}
@@ -168,8 +183,7 @@
168
183
border-color: var(--oxblood-dark);
169
184
}
170
185
171
Removed:
.eyebrow,
172
Removed:
.ledger-meta {
186
Added:
.eyebrow {
173
187
font: 700 var(--font-size-small)/1.35 var(--sans);
174
188
letter-spacing: 0.09em;
175
189
text-transform: uppercase;
@@ -302,14 +316,12 @@
302
316
303
317
.bottom-nav { display: none; }
304
318
319
Added:
/* Flat and native: the content sits directly on the page, no card chrome. The
320
Added:
oxblood brand marker now lives on the masthead, so the content starts clean.
321
Added:
The desktop breakpoint only constrains the reading width. */
305
322
.page-surface {
306
323
min-height: 26rem;
307
Removed:
border: 1px solid var(--rule-strong);
308
Removed:
border-top: 6px solid var(--oxblood);
309
Removed:
background: var(--paper-raised);
310
Removed:
border-radius: var(--radius);
311
Removed:
box-shadow: 0 0.4rem 1.5rem rgba(26, 26, 22, 0.09);
312
Removed:
padding: clamp(1.25rem, 3vw, 2rem);
324
Added:
padding: 1.25rem 0 0;
313
325
}
314
326
315
327
/* Page transitions. The SPA client swaps the content node on navigation
@@ -326,10 +338,17 @@
326
338
}
327
339
328
340
/* Keep the desktop masthead and navigation wide, while giving reading content
329
Removed:
a shorter line length. Mobile keeps the full page-surface width below. */
341
Added:
a shorter line length. The raised card returns at this width; mobile stays
342
Added:
flat and native above. */
343
Added:
/* On desktop the shell and content settle at a compact, readable ~50rem, rather
344
Added:
than stretching with the viewport. The shell frames the masthead, content,
345
Added:
and footer; the content fills it. */
330
346
@media (min-width: 42.0625rem) {
347
Added:
.app-shell {
348
Added:
width: min(100%, 50rem);
349
Added:
}
331
350
.page-surface {
332
Removed:
max-width: 52rem;
351
Added:
width: 100%;
333
352
margin-inline: auto;
334
353
}
335
354
}
@@ -345,20 +364,25 @@
345
364
margin: 0 0 0.45rem;
346
365
color: var(--ink);
347
366
font-weight: 700;
348
Removed:
line-height: 1.2;
349
367
letter-spacing: -0.03em;
350
368
}
351
Removed:
h1 { font-size: clamp(1.75rem, 4vw, var(--font-size-heading)); }
352
Removed:
h2 { font-size: 1.35rem; }
353
Removed:
h3 { font-size: 1.1rem; }
369
Added:
/* Line-height tightens as size grows, paired to the golden scale. */
370
Added:
h1 {
371
Added:
font-size: clamp(1.75rem, 4vw, var(--font-size-h1));
372
Added:
line-height: 1.15;
373
Added:
}
374
Added:
h2 {
375
Added:
font-size: var(--font-size-h2);
376
Added:
line-height: 1.25;
377
Added:
}
378
Added:
h3 {
379
Added:
font-size: var(--font-size-h3);
380
Added:
line-height: 1.35;
381
Added:
}
354
382
355
383
p { max-width: 46rem; margin: 0.65rem 0; color: var(--muted-ink); }
356
384
h2 + p { font-size: var(--font-size-lead); }
357
385
358
Removed:
.ledger-meta {
359
Removed:
color: var(--muted-ink);
360
Removed:
font-variant-numeric: tabular-nums;
361
Removed:
}
362
386
363
387
/* The upcoming workout's exercises, previewed on the Home card. A tight ledger
364
388
of what is coming, so the next session reads at a glance. */
@@ -371,19 +395,10 @@
371
395
372
396
fieldset {
373
397
margin: 1rem 0 0;
374
Removed:
padding: 0.875rem;
375
Removed:
border: 1px solid var(--rule);
376
Removed:
border-top: 3px solid var(--ink);
377
Removed:
background: var(--paper);
378
Removed:
border-radius: var(--radius);
398
Added:
padding: 0;
399
Added:
border: 0;
379
400
}
380
401
381
Removed:
/* The exercise logging fieldset drops the heavy ink top border: the card reads
382
Removed:
as a plain bordered panel so the exercise-name heading leads it, not a rule. */
383
Removed:
.logging-fieldset {
384
Removed:
border-top: 1px solid var(--rule);
385
Removed:
}
386
Removed:
387
402
legend {
388
403
max-width: 100%;
389
404
padding: 0 0.35rem;
@@ -392,16 +407,6 @@
392
407
letter-spacing: 0.025em;
393
408
}
394
409
395
Removed:
/* The exercise name inside a fieldset reads as a heading: black Work Sans, so
396
Removed:
the movement being logged stands out from the numeric fields below it. */
397
Removed:
.exercise-name {
398
Removed:
margin: 1rem 0 0;
399
Removed:
color: var(--ink);
400
Removed:
font: 700 var(--font-size-lead)/1.2 var(--sans);
401
Removed:
letter-spacing: -0.01em;
402
Removed:
}
403
Removed:
404
Removed:
.field { margin-top: 1rem; }
405
410
label {
406
411
display: block;
407
412
margin: 0 0 0.35rem;
@@ -467,6 +472,55 @@
467
472
cursor: not-allowed;
468
473
}
469
474
475
Added:
/* Correction actions (Save correction + Reset) appear only once a recorded
476
Added:
value has changed. With JavaScript, the pair is hidden until the client marks
477
Added:
the form dirty; the reset button then restores the recorded values, which
478
Added:
clears the dirty state and hides the pair again. Without JavaScript the pair
479
Added:
stays visible, so a correction is always possible. The buttons share the row
480
Added:
at equal width. */
481
Added:
.correction-actions {
482
Added:
display: grid;
483
Added:
grid-template-columns: 1fr 1fr;
484
Added:
gap: 0.6rem;
485
Added:
}
486
Added:
[data-hito-js="on"] .correction-actions[data-hito-correction-actions] {
487
Added:
display: none;
488
Added:
}
489
Added:
[data-hito-js="on"]
490
Added:
form[data-hito-dirty]
491
Added:
.correction-actions[data-hito-correction-actions] {
492
Added:
display: grid;
493
Added:
}
494
Added:
/* Save correction reads as a primary outline: oxblood border and text on a
495
Added:
transparent field, filling oxblood on hover. It pairs with the Reset button
496
Added:
beside it without competing as a solid fill. */
497
Added:
.correction-actions input[type="submit"] {
498
Added:
color: var(--oxblood);
499
Added:
background: transparent;
500
Added:
border: 2px solid var(--oxblood-dark);
501
Added:
}
502
Added:
.correction-actions input[type="submit"]:hover {
503
Added:
color: var(--on-primary);
504
Added:
background: var(--oxblood);
505
Added:
}
506
Added:
/* A reset button styled to match a secondary submit. */
507
Added:
button[type="reset"].secondary {
508
Added:
min-height: 3rem;
509
Added:
margin-top: 1rem;
510
Added:
border: 2px solid var(--brass);
511
Added:
border-radius: var(--radius);
512
Added:
color: var(--brass);
513
Added:
background: transparent;
514
Added:
cursor: pointer;
515
Added:
font: 700 var(--font-size-small)/1 var(--sans);
516
Added:
transition: background-color 150ms ease, transform 150ms ease;
517
Added:
}
518
Added:
button[type="reset"].secondary:hover {
519
Added:
color: var(--on-brass);
520
Added:
background: var(--brass);
521
Added:
transform: translateY(-2px);
522
Added:
}
523
Added:
470
524
/* A secondary submit: a lower-emphasis action next to the primary one. It reads
471
525
as a receding, brass-outlined control so the primary submit stays the obvious
472
526
choice. Used for "Cancel logging" beside "Finish workout". */
@@ -572,6 +626,7 @@
572
626
width: 100%;
573
627
margin-top: 1.5rem;
574
628
border-collapse: collapse;
629
Added:
table-layout: fixed;
575
630
}
576
631
.logbook-table th,
577
632
.logbook-table td {
@@ -605,7 +660,23 @@
605
660
font-family: var(--mono);
606
661
color: var(--muted-ink);
607
662
white-space: nowrap;
663
Added:
664
Added:
@media (max-width: 30rem) {
665
Added:
.logbook-table th,
666
Added:
.logbook-table td {
667
Added:
padding-inline: 0.25rem;
668
Added:
}
669
Added:
670
Added:
.logbook-actions-heading,
671
Added:
.logbook-actions {
672
Added:
width: 2.5rem;
673
Added:
}
674
Added:
675
Added:
.logbook-row-menu > summary {
676
Added:
width: 2.5rem;
677
Added:
}
608
678
}
679
Added:
}
609
680
.logbook-duration {
610
681
white-space: nowrap;
611
682
font-family: var(--mono);
@@ -731,17 +802,14 @@
731
802
/* The action bar under the logbook heading. */
732
803
.logbook-actions-bar { margin: 0.5rem 0 1rem; }
733
804
734
Removed:
/* The month calendar above the logbook: a self-contained widget. A bordered
735
Removed:
card holds a header row (prev arrow, month/year picker, next arrow), a Today
736
Removed:
link, and the seven-column day grid. Days with a workout are filled oxblood;
737
Removed:
today is ringed. */
805
Added:
/* The month calendar above the logbook: a self-contained widget, laid out flat
806
Added:
on the page. A header row (prev arrow, month/year picker, next arrow), a
807
Added:
Today link, and the seven-column day grid. Days with a workout are filled
808
Added:
oxblood; today is ringed. */
738
809
.calendar {
739
810
max-width: 22rem;
740
811
margin: 0 auto 1.5rem;
741
Removed:
padding: 0.6rem 0.75rem 0.75rem;
742
Removed:
border: 1px solid var(--rule-strong);
743
Removed:
border-radius: var(--radius);
744
Removed:
background: var(--paper-raised);
812
Added:
padding: 0.6rem 0 0.75rem;
745
813
}
746
814
.calendar-header {
747
815
display: flex;
@@ -763,16 +831,27 @@
763
831
}
764
832
.calendar-arrow:hover { background: var(--hover-surface); }
765
833
.calendar-today-link {
834
Added:
display: inline-flex;
835
Added:
align-items: center;
836
Added:
justify-content: center;
837
Added:
min-height: 2.25rem;
838
Added:
padding: 0.35rem 0.75rem;
839
Added:
border: 1px solid var(--oxblood-dark);
840
Added:
border-radius: var(--radius);
766
841
flex: 1 1 auto;
767
Removed:
text-align: center;
768
842
color: var(--oxblood-dark);
843
Added:
background: transparent;
769
844
font-weight: 700;
770
845
text-decoration: none;
771
846
}
772
Removed:
.calendar-today-link:hover { text-decoration: underline; }
847
Added:
.calendar-today-link:hover {
848
Added:
color: var(--oxblood-dark);
849
Added:
background: var(--hover-surface);
850
Added:
}
773
851
.calendar-grid {
774
852
width: 100%;
775
Removed:
border-collapse: collapse;
853
Added:
border-collapse: separate;
854
Added:
border-spacing: 0.2rem 0.2rem;
776
855
table-layout: fixed;
777
856
}
778
857
.calendar-grid th {
@@ -849,8 +928,12 @@
849
928
}
850
929
.chart-axis { stroke: var(--rule-strong); stroke-width: 1; }
851
930
.chart-grid { stroke: var(--rule); stroke-width: 1; opacity: 0.5; }
852
Removed:
.chart-tick { font: 500 0.7rem var(--mono); fill: var(--muted-ink); }
853
Removed:
.chart-axis-title { font: 700 0.72rem var(--sans); }
931
Added:
/* Chart text is sized in viewBox user units, not rem: the SVG scales to the
932
Added:
container, so rem labels shrank to ~half size on a phone. User-unit sizes
933
Added:
track the 640-unit viewBox, staying legible when the chart is narrow. At the
934
Added:
40rem cap they render ~20px; on a ~375px phone (≈0.59 scale) ~12px. */
935
Added:
.chart-tick { font: 500 20px var(--mono); fill: var(--muted-ink); }
936
Added:
.chart-axis-title { font: 700 22px var(--sans); }
854
937
.chart-axis-title.weight { fill: var(--oxblood-dark); }
855
938
.chart-axis-title.reps { fill: var(--brass); }
856
939
/* Weight is the primary series: a solid oxblood line leads. Reps is secondary:
@@ -859,31 +942,89 @@
859
942
.chart-series.weight { stroke: var(--oxblood); }
860
943
.chart-series.reps { stroke: var(--brass); stroke-dasharray: 5 3; }
861
944
862
Removed:
/* One responsive grid for the whole logging fieldset. Every movement's name,
863
Removed:
load, and reps line up in the same three columns — the name takes the
864
Removed:
remaining space, the two numeric inputs take fixed, tap-friendly columns — so
865
Removed:
a set fits on one line without scrolling. A [.movement] wrapper groups a
866
Removed:
movement's cells for the markup but opens no nested grid ([display:
867
Removed:
contents]), so all rows share this one grid. The [.field] top margin is
868
Removed:
dropped here; the grid gap handles spacing. */
869
Removed:
.logging-grid {
945
Added:
/* --- Workout logging fieldset ---
946
Added:
The set is a semantic table laid out as a grid so the three columns hold the
947
Added:
golden ratio: the exercise name is the smaller part (1fr); load and reps take
948
Added:
the wider φ portion, split evenly (0.809fr each). thead/tbody/tr are
949
Added:
display:contents so their cells fall on the grid. The set ending (extension
950
Added:
select + optional amount) is a separate flex row below the table. *)
951
Added:
.logging-fieldset {
952
Added:
border-top: 1px solid var(--rule);
953
Added:
}
954
Added:
955
Added:
.exercise-name-label {
956
Added:
display: block;
957
Added:
color: var(--ink);
958
Added:
font: 700 var(--font-size-lead)/1.2 var(--sans);
959
Added:
letter-spacing: -0.01em;
960
Added:
}
961
Added:
/* The record to beat: the exercise's most recent prior effort, an observed fact
962
Added:
under its name, muted so it does not compete with the inputs. */
963
Added:
.exercise-previous {
964
Added:
display: block;
965
Added:
margin: 0.15rem 0 0;
966
Added:
color: var(--muted-ink);
967
Added:
font: 500 var(--font-size-small)/1.2 var(--mono);
968
Added:
font-variant-numeric: tabular-nums;
969
Added:
}
970
Added:
971
Added:
.logging-table {
870
972
display: grid;
871
Removed:
grid-template-columns: minmax(0, 1fr) 5.5rem 5.5rem;
872
Removed:
align-items: end;
873
Removed:
gap: 0.6rem 0.6rem;
973
Added:
/* Golden ratio between the name column and the two number columns: name is
974
Added:
the smaller part (1), load and reps together are the larger part (φ ≈
975
Added:
1.618), split evenly (0.809fr each). So name : (load + reps) = 1 : φ. */
976
Added:
grid-template-columns: minmax(0, 1fr) minmax(0, 0.809fr) minmax(0, 0.809fr);
977
Added:
align-items: center;
978
Added:
gap: 0.35rem 0.6rem;
874
979
margin-top: 1rem;
875
980
}
876
Removed:
.movement { display: contents; }
877
Removed:
.logging-grid .exercise-name { margin: 0; align-self: center; }
878
Removed:
.logging-grid .field { margin-top: 0; }
879
Removed:
/* The Ending control spans the full width below the movement rows. */
880
Removed:
.logging-grid .field-ending { grid-column: 1 / -1; margin-top: 0.4rem; }
981
Added:
.logging-table thead,
982
Added:
.logging-table tbody,
983
Added:
.logging-table tr {
984
Added:
display: contents;
985
Added:
}
986
Added:
.logging-table th {
987
Added:
margin: 0;
988
Added:
padding: 0;
989
Added:
text-align: left;
990
Added:
}
991
Added:
.logging-table thead th {
992
Added:
padding-bottom: 0.35rem;
993
Added:
color: var(--muted-ink);
994
Added:
font: 700 var(--font-size-small)/1 var(--sans);
995
Added:
align-self: end;
996
Added:
}
997
Added:
.logging-table td { padding: 0; }
998
Added:
.logging-table input { width: 100%; }
881
999
882
Removed:
/* The extension amount follows the ending select without script: option:checked
883
Removed:
tracks the live selection and :has() lets the fieldset react to it. At
884
Removed:
positive failure there is nothing to count, so the field hides; forced reps
885
Removed:
and negatives count reps, a static hold counts seconds. Without :has()
886
Removed:
support the field stays and the label reads as the combined form. */
1000
Added:
/* The set ending: the extension select and its optional amount on one line.
1001
Added:
The select flexes to fill; the amount is a compact fixed width followed by a
1002
Added:
muted unit. The amount hides at positive failure (nothing to count). *)
1003
Added:
/* The set ending: the extension select and its optional amount on one line, as
1004
Added:
a two-column grid (select fills, amount sizes to content). A grid is
1005
Added:
deterministic here where flex min-content sizing proved fragile. *)
1006
Added:
.ending-block {
1007
Added:
display: grid;
1008
Added:
grid-template-columns: 1fr auto;
1009
Added:
align-items: end;
1010
Added:
gap: 0.6rem;
1011
Added:
margin-top: 0.9rem;
1012
Added:
}
1013
Added:
.ending-block .field { margin: 0; min-width: 0; }
1014
Added:
.ending-block .field-extension-amount {
1015
Added:
display: flex;
1016
Added:
align-items: baseline;
1017
Added:
gap: 0.35rem;
1018
Added:
}
1019
Added:
.ending-block .field-extension-amount input { width: 4.5rem; }
1020
Added:
.ending-block .field-unit {
1021
Added:
color: var(--muted-ink);
1022
Added:
font: 500 var(--font-size-small)/1 var(--sans);
1023
Added:
}
1024
Added:
1025
Added:
/* The extension amount follows the live selection with no script: :has() reads
1026
Added:
the checked option. Positive failure hides the amount; a static hold counts
1027
Added:
seconds, everything else counts reps. */
887
1028
@supports selector(:has(*)) {
888
1029
.logging-fieldset .amount-label-sep { display: none; }
889
1030
.logging-fieldset:has(select[name="extension"] option[value=""]:checked)
@@ -914,10 +1055,10 @@
914
1055
border: 0;
915
1056
}
916
1057
917
Removed:
/* The Home card shown while a workout is in progress. A full-width pressable
918
Removed:
panel that returns the user to the current workout. It reads as a raised
919
Removed:
surface with an oxblood accent, echoing the page surface. */
920
Removed:
.in-progress-card {
1058
Added:
/* The Home affordance shown while a workout is in progress: a full-width
1059
Added:
pressable panel that returns the user to the current workout. A left oxblood
1060
Added:
accent marks it; it is not the page-surface card metaphor. */
1061
Added:
.in-progress {
921
1062
display: block;
922
1063
margin: 1.25rem 0 0;
923
1064
border: 1px solid var(--rule-strong);
@@ -928,117 +1069,58 @@
928
1069
color: var(--ink);
929
1070
text-decoration: none;
930
1071
}
931
Removed:
.in-progress-card:hover {
1072
Added:
.in-progress:hover {
932
1073
background: var(--hover-surface);
933
1074
}
934
Removed:
.in-progress-card h1 { margin: 0.35rem 0; }
935
Removed:
.in-progress-card p { margin: 0; }
1075
Added:
.in-progress h1 { margin: 0.35rem 0; }
1076
Added:
.in-progress p { margin: 0; }
936
1077
937
Removed:
/* The exercise accordion: one <details> per prescription slot. They share a
938
Removed:
[name], so the browser keeps exactly one open — no script. The summary shows
939
Removed:
the slot number, the exercise, and a done mark once recorded. It borrows the
940
Removed:
routine accordion's shape so the two read as the same control. */
941
Removed:
.slot-accordions {
942
Removed:
display: grid;
943
Removed:
gap: 0.65rem;
1078
Added:
/* The prescription slots as a flat, always-visible list. Each slot is a section
1079
Added:
with a heading (number + exercise label, struck through once recorded) and its
1080
Added:
record or correction form directly below. With few slots per session, every
1081
Added:
slot's state is visible at once, no click needed. Vertical spacing alone
1082
Added:
separates slots — no rules between them. */
1083
Added:
.slot-sections {
944
1084
margin: 1.25rem 0 0;
945
1085
}
946
Removed:
.slot-accordion {
947
Removed:
border: 1px solid var(--rule);
948
Removed:
border-radius: var(--radius);
949
Removed:
background: var(--paper-raised);
950
Removed:
overflow: visible;
1086
Added:
.slot-section {
1087
Added:
padding: 1rem 0;
951
1088
}
952
Removed:
.slot-accordion > summary {
1089
Added:
.slot-heading {
953
1090
display: flex;
954
1091
align-items: baseline;
955
1092
gap: 0.6rem;
956
Removed:
padding: 0.8rem 1rem;
957
Removed:
color: var(--ink);
958
Removed:
font-weight: 700;
959
Removed:
cursor: pointer;
960
Removed:
list-style: none;
1093
Added:
margin: 0 0 0.5rem;
961
1094
}
962
Removed:
.slot-accordion > summary::-webkit-details-marker { display: none; }
963
Removed:
.slot-accordion > summary::marker { content: ""; }
964
Removed:
.slot-accordion > summary::after {
965
Removed:
margin-left: auto;
966
Removed:
content: "+";
1095
Added:
.slot-heading .slot-number { color: var(--muted-ink); }
1096
Added:
.slot-heading .slot-label-done {
967
1097
color: var(--muted-ink);
1098
Added:
text-decoration: line-through;
968
1099
}
969
Removed:
.slot-accordion[open] > summary::after { content: "−"; }
970
Removed:
.slot-accordion > summary .slot-number { color: var(--muted-ink); }
971
Removed:
.slot-accordion > summary .slot-done {
972
Removed:
font: 700 var(--font-size-small)/1 var(--sans);
973
Removed:
letter-spacing: 0.055em;
974
Removed:
text-transform: uppercase;
975
Removed:
color: var(--brass);
976
Removed:
}
977
Removed:
.slot-accordion[data-done="true"] > summary { border-left: 4px solid var(--brass); }
978
1100
979
Removed:
/* Unfold and collapse. ::details-content is the box the browser toggles for a
980
Removed:
<details>; keep it rendered when closed and drive a grid row from 0fr to 1fr
981
Removed:
off the [open] attribute, so both opening and closing ease in every browser
982
Removed:
that has ::details-content. A first-render starting style would not do: a
983
Removed:
closed panel stays in the tree with content-visibility hidden, so a toggle is
984
Removed:
not a first render and @starting-style never fires. visibility keeps a closed
985
Removed:
panel out of the tab order and the accessibility tree; it flips to visible at
986
Removed:
the start of an open and to hidden at the end of a close. Browsers without
987
Removed:
::details-content ignore all of this and snap. Reduced motion snaps too. */
988
Removed:
.slot-accordion::details-content {
989
Removed:
content-visibility: visible;
990
Removed:
display: grid;
991
Removed:
grid-template-rows: 0fr;
992
Removed:
visibility: hidden;
993
Removed:
transition:
994
Removed:
grid-template-rows 220ms ease,
995
Removed:
visibility 220ms;
996
Removed:
}
997
Removed:
.slot-accordion[open]::details-content {
998
Removed:
grid-template-rows: 1fr;
999
Removed:
visibility: visible;
1000
Removed:
}
1001
Removed:
.slot-accordion > .slot-panel {
1002
Removed:
min-height: 0;
1003
Removed:
overflow: clip;
1004
Removed:
}
1005
Removed:
@media (prefers-reduced-motion: reduce) {
1006
Removed:
.slot-accordion::details-content { transition: none; }
1007
Removed:
}
1008
Removed:
1009
1101
/* The sticky workout timer bar. It pins to the top of the scroll area while a
1010
Removed:
workout is being logged and shows the elapsed time. The client ticks the
1011
Removed:
value from the workout's start time; server-rendered markup shows a static
1012
Removed:
start so the bar is present without script. */
1102
Added:
workout is being logged and shows the elapsed time as a centered Courier
1103
Added:
Prime clock. The client ticks the value from the workout's start time;
1104
Added:
server-rendered markup shows a static start so the bar is present without
1105
Added:
script. */
1013
1106
.workout-timer {
1014
1107
position: sticky;
1015
1108
z-index: 10;
1016
1109
top: 0;
1017
1110
display: flex;
1018
Removed:
align-items: baseline;
1019
Removed:
justify-content: space-between;
1020
Removed:
gap: 0.75rem;
1111
Added:
align-items: center;
1112
Added:
justify-content: center;
1021
1113
margin: 0 0 1rem;
1022
Removed:
border: 1px solid var(--rule-strong);
1023
Removed:
border-radius: var(--radius);
1024
Removed:
background: var(--paper-raised);
1025
Removed:
box-shadow: 0 0.2rem 0.6rem rgba(26, 26, 22, 0.08);
1026
Removed:
padding: 0.55rem 0.9rem;
1114
Added:
border-bottom: 2px solid var(--rule-strong);
1115
Added:
background: var(--paper);
1116
Added:
padding: 0.7rem 0;
1027
1117
}
1028
Removed:
.workout-timer-label {
1029
Removed:
color: var(--muted-ink);
1030
Removed:
font: 700 var(--font-size-small)/1 var(--sans);
1031
Removed:
letter-spacing: 0.08em;
1032
Removed:
text-transform: uppercase;
1033
Removed:
}
1034
1118
.workout-timer-value {
1035
1119
color: var(--ink);
1036
Removed:
font: 700 var(--font-size-lead)/1 var(--mono);
1120
Added:
font: 700 var(--font-size-h3)/1 var(--mono);
1037
1121
font-variant-numeric: tabular-nums;
1038
1122
}
1039
1123
1040
Removed:
.slot-panel-body { padding: 0 1rem 1rem; }
1041
Removed:
1042
1124
/* The cancel confirmation modal. A native dialog styled as a raised paper
1043
1125
surface: the oxblood top accent, radius, border, and shadow echo the page
1044
1126
surface, so the modal reads as part of the same interface. The actions reuse
@@ -1109,18 +1191,11 @@
1109
1191
.feedback-modal-content { padding: 1.25rem; }
1110
1192
.feedback-modal-content h2 { margin-top: 0; text-align: center; }
1111
1193
1112
Removed:
.feedback-list {
1113
Removed:
margin-top: 1rem;
1114
Removed:
list-style: disc;
1115
Removed:
}
1116
Removed:
1117
1194
.settings-section {
1118
1195
max-width: 32rem;
1119
1196
margin-top: 1.25rem;
1120
Removed:
padding: 1rem;
1121
Removed:
border: 1px solid var(--rule);
1122
Removed:
border-radius: var(--radius);
1123
Removed:
background: var(--paper-raised);
1197
Added:
padding: 0 0 1rem;
1198
Added:
border-bottom: 1px solid var(--rule);
1124
1199
}
1125
1200
.settings-form { margin-top: 1rem; }
1126
1201
@@ -1139,10 +1214,10 @@
1139
1214
.notice {
1140
1215
max-width: 47rem;
1141
1216
margin: 1rem 0;
1142
Removed:
border: 1px solid var(--rule-strong);
1143
Removed:
border-left: 5px solid var(--oxblood);
1217
Added:
border: 1px solid var(--evergreen);
1218
Added:
border-left: 5px solid var(--evergreen);
1144
1219
color: var(--ink);
1145
Removed:
background: var(--paper-raised);
1220
Added:
background: var(--evergreen-wash);
1146
1221
padding: 0.9rem 1rem;
1147
1222
}
1148
1223
@@ -1250,6 +1325,18 @@
1250
1325
}
1251
1326
1252
1327
.page-surface { min-height: calc(100vh - 9rem); padding: 1.25rem; }
1328
Added:
1329
Added:
/* On mobile the elapsed bar mirrors the bottom navigation visually: it spans
1330
Added:
the full viewport width (breaking out of the page-surface 1.25rem and
1331
Added:
app-shell 0.75rem side padding, then re-padding by the same 2rem), and
1332
Added:
carries the bottom-nav's paper background, hairline border, and elevation
1333
Added:
shadow — flipped to the top edge. It stays sticky, not fixed. */
1334
Added:
.workout-timer {
1335
Added:
margin-inline: -2rem;
1336
Added:
padding-inline: 2rem;
1337
Added:
border-bottom: 1px solid var(--rule);
1338
Added:
box-shadow: 0 0.35rem 1.2rem rgba(26, 26, 22, 0.15);
1339
Added:
}
1253
1340
}
1254
1341
1255
1342
@media (max-width: 26rem) {
@@ -1393,61 +1480,6 @@
1393
1480
.app-feedback-message {
1394
1481
white-space: pre-wrap;
1395
1482
}
1396
Removed:
1397
Removed:
/* The early-workout toast. It reports that the workout began before recovery
1398
Removed:
finished. The modal already confirmed the override, so on the workout screen
1399
Removed:
this is informational only: a fixed notice above the content rather than a
1400
Removed:
block in the flow. Rendered visible so a no-JS visitor reads it; the client
1401
Removed:
marks it done and removes it after three seconds. The brass wash keeps it
1402
Removed:
distinct from an oxblood error. */
1403
Removed:
/* The transient notice. Served from the top edge: it slides down into view on
1404
Removed:
arrival and, once the client marks it done — after five seconds or on
1405
Removed:
Dismiss — slides back up out of view before it is removed. Without script it
1406
Removed:
simply stays. */
1407
Removed:
.toast {
1408
Removed:
position: fixed;
1409
Removed:
top: 0;
1410
Removed:
left: 50%;
1411
Removed:
z-index: 20;
1412
Removed:
display: flex;
1413
Removed:
align-items: center;
1414
Removed:
gap: 0.75rem;
1415
Removed:
max-width: min(30rem, calc(100% - 2rem));
1416
Removed:
padding: 0.7rem 0.6rem 0.7rem 1rem;
1417
Removed:
border: 1px solid var(--brass);
1418
Removed:
border-top: none;
1419
Removed:
border-radius: 0 0 var(--radius) var(--radius);
1420
Removed:
color: var(--ink);
1421
Removed:
background: var(--brass-wash);
1422
Removed:
box-shadow: 0 6px 18px rgba(26, 26, 22, 0.18);
1423
Removed:
font-weight: 600;
1424
Removed:
transform: translate(-50%, 0);
1425
Removed:
animation: toast-in 260ms ease both;
1426
Removed:
transition: transform 320ms ease, opacity 320ms ease;
1427
Removed:
}
1428
Removed:
@keyframes toast-in {
1429
Removed:
from { transform: translate(-50%, -100%); }
1430
Removed:
to { transform: translate(-50%, 0); }
1431
Removed:
}
1432
Removed:
.toast[data-hito-toast-done] {
1433
Removed:
transform: translate(-50%, -100%);
1434
Removed:
opacity: 0;
1435
Removed:
}
1436
Removed:
.toast-message { flex: 1 1 auto; }
1437
Removed:
.toast-dismiss {
1438
Removed:
flex: none;
1439
Removed:
width: 2rem;
1440
Removed:
height: 2rem;
1441
Removed:
padding: 0;
1442
Removed:
border: none;
1443
Removed:
border-radius: 50%;
1444
Removed:
color: var(--ink);
1445
Removed:
background: transparent;
1446
Removed:
font-size: 1.25rem;
1447
Removed:
line-height: 1;
1448
Removed:
cursor: pointer;
1449
Removed:
}
1450
Removed:
.toast-dismiss:hover { background: rgba(26, 26, 22, 0.08); }
1451
1483
1452
1484
/* Subjective feedback uses one inline three-choice group per signal. The radio
1453
1485
input keeps native keyboard and form behavior; its adjacent span is the