@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 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;
@@ -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
+ }
@@ -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";