@half-built/css 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/package.json +18 -0
- package/src/base/link-tip.css +35 -0
- package/src/base/reset.css +61 -0
- package/src/base/scroll-top.css +35 -0
- package/src/base/shell.css +31 -0
- package/src/code.css +51 -0
- package/src/global.css +10 -0
- package/src/layers.css +10 -0
- package/src/lightbox.css +170 -0
- package/src/path-player.css +138 -0
- package/src/patterns.css +270 -0
- package/src/plate-modal.css +109 -0
- package/src/prose.css +89 -0
- package/src/tokens/breakpoints.css +12 -0
- package/src/tokens/primitives.css +102 -0
- package/src/tokens/theme-dark.css +60 -0
- package/src/tokens/theme-light.css +74 -0
- package/src/tokens.css +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Curt Henrichs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @half-built/css
|
|
2
|
+
|
|
3
|
+
Design tokens, base styles, patterns, and prose CSS for the half-built
|
|
4
|
+
design system.
|
|
5
|
+
|
|
6
|
+
## Provenance
|
|
7
|
+
|
|
8
|
+
Extracted from the private `half-built-robots-blog` repository, where
|
|
9
|
+
this CSS was built and used in production. The extraction review that
|
|
10
|
+
checked it for blog-specific assumptions before the move is the design
|
|
11
|
+
record for this package.
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@half-built/css",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Design tokens, base, patterns, and prose CSS for the half-built design system.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": ["src"],
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@fontsource/roboto-mono": "^5.3.0"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/global.css",
|
|
13
|
+
"./layers": "./src/layers.css",
|
|
14
|
+
"./tokens": "./src/tokens.css",
|
|
15
|
+
"./*": "./src/*"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": { "access": "public" }
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Layer: base. The link tooltip: one island-owned singleton element
|
|
2
|
+
(scripts/link-tip.ts) revealing an external link's href or an
|
|
3
|
+
internal link's build-time data-tooltip, in the boxed grammar the
|
|
4
|
+
live theme used (same visual language as the search flyout). JS
|
|
5
|
+
measures and places it position: fixed; this file carries only the
|
|
6
|
+
look. pointer-events: none so the box never steals the hover that
|
|
7
|
+
opened it. */
|
|
8
|
+
@layer base {
|
|
9
|
+
.link-tip {
|
|
10
|
+
position: fixed;
|
|
11
|
+
background: var(--surface);
|
|
12
|
+
border: var(--stroke) solid;
|
|
13
|
+
color: var(--ink);
|
|
14
|
+
padding: 5px;
|
|
15
|
+
font-size: 0.85rem;
|
|
16
|
+
line-height: 1.3;
|
|
17
|
+
/* A URL is one unbroken word; capped and wrapped it folds into a
|
|
18
|
+
few short lines instead of a spear off the viewport edge. UI
|
|
19
|
+
label, not prose: the no-cap rule does not apply. */
|
|
20
|
+
max-width: calc(100vw - 40px);
|
|
21
|
+
width: max-content;
|
|
22
|
+
overflow-wrap: anywhere;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
opacity: 0;
|
|
26
|
+
transition: var(--transition);
|
|
27
|
+
transition-property: opacity;
|
|
28
|
+
z-index: var(--z-tooltip);
|
|
29
|
+
}
|
|
30
|
+
.link-tip.is-open { opacity: 1; }
|
|
31
|
+
/* Backstop only: the island already mounts inert under (hover: none). */
|
|
32
|
+
@media (hover: none) {
|
|
33
|
+
.link-tip { display: none; }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* Layer: base. Element defaults and accessibility utilities: the part of
|
|
2
|
+
the base layer any site keeps. Page containers are in shell.css.
|
|
3
|
+
See docs/css-architecture.md. */
|
|
4
|
+
@layer base {
|
|
5
|
+
* { box-sizing: border-box; }
|
|
6
|
+
|
|
7
|
+
html { scroll-behavior: smooth; }
|
|
8
|
+
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
color: var(--ink);
|
|
12
|
+
background: var(--surface);
|
|
13
|
+
font-size: 1rem;
|
|
14
|
+
line-height: 1.5;
|
|
15
|
+
font-family: var(--font-body);
|
|
16
|
+
|
|
17
|
+
/* The layout stops trying to shrink below the narrowest mainstream
|
|
18
|
+
phone width; under it the viewport scrolls horizontally as one
|
|
19
|
+
intact unit instead of clipping card heroes and text (owner call
|
|
20
|
+
2026-08-20, found squeezing devtools toward 222px). An overridable
|
|
21
|
+
house default, not a system constant (step 11.1). */
|
|
22
|
+
min-width: 360px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
a { color: var(--ink); }
|
|
26
|
+
|
|
27
|
+
/* Keyboard focus, one answer for the whole site (owner call
|
|
28
|
+
2026-08-26): a 2px amber ring held 2px off the element, on
|
|
29
|
+
:focus-visible only, so a mouse click never leaves a ring behind and
|
|
30
|
+
focus never looks like hover. Components keep their own hover looks;
|
|
31
|
+
the only local focus rules left are the joined field's inset rings,
|
|
32
|
+
which keep its fused box intact, and two offsets where a container's
|
|
33
|
+
overflow would clip an outside ring. */
|
|
34
|
+
:focus-visible {
|
|
35
|
+
outline: 2px solid var(--focus-ring);
|
|
36
|
+
outline-offset: 2px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.skip-link {
|
|
40
|
+
position: absolute;
|
|
41
|
+
left: -9999px;
|
|
42
|
+
top: 0;
|
|
43
|
+
background: var(--surface);
|
|
44
|
+
border: var(--stroke) solid var(--rule);
|
|
45
|
+
padding: 8px 14px;
|
|
46
|
+
z-index: var(--z-skip-link);
|
|
47
|
+
}
|
|
48
|
+
.skip-link:focus { left: 10px; }
|
|
49
|
+
|
|
50
|
+
.screen-reader-text {
|
|
51
|
+
position: absolute;
|
|
52
|
+
left: -9999px;
|
|
53
|
+
width: 1px;
|
|
54
|
+
height: 1px;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@media (prefers-reduced-motion: reduce) {
|
|
59
|
+
* { animation: none !important; transition: none !important; }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Layer: base. Floating scroll-to-top, matching the live button: fixed
|
|
2
|
+
bottom-right, 40x40, hidden until the header scrolls out of the
|
|
3
|
+
viewport. The JS half (the .show toggle) lives in Base.astro. */
|
|
4
|
+
@layer base {
|
|
5
|
+
a.scroll-top {
|
|
6
|
+
position: fixed;
|
|
7
|
+
bottom: 53px;
|
|
8
|
+
right: 30px;
|
|
9
|
+
height: 40px;
|
|
10
|
+
width: 40px;
|
|
11
|
+
opacity: 0;
|
|
12
|
+
visibility: hidden;
|
|
13
|
+
z-index: var(--z-scroll-top);
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
text-decoration: none;
|
|
18
|
+
color: var(--ink);
|
|
19
|
+
background-color: var(--surface);
|
|
20
|
+
border: 1px solid var(--rule);
|
|
21
|
+
transition: var(--transition);
|
|
22
|
+
}
|
|
23
|
+
a.scroll-top:hover { outline: var(--stroke) solid var(--rule); }
|
|
24
|
+
a.scroll-top:hover,
|
|
25
|
+
a.scroll-top:focus-visible { transform: translateY(-10px); }
|
|
26
|
+
a.scroll-top.show {
|
|
27
|
+
opacity: 1;
|
|
28
|
+
visibility: visible;
|
|
29
|
+
}
|
|
30
|
+
/* Phones flick-scroll and tap the status bar; the pinned button only
|
|
31
|
+
ate column pixels there (owner call 2026-08-20). */
|
|
32
|
+
@media (--bp-phone) {
|
|
33
|
+
a.scroll-top { display: none; }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* Layer: base. The page shell (handoff section 5.3): sticky-footer frame,
|
|
2
|
+
the 1380px wrapper, the two-column row, the content column, and the
|
|
3
|
+
sidebar width with its collapse. The grid is defined here and nowhere
|
|
4
|
+
else. See docs/css-architecture.md. */
|
|
5
|
+
@layer base {
|
|
6
|
+
/* Sticky footer: the page shell fills the viewport and the content region
|
|
7
|
+
absorbs the slack, so short pages (404, search, thin archives) keep the
|
|
8
|
+
footer at the bottom edge. dvh tracks mobile browser chrome; the vh
|
|
9
|
+
line is the fallback for engines without it. */
|
|
10
|
+
.site {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
min-height: 100dvh;
|
|
15
|
+
}
|
|
16
|
+
.primary-site-content { flex: 1 0 auto; }
|
|
17
|
+
.shell { max-width: var(--shell-max); margin-inline: auto; padding-inline: var(--shell-pad); }
|
|
18
|
+
.two-column { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--column-gap); }
|
|
19
|
+
/* The content column beside the sidebar: takes the remaining width and
|
|
20
|
+
may shrink below its content's intrinsic width (long code lines). */
|
|
21
|
+
.site-main { flex: 1; min-width: 0; }
|
|
22
|
+
|
|
23
|
+
/* Content sits below the header: live theme used 50px; bumped by owner preference. */
|
|
24
|
+
.site-content { margin-top: var(--header-clear); }
|
|
25
|
+
|
|
26
|
+
/* Sidebar: not sticky (owner ruling 2026-07-26), it sits in normal flow
|
|
27
|
+
and scrolls away with the page like the main column does. Width is
|
|
28
|
+
30% less half the column gap; below 991px it stacks full width. */
|
|
29
|
+
.sidebar { width: calc(30% - var(--column-gap) / 2); flex-shrink: 0; }
|
|
30
|
+
@media (--bp-sidebar) { .sidebar { width: 100%; position: static; } }
|
|
31
|
+
}
|
package/src/code.css
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* Layer: components (code island stylesheet).
|
|
2
|
+
See docs/css-architecture.md. */
|
|
3
|
+
@layer components {
|
|
4
|
+
/* Terminal island (spec section 5): the one deliberate dark surface on the
|
|
5
|
+
light page, in the parent theme's own palette. */
|
|
6
|
+
.prose pre.astro-code {
|
|
7
|
+
border: var(--stroke) solid var(--rule);
|
|
8
|
+
padding: 12px 14px;
|
|
9
|
+
margin: 20px 0;
|
|
10
|
+
overflow-x: auto;
|
|
11
|
+
font-size: var(--font-size-sm);
|
|
12
|
+
line-height: 1.7;
|
|
13
|
+
}
|
|
14
|
+
/* Inline code in running text (spec 2026-07-28, toned down same day by
|
|
15
|
+
owner request: the dark chip read too loud in prose). Muted gray ground
|
|
16
|
+
with a hairline border; the body font is already mono, so the box alone
|
|
17
|
+
marks backticked names. Excludes code inside pre, which the island owns. */
|
|
18
|
+
.prose :not(pre) > code {
|
|
19
|
+
background: var(--band);
|
|
20
|
+
border: 1px solid var(--hairline);
|
|
21
|
+
padding: 1px 5px;
|
|
22
|
+
font-size: 0.9em;
|
|
23
|
+
}
|
|
24
|
+
.code-island-bar {
|
|
25
|
+
display: flex;
|
|
26
|
+
justify-content: space-between;
|
|
27
|
+
align-items: center;
|
|
28
|
+
border: var(--stroke) solid var(--rule);
|
|
29
|
+
border-bottom: 1px solid var(--code-line);
|
|
30
|
+
background: var(--code-bg);
|
|
31
|
+
color: var(--accent-1);
|
|
32
|
+
font-size: var(--font-size-xs);
|
|
33
|
+
letter-spacing: 0.08em;
|
|
34
|
+
text-transform: uppercase;
|
|
35
|
+
padding: 6px 10px;
|
|
36
|
+
margin: 20px 0 0;
|
|
37
|
+
}
|
|
38
|
+
.code-island-bar + pre.astro-code { margin-top: 0; border-top: 0; }
|
|
39
|
+
.code-copy {
|
|
40
|
+
border: 1px solid currentColor;
|
|
41
|
+
background: none;
|
|
42
|
+
color: inherit;
|
|
43
|
+
font-family: var(--font-body);
|
|
44
|
+
font-size: inherit;
|
|
45
|
+
letter-spacing: inherit;
|
|
46
|
+
text-transform: inherit;
|
|
47
|
+
padding: 2px 8px;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
}
|
|
50
|
+
.code-copy:hover { outline: 1px solid currentColor; }
|
|
51
|
+
}
|
package/src/global.css
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* Layer: base. Entry point Base.astro imports; the rules live under
|
|
2
|
+
base/ in the order they had in this file (same-layer ties resolve by
|
|
3
|
+
source order). reset: element defaults and a11y utilities. shell: the
|
|
4
|
+
page grid. link-tip and scroll-top: site chrome with JS halves,
|
|
5
|
+
kept in base so the patterns layer outranks them.
|
|
6
|
+
See docs/css-architecture.md. */
|
|
7
|
+
@import "./base/reset.css";
|
|
8
|
+
@import "./base/shell.css";
|
|
9
|
+
@import "./base/link-tip.css";
|
|
10
|
+
@import "./base/scroll-top.css";
|
package/src/layers.css
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* Cascade order for the whole site, declared once and imported first.
|
|
2
|
+
Later layers win; Astro's scoped component styles are unlayered and
|
|
3
|
+
therefore always win over every layer (the intended override point).
|
|
4
|
+
The plate-modal family is three sublayers of components so the shared
|
|
5
|
+
plate chrome loses to the lightbox and the player by layer order, not
|
|
6
|
+
by stylesheet import order. site is the blog's own layer (step 11.1):
|
|
7
|
+
rules that never enter the package (Henry, WP-artifact prose
|
|
8
|
+
overrides) and must outrank whatever the package layers say. See
|
|
9
|
+
docs/css-architecture.md. */
|
|
10
|
+
@layer tokens, base, patterns, components, components.plate, components.lightbox, components.player, site;
|
package/src/lightbox.css
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/* Layer: components. Image lightbox chrome (spec
|
|
2
|
+
docs/superpowers/specs/2026-07-30-image-lightbox-design.md). The plate
|
|
3
|
+
reuses .bracket-frame, badges reuse .boxed-label, buttons reuse
|
|
4
|
+
.icon-box; only offsets and modal-specific layout live here. */
|
|
5
|
+
@layer components.lightbox {
|
|
6
|
+
/* Build-time anchors around content images. Positioned so a corner
|
|
7
|
+
badge (CornerBadges.astro) pins to the image's corner. */
|
|
8
|
+
.lightbox-link { display: block; position: relative; line-height: 0; cursor: zoom-in; }
|
|
9
|
+
|
|
10
|
+
.lb-main { display: flex; align-items: center; gap: 16px; }
|
|
11
|
+
|
|
12
|
+
/* Arrows: ink on filled white squares (mockup v3 decision). .icon-box
|
|
13
|
+
supplies the 30x30 bordered square and the button reset. */
|
|
14
|
+
.lb-arrow { font-size: var(--font-size-md); }
|
|
15
|
+
|
|
16
|
+
/* Desktop: bar and foot are pass-throughs; their children straddle the
|
|
17
|
+
plate rules as boxed labels (counter top-left, close top-right, dims
|
|
18
|
+
bottom-left, readout bottom-right). */
|
|
19
|
+
.lb-bar,
|
|
20
|
+
.lb-foot { display: contents; }
|
|
21
|
+
|
|
22
|
+
/* Normalized viewbox: constant viewport-derived size; fine-grid mat
|
|
23
|
+
centered behind the image (also the ground under transparent
|
|
24
|
+
PNG/SVG). touch-action off: the script owns pan/pinch. */
|
|
25
|
+
.lb-viewbox {
|
|
26
|
+
position: relative;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
border: var(--stroke) solid var(--rule);
|
|
29
|
+
width: min(70vw, 990px);
|
|
30
|
+
height: min(66vh, 740px);
|
|
31
|
+
background-image:
|
|
32
|
+
linear-gradient(to right, var(--mat-line) 1px, transparent 1px),
|
|
33
|
+
linear-gradient(to bottom, var(--mat-line) 1px, transparent 1px);
|
|
34
|
+
background-size: 12px 12px;
|
|
35
|
+
background-position: center center;
|
|
36
|
+
touch-action: none;
|
|
37
|
+
}
|
|
38
|
+
.lb-img {
|
|
39
|
+
position: absolute;
|
|
40
|
+
left: 50%;
|
|
41
|
+
top: 50%;
|
|
42
|
+
max-width: none;
|
|
43
|
+
border: 0;
|
|
44
|
+
cursor: grab;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Generous air after the caption before the bottom rule (owner request
|
|
48
|
+
in the mockup session). */
|
|
49
|
+
.lb-caption {
|
|
50
|
+
font-size: var(--font-size-xs);
|
|
51
|
+
text-align: center;
|
|
52
|
+
color: var(--ink);
|
|
53
|
+
margin: 12px 0 14px;
|
|
54
|
+
|
|
55
|
+
/* A long caption must not size the dialog: zero intrinsic width,
|
|
56
|
+
then stretch to the width the viewbox establishes, wrapping
|
|
57
|
+
there. Found 2026-08-20 when a meme grew a two-sentence
|
|
58
|
+
caption and the whole window opened caption-wide. */
|
|
59
|
+
width: 0;
|
|
60
|
+
min-width: 100%;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* HOME: appears inside the viewbox when the view is dragged far out
|
|
64
|
+
of whack; snaps back to the fit view. */
|
|
65
|
+
.lb-home {
|
|
66
|
+
position: absolute;
|
|
67
|
+
bottom: 10px;
|
|
68
|
+
left: 50%;
|
|
69
|
+
transform: translateX(-50%);
|
|
70
|
+
/* .boxed-label gives border, ink, and surface; .micro-label the type;
|
|
71
|
+
a <button> still needs the family reset. */
|
|
72
|
+
font: inherit;
|
|
73
|
+
padding: 2px 10px;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
z-index: 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* The strip scrolls instead of wrapping: swiping it is the scroll
|
|
79
|
+
gesture, tapping selects, and goTo keeps the active thumb in view.
|
|
80
|
+
"safe center" keeps short strips centered without clipping the
|
|
81
|
+
start of long ones. */
|
|
82
|
+
.lb-thumbs {
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-wrap: nowrap;
|
|
85
|
+
overflow-x: auto;
|
|
86
|
+
max-width: 100%;
|
|
87
|
+
gap: 8px;
|
|
88
|
+
justify-content: safe center;
|
|
89
|
+
/* A scrolling strip clips on every side, so the active and focus
|
|
90
|
+
rings (2px, offset 2px) need 4px of room all round; the top margin
|
|
91
|
+
gives up the same 4px so the thumbs do not move (owner report
|
|
92
|
+
2026-08-26: the ring read as a U). */
|
|
93
|
+
margin-top: 12px;
|
|
94
|
+
padding: 4px;
|
|
95
|
+
}
|
|
96
|
+
.lb-thumb {
|
|
97
|
+
flex: 0 0 auto;
|
|
98
|
+
width: 44px;
|
|
99
|
+
height: 44px;
|
|
100
|
+
padding: 0;
|
|
101
|
+
border: var(--stroke) solid var(--on-ink);
|
|
102
|
+
background: none;
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
opacity: 0.65;
|
|
105
|
+
}
|
|
106
|
+
.lb-thumb img {
|
|
107
|
+
width: 100%;
|
|
108
|
+
height: 100%;
|
|
109
|
+
object-fit: cover;
|
|
110
|
+
display: block;
|
|
111
|
+
border: 0;
|
|
112
|
+
}
|
|
113
|
+
.lb-thumb.active {
|
|
114
|
+
opacity: 1;
|
|
115
|
+
outline: 2px solid var(--accent-1);
|
|
116
|
+
outline-offset: 2px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Solo opens set [hidden] on the arrows, counter, and thumb strip, but
|
|
120
|
+
.icon-box and .lb-thumbs both set their own display: flex, which beats
|
|
121
|
+
the UA [hidden] rule at equal specificity. Two simple selectors outrank
|
|
122
|
+
one, so this wins without !important and stays scoped to the dialog. */
|
|
123
|
+
.lb-dialog [hidden] { display: none; }
|
|
124
|
+
|
|
125
|
+
/* Mobile (the gallery two-column breakpoint): no arrows, the counter
|
|
126
|
+
takes a header row inside the plate, dims and readout collapse to one
|
|
127
|
+
hairline-ruled instrument bar (mockup mobile-x decision B). The
|
|
128
|
+
close box stays on the top-right corner of the plate, straddling the
|
|
129
|
+
rule at the player's phone offsets (plate-modal.css); decision B had
|
|
130
|
+
parked it in the header row and the owner asked for the corner back
|
|
131
|
+
after launch (2026-08-27). */
|
|
132
|
+
@media (--bp-phone) {
|
|
133
|
+
.lb-arrow { display: none; }
|
|
134
|
+
/* The box caps against the plate: 86vw plus the plate and zone
|
|
135
|
+
padding (72px) only fits above 514px, and below that the plate
|
|
136
|
+
clamped and the box overflowed its right padding, reading as a mat
|
|
137
|
+
shifted right by about a scrollbar (owner report 2026-08-30). The
|
|
138
|
+
zone and plate fill the width explicitly because nothing in the
|
|
139
|
+
lightbox has an intrinsic width for the 100% to resolve against
|
|
140
|
+
(the caption is width: 0 on purpose): without these two, min()
|
|
141
|
+
collapsed the box to 186px. The player gets the same width from
|
|
142
|
+
its track canvas by accident. Above 514px the box is narrower
|
|
143
|
+
than the plate it now fills, so it centers itself. */
|
|
144
|
+
.lb-zone,
|
|
145
|
+
.lb-plate { width: 100%; }
|
|
146
|
+
.lb-viewbox { width: min(86vw, 100%); height: 52vh; margin-inline: auto; }
|
|
147
|
+
.lb-bar {
|
|
148
|
+
display: flex;
|
|
149
|
+
justify-content: space-between;
|
|
150
|
+
align-items: center;
|
|
151
|
+
margin-bottom: 10px;
|
|
152
|
+
}
|
|
153
|
+
/* In flow on phones, no longer sharing the bottom rule: the label
|
|
154
|
+
cap (plate-modal.css) does not apply. */
|
|
155
|
+
.lb-dialog .lb-counter { position: static; border: 0; padding: 0; max-width: none; }
|
|
156
|
+
.lb-foot {
|
|
157
|
+
display: flex;
|
|
158
|
+
justify-content: space-between;
|
|
159
|
+
gap: 14px;
|
|
160
|
+
border-top: 1px solid var(--hairline);
|
|
161
|
+
padding-top: 6px;
|
|
162
|
+
font-size: var(--font-size-xs);
|
|
163
|
+
letter-spacing: 0.1em;
|
|
164
|
+
}
|
|
165
|
+
/* In flow on phones, no longer sharing the bottom rule: the label
|
|
166
|
+
cap (plate-modal.css) does not apply. */
|
|
167
|
+
.lb-dialog .lb-dims,
|
|
168
|
+
.lb-dialog .lb-readout { position: static; border: 0; padding: 0; font-weight: 400; max-width: none; }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* Layer: components. Path-player content inside a plate-modal (spec
|
|
2
|
+
2026-08-22). Values echo the lightbox family; colors from tokens
|
|
3
|
+
only. Imported by FluidPathPlayer.astro so it ships only on pages
|
|
4
|
+
that embed a player. */
|
|
5
|
+
@layer components.player {
|
|
6
|
+
/* Desktop: the plate is a wide instrument panel, not a sleeve around
|
|
7
|
+
the window (owner call 2026-08-26). The square window centers in it
|
|
8
|
+
and the track strip, caption, and labels take the full width, which
|
|
9
|
+
also keeps the centered transport clear of the source label at
|
|
10
|
+
short viewports, where the window shrinks. Phones keep the triage's
|
|
11
|
+
flow layout below 768px. */
|
|
12
|
+
@media (--bp-phone-up) {
|
|
13
|
+
.pp-plate { width: min(88vw, 1100px); }
|
|
14
|
+
}
|
|
15
|
+
/* Square (owner call 2026-08-25). The 55vh term keeps the square plus
|
|
16
|
+
the plate's ~260px of chrome inside any window 580px or taller. */
|
|
17
|
+
.pp-viewbox {
|
|
18
|
+
position: relative;
|
|
19
|
+
width: min(72vw, 55vh, 640px);
|
|
20
|
+
margin-inline: auto;
|
|
21
|
+
aspect-ratio: 1;
|
|
22
|
+
border: var(--stroke) solid var(--rule);
|
|
23
|
+
background-image:
|
|
24
|
+
linear-gradient(to right, var(--mat-line) 1px, transparent 1px),
|
|
25
|
+
linear-gradient(to bottom, var(--mat-line) 1px, transparent 1px);
|
|
26
|
+
background-size: 12px 12px;
|
|
27
|
+
background-position: center center;
|
|
28
|
+
}
|
|
29
|
+
.pp-viewbox canvas {
|
|
30
|
+
position: absolute;
|
|
31
|
+
inset: 0;
|
|
32
|
+
width: 100%;
|
|
33
|
+
height: 100%;
|
|
34
|
+
display: block;
|
|
35
|
+
}
|
|
36
|
+
/* Type from .micro-label; only the centering is here. */
|
|
37
|
+
.pp-fallback {
|
|
38
|
+
position: absolute;
|
|
39
|
+
inset: 0;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Transport straddles the plate's bottom edge, centered, in the
|
|
46
|
+
manner of the boxed labels and the close box (owner call
|
|
47
|
+
2026-08-23). */
|
|
48
|
+
.pp-transport {
|
|
49
|
+
position: absolute;
|
|
50
|
+
bottom: -17px;
|
|
51
|
+
left: 50%;
|
|
52
|
+
transform: translateX(-50%);
|
|
53
|
+
display: flex;
|
|
54
|
+
gap: 10px;
|
|
55
|
+
z-index: 1;
|
|
56
|
+
}
|
|
57
|
+
/* Transport buttons are .icon-box; only the glyph size is here. */
|
|
58
|
+
.pp-play,
|
|
59
|
+
.pp-restart { font-size: var(--font-size-md); }
|
|
60
|
+
/* The rotate glyph's circle fills its viewBox denser than the play
|
|
61
|
+
and pause marks; pulled in a step so the pair reads even. */
|
|
62
|
+
.pp-restart svg { width: 0.85em; height: 0.85em; }
|
|
63
|
+
/* Press feedback comes from .press-box; a disabled button never
|
|
64
|
+
reaches :active. */
|
|
65
|
+
.pp-play:disabled, .pp-restart:disabled { opacity: 0.4; cursor: default; }
|
|
66
|
+
/* Air between the square window and the track strip, so the two read
|
|
67
|
+
as separate instruments instead of one tall panel (owner call
|
|
68
|
+
2026-08-26; at 14px the window read taller than square). */
|
|
69
|
+
.pp-timeline { position: relative; margin-top: 24px; }
|
|
70
|
+
.pp-tracks { width: 100%; display: block; }
|
|
71
|
+
|
|
72
|
+
/* Same zero-intrinsic-width trick as the lightbox caption: a long
|
|
73
|
+
line wraps at the viewbox width instead of sizing the plate. */
|
|
74
|
+
.pp-caption {
|
|
75
|
+
font-size: var(--font-size-xs);
|
|
76
|
+
line-height: 1.6;
|
|
77
|
+
text-align: center;
|
|
78
|
+
color: var(--ink);
|
|
79
|
+
margin: 14px 0 0;
|
|
80
|
+
width: 0;
|
|
81
|
+
min-width: 100%;
|
|
82
|
+
}
|
|
83
|
+
/* The caption toggle exists for phones only; see the media block. */
|
|
84
|
+
.pp-caption-toggle { display: none; }
|
|
85
|
+
|
|
86
|
+
@media (--bp-phone) {
|
|
87
|
+
/* Still square; 45vh keeps it plus ~350px of chrome inside a 664px
|
|
88
|
+
viewport (299px there, the 86vw cap of 335px on taller phones).
|
|
89
|
+
min() against 100% caps against the plate so viewbox + chrome
|
|
90
|
+
can never outgrow the visible width (86vw alone overflowed at
|
|
91
|
+
390px). */
|
|
92
|
+
.pp-viewbox { width: min(86vw, 100%, 45vh); }
|
|
93
|
+
/* Narrow plates: the straddling transport and readout would collide
|
|
94
|
+
with the bottom-left source label, so both join the plate flow:
|
|
95
|
+
controls centered under the caption, the time under the controls
|
|
96
|
+
(owner call 2026-08-23). The plate goes flex-column so the two
|
|
97
|
+
static pieces can order after the caption; the absolutely
|
|
98
|
+
positioned labels (title, source, close) ignore flex and keep
|
|
99
|
+
straddling. */
|
|
100
|
+
.pp-plate { display: flex; flex-direction: column; }
|
|
101
|
+
.pp-transport {
|
|
102
|
+
position: static;
|
|
103
|
+
transform: none;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
margin-top: 12px;
|
|
106
|
+
order: 1;
|
|
107
|
+
}
|
|
108
|
+
/* In flow on phones, no longer sharing the bottom rule: the label
|
|
109
|
+
cap (plate-modal.css) does not apply. */
|
|
110
|
+
.pp-plate .pp-readout {
|
|
111
|
+
position: static;
|
|
112
|
+
order: 2;
|
|
113
|
+
margin: 8px auto 0;
|
|
114
|
+
max-width: none;
|
|
115
|
+
}
|
|
116
|
+
/* With the readout in flow, the bottom rule has one label, and it
|
|
117
|
+
reads better centered (owner call 2026-08-25). Two classes so it
|
|
118
|
+
beats .pm-label-bl regardless of stylesheet order. */
|
|
119
|
+
.pp-plate .pp-source {
|
|
120
|
+
left: 50%;
|
|
121
|
+
translate: -50% 0;
|
|
122
|
+
max-width: calc(100% - 50px);
|
|
123
|
+
}
|
|
124
|
+
/* Caption toggle: box, type, and press feedback come from
|
|
125
|
+
.boxed-label, .micro-label, and .press-box; placement and the
|
|
126
|
+
open state (held inverted, like a pressed transport button) are
|
|
127
|
+
here. */
|
|
128
|
+
.pp-caption-toggle {
|
|
129
|
+
display: block;
|
|
130
|
+
margin: 14px auto 0;
|
|
131
|
+
padding: 2px 10px;
|
|
132
|
+
}
|
|
133
|
+
.pp-caption-toggle[aria-expanded="true"] {
|
|
134
|
+
background-color: var(--ink);
|
|
135
|
+
color: var(--on-ink);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
package/src/patterns.css
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/* Layer: patterns. The theme's visual grammar, each pattern defined exactly
|
|
2
|
+
once. Components apply the class and add only contextual offsets/margins
|
|
3
|
+
in their scoped styles. See docs/css-architecture.md for the full grammar
|
|
4
|
+
description; these classes mirror the live site's look and are pinned by
|
|
5
|
+
the fidelity tests. */
|
|
6
|
+
@layer patterns {
|
|
7
|
+
/* Bracket frame: 2px top/bottom rules, 2px edge verticals fading through
|
|
8
|
+
the middle. The site's signature enclosure (header, latest-posts wrap,
|
|
9
|
+
articles, pages, 404, search results). */
|
|
10
|
+
.bracket-frame {
|
|
11
|
+
position: relative;
|
|
12
|
+
border: var(--stroke) solid var(--rule);
|
|
13
|
+
border-width: var(--stroke) 0;
|
|
14
|
+
padding: var(--frame-pad);
|
|
15
|
+
}
|
|
16
|
+
.bracket-frame::before,
|
|
17
|
+
.bracket-frame::after {
|
|
18
|
+
content: "";
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: 0;
|
|
21
|
+
width: var(--stroke);
|
|
22
|
+
height: 100%;
|
|
23
|
+
background: linear-gradient(
|
|
24
|
+
to bottom,
|
|
25
|
+
var(--rule) 0%,
|
|
26
|
+
rgba(0, 0, 0, 0) 35%,
|
|
27
|
+
rgba(0, 0, 0, 0) 65%,
|
|
28
|
+
var(--rule) 100%
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
.bracket-frame::before { left: 0; }
|
|
32
|
+
.bracket-frame::after { right: 0; }
|
|
33
|
+
|
|
34
|
+
/* Boxed label: bordered box on the page background, typically straddling
|
|
35
|
+
a frame rule (date box, Latest Posts title). Padding and straddle
|
|
36
|
+
offsets are contextual and stay in the component. */
|
|
37
|
+
.boxed-label {
|
|
38
|
+
margin: 0;
|
|
39
|
+
color: var(--ink);
|
|
40
|
+
border: var(--stroke) solid var(--rule);
|
|
41
|
+
background-color: var(--surface);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Icon box: 30x30 bordered interactive square (social icons, magnifier). */
|
|
45
|
+
.icon-box {
|
|
46
|
+
width: 30px;
|
|
47
|
+
height: 30px;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
color: var(--ink);
|
|
52
|
+
border: var(--stroke) solid var(--rule);
|
|
53
|
+
background-color: var(--surface);
|
|
54
|
+
text-decoration: none;
|
|
55
|
+
padding: 0;
|
|
56
|
+
font: inherit;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
transition: var(--transition);
|
|
59
|
+
}
|
|
60
|
+
/* Hover ring in the box's own ink; keyboard focus is the site-wide
|
|
61
|
+
amber ring (base/reset.css), which is why this is :hover alone (a
|
|
62
|
+
mouse click must never leave a ring behind, review 2026-08-23, R3). */
|
|
63
|
+
.icon-box:hover { outline: 2px solid currentColor; }
|
|
64
|
+
|
|
65
|
+
/* Pressable box: a bordered light box that inverts to ink while held,
|
|
66
|
+
the house press language (2026-07-30). Nav links, pagination cells,
|
|
67
|
+
the fluid CTA, and the player transport share it; hover and focus
|
|
68
|
+
treatments differ per site and stay local. */
|
|
69
|
+
.press-box {
|
|
70
|
+
color: var(--ink);
|
|
71
|
+
background-color: var(--surface);
|
|
72
|
+
border: var(--stroke) solid var(--rule);
|
|
73
|
+
text-decoration: none;
|
|
74
|
+
font: inherit;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
}
|
|
77
|
+
.press-box:active { color: var(--on-ink); background-color: var(--ink); }
|
|
78
|
+
|
|
79
|
+
/* Accent press box (owner rule 2026-08-26): the callout's combination
|
|
80
|
+
worn by a button. Accent line, the accent's legible ink for the
|
|
81
|
+
text, and the same faint tint of the accent as its ground (the
|
|
82
|
+
callout tints at 6%), in both themes; held, it fills solid in the
|
|
83
|
+
accent with the on-accent text, the press-box inversion. Knobs pick
|
|
84
|
+
the accent; the default is the second accent, which the demo
|
|
85
|
+
launcher wears. */
|
|
86
|
+
.press-box-accent {
|
|
87
|
+
--press-accent: var(--accent-2);
|
|
88
|
+
--press-accent-ink: var(--accent-2-ink);
|
|
89
|
+
--press-on-accent: var(--on-accent-2);
|
|
90
|
+
|
|
91
|
+
color: var(--press-accent-ink);
|
|
92
|
+
background-color: color-mix(in srgb, var(--press-accent) 6%, var(--surface));
|
|
93
|
+
border-color: var(--press-accent);
|
|
94
|
+
}
|
|
95
|
+
.press-box-accent:hover {
|
|
96
|
+
background-color: color-mix(in srgb, var(--press-accent) 14%, var(--surface));
|
|
97
|
+
}
|
|
98
|
+
.press-box-accent:active {
|
|
99
|
+
color: var(--press-on-accent);
|
|
100
|
+
background-color: var(--press-accent);
|
|
101
|
+
}
|
|
102
|
+
/* Which accent: the brand amber (--accent-1) or the second, cyan
|
|
103
|
+
(--accent-2, also the default above). The Button component renders
|
|
104
|
+
these as its variants. */
|
|
105
|
+
.press-box-accent-1 {
|
|
106
|
+
--press-accent: var(--accent-1);
|
|
107
|
+
--press-accent-ink: var(--accent-1-ink);
|
|
108
|
+
--press-on-accent: var(--on-accent-1);
|
|
109
|
+
}
|
|
110
|
+
.press-box-accent-2 {
|
|
111
|
+
--press-accent: var(--accent-2);
|
|
112
|
+
--press-accent-ink: var(--accent-2-ink);
|
|
113
|
+
--press-on-accent: var(--on-accent-2);
|
|
114
|
+
}
|
|
115
|
+
/* Shaded press box (owner request 2026-08-26): the plain box on a
|
|
116
|
+
ground tinted with ink at the nav's current-item strength (the
|
|
117
|
+
tint overlay at 0.2), for a second, non-accent button beside a
|
|
118
|
+
plain one. Held, it inverts like any press box. */
|
|
119
|
+
.press-box-shaded {
|
|
120
|
+
background-color: color-mix(in srgb, var(--ink) 20%, var(--surface));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Button row: a run of buttons side by side with air between them,
|
|
124
|
+
wrapping onto a second line when the column runs out (the style
|
|
125
|
+
guide's variant rows). The gap is the row's only spacing; the
|
|
126
|
+
buttons carry none of their own. */
|
|
127
|
+
.button-row {
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-wrap: wrap;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 10px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Tint overlay: fills the element with ink at low opacity without
|
|
135
|
+
introducing a second color (current nav item 0.2, footer bar 0.1).
|
|
136
|
+
Knobs: --tint-opacity, and --tint-color for a caller whose line is
|
|
137
|
+
not ink (the callout's typed line color). */
|
|
138
|
+
.tint-overlay { position: relative; }
|
|
139
|
+
.tint-overlay::before {
|
|
140
|
+
content: "";
|
|
141
|
+
position: absolute;
|
|
142
|
+
inset: 0;
|
|
143
|
+
background-color: var(--tint-color, var(--ink));
|
|
144
|
+
opacity: var(--tint-opacity, 0.1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Back link: the deterministic return-to-parent nav under posts (to the
|
|
148
|
+
archive page holding the post) and pages (to parent, or home). One
|
|
149
|
+
definition; both layouts render <nav class="back-link">. */
|
|
150
|
+
.back-link { margin-top: 20px; font-size: var(--font-size-sm); }
|
|
151
|
+
.back-link a { color: var(--ink); }
|
|
152
|
+
|
|
153
|
+
/* Chip: filled primary label with white text (reading-time chip,
|
|
154
|
+
category-card label). */
|
|
155
|
+
.chip {
|
|
156
|
+
display: inline-flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
gap: 5px;
|
|
159
|
+
padding: 5px 7px;
|
|
160
|
+
color: var(--on-ink);
|
|
161
|
+
background-color: var(--ink);
|
|
162
|
+
font-size: var(--font-size-xs);
|
|
163
|
+
font-weight: bold;
|
|
164
|
+
line-height: 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Draft stamp: the amber DRAFT overlay on a post's hero and its
|
|
168
|
+
archive card. Reaches a page only in SHOW_DRAFTS=1 preview builds
|
|
169
|
+
(deploy builds exclude drafts entirely). Size and stacking are
|
|
170
|
+
contextual. */
|
|
171
|
+
.draft-stamp {
|
|
172
|
+
position: absolute;
|
|
173
|
+
inset: 0;
|
|
174
|
+
display: grid;
|
|
175
|
+
place-items: center;
|
|
176
|
+
pointer-events: none;
|
|
177
|
+
color: var(--accent-1);
|
|
178
|
+
font-weight: 700;
|
|
179
|
+
letter-spacing: 0.2em;
|
|
180
|
+
text-shadow: 0 2px 10px rgb(0 0 0 / 65%);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Rule box: the plain bordered prose container at frame padding
|
|
184
|
+
(Group, and Callout's box under its tint and label). Margins are
|
|
185
|
+
contextual and stay in the component. */
|
|
186
|
+
.rule-box {
|
|
187
|
+
border: var(--stroke) solid var(--rule);
|
|
188
|
+
padding: var(--frame-pad);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Caption: the quiet small line under an image, beside media text, or
|
|
192
|
+
after a quote. Alignment and spacing are contextual. */
|
|
193
|
+
.caption { font-size: var(--font-size-xs); opacity: 0.75; }
|
|
194
|
+
|
|
195
|
+
/* Micro label: the small bold tracked line on plate labels, the
|
|
196
|
+
lightbox HOME box, the player fallback, and the stasis tag. Case is
|
|
197
|
+
the caller's. */
|
|
198
|
+
.micro-label { font-size: var(--font-size-xs); font-weight: 700; letter-spacing: 0.08em; }
|
|
199
|
+
|
|
200
|
+
/* Joined field: a text entry and its action button fused in one
|
|
201
|
+
2px-bordered box (subscribe form, search page form). House rule
|
|
202
|
+
settled 2026-07-30 after trying inset and side-by-side variants.
|
|
203
|
+
Anatomy: borderless transparent input, then the submit button,
|
|
204
|
+
which IS the house button: every .field-join-button also carries
|
|
205
|
+
.press-box and .press-box-shaded (owner call 2026-08-26: the style
|
|
206
|
+
guide's shaded Button, ink text on the nav's current-item gray,
|
|
207
|
+
filling with the ink while held), and this class adds only the
|
|
208
|
+
fused geometry (no radius, the
|
|
209
|
+
box's own border replaced by a 2px rule where it meets the input,
|
|
210
|
+
the button's type and padding). Square focus rings, inset (the UA
|
|
211
|
+
auto ring is rounded and garbles the box, and an outside ring would
|
|
212
|
+
cut across the fused box).
|
|
213
|
+
Color knobs, each defaulting to the theme's own ink and surface:
|
|
214
|
+
--field-ink (box border and the rule), --field-text (input text),
|
|
215
|
+
--field-well (the ground behind the input), --field-ring (the
|
|
216
|
+
input's highlight when clicked into; a keyboard focus always shows
|
|
217
|
+
the site ring instead, told apart by the root's data-focus stamp,
|
|
218
|
+
which comes from the packaged focus-mode island's mountFocusMode
|
|
219
|
+
(step 11.2)). The subscribe panel sets them from its
|
|
220
|
+
--panel-* roles; the button takes no knob, so it reads the same on
|
|
221
|
+
the panel as anywhere else. */
|
|
222
|
+
.field-join {
|
|
223
|
+
display: flex;
|
|
224
|
+
flex-wrap: wrap;
|
|
225
|
+
border: var(--stroke) solid var(--field-ink, var(--rule));
|
|
226
|
+
/* The well behind the input: the theme's --well (white by day, one
|
|
227
|
+
step up from the black by night so the field reads as recessed
|
|
228
|
+
and the button separates from it, owner report 2026-08-26);
|
|
229
|
+
--field-well overrides it for a panel with its own ground. */
|
|
230
|
+
background: var(--field-well, var(--well));
|
|
231
|
+
}
|
|
232
|
+
.field-join-input {
|
|
233
|
+
flex: 1;
|
|
234
|
+
min-width: 0;
|
|
235
|
+
border: 0;
|
|
236
|
+
border-radius: 0;
|
|
237
|
+
padding: 10px 12px;
|
|
238
|
+
background: transparent;
|
|
239
|
+
color: var(--field-text, var(--field-ink, var(--ink)));
|
|
240
|
+
font-family: var(--font-body);
|
|
241
|
+
font-size: var(--font-size-sm);
|
|
242
|
+
}
|
|
243
|
+
/* Inset, not the site-wide outside ring: an outside ring around the
|
|
244
|
+
input alone would cut across the fused box. */
|
|
245
|
+
.field-join-input:focus-visible {
|
|
246
|
+
outline: 2px solid var(--field-ring, var(--focus-ring));
|
|
247
|
+
outline-offset: -2px;
|
|
248
|
+
}
|
|
249
|
+
:root[data-focus="keyboard"] .field-join-input:focus-visible {
|
|
250
|
+
outline-color: var(--focus-ring);
|
|
251
|
+
}
|
|
252
|
+
.field-join-button {
|
|
253
|
+
border-width: 0 0 0 var(--stroke);
|
|
254
|
+
border-color: var(--field-ink, var(--rule));
|
|
255
|
+
border-radius: 0;
|
|
256
|
+
padding: 10px 18px;
|
|
257
|
+
font-family: var(--font-body);
|
|
258
|
+
font-size: var(--font-size-sm);
|
|
259
|
+
font-weight: 700;
|
|
260
|
+
text-transform: uppercase;
|
|
261
|
+
letter-spacing: 0.08em;
|
|
262
|
+
cursor: pointer;
|
|
263
|
+
position: relative;
|
|
264
|
+
z-index: 1;
|
|
265
|
+
}
|
|
266
|
+
.field-join-button:focus-visible {
|
|
267
|
+
outline: 2px solid var(--focus-ring);
|
|
268
|
+
outline-offset: -4px;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* Layer: components.plate. Shared plate-window chrome (spec
|
|
2
|
+
docs/superpowers/specs/2026-08-22-fluid-path-player-design.md).
|
|
3
|
+
Consumers add their own content rules in components.lightbox and
|
|
4
|
+
components.player, which sit above this sublayer (layers.css), so the
|
|
5
|
+
import order of the three files does not matter. The lightbox's
|
|
6
|
+
elements carry both pm-* and lb-* names. */
|
|
7
|
+
@layer components.plate {
|
|
8
|
+
/* Percentages, not vw/vh: a top-layer dialog's containing block is
|
|
9
|
+
the initial containing block, which excludes the page scrollbar;
|
|
10
|
+
100vw includes it and leaked a horizontal scrollbar on narrow
|
|
11
|
+
viewports (found 2026-08-23). overflow: auto is the UA default made
|
|
12
|
+
explicit; overscroll-behavior keeps a touch scroll that reaches the
|
|
13
|
+
dialog's end from chaining to the page (phone report 2026-08-25). */
|
|
14
|
+
.pm-dialog {
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
max-width: 100%;
|
|
18
|
+
max-height: 100%;
|
|
19
|
+
border: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
margin: 0;
|
|
22
|
+
background: transparent;
|
|
23
|
+
overflow: auto;
|
|
24
|
+
overscroll-behavior: contain;
|
|
25
|
+
}
|
|
26
|
+
/* No align-items: center here. A zone taller than the phone would be
|
|
27
|
+
centered with its top above y=0, and a scroll container cannot
|
|
28
|
+
scroll into negative space, so the close box was unreachable
|
|
29
|
+
(measured 2026-08-25: zone 766px in a 664px viewport, close at
|
|
30
|
+
y=-50). The zone's auto margins center it when there is room and
|
|
31
|
+
collapse to zero when there is not, and then the dialog scrolls. */
|
|
32
|
+
.pm-dialog[open] {
|
|
33
|
+
display: flex;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
}
|
|
36
|
+
.pm-dialog::backdrop {
|
|
37
|
+
background: var(--veil);
|
|
38
|
+
backdrop-filter: blur(7px);
|
|
39
|
+
}
|
|
40
|
+
/* The page must not scroll under an open modal; plate-modal.ts sets
|
|
41
|
+
the class on <html> at open and clears it on close. */
|
|
42
|
+
html.pm-open,
|
|
43
|
+
html.pm-open body { overflow: hidden; }
|
|
44
|
+
/* Classic-scrollbar desktops: keep the gutter so the page does not
|
|
45
|
+
shift 17px when the scrollbar goes. */
|
|
46
|
+
html.pm-open { scrollbar-gutter: stable; }
|
|
47
|
+
|
|
48
|
+
.pm-zone {
|
|
49
|
+
position: relative;
|
|
50
|
+
margin: auto;
|
|
51
|
+
padding: 26px 30px;
|
|
52
|
+
max-width: 100%;
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
}
|
|
55
|
+
.pm-corner {
|
|
56
|
+
position: absolute;
|
|
57
|
+
width: 22px;
|
|
58
|
+
height: 22px;
|
|
59
|
+
border: 0 solid var(--accent-1);
|
|
60
|
+
}
|
|
61
|
+
.pm-c-tl { top: 0; left: 0; border-top-width: var(--stroke); border-left-width: var(--stroke); }
|
|
62
|
+
.pm-c-tr { top: 0; right: 0; border-top-width: var(--stroke); border-right-width: var(--stroke); }
|
|
63
|
+
.pm-c-bl { bottom: 0; left: 0; border-bottom-width: var(--stroke); border-left-width: var(--stroke); }
|
|
64
|
+
.pm-c-br { bottom: 0; right: 0; border-bottom-width: var(--stroke); border-right-width: var(--stroke); }
|
|
65
|
+
|
|
66
|
+
.pm-plate {
|
|
67
|
+
background: var(--surface);
|
|
68
|
+
box-shadow: var(--shadow);
|
|
69
|
+
line-height: 1.5;
|
|
70
|
+
max-width: 100%;
|
|
71
|
+
box-sizing: border-box;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Labels are .boxed-label (border, ink, surface) plus .micro-label
|
|
75
|
+
(type); placement is here. Every label is one line and truncates
|
|
76
|
+
with an ellipsis rather than wrapping or crowding the close box
|
|
77
|
+
(owner call 2026-08-25, a standard for all plate modals). max-width
|
|
78
|
+
is content-box, so the 8px padding and 2px border sit outside it. */
|
|
79
|
+
.pm-label {
|
|
80
|
+
position: absolute;
|
|
81
|
+
padding: 2px 8px;
|
|
82
|
+
z-index: 1;
|
|
83
|
+
white-space: nowrap;
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
text-overflow: ellipsis;
|
|
86
|
+
}
|
|
87
|
+
/* Top-left leaves the close box's corner free; the bottom pair share
|
|
88
|
+
the bottom rule, so each gets half less its inset and a gap. */
|
|
89
|
+
.pm-label-tl { top: -12px; left: 25px; max-width: calc(100% - 25px - 70px); }
|
|
90
|
+
.pm-label-bl,
|
|
91
|
+
.pm-label-br { max-width: calc(50% - 25px - 8px); }
|
|
92
|
+
.pm-label-bl { bottom: -12px; left: 25px; }
|
|
93
|
+
.pm-label-br { bottom: -12px; right: 25px; }
|
|
94
|
+
|
|
95
|
+
/* The close box is an .icon-box; only its straddle offset is here. */
|
|
96
|
+
.pm-close {
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: -17px;
|
|
99
|
+
right: -17px;
|
|
100
|
+
z-index: 1;
|
|
101
|
+
}
|
|
102
|
+
/* Phone: the corner framing device crowds the edges (owner call
|
|
103
|
+
2026-08-20, mirrored from the lightbox). */
|
|
104
|
+
@media (--bp-phone) {
|
|
105
|
+
.pm-zone { padding: 16px 9px; }
|
|
106
|
+
.pm-corner { display: none; }
|
|
107
|
+
.pm-close { right: 16px; }
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/prose.css
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* Layer: components (content-domain stylesheet for .prose).
|
|
2
|
+
See docs/css-architecture.md. */
|
|
3
|
+
@layer components {
|
|
4
|
+
/* Prose rhythm for .prose. Lists verified to 3+ depths (7 live posts
|
|
5
|
+
nest, 3 go 3+ deep). Table treatment: boxed ledger (spec section 5). */
|
|
6
|
+
.prose { margin-top: 0; }
|
|
7
|
+
/* p margin fidelity-corrected to the live 1.5em (handoff 5.5); heading
|
|
8
|
+
sizes/margins are kept on our fluid type scale rather than reverted to
|
|
9
|
+
bare browser defaults, recorded as a deliberate deviation in the handoff. */
|
|
10
|
+
.prose p { margin: 0 0 1.5em; }
|
|
11
|
+
.prose h2 { font-size: var(--font-size-md); margin: 1.6em 0 0.6em; }
|
|
12
|
+
.prose h3 { font-size: var(--font-size-base); margin: 1.4em 0 0.5em; }
|
|
13
|
+
.prose h4 { font-size: var(--font-size-sm); text-transform: uppercase; letter-spacing: 0.5px; margin: 1.4em 0 0.5em; }
|
|
14
|
+
|
|
15
|
+
/* List indentation: outside-positioned markers with enough padding that
|
|
16
|
+
the marker sits right of the content edge and wrapped lines align under
|
|
17
|
+
the text. Owner-approved divergence from live (2026-07-28, kept
|
|
18
|
+
2026-09-02 after the inline-block links it originally worked around
|
|
19
|
+
were retired): outside-positioned markers with wrapped lines aligned
|
|
20
|
+
under the text. Disc (not square) at the top level, 15px
|
|
21
|
+
rhythm. Nested alpha/roman numbering is a kept editorial enhancement
|
|
22
|
+
(live has no nested-list override at all). */
|
|
23
|
+
.prose ul, .prose ol { margin: 0 0 15px 15px; padding-left: 1.2em; list-style-position: outside; }
|
|
24
|
+
/* Longhand on purpose: the list-style shorthand would also reset
|
|
25
|
+
list-style-position (owner bug report 2026-07-28; carryovers test pins
|
|
26
|
+
this). */
|
|
27
|
+
.prose ul { list-style-type: disc; }
|
|
28
|
+
.prose ul ul { list-style-type: circle; }
|
|
29
|
+
.prose ul ul ul { list-style-type: square; }
|
|
30
|
+
.prose ol { list-style-type: decimal; }
|
|
31
|
+
.prose ol ol { list-style-type: lower-alpha; }
|
|
32
|
+
.prose ol ol ol { list-style-type: lower-roman; }
|
|
33
|
+
.prose li { padding: 5px 0; }
|
|
34
|
+
.prose li > ul, .prose li > ol { margin: 4px 0 0 15px; }
|
|
35
|
+
|
|
36
|
+
/* Blockquote: boxed on the muted gray pattern (owner request 2026-08-14,
|
|
37
|
+
spec docs/superpowers/specs/2026-08-14-boxed-blockquotes-design.md), a
|
|
38
|
+
deliberate deviation from live, which ships only the indent (handoff 5.5).
|
|
39
|
+
Full column width; the box, not indentation, is the signal. Nested quotes
|
|
40
|
+
get no special treatment: this rule cascades, so an inner quote sits on
|
|
41
|
+
the same gray ground with only its own hairline marking it. */
|
|
42
|
+
.prose blockquote {
|
|
43
|
+
background: var(--band);
|
|
44
|
+
border: 1px solid var(--hairline);
|
|
45
|
+
padding: 12px 16px;
|
|
46
|
+
margin: 20px 0;
|
|
47
|
+
}
|
|
48
|
+
/* Paragraphs carry 1.5em bottom margin; without this the box ends in a
|
|
49
|
+
dead gray band. */
|
|
50
|
+
.prose blockquote > :last-child { margin-bottom: 0; }
|
|
51
|
+
/* The small attribution line (Quote component) sits close under the quote
|
|
52
|
+
text: whatever element precedes it sheds most of its 1.5em margin. */
|
|
53
|
+
.prose blockquote > :has(+ .quote-attribution) { margin-bottom: 0.5em; }
|
|
54
|
+
|
|
55
|
+
.prose hr {
|
|
56
|
+
border: 0;
|
|
57
|
+
border-top: var(--stroke) solid var(--rule);
|
|
58
|
+
margin: 30px 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Boxed ledger table */
|
|
62
|
+
.prose .table-scroll { overflow-x: auto; margin: 20px 0; }
|
|
63
|
+
.prose table {
|
|
64
|
+
border-collapse: collapse;
|
|
65
|
+
width: 100%;
|
|
66
|
+
border: var(--stroke) solid var(--rule);
|
|
67
|
+
font-size: var(--font-size-sm);
|
|
68
|
+
}
|
|
69
|
+
.prose thead tr {
|
|
70
|
+
background: var(--band);
|
|
71
|
+
border-bottom: 3px solid var(--rule);
|
|
72
|
+
}
|
|
73
|
+
.prose th {
|
|
74
|
+
text-align: left;
|
|
75
|
+
font-size: var(--font-size-xs);
|
|
76
|
+
text-transform: uppercase;
|
|
77
|
+
letter-spacing: 0.1em;
|
|
78
|
+
padding: 7px 12px;
|
|
79
|
+
}
|
|
80
|
+
.prose td { padding: 7px 12px; }
|
|
81
|
+
.prose tbody tr:not(:last-child) { border-bottom: 1px solid var(--hairline); }
|
|
82
|
+
/* Legacy tolerance: a header faked as a bold first body row still reads fine
|
|
83
|
+
because rows carry only faint hairlines. */
|
|
84
|
+
/* Every content image carries the signature 2px outline BY DEFAULT
|
|
85
|
+
(owner ruling 2026-07-26): the standard is baked into rendering, not
|
|
86
|
+
opted into per component. Framed plates keep the line on the frame
|
|
87
|
+
and zero it on the inner img (see GalleryImage). */
|
|
88
|
+
.prose img { max-width: 100%; height: auto; border: var(--stroke) solid var(--rule); }
|
|
89
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* The system breakpoints, defined once (step 11.1; the step 1 plan's
|
|
2
|
+
deferred slice). Three names, per the extraction review: phone is
|
|
3
|
+
where the top header band retires and footer groups collapse;
|
|
4
|
+
sidebar is where the two-column row stacks; tablet is the wide-image
|
|
5
|
+
step. Component-intrinsic queries (a gallery's own 480, a step's own
|
|
6
|
+
800) stay literal in their components on purpose.
|
|
7
|
+
The JS mirror is src/scripts/core/breakpoints.ts; a test pins the
|
|
8
|
+
two files equal. */
|
|
9
|
+
@custom-media --bp-phone (max-width: 768px);
|
|
10
|
+
@custom-media --bp-phone-up (min-width: 769px);
|
|
11
|
+
@custom-media --bp-sidebar (max-width: 991px);
|
|
12
|
+
@custom-media --bp-tablet (max-width: 1024px);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* Layer: tokens. Primitives: the raw palette and structural scales. No
|
|
2
|
+
component reads these directly; the roles in theme-light.css and
|
|
3
|
+
theme-dark.css do. Hex is permitted under tokens/ only
|
|
4
|
+
(stylelint-enforced). See docs/css-architecture.md. */
|
|
5
|
+
@layer tokens {
|
|
6
|
+
:root {
|
|
7
|
+
/* Neutrals (live-resolved Legacy Blog palette, handoff section 5.1) */
|
|
8
|
+
--white: #ffffff;
|
|
9
|
+
--gray-075: #ececec;
|
|
10
|
+
--gray-100: #efefef;
|
|
11
|
+
--gray-200: #d9d9d9;
|
|
12
|
+
--gray-300: #cccccc;
|
|
13
|
+
--gray-600: #555555;
|
|
14
|
+
--gray-700: #404040;
|
|
15
|
+
--black-a10: rgba(0, 0, 0, 0.1);
|
|
16
|
+
--graphite-a55: rgba(48, 48, 48, 0.55);
|
|
17
|
+
|
|
18
|
+
/* Warm darks: the parent theme's dark palette, shared with BEADZ
|
|
19
|
+
(ink-900 is BEADZ --line, ink-800 is BEADZ --monitor-line). */
|
|
20
|
+
--ink-950: #110d08;
|
|
21
|
+
--ink-900: #1b140c;
|
|
22
|
+
--ink-850: #2a2016;
|
|
23
|
+
--ink-800: #3b2e1e;
|
|
24
|
+
--ink-700: #5a4630;
|
|
25
|
+
--parchment: #e8d9c3;
|
|
26
|
+
--parchment-dim: #b09a7c;
|
|
27
|
+
--white-a10: rgba(255, 255, 255, 0.1);
|
|
28
|
+
--black-a60: rgba(0, 0, 0, 0.6);
|
|
29
|
+
|
|
30
|
+
/* Neutral darks: the dark theme's ground. black-900 is Henry's amber
|
|
31
|
+
colorway ground (henry-mascot, the terminal aesthetic), so the
|
|
32
|
+
page and the mascot share one black. */
|
|
33
|
+
--black-900: #111111;
|
|
34
|
+
--black-850: #1a1a1a;
|
|
35
|
+
--gray-800: #2a2a2a;
|
|
36
|
+
--gray-750: #3a3a3a;
|
|
37
|
+
--gray-400: #9a9a9a;
|
|
38
|
+
|
|
39
|
+
/* Accents. amber-500 is the shared accent across the ecosystem;
|
|
40
|
+
amber-700 is its legible kin for text, tuned here for this site
|
|
41
|
+
the way cyan-700 was: the amber hue at full saturation, taken
|
|
42
|
+
down until it clears WCAG AA (4.5:1) as body text on the white
|
|
43
|
+
surface (4.84) and on the accent's 6% tint (4.66). It replaced
|
|
44
|
+
#b5680c, a value borrowed from BEADZ that sat at 4.24:1 on white
|
|
45
|
+
(owner call 2026-09-03; BEADZ is not a source of values). */
|
|
46
|
+
--amber-500: #ffaa3c;
|
|
47
|
+
--amber-700: #a36300;
|
|
48
|
+
/* Second accent, the cyan family (owner request 2026-08-26): chosen
|
|
49
|
+
to sit where the amber sits, cyan-500 matches amber-500's contrast
|
|
50
|
+
on both grounds (line and fill, not text on white) and cyan-700
|
|
51
|
+
is its legible kin for text on the light ground. */
|
|
52
|
+
--cyan-300: #8ee6f2;
|
|
53
|
+
--cyan-500: #3cc7dd;
|
|
54
|
+
--cyan-700: #1a7f90;
|
|
55
|
+
--red-800: #7a1f16;
|
|
56
|
+
--red-300: #ff8a7a;
|
|
57
|
+
--ochre-600: #d98a1f;
|
|
58
|
+
--violet-600: #7a5aa0;
|
|
59
|
+
--violet-300: #b39ad6;
|
|
60
|
+
|
|
61
|
+
/* Strokes, spacing, elevation, motion */
|
|
62
|
+
--stroke-1: 1px;
|
|
63
|
+
--stroke-2: 2px;
|
|
64
|
+
--stroke-3: 3px;
|
|
65
|
+
--frame-pad: 25px;
|
|
66
|
+
--column-gap: 30px;
|
|
67
|
+
/* The shell (step 11.1): --shell-max is the ONLY width authority on
|
|
68
|
+
the page column (the never-cap-prose rule's positive statement);
|
|
69
|
+
--shell-pad its gutter; --header-clear the content offset under
|
|
70
|
+
the header (live theme used 50px; bumped by owner preference). */
|
|
71
|
+
--shell-max: 1380px;
|
|
72
|
+
--shell-pad: 15px;
|
|
73
|
+
--header-clear: 65px;
|
|
74
|
+
--shadow: 0 3px 12px -1px rgb(7 10 25 / 20%), 0 22px 27px -20px rgb(7 10 25 / 20%);
|
|
75
|
+
--transition: all 0.1s ease;
|
|
76
|
+
|
|
77
|
+
/* z ladder. Skip link above content, scroll-to-top above the
|
|
78
|
+
header's search flyout would be wrong, so the flyout tops the
|
|
79
|
+
ladder; loose Henry's rungs sit under the flyout but over
|
|
80
|
+
everything else, with his teleport hole one step under him, both
|
|
81
|
+
defined blog-side in henry-loose.css (step 11.1). */
|
|
82
|
+
--z-raised: 1;
|
|
83
|
+
--z-skip-link: 10;
|
|
84
|
+
--z-scroll-top: 17;
|
|
85
|
+
--z-tooltip: 20;
|
|
86
|
+
/* 49 and 50 belong to loose Henry, defined blog-side in
|
|
87
|
+
henry-loose.css (step 11.1). */
|
|
88
|
+
--z-search-flyout: 99;
|
|
89
|
+
|
|
90
|
+
/* Fluid type scale (handoff section 5.2, verbatim) */
|
|
91
|
+
--font-size-xs: clamp(0.7rem, 0.17vw + 0.76rem, 0.79rem);
|
|
92
|
+
--font-size-sm: clamp(0.8rem, 0.17vw + 0.76rem, 0.89rem);
|
|
93
|
+
--font-size-base: clamp(1rem, 0.34vw + 0.91rem, 1.19rem);
|
|
94
|
+
--font-size-md: clamp(1.25rem, 0.61vw + 1.1rem, 1.58rem);
|
|
95
|
+
--font-size-lg: clamp(1.56rem, 1vw + 1.31rem, 2.11rem);
|
|
96
|
+
--font-size-xl: clamp(1.95rem, 1.56vw + 1.56rem, 2.81rem);
|
|
97
|
+
--font-size-xxl: clamp(2.44rem, 2.38vw + 1.85rem, 3.75rem);
|
|
98
|
+
--font-size-xxxl: clamp(2.95rem, 3.54vw + 2.17rem, 5rem);
|
|
99
|
+
|
|
100
|
+
--font-body: "Roboto Mono", ui-monospace, monospace;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* Layer: tokens. Dark roles, applied through data-theme="dark" on <html>
|
|
2
|
+
(the toggle and the head stamp in Base.astro set it; nothing in CSS
|
|
3
|
+
reads the media query). The ground is Henry's amber-colorway black
|
|
4
|
+
(owner call 2026-08-26: the warm ink-900 read too brown as a page
|
|
5
|
+
ground), with neutral bands and hairlines; the ink stays parchment and
|
|
6
|
+
the accent amber, so the type reads as the terminal aesthetic the
|
|
7
|
+
mascot already uses. The code island keeps its warm block, the same
|
|
8
|
+
one it has by day. Every value is a primitive. */
|
|
9
|
+
@layer tokens {
|
|
10
|
+
:root[data-theme="dark"] {
|
|
11
|
+
color-scheme: dark;
|
|
12
|
+
--surface: var(--black-900);
|
|
13
|
+
--ink: var(--parchment);
|
|
14
|
+
--rule: var(--parchment);
|
|
15
|
+
--on-ink: var(--black-900);
|
|
16
|
+
--ink-muted: var(--gray-400);
|
|
17
|
+
--band: var(--black-850);
|
|
18
|
+
--hairline: var(--gray-800);
|
|
19
|
+
--divider-faint: var(--white-a10);
|
|
20
|
+
--input-border: var(--gray-750);
|
|
21
|
+
/* One step up from the page so a field reads as a field, not a hole. */
|
|
22
|
+
--well: var(--black-850);
|
|
23
|
+
|
|
24
|
+
/* The bright amber is legible as text on the dark ground, so the
|
|
25
|
+
accent and its ink form collapse to one value here. */
|
|
26
|
+
--accent-1: var(--amber-500);
|
|
27
|
+
--accent-1-ink: var(--amber-500);
|
|
28
|
+
--accent-2: var(--cyan-500);
|
|
29
|
+
--accent-2-ink: var(--cyan-300);
|
|
30
|
+
--on-accent-1: var(--black-900);
|
|
31
|
+
--on-accent-2: var(--black-900);
|
|
32
|
+
--error-ink: var(--red-300);
|
|
33
|
+
|
|
34
|
+
/* Code island: the same warm block as in the light theme, so a code
|
|
35
|
+
sample reads identically in both. */
|
|
36
|
+
--code-bg: var(--ink-900);
|
|
37
|
+
--code-fg: var(--parchment);
|
|
38
|
+
--code-line: var(--ink-800);
|
|
39
|
+
|
|
40
|
+
--veil: var(--black-a60);
|
|
41
|
+
--mat-line: var(--gray-800);
|
|
42
|
+
/* Keyboard ring: the second accent by night, the first by day
|
|
43
|
+
(owner call 2026-08-26). */
|
|
44
|
+
--focus-ring: var(--cyan-500);
|
|
45
|
+
|
|
46
|
+
--track-x: var(--amber-500);
|
|
47
|
+
--track-y: var(--cyan-300);
|
|
48
|
+
--track-z: var(--violet-300);
|
|
49
|
+
|
|
50
|
+
/* The subscribe panel inverts by night: a dark panel with an amber
|
|
51
|
+
double rule and an amber button, so the call to action is the
|
|
52
|
+
brightest thing in the block in both themes (owner call
|
|
53
|
+
2026-08-26). */
|
|
54
|
+
--panel-bg: var(--black-850);
|
|
55
|
+
--panel-ink: var(--parchment);
|
|
56
|
+
--panel-rule: var(--amber-500);
|
|
57
|
+
--panel-error: var(--red-300);
|
|
58
|
+
--panel-ring: var(--amber-500);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* Layer: tokens. The light theme's roles, the only names components
|
|
2
|
+
read. The theme is monochrome: ink and rule share one value, which is
|
|
3
|
+
why one "primary color" used to do both jobs; they are separate roles
|
|
4
|
+
so a second theme (or BEADZ, which separates them) can differ.
|
|
5
|
+
Components read these names and never a primitive; test/tokens.test.ts
|
|
6
|
+
fails the build on any retired name. */
|
|
7
|
+
@layer tokens {
|
|
8
|
+
:root {
|
|
9
|
+
--surface: var(--white);
|
|
10
|
+
--ink: var(--gray-700);
|
|
11
|
+
--rule: var(--gray-700);
|
|
12
|
+
--on-ink: var(--white);
|
|
13
|
+
--ink-muted: var(--gray-600);
|
|
14
|
+
--band: var(--gray-100);
|
|
15
|
+
--hairline: var(--gray-200);
|
|
16
|
+
--divider-faint: var(--black-a10);
|
|
17
|
+
--input-border: var(--gray-300);
|
|
18
|
+
/* The ground inside a text field (the joined field's well). */
|
|
19
|
+
--well: var(--white);
|
|
20
|
+
|
|
21
|
+
/* Amber survives inside the code island and as a line or fill
|
|
22
|
+
color; --accent-1-ink is its legible kin for any text on the
|
|
23
|
+
surface (the tagline's box, Callout tip and warning types). */
|
|
24
|
+
--accent-1: var(--amber-500);
|
|
25
|
+
--accent-1-ink: var(--amber-700);
|
|
26
|
+
/* Second accent (cyan), the amber's complement. Same split: the
|
|
27
|
+
bright value is a line or fill, the dark one is text. No component
|
|
28
|
+
reads it yet; its jobs are an owner decision (2026-08-26). */
|
|
29
|
+
--accent-2: var(--cyan-500);
|
|
30
|
+
--accent-2-ink: var(--cyan-700);
|
|
31
|
+
/* Text on an accent fill (a hovered or pressed accent button). */
|
|
32
|
+
--on-accent-1: var(--ink-900);
|
|
33
|
+
--on-accent-2: var(--ink-900);
|
|
34
|
+
--error-ink: var(--red-800);
|
|
35
|
+
|
|
36
|
+
/* Code island (parent-theme dark palette, deliberate exception) */
|
|
37
|
+
--code-bg: var(--ink-900);
|
|
38
|
+
--code-fg: var(--parchment);
|
|
39
|
+
--code-line: var(--ink-800);
|
|
40
|
+
|
|
41
|
+
/* Plate-modal chrome (spec 2026-07-30): veil over the blurred page,
|
|
42
|
+
and the viewbox mat one step lighter than --hairline. Values
|
|
43
|
+
validated in the owner mockup session. */
|
|
44
|
+
--veil: var(--graphite-a55);
|
|
45
|
+
--mat-line: var(--gray-075);
|
|
46
|
+
|
|
47
|
+
/* Path-player timeline series (spec 2026-08-22, owner question 1). */
|
|
48
|
+
--track-x: var(--ochre-600);
|
|
49
|
+
/* The second data track is the second accent (owner call 2026-08-26). */
|
|
50
|
+
--track-y: var(--cyan-500);
|
|
51
|
+
--track-z: var(--violet-600);
|
|
52
|
+
|
|
53
|
+
/* The subscribe panel, the amber block that bridges every property
|
|
54
|
+
back to the blog. By day: amber ground, warm-dark ink and rule, a
|
|
55
|
+
dark button with white text. By night the panel inverts (see
|
|
56
|
+
theme-dark.css) so the button stays the brightest thing in it. */
|
|
57
|
+
--panel-bg: var(--amber-500);
|
|
58
|
+
--panel-ink: var(--ink-900);
|
|
59
|
+
--panel-rule: var(--ink-900);
|
|
60
|
+
--panel-error: var(--red-800);
|
|
61
|
+
/* The email box's click highlight in the panel: ink on the amber by
|
|
62
|
+
day, amber by night. Not the keyboard ring, which is the site's. */
|
|
63
|
+
--panel-ring: var(--ink-900);
|
|
64
|
+
|
|
65
|
+
/* Keyboard focus: one ring for the whole site (base/reset.css), amber
|
|
66
|
+
so it reads on both grounds and never means the same as hover. */
|
|
67
|
+
/* Keyboard ring: the first accent by day, the second by night
|
|
68
|
+
(owner call 2026-08-26). */
|
|
69
|
+
--focus-ring: var(--amber-500);
|
|
70
|
+
|
|
71
|
+
/* The signature stroke: 2px here, 3px on BEADZ. */
|
|
72
|
+
--stroke: var(--stroke-2);
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/tokens.css
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* Layer: tokens. Entry point Base.astro imports; the values live under
|
|
2
|
+
tokens/ (primitives, then the light roles, then the opt-in dark roles).
|
|
3
|
+
Custom properties only, no selectors or element styles. Hex is
|
|
4
|
+
permitted under tokens/ alone (stylelint-enforced).
|
|
5
|
+
See docs/css-architecture.md. */
|
|
6
|
+
@import "@fontsource/roboto-mono/400.css";
|
|
7
|
+
@import "@fontsource/roboto-mono/700.css";
|
|
8
|
+
@import "./tokens/primitives.css";
|
|
9
|
+
@import "./tokens/theme-light.css";
|
|
10
|
+
@import "./tokens/theme-dark.css";
|