[OCaml] High Intensity Training Online
feat Add mobile logbook theme
Apply an accessible Work Sans and Courier Prime ledger theme with workout-aware sticky mobile navigation and labeled logging controls.
Changed files
lib/web/assets/hito.css
@@ -1,49 +1,72 @@
1
Added:
@import url("https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&family=Work+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400;1,600&display=swap");
2
Added:
1
3
:root {
2
Removed:
color-scheme: dark;
3
Removed:
--ink: #17191d;
4
Removed:
--panel: #202328;
5
Removed:
--panel-raised: #292d33;
6
Removed:
--line: #464b54;
7
Removed:
--muted: #afb2b8;
8
Removed:
--bone: #f1eee8;
9
Removed:
--accent: #d9877b;
10
Removed:
--accent-strong: #a84438;
11
Removed:
--brass: #c69a56;
12
Removed:
--danger: #d77b70;
13
Removed:
--shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.28);
4
Added:
color-scheme: light;
5
Added:
--paper: #f7f2e8;
6
Added:
--paper-raised: #fffdf8;
7
Added:
--ink: #1a1a16;
8
Added:
--muted-ink: #5b584f;
9
Added:
--rule: #b9b1a2;
10
Added:
--rule-strong: #756f63;
11
Added:
--oxblood: #8c201b;
12
Added:
--oxblood-dark: #65130f;
13
Added:
--brass: #7b5714;
14
Added:
--brass-wash: #f4e7c5;
15
Added:
--evergreen: #1f5b45;
16
Added:
--evergreen-wash: #dcebe2;
17
Added:
--focus: #005fcc;
18
Added:
--error: #8c201b;
19
Added:
--font-size-small: 0.875rem;
20
Added:
--font-size-base: 1rem;
21
Added:
--font-size-lead: 1.125rem;
22
Added:
--font-size-heading: 2rem;
23
Added:
--sans: "Work Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
24
Added:
--mono: "Courier Prime", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
14
25
}
15
26
16
Removed:
* {
17
Removed:
box-sizing: border-box;
18
Removed:
}
27
Added:
* { box-sizing: border-box; }
19
28
29
Added:
html { background: var(--paper); }
30
Added:
20
31
body {
21
32
min-width: 20rem;
22
33
margin: 0;
23
Removed:
color: var(--bone);
24
Removed:
background:
25
Removed:
radial-gradient(circle at 14% -10%, #343840 0, transparent 27rem),
26
Removed:
radial-gradient(circle at 94% 12%, #39343a 0, transparent 24rem),
27
Removed:
var(--ink);
28
Removed:
font:
29
Removed:
16px/1.55 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
30
Removed:
"Segoe UI", sans-serif;
34
Added:
color: var(--ink);
35
Added:
background: var(--paper);
36
Added:
font: 400 var(--font-size-base)/1.5 var(--sans);
37
Added:
text-rendering: optimizeLegibility;
31
38
}
32
39
40
Added:
body::before {
41
Added:
position: fixed;
42
Added:
z-index: -1;
43
Added:
inset: 0;
44
Added:
background-image: linear-gradient(to bottom, rgba(26, 26, 22, 0.035) 1px, transparent 1px);
45
Added:
background-size: 100% 2rem;
46
Added:
content: "";
47
Added:
pointer-events: none;
48
Added:
}
49
Added:
33
50
a {
34
Removed:
color: var(--accent);
35
Removed:
text-decoration-thickness: 0.09em;
36
Removed:
text-underline-offset: 0.18em;
51
Added:
color: var(--oxblood-dark);
52
Added:
font-weight: 600;
53
Added:
text-decoration-thickness: 0.1em;
54
Added:
text-underline-offset: 0.16em;
37
55
}
38
56
39
Removed:
a:hover {
40
Removed:
color: #efb0a7;
57
Added:
a:hover { color: var(--oxblood); }
58
Added:
59
Added:
a:focus-visible,
60
Added:
input:focus-visible,
61
Added:
select:focus-visible {
62
Added:
outline: 3px solid var(--focus);
63
Added:
outline-offset: 3px;
41
64
}
42
65
43
66
.app-shell {
44
Removed:
width: min(100%, 68rem);
67
Added:
width: min(100%, 72rem);
45
68
margin: 0 auto;
46
Removed:
padding: 1.25rem;
69
Added:
padding: 0 1rem 2.5rem;
47
70
}
48
71
49
72
.masthead {
@@ -51,422 +74,284 @@
51
74
align-items: center;
52
75
justify-content: space-between;
53
76
gap: 1rem;
54
Removed:
padding: 1rem 0 1.25rem;
55
Removed:
border-bottom: 1px solid rgba(241, 238, 227, 0.18);
77
Added:
min-height: 4.75rem;
78
Added:
border-bottom: 3px solid var(--ink);
56
79
}
57
80
58
81
.brand {
59
Removed:
display: flex;
82
Added:
display: inline-flex;
60
83
align-items: center;
61
Removed:
gap: 0.75rem;
62
Removed:
color: var(--bone);
84
Added:
gap: 0.7rem;
85
Added:
color: var(--ink);
63
86
text-decoration: none;
64
87
}
65
88
66
89
.brand-mark {
67
90
display: grid;
68
Removed:
width: 2.5rem;
69
Removed:
height: 2.5rem;
91
Added:
width: 2.25rem;
92
Added:
height: 2.25rem;
70
93
place-items: center;
71
Removed:
border: 1px solid var(--accent);
72
Removed:
border-radius: 0.7rem 1.15rem 0.7rem 1.15rem;
73
Removed:
color: var(--accent);
74
Removed:
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
75
Removed:
font-size: 0.72rem;
76
Removed:
font-weight: 800;
94
Added:
border: 2px solid var(--ink);
95
Added:
color: var(--paper-raised);
96
Added:
background: var(--oxblood);
97
Added:
font: 700 0.78rem/1 var(--mono);
77
98
letter-spacing: 0.08em;
78
99
}
79
100
80
Removed:
.brand-copy {
81
Removed:
display: grid;
82
Removed:
line-height: 1.05;
83
Removed:
}
84
Removed:
85
Removed:
.brand-copy strong {
86
Removed:
font-size: 1.2rem;
87
Removed:
letter-spacing: -0.03em;
88
Removed:
}
89
Removed:
101
Added:
.brand-copy { display: grid; line-height: 1.1; }
102
Added:
.brand-copy strong { font-size: 1.125rem; font-weight: 800; letter-spacing: -0.025em; }
90
103
.brand-copy small,
91
Removed:
.eyebrow {
92
Removed:
color: var(--accent);
93
Removed:
font-size: 0.68rem;
94
Removed:
font-weight: 800;
95
Removed:
letter-spacing: 0.14em;
104
Added:
.eyebrow,
105
Added:
.ledger-meta {
106
Added:
font: 700 var(--font-size-small)/1.35 var(--mono);
107
Added:
letter-spacing: 0.09em;
96
108
text-transform: uppercase;
97
109
}
110
Added:
.brand-copy small, .eyebrow { color: var(--oxblood-dark); }
98
111
99
112
.primary-nav {
100
113
display: flex;
101
Removed:
align-items: center;
114
Added:
flex-wrap: wrap;
115
Added:
justify-content: flex-end;
102
116
gap: 0.2rem;
103
117
}
104
118
105
Removed:
.primary-nav > a,
106
Removed:
.ot-dropdown-button {
119
Added:
.primary-nav > a {
107
120
display: inline-flex;
108
121
align-items: center;
109
Removed:
gap: 0.45rem;
110
Removed:
min-height: 2.5rem;
122
Added:
min-height: 2.75rem;
111
123
padding: 0.55rem 0.7rem;
112
Removed:
border-radius: 0.55rem;
113
Removed:
color: var(--bone);
114
Removed:
font-size: 0.86rem;
115
Removed:
font-weight: 700;
124
Added:
color: var(--ink);
125
Added:
font-size: 0.88rem;
116
126
text-decoration: none;
117
127
}
118
128
119
Removed:
.primary-nav > a:hover,
120
Removed:
.ot-dropdown:hover {
121
Removed:
background: rgba(217, 135, 123, 0.12);
122
Removed:
color: var(--accent);
129
Added:
.primary-nav > a:hover {
130
Added:
background: #ece4d5;
131
Added:
color: var(--oxblood-dark);
123
132
}
124
133
125
Removed:
.nav-menu {
126
Removed:
display: none;
134
Added:
.page-home .primary-nav > a[href="/"],
135
Added:
.page-routine .primary-nav > a[href="/routine"],
136
Added:
.page-workout .primary-nav > a[href="/workout"],
137
Added:
.page-history .primary-nav > a[href="/history"] {
138
Added:
color: var(--paper-raised);
139
Added:
background: var(--oxblood);
127
140
}
128
141
129
Removed:
/* Compact navigation for small screens. */
130
Removed:
.ot-dropdown {
131
Removed:
position: relative;
132
Removed:
transition: background-color 0.2s ease;
142
Added:
.primary-nav > a[aria-current] {
143
Added:
font-weight: 800;
133
144
}
134
145
135
Removed:
.ot-dropdown-button {
136
Removed:
position: relative;
137
Removed:
z-index: 5;
138
Removed:
cursor: default;
146
Added:
.page-home .bottom-nav > a[href="/"],
147
Added:
.page-routine .bottom-nav > a[href="/routine"],
148
Added:
.page-workout .bottom-nav > a[href="/workout"],
149
Added:
.page-history .bottom-nav > a[href="/history"] {
150
Added:
color: var(--paper-raised);
151
Added:
background: var(--oxblood);
139
152
}
140
153
141
Removed:
.ot-dropdown-background {
142
Removed:
position: fixed;
143
Removed:
inset: 0;
144
Removed:
z-index: 1;
145
Removed:
visibility: hidden;
146
Removed:
background: #000;
147
Removed:
opacity: 0;
148
Removed:
transition: opacity 0.15s, visibility 0.15s;
149
Removed:
}
154
Added:
main { padding: 1.5rem 0; }
150
155
151
Removed:
.ot-dropdown-menu {
152
Removed:
position: absolute;
153
Removed:
top: calc(100% + 0.35rem);
154
Removed:
right: 0;
155
Removed:
z-index: 10;
156
Removed:
display: grid;
157
Removed:
min-width: 12rem;
158
Removed:
overflow: hidden;
159
Removed:
visibility: hidden;
160
Removed:
border: 1px solid var(--line);
161
Removed:
border-radius: 0.7rem;
162
Removed:
background: #262a30;
163
Removed:
box-shadow: var(--shadow);
164
Removed:
opacity: 0;
165
Removed:
transition: opacity 0.2s, visibility 0.2s;
166
Removed:
}
156
Added:
.bottom-nav { display: none; }
167
157
168
Removed:
.ot-dropdown:hover > .ot-dropdown-background {
169
Removed:
visibility: visible;
170
Removed:
opacity: 0.42;
158
Added:
.page-surface {
159
Added:
min-height: 26rem;
160
Added:
border: 1px solid var(--rule-strong);
161
Added:
border-top: 6px solid var(--oxblood);
162
Added:
background: var(--paper-raised);
163
Added:
border-radius: 0.45rem;
164
Added:
box-shadow: 0 0.4rem 1.5rem rgba(26, 26, 22, 0.09);
165
Added:
padding: clamp(1.25rem, 3vw, 2rem);
171
166
}
172
167
173
Removed:
.ot-dropdown:hover > .ot-dropdown-menu {
174
Removed:
visibility: visible;
175
Removed:
opacity: 1;
176
Removed:
}
177
Removed:
178
Removed:
.ot-dropdown-menu a {
179
Removed:
padding: 0.8rem 1rem;
180
Removed:
color: var(--bone);
181
Removed:
font-size: 0.9rem;
168
Added:
h2 {
169
Added:
max-width: 38rem;
170
Added:
margin: 0 0 0.45rem;
171
Added:
color: var(--ink);
172
Added:
font-size: clamp(1.75rem, 4vw, var(--font-size-heading));
182
173
font-weight: 700;
183
Removed:
text-decoration: none;
174
Added:
line-height: 1.2;
175
Added:
letter-spacing: -0.03em;
184
176
}
185
177
186
Removed:
.ot-dropdown-menu a + a {
187
Removed:
border-top: 1px solid var(--line);
188
Removed:
}
178
Added:
h3 { margin: 0; color: var(--ink); font-size: 1.5rem; line-height: 1.2; }
189
179
190
Removed:
.ot-dropdown-menu a:hover {
191
Removed:
background: rgba(217, 135, 123, 0.13);
192
Removed:
color: var(--accent);
193
Removed:
}
180
Added:
p { max-width: 46rem; margin: 0.65rem 0; color: var(--muted-ink); }
181
Added:
h2 + p { font-size: var(--font-size-lead); }
194
182
195
Removed:
/* Navigation icon styling. */
196
Removed:
.ot-icon {
197
Removed:
display: inline-block;
198
Removed:
width: 1em;
199
Removed:
height: 1em;
200
Removed:
overflow: hidden;
201
Removed:
position: relative;
183
Added:
.ledger-meta {
184
Added:
color: var(--muted-ink);
185
Added:
font-variant-numeric: tabular-nums;
202
186
}
203
187
204
Removed:
.ot-icon::before,
205
Removed:
.ot-icon::after {
206
Removed:
box-sizing: border-box;
207
Removed:
display: inline-block;
208
Removed:
content: "";
188
Added:
.doctrine-note {
189
Added:
max-width: 46rem;
190
Added:
margin: 1.25rem 0;
191
Added:
border-left: 4px solid var(--brass);
192
Added:
color: #47350f;
193
Added:
background: var(--brass-wash);
194
Added:
font-size: 0.95rem;
195
Added:
padding: 0.85rem 1rem;
209
196
}
210
197
211
Removed:
.ot-icon-gear::before {
212
Removed:
width: 1em;
213
Removed:
height: 1em;
214
Removed:
border: 0.18em dotted currentColor;
215
Removed:
border-radius: 50%;
216
Removed:
}
217
Removed:
218
Removed:
.ot-icon-gear::after {
219
Removed:
position: absolute;
220
Removed:
top: 0.22em;
221
Removed:
left: 0.22em;
222
Removed:
width: 0.56em;
223
Removed:
height: 0.56em;
224
Removed:
border: 0.17em solid currentColor;
225
Removed:
border-radius: 50%;
226
Removed:
}
227
Removed:
228
Removed:
main {
229
Removed:
padding: 1.65rem 0 2rem;
230
Removed:
}
231
Removed:
232
Removed:
/* Ot_popup.hcf supplies the structural header/content/footer card wrapper. */
233
Removed:
.ot-hcf {
234
Removed:
display: flex;
235
Removed:
width: 100%;
236
Removed:
flex-direction: column;
237
Removed:
overflow: hidden;
238
Removed:
}
239
Removed:
240
Removed:
.ot-hcf-header,
241
Removed:
.ot-hcf-footer {
242
Removed:
flex-shrink: 0;
243
Removed:
}
244
Removed:
245
Removed:
.ot-hcf-header:empty,
246
Removed:
.ot-hcf-footer:empty {
247
Removed:
display: none;
248
Removed:
}
249
Removed:
250
Removed:
.ot-hcf-content {
251
Removed:
flex-grow: 1;
252
Removed:
overflow: auto;
253
Removed:
}
254
Removed:
255
Removed:
.page-surface {
256
Removed:
min-height: 30rem;
257
Removed:
border: 1px solid var(--line);
258
Removed:
border-radius: 1.15rem;
259
Removed:
background: linear-gradient(145deg, rgba(42, 45, 51, 0.97), rgba(25, 27, 31, 0.98));
260
Removed:
box-shadow: var(--shadow);
261
Removed:
}
262
Removed:
263
Removed:
.page-surface .ot-hcf-content {
264
Removed:
padding: clamp(1.2rem, 4vw, 2.4rem);
265
Removed:
}
266
Removed:
267
Removed:
h2 {
268
Removed:
max-width: 35rem;
269
Removed:
margin: 0 0 0.4rem;
270
Removed:
font-size: clamp(1.65rem, 5vw, 2.55rem);
271
Removed:
line-height: 1.04;
272
Removed:
letter-spacing: -0.05em;
273
Removed:
}
274
Removed:
275
Removed:
h3 {
276
Removed:
margin: 0;
277
Removed:
color: var(--bone);
278
Removed:
font-size: 1rem;
279
Removed:
}
280
Removed:
281
Removed:
p {
282
Removed:
max-width: 45rem;
283
Removed:
margin: 0.55rem 0;
284
Removed:
color: var(--muted);
285
Removed:
}
286
Removed:
287
Removed:
h2 + p {
288
Removed:
max-width: 39rem;
289
Removed:
font-size: 1.03rem;
290
Removed:
}
291
Removed:
292
198
fieldset {
293
Removed:
margin: 1.15rem 0 0;
294
Removed:
padding: 1rem;
295
Removed:
border: 1px solid var(--line);
296
Removed:
border-radius: 0.8rem;
297
Removed:
background: rgba(9, 10, 12, 0.3);
199
Added:
margin: 1rem 0 0;
200
Added:
padding: 0.875rem;
201
Added:
border: 1px solid var(--rule);
202
Added:
border-top: 3px solid var(--ink);
203
Added:
background: var(--paper);
204
Added:
border-radius: 0.3rem;
298
205
}
299
206
300
207
legend {
301
Removed:
padding: 0 0.45rem;
302
Removed:
color: var(--accent);
303
Removed:
font-size: 0.79rem;
304
Removed:
font-weight: 800;
305
Removed:
letter-spacing: 0.05em;
208
Added:
max-width: 100%;
209
Added:
padding: 0 0.35rem;
210
Added:
color: var(--ink);
211
Added:
font: 700 var(--font-size-small)/1.35 var(--mono);
212
Added:
letter-spacing: 0.025em;
306
213
}
307
214
215
Added:
.field { margin-top: 1rem; }
308
216
label {
309
217
display: block;
310
Removed:
margin: 0.85rem 0 0.3rem;
311
Removed:
color: var(--bone);
312
Removed:
font-size: 0.78rem;
313
Removed:
font-weight: 800;
314
Removed:
letter-spacing: 0.04em;
315
Removed:
text-transform: uppercase;
218
Added:
margin: 0 0 0.35rem;
219
Added:
color: var(--ink);
220
Added:
font-size: var(--font-size-small);
221
Added:
font-weight: 700;
316
222
}
317
223
318
Removed:
input,
319
Removed:
select {
224
Added:
input, select {
320
225
width: 100%;
321
Removed:
min-height: 2.7rem;
322
Removed:
border: 1px solid #5c626c;
323
Removed:
border-radius: 0.55rem;
324
Removed:
background: rgba(8, 9, 11, 0.5);
325
Removed:
color: var(--bone);
326
Removed:
font: inherit;
327
Removed:
outline: none;
328
Removed:
padding: 0.55rem 0.7rem;
226
Added:
min-height: 3rem;
227
Added:
border: 1px solid var(--rule-strong);
228
Added:
border-radius: 0.25rem;
229
Added:
color: var(--ink);
230
Added:
background: var(--paper-raised);
231
Added:
font: 500 1rem/1.3 var(--sans);
232
Added:
padding: 0.65rem 0.75rem;
329
233
}
330
234
331
Removed:
input:focus,
332
Removed:
select:focus {
333
Removed:
border-color: var(--accent);
334
Removed:
box-shadow: 0 0 0 3px rgba(217, 135, 123, 0.16);
335
Removed:
}
235
Added:
input:invalid { border-color: var(--error); }
236
Added:
input[type="number"] { font-family: var(--mono); font-variant-numeric: tabular-nums; }
336
237
337
Removed:
input[type="checkbox"] {
338
Removed:
width: auto;
339
Removed:
min-height: auto;
340
Removed:
margin-right: 0.45rem;
341
Removed:
accent-color: var(--accent-strong);
342
Removed:
}
343
Removed:
344
238
input[type="submit"] {
345
Removed:
width: auto;
346
Removed:
min-height: 2.8rem;
239
Added:
width: 100%;
240
Added:
min-height: 3rem;
347
241
margin-top: 1rem;
348
Removed:
border: 0;
349
Removed:
border-radius: 0.6rem;
350
Removed:
background: var(--accent-strong);
351
Removed:
color: #fffaf7;
242
Added:
border: 2px solid var(--oxblood-dark);
243
Added:
border-radius: 0.3rem;
244
Added:
color: var(--paper-raised);
245
Added:
background: var(--oxblood);
352
246
cursor: pointer;
353
Removed:
font-weight: 900;
354
Removed:
letter-spacing: 0.01em;
355
Removed:
padding: 0.65rem 1rem;
356
Removed:
transition: transform 0.15s ease, background 0.15s ease;
247
Added:
font: 700 var(--font-size-small)/1 var(--sans);
248
Added:
letter-spacing: 0.055em;
249
Added:
text-transform: uppercase;
250
Added:
transition: background-color 150ms ease, transform 150ms ease;
357
251
}
358
252
359
253
input[type="submit"]:hover {
360
Removed:
background: #c45b4e;
361
Removed:
transform: translateY(-1px);
254
Added:
background: var(--oxblood-dark);
255
Added:
transform: translateY(-2px);
362
256
}
363
Removed:
364
257
input[type="submit"]:disabled {
365
Removed:
background: #5a5d63;
366
Removed:
color: #ddd9d2;
258
Added:
border-color: var(--rule-strong);
259
Added:
color: var(--muted-ink);
260
Added:
background: #ded8cc;
367
261
cursor: not-allowed;
368
Removed:
transform: none;
369
262
}
370
263
371
Removed:
.row {
372
Removed:
display: flex;
373
Removed:
gap: 0.75rem;
374
Removed:
}
264
Added:
.row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.8rem; }
375
265
376
Removed:
.row > div {
377
Removed:
flex: 1;
378
Removed:
min-width: 0;
379
Removed:
}
380
Removed:
381
266
.warn {
382
267
max-width: 47rem;
383
268
margin: 1rem 0;
384
Removed:
border: 1px solid rgba(198, 154, 86, 0.58);
385
Removed:
border-left: 0.28rem solid var(--brass);
386
Removed:
border-radius: 0.65rem;
387
Removed:
background: rgba(198, 154, 86, 0.1);
388
Removed:
color: #eed5aa;
389
Removed:
padding: 0.85rem 1rem;
269
Added:
border: 1px solid var(--brass);
270
Added:
border-left: 5px solid var(--brass);
271
Added:
color: #47350f;
272
Added:
background: var(--brass-wash);
273
Added:
padding: 0.9rem 1rem;
390
274
}
391
275
392
276
.done {
393
Removed:
color: #d8d5ce;
394
Removed:
font-size: 0.86rem;
395
Removed:
font-weight: 700;
277
Added:
color: var(--evergreen);
278
Added:
font: 700 0.85rem/1.4 var(--mono);
279
Added:
font-variant-numeric: tabular-nums;
280
Added:
letter-spacing: 0.02em;
396
281
}
397
282
398
Removed:
ul {
399
Removed:
margin: 1rem 0;
400
Removed:
padding: 0;
401
Removed:
list-style: none;
402
Removed:
}
403
Removed:
283
Added:
ul { margin: 1rem 0; padding: 0; list-style: none; }
404
284
li {
405
285
position: relative;
406
Removed:
margin: 0.45rem 0;
407
Removed:
padding: 0.75rem 0.85rem 0.75rem 1.35rem;
408
Removed:
border-left: 1px solid var(--line);
409
Removed:
background: rgba(255, 255, 255, 0.025);
286
Added:
margin: 0;
287
Added:
padding: 0.8rem 0.85rem 0.8rem 1.25rem;
288
Added:
border-top: 1px solid var(--rule);
289
Added:
color: var(--ink);
290
Added:
background: var(--paper-raised);
410
291
}
411
Removed:
412
292
li::before {
413
293
position: absolute;
414
Removed:
top: 1.28rem;
294
Added:
top: 1.2rem;
415
295
left: 0.55rem;
416
Removed:
width: 0.3rem;
417
Removed:
height: 0.3rem;
418
Removed:
border-radius: 50%;
419
Removed:
background: var(--accent);
296
Added:
width: 0.35rem;
297
Added:
height: 0.35rem;
298
Added:
background: var(--evergreen);
420
299
content: "";
421
300
}
301
Added:
li ul { margin: 0.55rem 0 0; }
302
Added:
li li { border-top-color: #ded8cc; color: var(--muted-ink); font-size: 0.92rem; }
422
303
423
Removed:
li ul {
424
Removed:
margin: 0.55rem 0 0;
425
Removed:
}
426
Removed:
427
Removed:
li li {
428
Removed:
border-left-color: rgba(164, 180, 170, 0.28);
429
Removed:
color: var(--muted);
430
Removed:
font-size: 0.92rem;
431
Removed:
}
432
Removed:
433
304
@media (max-width: 42rem) {
434
305
.app-shell {
435
Removed:
padding: 0.85rem;
306
Added:
padding: 0 0.75rem calc(6.75rem + env(safe-area-inset-bottom));
436
307
}
437
308
438
309
.masthead {
439
Removed:
align-items: flex-start;
310
Added:
min-height: 4.5rem;
311
Added:
padding: 0.9rem 0 0.7rem;
440
312
}
441
313
442
Removed:
.primary-nav > a {
443
Removed:
display: none;
444
Removed:
}
314
Added:
.primary-nav { display: none; }
445
315
446
Removed:
.nav-menu {
447
Removed:
display: block;
316
Added:
.bottom-nav {
317
Added:
position: fixed;
318
Added:
z-index: 20;
319
Added:
right: 0;
320
Added:
bottom: 0;
321
Added:
left: 0;
322
Added:
display: grid;
323
Added:
grid-template-columns: repeat(3, minmax(0, 1fr));
324
Added:
gap: 1px;
325
Added:
border-top: 2px solid var(--ink);
326
Added:
background: var(--ink);
327
Added:
box-shadow: 0 -0.35rem 1.2rem rgba(26, 26, 22, 0.15);
328
Added:
padding-bottom: env(safe-area-inset-bottom);
448
329
}
449
330
450
Removed:
.page-surface {
451
Removed:
min-height: calc(100vh - 8.25rem);
452
Removed:
border-radius: 0.9rem;
331
Added:
.bottom-nav > a {
332
Added:
display: grid;
333
Added:
min-height: 3.5rem;
334
Added:
place-items: center;
335
Added:
color: var(--ink);
336
Added:
background: var(--paper-raised);
337
Added:
font: 700 0.76rem/1.1 var(--sans);
338
Added:
letter-spacing: 0.025em;
339
Added:
text-align: center;
340
Added:
text-decoration: none;
453
341
}
454
342
455
Removed:
.page-surface .ot-hcf-content {
456
Removed:
padding: 1.1rem;
343
Added:
.bottom-nav > a:hover {
344
Added:
color: var(--oxblood-dark);
345
Added:
background: #ece4d5;
457
346
}
458
347
459
Removed:
.row {
460
Removed:
gap: 0.55rem;
461
Removed:
}
348
Added:
.page-surface { min-height: calc(100vh - 9rem); padding: 1.25rem; }
462
349
}
463
350
464
351
@media (max-width: 26rem) {
465
Removed:
.row {
466
Removed:
display: block;
467
Removed:
}
352
Added:
.row { grid-template-columns: 1fr; }
353
Added:
}
468
354
469
Removed:
.row > div + div {
470
Removed:
margin-top: 0.75rem;
471
Removed:
}
355
Added:
@media (prefers-reduced-motion: reduce) {
356
Added:
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
472
357
}
lib/web/pages.ml
@@ -13,7 +13,17 @@
13
13
let step = Dream_html.string_attr "step"
14
14
let required = Dream_html.attr "required"
15
15
16
Removed:
let html_page title content =
16
Added:
let html_page ?(active = "") ?(workout_in_progress = false) title content =
17
Added:
let mobile_middle =
18
Added:
if workout_in_progress then
19
Added:
tag "a"
20
Added:
[ Dream_html.path_attr (Dream_html.uri_attr "href") Routes.workout ]
21
Added:
[ txt "Current Workout" ]
22
Added:
else
23
Added:
tag "a"
24
Added:
[ Dream_html.path_attr (Dream_html.uri_attr "href") Routes.routine ]
25
Added:
[ txt "Routine" ]
26
Added:
in
17
27
tag "html" []
18
28
[
19
29
tag "head" []
@@ -35,7 +45,7 @@
35
45
];
36
46
];
37
47
tag "body"
38
Removed:
[ class_ "hito-app" ]
48
Added:
[ Dream_html.string_attr "class" "hito-app page-%s" active ]
39
49
[
40
50
tag "div"
41
51
[ class_ "app-shell" ]
@@ -56,7 +66,7 @@
56
66
[ class_ "brand-copy" ]
57
67
[
58
68
tag "strong" [] [ txt "hito" ];
59
Removed:
tag "small" [] [ txt "Heavy Duty logbook" ];
69
Added:
tag "small" [] [ txt "High Intensity Tracker Online" ];
60
70
];
61
71
];
62
72
tag "nav"
@@ -94,6 +104,26 @@
94
104
];
95
105
tag "main" [] [ tag "div" [ class_ "page-surface" ] content ];
96
106
];
107
Added:
tag "nav"
108
Added:
[
109
Added:
class_ "bottom-nav";
110
Added:
Dream_html.string_attr "aria-label" "Mobile navigation";
111
Added:
]
112
Added:
[
113
Added:
tag "a"
114
Added:
[
115
Added:
Dream_html.path_attr (Dream_html.uri_attr "href") Routes.home;
116
Added:
]
117
Added:
[ txt "Home" ];
118
Added:
mobile_middle;
119
Added:
tag "a"
120
Added:
[
121
Added:
Dream_html.path_attr
122
Added:
(Dream_html.uri_attr "href")
123
Added:
Routes.history;
124
Added:
]
125
Added:
[ txt "Log Book" ];
126
Added:
];
97
127
];
98
128
]
99
129
@@ -119,9 +149,9 @@
119
149
Printf.sprintf "%s into %s, no pause — %s" (Exercise.name isolation)
120
150
(Exercise.name compound) window
121
151
122
Removed:
let extension_select () =
152
Added:
let extension_select ~input_id =
123
153
tag "select"
124
Removed:
[ name "extension" ]
154
Added:
[ name "extension"; Dream_html.string_attr "id" "%s" input_id ]
125
155
[
126
156
tag "option" [ value "" ] [ txt "to positive failure" ];
127
157
tag "option" [ value "forced" ] [ txt "then forced reps" ];
@@ -131,7 +161,7 @@
131
161
]
132
162
133
163
let choose_routine ~routines =
134
Removed:
html_page "Choose a routine"
164
Added:
html_page ~active:"home" "Choose a routine"
135
165
[
136
166
tag "h2" [] [ txt "Routines" ];
137
167
tag "div" []
@@ -199,20 +229,29 @@
199
229
[
200
230
txt "Heavy Duty requires recovery before the next workout.";
201
231
];
232
Added:
tag "p"
233
Added:
[ class_ "doctrine-note" ]
234
Added:
[
235
Added:
txt
236
Added:
"The stimulus is immediate. Recovery is the work that \
237
Added:
follows it.";
238
Added:
];
202
239
begin_form ~routine ~override:true "Begin under override";
203
240
];
204
241
] )
205
242
in
206
Removed:
html_page "Home"
243
Added:
html_page ~active:"home" "Home"
207
244
([
208
245
tag "h2" [] [ txt "%s" routine_name ];
209
Removed:
tag "p" [] [ txt "Next: %s" (Prescription.Workout.name next) ];
210
Removed:
tag "p" [] [ txt "%s" status ];
246
Added:
tag "p"
247
Added:
[ class_ "ledger-meta" ]
248
Added:
[ txt "Next: %s" (Prescription.Workout.name next) ];
249
Added:
tag "p" [ class_ "ledger-meta" ] [ txt "%s" status ];
211
250
]
212
251
@ gate)
213
252
214
253
let routine routine =
215
Removed:
html_page "Routine"
254
Added:
html_page ~active:"routine" "Routine"
216
255
[
217
256
tag "h2" [] [ txt "%s" (Prescription.Routine.name routine) ];
218
257
tag "ul" []
@@ -241,26 +280,40 @@
241
280
[ txt "%s" (Decode.errors_to_text [ (field, error) ]) ];
242
281
]
243
282
244
Removed:
let input_row field label =
245
Removed:
tag "div" []
283
Added:
let input_row ~input_id field label =
284
Added:
tag "div"
285
Added:
[ class_ "field" ]
246
286
[
247
Removed:
tag "label" [] [ txt "%s" label ];
248
Removed:
void "input" [ type_ "number"; name field; step "0.5"; required ];
287
Added:
tag "label"
288
Added:
[ Dream_html.string_attr "for" "%s" input_id ]
289
Added:
[ txt "%s" label ];
290
Added:
void "input"
291
Added:
[
292
Added:
type_ "number";
293
Added:
name field;
294
Added:
Dream_html.string_attr "id" "%s" input_id;
295
Added:
step "0.5";
296
Added:
required;
297
Added:
];
249
298
]
250
299
251
Removed:
let form_for_stimulus ~action_path ~prescription ~errors =
300
Added:
let form_for_stimulus ~action_path ~slot ~prescription ~errors =
301
Added:
let field_id field = Printf.sprintf "slot-%d-%s" slot field in
252
302
let fields =
253
303
match Prescription.Stimulus.delivery prescription with
254
304
| Prescription.Stimulus.Single _ ->
255
Removed:
[ input_row "load" "Load (kg)"; input_row "reps" "Reps to failure" ]
305
Added:
[
306
Added:
input_row ~input_id:(field_id "load") "load" "Load (kg)";
307
Added:
input_row ~input_id:(field_id "reps") "reps" "Reps to failure";
308
Added:
]
256
309
| Prescription.Stimulus.Pre_exhaust { isolation; compound } ->
257
310
[
258
311
tag "p" [ class_ "done" ] [ txt "%s" (Exercise.name isolation) ];
259
Removed:
input_row "iso_load" "Load (kg)";
260
Removed:
input_row "iso_reps" "Reps";
312
Added:
input_row ~input_id:(field_id "iso_load") "iso_load" "Load (kg)";
313
Added:
input_row ~input_id:(field_id "iso_reps") "iso_reps" "Reps";
261
314
tag "p" [ class_ "done" ] [ txt "%s" (Exercise.name compound) ];
262
Removed:
input_row "comp_load" "Load (kg)";
263
Removed:
input_row "comp_reps" "Reps";
315
Added:
input_row ~input_id:(field_id "comp_load") "comp_load" "Load (kg)";
316
Added:
input_row ~input_id:(field_id "comp_reps") "comp_reps" "Reps";
264
317
]
265
318
in
266
319
tag "form"
@@ -269,7 +322,12 @@
269
322
tag "fieldset" []
270
323
([ tag "legend" [] [ txt "%s" (describe_prescription prescription) ] ]
271
324
@ fields
272
Removed:
@ [ tag "label" [] [ txt "Ending" ]; extension_select () ]
325
Added:
@ [
326
Added:
tag "label"
327
Added:
[ Dream_html.string_attr "for" "%s" (field_id "extension") ]
328
Added:
[ txt "Ending" ];
329
Added:
extension_select ~input_id:(field_id "extension");
330
Added:
]
273
331
@ List.concat_map
274
332
(fun field -> error_for field errors)
275
333
[
@@ -341,13 +399,31 @@
341
399
in
342
400
let outstanding_section =
343
401
if outstanding = [] then
344
Removed:
[ tag "p" [] [ txt "Everything prescribed has been recorded." ] ]
402
Added:
[
403
Added:
tag "p"
404
Added:
[ class_ "done" ]
405
Added:
[ txt "Everything prescribed has been recorded." ];
406
Added:
tag "p"
407
Added:
[ class_ "doctrine-note" ]
408
Added:
[
409
Added:
txt
410
Added:
"The record is evidence. Keep it complete before drawing \
411
Added:
conclusions from it.";
412
Added:
];
413
Added:
]
345
414
else
346
415
tag "h2" [] [ txt "Still to do" ]
416
Added:
:: tag "p"
417
Added:
[ class_ "doctrine-note" ]
418
Added:
[
419
Added:
txt
420
Added:
"Record each effort as it occurred. The logbook does not revise \
421
Added:
the fact.";
422
Added:
]
347
423
:: List.map
348
424
(fun (slot, prescription) ->
349
Removed:
form_for_stimulus ~action_path:(form_action slot) ~prescription
350
Removed:
~errors)
425
Added:
form_for_stimulus ~action_path:(form_action slot) ~slot
426
Added:
~prescription ~errors)
351
427
outstanding
352
428
in
353
429
let finish_section =
@@ -365,7 +441,7 @@
365
441
[ void "input" [ type_ "submit"; value "Finish workout" ] ];
366
442
]
367
443
in
368
Removed:
html_page
444
Added:
html_page ~active:"workout" ~workout_in_progress:(Option.is_none record_id)
369
445
(Prescription.Workout.name prescription)
370
446
(override_note
371
447
@ [
@@ -379,7 +455,7 @@
379
455
@ recorded_section @ outstanding_section @ finish_section)
380
456
381
457
let history records =
382
Removed:
html_page "History"
458
Added:
html_page ~active:"history" "History"
383
459
[
384
460
tag "h2" [] [ txt "History" ];
385
461
(if records = [] then tag "p" [] [ txt "Nothing logged yet." ]
test/test_web.ml
@@ -27,7 +27,69 @@
27
27
`Quick,
28
28
fun () ->
29
29
let response = Dream.test (app ()) (Dream.request "") in
30
Removed:
Alcotest.(check int) "status" 200 (status response) );
30
Added:
Alcotest.(check int) "status" 200 (status response);
31
Added:
Alcotest.(check bool)
32
Added:
"Overview active class" true
33
Added:
(contains ~substring:"page-home" (body response));
34
Added:
Alcotest.(check bool)
35
Added:
"expanded brand subtitle" true
36
Added:
(contains ~substring:"High Intensity Tracker Online"
37
Added:
(body response));
38
Added:
Alcotest.(check bool)
39
Added:
"mobile navigation shell" true
40
Added:
(contains ~substring:"bottom-nav" (body response));
41
Added:
Alcotest.(check bool)
42
Added:
"routine mobile action" true
43
Added:
(contains ~substring:">Routine<" (body response));
44
Added:
Alcotest.(check bool)
45
Added:
"log book mobile action" true
46
Added:
(contains ~substring:">Log Book<" (body response)) );
47
Added:
( "stylesheet declares requested fonts and focus rules",
48
Added:
`Quick,
49
Added:
fun () ->
50
Added:
let response =
51
Added:
Dream.test (app ()) (Dream.request ~target:"/assets/hito.css" "")
52
Added:
in
53
Added:
Alcotest.(check int) "stylesheet" 200 (status response);
54
Added:
let stylesheet = body response in
55
Added:
Alcotest.(check bool)
56
Added:
"Work Sans" true
57
Added:
(contains ~substring:"Work Sans" stylesheet);
58
Added:
Alcotest.(check bool)
59
Added:
"Courier Prime" true
60
Added:
(contains ~substring:"Courier Prime" stylesheet);
61
Added:
Alcotest.(check bool)
62
Added:
"font display swap" true
63
Added:
(contains ~substring:"display=swap" stylesheet);
64
Added:
Alcotest.(check bool)
65
Added:
"visible focus" true
66
Added:
(contains ~substring:":focus-visible" stylesheet);
67
Added:
Alcotest.(check bool)
68
Added:
"active workout navigation" true
69
Added:
(contains ~substring:".page-workout .primary-nav" stylesheet);
70
Added:
Alcotest.(check bool)
71
Added:
"42rem mobile breakpoint" true
72
Added:
(contains ~substring:"@media (max-width: 42rem)" stylesheet);
73
Added:
Alcotest.(check bool)
74
Added:
"sticky bottom navigation" true
75
Added:
(contains ~substring:".bottom-nav" stylesheet);
76
Added:
Alcotest.(check bool)
77
Added:
"fixed bottom navigation" true
78
Added:
(contains ~substring:"position: fixed" stylesheet);
79
Added:
Alcotest.(check bool)
80
Added:
"safe area spacing" true
81
Added:
(contains ~substring:"safe-area-inset-bottom" stylesheet);
82
Added:
Alcotest.(check bool)
83
Added:
"Bootstrap small text scale" true
84
Added:
(contains ~substring:"--font-size-small: 0.875rem" stylesheet);
85
Added:
Alcotest.(check bool)
86
Added:
"balanced heading scale" true
87
Added:
(contains
88
Added:
~substring:"clamp(1.75rem, 4vw, var(--font-size-heading))"
89
Added:
stylesheet);
90
Added:
Alcotest.(check bool)
91
Added:
"accessible control height" true
92
Added:
(contains ~substring:"min-height: 3rem" stylesheet) );
31
93
( "a new workout exposes outstanding effort forms on Overview",
32
94
`Quick,
33
95
fun () ->
@@ -40,11 +102,26 @@
40
102
Alcotest.(check int) "overview" 200 (status overview);
41
103
let page = body overview in
42
104
Alcotest.(check bool)
105
Added:
"Current workout active class" true
106
Added:
(contains ~substring:"page-workout" page);
107
Added:
Alcotest.(check bool)
108
Added:
"current workout mobile action" true
109
Added:
(contains ~substring:">Current Workout<" page);
110
Added:
Alcotest.(check bool)
43
111
"outstanding heading" true
44
112
(contains ~substring:"Still to do" page);
45
113
Alcotest.(check bool)
46
114
"first effort form" true
47
Removed:
(contains ~substring:"/workout/slots/0" page) );
115
Added:
(contains ~substring:"/workout/slots/0" page);
116
Added:
Alcotest.(check bool)
117
Added:
"load label references control" true
118
Added:
(contains ~substring:"for=\"slot-0-iso_load\"" page);
119
Added:
Alcotest.(check bool)
120
Added:
"load control has ID" true
121
Added:
(contains ~substring:"id=\"slot-0-iso_load\"" page);
122
Added:
Alcotest.(check bool)
123
Added:
"ending label references control" true
124
Added:
(contains ~substring:"for=\"slot-0-extension\"" page) );
48
125
] );
49
126
]
50
127