site/static/css/main.css (16065B)
1 /* ommp landing page.
2 *
3 * Every colour here is lifted from the application's own source rather than
4 * invented, so the page and the thing it is advertising cannot drift apart.
5 * The comments name where each one lives. */
6
7 /* Relative URLs, not root-absolute: this file is served from a repository
8 subpath and the stylesheet's own name is content-hashed, but ../fonts/
9 resolves correctly either way. */
10 @font-face {
11 font-family: "JetBrains Mono";
12 src: url("../fonts/jetbrains-mono-400.woff2") format("woff2");
13 font-weight: 400; font-style: normal; font-display: swap;
14 }
15 @font-face {
16 font-family: "JetBrains Mono";
17 src: url("../fonts/jetbrains-mono-700.woff2") format("woff2");
18 font-weight: 700; font-style: normal; font-display: swap;
19 }
20 @font-face {
21 font-family: "Bricolage Grotesque";
22 src: url("../fonts/bricolage-grotesque-var.woff2") format("woff2");
23 font-weight: 400 800; font-style: normal; font-display: swap;
24 }
25
26 :root {
27 /* about_modal.rs */
28 --bg: #0A051E; /* BG_DARK */
29 --pink: #FF3296; /* NEON_PINK, the logo gradient's start */
30 --cyan: #00FFFF; /* NEON_CYAN, its end */
31 /* theme.rs */
32 --sky: #64DCFF; /* border_focused, and the current track */
33 --dim: #46505F; /* border_unfocused */
34 --violet: #C850FF; /* progress_filled */
35 /* status_bar.rs */
36 --playing: #50FF78;
37
38 --ink: #EDEAF6;
39 --ink-2: #A9A6BC;
40 --panel: #120B2E;
41 --line: #241945;
42
43 --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
44 --disp: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
45
46 /* Two measures, not one. Prose wants a short line; a section built around a
47 screenshot wants room, and holding it to the prose width leaves the copy
48 pinned against one edge with dead margin beyond it. */
49 --wrap: 68rem;
50 --wrap-wide: 78rem;
51 --sb-h: 3.25rem;
52 }
53
54 *, *::before, *::after { box-sizing: border-box; }
55
56 html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
57 @media (prefers-reduced-motion: reduce) {
58 html { scroll-behavior: auto; }
59 *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
60 }
61
62 body {
63 margin: 0;
64 padding-bottom: var(--sb-h);
65 background: var(--bg);
66 color: var(--ink);
67 font-family: var(--disp);
68 font-size: clamp(1rem, .95rem + .2vw, 1.125rem);
69 line-height: 1.6;
70 -webkit-font-smoothing: antialiased;
71 }
72
73 /* The terminal face is reserved for things that are literally the app:
74 commands you type, keys you press, tab names, the status bar. */
75 code, kbd, .cmd, .kicker, .install-label, .tabstrip, .setup,
76 .sec-title-mono, .sb-row { font-family: var(--mono); }
77
78 a { color: var(--sky); text-underline-offset: .2em; text-decoration-thickness: 1px; }
79 a:hover { color: var(--cyan); }
80 :focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; border-radius: 2px; }
81
82 .skip {
83 position: absolute; left: -9999px;
84 background: var(--cyan); color: var(--bg); padding: .6rem 1rem; font-weight: 700;
85 }
86 .skip:focus { left: 1rem; top: 1rem; z-index: 100; }
87
88 /* Three limits, and the smallest wins. The 92% is what keeps the two measures
89 moving together: without it, a viewport between the two caps (1128–1288px)
90 pins the wide sections 20px off the edge while the prose sections stay 96px
91 in, and the 76px stagger reads as a mistake. The fixed 2.5rem only takes over
92 below ~500px, where a proportional gutter would be too tight to touch. */
93 .wrap { width: min(100% - 2.5rem, 92%, var(--wrap)); margin-inline: auto; }
94 .wrap-wide { width: min(100% - 2.5rem, 92%, var(--wrap-wide)); margin-inline: auto; }
95
96 /* ── Hero ─────────────────────────────────────────────────────────── */
97
98 .hero {
99 position: relative;
100 padding-block: clamp(3rem, 8vw, 6rem) 0;
101 background-color: var(--bg);
102 }
103
104 /* The splash screen, as the page opens on. ommp scatters music notes across a
105 dark field while it loads (about_modal.rs, render_splash_screen); the tile is
106 that same scatter, generated from the same hash by tools/note-field.py. */
107 .hero::before {
108 content: "";
109 position: absolute; inset: 0; z-index: 0;
110 pointer-events: none;
111 background:
112 radial-gradient(60rem 32rem at 18% -8%, #3C145066, transparent 70%),
113 radial-gradient(52rem 30rem at 88% 4%, #143C5066, transparent 70%),
114 url("../img/note-field.png") repeat;
115 background-size: auto, auto, 370px 740px;
116 -webkit-mask-image: linear-gradient(#000 60%, #0000 100%);
117 mask-image: linear-gradient(#000 60%, #0000 100%);
118 }
119 .hero-in { position: relative; z-index: 1; }
120
121 .hero-in { text-align: center; }
122
123 .hero-logo {
124 width: min(100%, 26rem); height: auto; display: block; margin: 0 auto 2.5rem;
125 animation: rise .6s ease-out both;
126 }
127 @keyframes rise { from { opacity: 0; transform: translateY(.5rem); } to { opacity: 1; transform: none; } }
128
129 .kicker {
130 margin: 0 0 1rem;
131 font-size: .8125rem; letter-spacing: .22em; text-transform: uppercase;
132 color: var(--ink-2);
133 }
134
135 .display {
136 font-family: var(--disp);
137 font-weight: 700;
138 font-size: clamp(2.4rem, 1.4rem + 4.4vw, 4.6rem);
139 line-height: 1.02;
140 letter-spacing: -.03em;
141 margin: 0 0 1.5rem;
142 }
143 .display em {
144 font-style: normal;
145 background: linear-gradient(100deg, var(--pink), var(--cyan));
146 -webkit-background-clip: text; background-clip: text; color: transparent;
147 }
148
149 .lede {
150 max-width: 46rem; margin: 0 auto 2.5rem;
151 color: var(--ink-2); font-size: 1.0625rem;
152 }
153
154 .installs { display: flex; flex-wrap: wrap; gap: 1rem 1.5rem; justify-content: center; }
155 .installs-left { justify-content: flex-start; margin-bottom: 2.5rem; }
156 .install { text-align: left; }
157 .install-label {
158 display: block; font-size: .75rem; letter-spacing: .14em; text-transform: uppercase;
159 color: var(--ink-2); margin-bottom: .4rem;
160 }
161
162 /* One box, holding the command and the button that copies it. The border and
163 the background belong to the box, not to either thing inside it, and the
164 feedback is the icon changing, so nothing resizes when you copy. */
165 .cmdline {
166 display: flex; align-items: center; gap: .5rem;
167 width: max-content; max-width: 100%;
168 background: var(--panel); border: 1px solid var(--line); border-radius: 4px;
169 padding: .35rem .4rem .35rem .9rem;
170 transition: border-color .15s;
171 }
172 .cmdline:focus-within { border-color: var(--sky); }
173 .cmd {
174 /* min-width:auto would refuse to shrink below the command's full width and
175 push the box past a narrow viewport. */
176 flex: 1 1 auto; min-width: 0; overflow-x: auto;
177 font-family: var(--mono); font-size: .9375rem;
178 color: var(--ink); white-space: nowrap;
179 scrollbar-width: thin; scrollbar-color: var(--line) transparent;
180 }
181 /* The prompt lives in a pseudo-element, which never reaches the clipboard. */
182 .cmd::before { content: "$ "; color: var(--dim); }
183
184 .cmd-copy {
185 flex: none; display: grid; place-items: center;
186 width: 1.9rem; height: 1.9rem; padding: 0;
187 background: none; border: 0; border-radius: 3px;
188 color: var(--dim); cursor: pointer;
189 transition: color .15s, background .15s;
190 }
191 .cmd-copy:hover { color: var(--ink); background: #ffffff12; }
192 .cmd-copy svg { width: 15px; height: 15px; }
193 .cmd-copy svg:last-child { display: none; }
194 .cmd-copy.is-copied { color: var(--playing); }
195 .cmd-copy.is-copied svg:first-child { display: none; }
196 .cmd-copy.is-copied svg:last-child { display: block; }
197
198 .hero-meta { margin-top: 2rem; color: var(--ink-2); font-size: .875rem; }
199
200 /* ── Screenshots ──────────────────────────────────────────────────── */
201
202 .shot { margin: 0; }
203 .shot img {
204 display: block; width: 100%; height: auto;
205 border: 1px solid var(--line); border-radius: 6px;
206 }
207 .shot-hero {
208 /* The one element allowed past the wide measure: it is the product, and it is
209 the first thing you scroll into. */
210 width: min(100% - 2.5rem, 92%, 84rem);
211 margin: clamp(2.5rem, 5vw, 3.5rem) auto clamp(2.5rem, 5vw, 3.5rem);
212 }
213 .shot-hero img { box-shadow: 0 2rem 5rem -1.5rem #00000099; }
214 .shot-wide { margin-top: clamp(2.5rem, 5vw, 3.5rem); }
215
216 /* ── Bands ────────────────────────────────────────────────────────── */
217
218 .band { padding-block: clamp(4rem, 9vw, 7rem); }
219 .band-alt { background: #0D0725; border-block: 1px solid var(--line); }
220 .band-close { padding-bottom: clamp(3rem, 6vw, 4.5rem); }
221
222 .sec-title {
223 font-family: var(--disp); font-weight: 600;
224 font-size: clamp(1.7rem, 1.2rem + 1.9vw, 2.6rem);
225 line-height: 1.1; letter-spacing: -.02em; margin: 0 0 1rem;
226 }
227 .sec-lede { max-width: 44rem; color: var(--ink-2); margin: 0 0 2.5rem; }
228 .sec-body { max-width: 44rem; color: var(--ink-2); margin: 0 0 1.1rem; font-size: .9375rem; }
229 .sec-body:last-child { margin-bottom: 0; }
230
231 /* The section that opens the page carries the weight the hero used to. */
232 .band-art { padding-top: clamp(2.5rem, 6vw, 4.5rem); }
233 .sec-title-lg { font-size: clamp(2rem, 1.3rem + 2.6vw, 3.2rem); }
234 .sec-lede-lg { font-size: 1.0625rem; margin-bottom: 0; }
235 /* A mono heading, because the heading is keys. */
236 .sec-title-mono { font-size: clamp(1.5rem, 1.1rem + 1.5vw, 2.2rem); letter-spacing: 0; }
237
238 /* Two columns of prose under a full-width image, so a long explanation does not
239 become one very long line. */
240 .two-up {
241 display: grid; gap: clamp(1.5rem, 4vw, 3rem);
242 color: var(--ink-2); font-size: .9375rem;
243 }
244 .two-up p { margin: 0; }
245 @media (min-width: 52rem) { .two-up { grid-template-columns: 1fr 1fr; } }
246
247 /* ── Split sections ───────────────────────────────────────────────── */
248
249 /* Copy in a fixed column, screenshot in the rest. The hero shot is the one that
250 earns full width; from here down the screenshots illustrate a point the copy
251 is already making, and blowing them up to fill the page only pushes the
252 reader further from it. */
253 .split { display: grid; gap: clamp(2rem, 5vw, 3.5rem); align-items: center; }
254 @media (min-width: 56rem) {
255 .split { grid-template-columns: minmax(0, 26rem) 1fr; }
256 /* Reordering alone leaves the figure in the narrow track and shrinks it to
257 half the size of the one above. Mirror the tracks so both screenshots come
258 out the same size, and only the side they sit on changes. */
259 .split-reverse { grid-template-columns: 1fr minmax(0, 26rem); }
260 .split-reverse .split-text { order: 2; }
261 }
262
263 /* ── The setup you skip ───────────────────────────────────────────── */
264
265 /* Four things you do not do, struck out, and the one line you do type. The
266 argument is made by the ratio, so nothing here is decorated further. */
267 .setup { font-size: .9375rem; }
268 .setup-skipped { list-style: none; margin: 0 0 1.5rem; padding: 0; display: grid; gap: .7rem; }
269 .setup-skipped li { color: var(--dim); }
270 /* Drawn on the span rather than the list item: an inline box is the height of
271 the text itself, so 50% of it lands on the middle of the letters instead of
272 the middle of the line box, which sits higher. */
273 .setup-skipped span { position: relative; }
274 .setup-skipped span::after {
275 content: ""; position: absolute; inset-inline: 0; top: 50%;
276 height: 1px; background: var(--pink);
277 transform-origin: left;
278 }
279
280 /* Once the script is running the lines arrive unstruck and are crossed out one
281 at a time as the section comes into view. Without it they are simply already
282 crossed out, which is the state that makes the point. */
283 .setup-armed .setup-skipped li { color: var(--ink-2); transition: color .4s ease; }
284 .setup-armed .setup-skipped span::after { transform: scaleX(0); transition: transform .45s ease; }
285 .setup-armed .setup-skipped li.is-struck { color: var(--dim); }
286 .setup-armed .setup-skipped li.is-struck span::after { transform: scaleX(1); }
287 @media (prefers-reduced-motion: reduce) {
288 .setup-armed .setup-skipped span::after { transition: none; }
289 }
290 .setup-real {
291 margin: 0; font-weight: 700; font-size: 1.5rem;
292 background: linear-gradient(100deg, var(--pink), var(--cyan));
293 -webkit-background-clip: text; background-clip: text; color: transparent;
294 }
295 .setup-real::before { content: "$ "; -webkit-text-fill-color: var(--dim); }
296
297 /* ── The tab strip, set as type ────────────────────────────────────── */
298
299 /* ommp's own tab bar. Same names, same order, same seven accents it draws them
300 in — the separators included, because that is how the app renders them. */
301 .tabstrip {
302 list-style: none; margin: 0 0 2.5rem; padding: 0;
303 display: flex; flex-wrap: wrap; align-items: baseline;
304 font-size: clamp(.875rem, .75rem + .4vw, 1.0625rem); font-weight: 700;
305 }
306 .tabstrip li { color: var(--accent); }
307 .tabstrip li + li::before {
308 content: "│"; color: var(--line); font-weight: 400;
309 margin-inline: .75em; -webkit-text-fill-color: var(--line);
310 }
311
312 /* ── The small decisions ──────────────────────────────────────────── */
313
314 .details {
315 list-style: none; margin: 0 0 2.5rem; padding: 0;
316 display: grid; gap: 1.75rem;
317 }
318 @media (min-width: 46rem) { .details { grid-template-columns: 1fr 1fr; gap: 2rem 3rem; } }
319 .details li {
320 border-top: 2px solid var(--accent); padding-top: 1rem;
321 color: var(--ink-2); font-size: .9375rem;
322 }
323 .details li b { color: var(--ink); font-weight: 700; }
324 .details li i { font-style: italic; color: var(--ink); }
325 .details-close { max-width: 44rem; margin: 0; color: var(--ink-2); font-size: .9375rem; }
326
327 kbd {
328 font-family: var(--mono); font-size: .8125rem; font-weight: 700;
329 background: var(--panel); border: 1px solid var(--line); border-bottom-width: 2px;
330 border-radius: 4px; padding: .2rem .5rem; color: var(--sky);
331 display: inline-block; text-align: center;
332 }
333
334 /* ── Install ──────────────────────────────────────────────────────── */
335
336 .install-close { margin: 0; font-size: 1.0625rem; }
337
338 /* ── Footer ───────────────────────────────────────────────────────── */
339
340 .foot { border-top: 1px solid var(--line); padding-block: 2.5rem; }
341 .foot-in { display: grid; gap: 1rem; }
342 .foot-name {
343 margin: 0; font-weight: 700; letter-spacing: .1em;
344 background: linear-gradient(100deg, var(--pink), var(--cyan));
345 -webkit-background-clip: text; background-clip: text; color: transparent;
346 width: max-content;
347 }
348 .foot-links { display: flex; flex-wrap: wrap; gap: 1.25rem; font-size: .875rem; }
349 .foot-fine { margin: 0; color: var(--ink-2); font-size: .8125rem; }
350
351 /* ── The signature: ommp's status bar, tracking the page ──────────── */
352
353 .statusbar {
354 position: fixed; inset: auto 0 0 0; z-index: 50;
355 height: var(--sb-h);
356 background: #0A051EF2;
357 backdrop-filter: blur(8px);
358 border-top: 1px solid var(--line);
359 font-size: .8125rem;
360 }
361 .sb-fill {
362 height: 2px; width: 0%;
363 background: var(--violet);
364 transition: width .1s linear;
365 }
366 .sb-row {
367 height: calc(var(--sb-h) - 2px);
368 display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
369 gap: 1rem;
370 width: min(100% - 2.5rem, var(--wrap)); margin-inline: auto;
371 }
372 .sb-state { color: var(--playing); font-weight: 700; white-space: nowrap; }
373 .sb-track {
374 color: var(--sky); text-align: center;
375 overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
376 }
377 .sb-time { color: var(--ink-2); text-align: right; white-space: nowrap; }
378
379 @media (max-width: 34rem) {
380 .sb-row { grid-template-columns: auto 1fr; }
381 .sb-state { display: none; }
382 .sb-time { font-size: .75rem; }
383 }