Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/css/crystal.css

13.3 KiB, 1 run

created by r2519314175:1095, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1/* crystal.css — the crystal face: the page's frame, the built-in view that
2 * stands in when the page fails, the ask row beneath it, and the form the `✎`
3 * button opens.
4 *
5 * Every colour comes from the palette variables, never from a literal, so all
6 * eleven themes and both skins are covered without a rule per theme. There is
7 * one deliberate exception: the frame's own ground, which must match the panel
8 * behind it while the page is still loading, and does so by naming the same
9 * variable the panel does rather than a colour of its own.
10 *
11 * THE FRAME IS NOT STYLED FROM HERE. The page inside it is in an opaque origin
12 * and cannot see this file, or any file; it is handed resolved colours over the
13 * channel instead (`_theme` in crystal.js). So the rules below only give the
14 * `iframe` its box — no border, no ground of its own past the first paint, and a
15 * height that JavaScript drives from what the page says it needs. A page that
16 * never reports one keeps the height set here and scrolls inside itself.
17 *
18 * The built-in view is a different matter: it renders in the app's own document
19 * and is styled like any other panel content.
20 *
21 * At 380px everything here is one column. The facts list drops its second
22 * column, the fallback note stacks its button under its sentence, and nothing
23 * has a fixed width to overflow with.
24 */
25
26/* ── The frame ───────────────────────────────────────────────── */
27
28/* A page shorter than the panel used to leave the panel's OWN ground showing
29 below it -- the frame was sized to exactly what `measure()` reported and
30 nothing made up the difference, so a crystal whose author asked for a
31 colour of its own got that colour for a hundred pixels and the ordinary
32 panel behind it for the rest. `#crystal-body` scrolls the whole crystal
33 FACE (this frame, but also the history list, the edit form, the tag
34 editor -- everything `renderCrystal` and its neighbours draw into it), so
35 the switch to flex is scoped with `:has()` to the one child that asks for
36 it; none of those other views gain or lose anything. */
37.crystal-body:has(#crystal-frame-wrap) {
38 display: flex;
39 flex-direction: column;
40}
41
42#crystal-frame-wrap {
43 /* Flex, not grid: still one box, still no inline-element descender gap,
44 and now able to say "take whatever height crystal-body has left". */
45 display: flex;
46 flex-direction: column;
47 flex: 1 0 auto;
48 /* A flex item's default min-height is its content's own minimum, which
49 would refuse to shrink the wrap below the iframe's intrinsic size and
50 defeat the fill below. */
51 min-height: 0;
52 width: 100%;
53 min-width: 0;
54 margin: 0 0 4px;
55}
56
57.crystal-frame {
58 display: block;
59 width: 100%;
60 /* The FLOOR is the page's own measured content, carried in on `min-height`
61 by crystal.js rather than `height` -- so it can never win against the
62 rule below, only raise it. The CEILING -- filling whatever `flex: 1`
63 above handed the wrap -- is `height: 100%`, which a `min-height` never
64 overrides because the two are different properties: the browser takes
65 the larger of the two candidates. A page taller than the panel still
66 grows past 100% from its own min-height and `crystal-body` scrolls it,
67 same as before; a page shorter than the panel now reaches the floor of
68 the panel instead of stopping short of it. */
69 height: 100%;
70 min-height: 320px; /* before crystal.js has heard from the page at all */
71 border: 0;
72 /* The page paints itself transparent until its theme arrives, so this is the
73 colour of the first frame or two. Naming the panel's own variable is what
74 stops that showing as a white flash on a dark palette. */
75 background: var(--bg-secondary);
76 color-scheme: normal;
77}
78
79/* The ask row that used to live here is gone. It was a second text box under
80 the frame doing exactly what the composer at the foot of the panel already
81 does, so the daimon had to guess which of the crystal's two files a request
82 was about. `✎ Page` in the bar above fills the one composer instead. */
83
84/* ── Falling back, visibly ───────────────────────────────────────
85 A note that says WHICH failure happened and a button that puts the standard
86 page back. Silent degradation is how a broken page stays broken for a month,
87 so this is deliberately not quiet: it takes the warning ground, not the
88 muted one. */
89
90.crystal-fallback {
91 display: block;
92 width: 100%;
93 min-width: 0;
94}
95
96.crystal-fallback-note {
97 display: flex;
98 flex-wrap: wrap;
99 align-items: center;
100 gap: 8px;
101 margin: 0 0 12px;
102 padding: 8px 10px;
103 background: var(--warn-bg);
104 border: 1px solid var(--warn);
105 border-radius: var(--radius-sm);
106 color: var(--text-primary);
107 font-size: var(--fs-sm);
108 line-height: 1.45;
109}
110
111.crystal-fallback-why {
112 flex: 1 1 220px;
113 min-width: 0;
114}
115
116.crystal-reset {
117 flex: 0 0 auto;
118 font-size: var(--fs-xs);
119 padding: 4px 10px;
120 background: var(--bg-tertiary);
121 color: var(--text-secondary);
122 border: 1px solid var(--border-strong);
123 border-radius: var(--radius-sm);
124 cursor: pointer;
125}
126
127.crystal-reset:hover {
128 background: var(--bg-hover);
129 color: var(--text-primary);
130}
131
132.crystal-reset:focus-visible {
133 outline: 2px solid var(--accent);
134 outline-offset: 1px;
135}
136
137/* ── The built-in view ───────────────────────────────────────────
138 The core schema rendered properly, and every key nobody recognised rendered
139 generically below it. A key that vanishes because nothing knew its name is
140 the defect this whole design is shaped around, so the second half is not a
141 debug affordance -- it is the point. */
142
143.crystal-fb-body {
144 min-width: 0;
145}
146
147.crystal-fb-title {
148 margin: 0 0 10px;
149 font-size: var(--fs-3xl);
150 line-height: 1.25;
151 font-weight: 650;
152 color: var(--text-primary);
153}
154
155.crystal-fb-summary {
156 color: var(--text-primary);
157}
158
159.crystal-fb-sec {
160 margin: 18px 0 0;
161}
162
163.crystal-fb-sec > h3 {
164 margin: 0 0 6px;
165 font-size: var(--fs-lg);
166 line-height: 1.35;
167 font-weight: 650;
168 color: var(--text-primary);
169}
170
171.crystal-fb-field-head {
172 margin: 20px 0 6px;
173 font-size: var(--fs-lg);
174 line-height: 1.35;
175 font-weight: 650;
176 color: var(--text-primary);
177}
178
179.crystal-fb-md > :first-child { margin-top: 0; }
180.crystal-fb-md > :last-child { margin-bottom: 0; }
181
182/* Facts: a two-column reading list, not a table. The key column sizes to its
183 longest word and the value column takes the rest, which is what keeps a
184 forty-character value from squeezing the keys into one letter per line. */
185.crystal-fb-facts {
186 display: grid;
187 grid-template-columns: auto minmax(0, 1fr);
188 gap: 4px 16px;
189 margin: 0;
190}
191
192.crystal-fb-facts > dt {
193 color: var(--text-muted);
194 font-size: var(--fs-sm);
195 min-width: 0;
196}
197
198.crystal-fb-facts > dd {
199 margin: 0;
200 min-width: 0;
201 color: var(--text-primary);
202}
203
204.crystal-fb-open,
205.crystal-fb-links {
206 margin: 0;
207 padding-left: 20px;
208}
209
210.crystal-fb-open > li,
211.crystal-fb-links > li {
212 margin: 2px 0;
213}
214
215.crystal-fb-link {
216 color: var(--accent-text);
217 text-decoration: underline;
218 text-underline-offset: 2px;
219 cursor: pointer;
220 word-break: break-word;
221}
222
223/* Brightened to the full text colour rather than to `--accent`, which is the
224 fill token and reads a rung darker than `--accent-text` on every palette --
225 hovering a link must not dim it. */
226.crystal-fb-link:hover {
227 color: var(--text-primary);
228}
229
230/* Everything nobody recognised. Boxed rather than run in with the rest, because
231 a reader has to be able to tell what the schema knows about from what it is
232 merely carrying. */
233.crystal-fb-extra-note {
234 margin: 0 0 10px;
235 color: var(--text-muted);
236 font-size: var(--fs-sm);
237 line-height: 1.45;
238}
239
240.crystal-fb-field {
241 margin: 0 0 8px;
242 padding: 9px 11px;
243 background: var(--bg-tertiary);
244 border: 1px solid var(--border);
245 border-radius: var(--radius-sm);
246 min-width: 0;
247}
248
249.crystal-fb-key {
250 margin: 0 0 5px;
251 color: var(--text-muted);
252 font-family: var(--font-mono);
253 font-size: var(--fs-xs);
254 word-break: break-word;
255}
256
257.crystal-fb-field > :last-child { margin-bottom: 0; }
258
259.crystal-fb-scalar {
260 color: var(--text-primary);
261}
262
263.crystal-fb-list {
264 margin: 0;
265 padding-left: 20px;
266}
267
268.crystal-fb-map {
269 display: grid;
270 grid-template-columns: auto minmax(0, 1fr);
271 gap: 3px 12px;
272 margin: 0;
273}
274
275.crystal-fb-map > dt {
276 color: var(--text-muted);
277 font-family: var(--font-mono);
278 font-size: var(--fs-xs);
279 min-width: 0;
280 word-break: break-word;
281}
282
283.crystal-fb-map > dd {
284 margin: 0;
285 min-width: 0;
286}
287
288/* A value too deep to draw goes out as JSON rather than being flattened:
289 unreadable is recoverable, absent is not. */
290.crystal-fb-json {
291 margin: 0;
292 padding: 8px 10px;
293 background: var(--bg-secondary);
294 border: 1px solid var(--border);
295 border-radius: var(--radius-sm);
296 font-family: var(--font-mono);
297 font-size: var(--fs-xs);
298 line-height: 1.5;
299 color: var(--text-secondary);
300 overflow-x: auto;
301 white-space: pre;
302}
303
304/* ── The `✎` form ────────────────────────────────────────────────
305 Built from the core keys, with the raw JSON behind a second click. The form
306 must round-trip unknown keys untouched, so they are shown read-only under
307 `.crystal-form-extra` and written back exactly as they came in -- a form that
308 drops what it cannot render is the same defect in a different costume. */
309
310.crystal-form {
311 display: flex;
312 flex-direction: column;
313 gap: 12px;
314 min-width: 0;
315}
316
317.crystal-form-row {
318 display: flex;
319 flex-direction: column;
320 gap: 4px;
321 min-width: 0;
322}
323
324.crystal-form-row > label {
325 color: var(--text-secondary);
326 font-size: var(--fs-xs);
327}
328
329.crystal-form-row input[type="text"],
330.crystal-form-row textarea {
331 width: 100%;
332 min-width: 0;
333 background: var(--bg-tertiary);
334 border: 1px solid var(--border-strong);
335 border-radius: var(--radius-sm);
336 color: var(--text-primary);
337 font-family: inherit;
338 font-size: var(--fs-sm);
339 line-height: 1.55;
340 padding: 7px 9px;
341 resize: vertical;
342}
343
344.crystal-form-row textarea {
345 min-height: 90px;
346}
347
348.crystal-form-row input[type="text"]:focus,
349.crystal-form-row textarea:focus {
350 outline: 2px solid var(--accent);
351 outline-offset: 1px;
352 border-color: var(--accent);
353}
354
355/* A section of the form: heading and body together, with its own remove button.
356 Bordered so two of them do not read as one long field. */
357.crystal-form-row.crystal-form-group {
358 padding: 9px 11px;
359 border: 1px solid var(--border);
360 border-radius: var(--radius-sm);
361 background: var(--bg-tertiary);
362}
363
364.crystal-form-extra {
365 padding: 9px 11px;
366 border: 1px solid var(--border);
367 border-radius: var(--radius-sm);
368 background: var(--bg-tertiary);
369 color: var(--text-secondary);
370 font-size: var(--fs-sm);
371 min-width: 0;
372}
373
374.crystal-form-extra > h4 {
375 margin: 0 0 4px;
376 font-size: var(--fs-sm);
377 font-weight: 650;
378 color: var(--text-primary);
379}
380
381.crystal-form-extra > p {
382 margin: 0 0 8px;
383 color: var(--text-muted);
384 font-size: var(--fs-xs);
385 line-height: 1.45;
386}
387
388.crystal-form-extra pre {
389 margin: 0;
390 font-family: var(--font-mono);
391 font-size: var(--fs-xs);
392 line-height: 1.5;
393 color: var(--text-secondary);
394 overflow-x: auto;
395 white-space: pre;
396}
397
398/* The raw JSON, behind a second click. It refuses to save unparseable text --
399 a textarea written straight through is harmless for markdown, which has no
400 parse failure, and a data-loss path for JSON. */
401.crystal-json {
402 width: 100%;
403 min-height: 320px;
404 background: var(--bg-tertiary);
405 border: 1px solid var(--border-strong);
406 border-radius: var(--radius-sm);
407 color: var(--text-primary);
408 font-family: var(--font-mono);
409 font-size: var(--fs-sm);
410 line-height: 1.6;
411 padding: 12px;
412 resize: vertical;
413}
414
415.crystal-json:focus {
416 outline: 2px solid var(--accent);
417 outline-offset: 1px;
418 border-color: var(--accent);
419}
420
421.crystal-json.invalid {
422 border-color: var(--danger);
423}
424
425/* ── Narrow ──────────────────────────────────────────────────────
426 The AI panel is the whole width on a phone, so the crystal is the narrowest
427 thing here at 380px. Both two-column grids give up their second column
428 rather than wrapping a key one letter at a time. */
429
430@media (max-width: 420px) {
431 .crystal-fb-facts,
432 .crystal-fb-map {
433 grid-template-columns: minmax(0, 1fr);
434 gap: 0;
435 }
436
437 .crystal-fb-facts > dt,
438 .crystal-fb-map > dt {
439 margin-top: 8px;
440 }
441
442 .crystal-fb-facts > dt:first-child,
443 .crystal-fb-map > dt:first-child {
444 margin-top: 0;
445 }
446
447 .crystal-fb-title {
448 font-size: var(--fs-2xl);
449 }
450
451 .crystal-fallback-note {
452 align-items: stretch;
453 }
454
455 .crystal-reset {
456 width: 100%;
457 }
458}
459
460/* ── The warm skin ───────────────────────────────────────────────
461 The shape tokens come from variables.css and are already spent above. What
462 the skin also asks for is less hard ruling: cards are lifted rather than
463 outlined, and the dense monospace furniture speaks in the friendly face. */
464
465:root[data-skin="warm"] .crystal-fb-key,
466:root[data-skin="warm"] .crystal-fb-map > dt {
467 font-family: var(--font);
468 letter-spacing: 0;
469}
470
471:root[data-skin="warm"] .crystal-fb-field,
472:root[data-skin="warm"] .crystal-form-extra,
473:root[data-skin="warm"] .crystal-form-row.crystal-form-group {
474 border-color: transparent;
475 box-shadow: var(--shadow-1);
476}