@codecavepro/brand 1.0.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 +24 -0
- package/README.md +156 -0
- package/dist/colors_and_type.css +900 -0
- package/dist/fonts.css +71 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/tokens/colors.d.ts +94 -0
- package/dist/tokens/colors.d.ts.map +1 -0
- package/dist/tokens/colors.js +90 -0
- package/dist/tokens/layout.d.ts +65 -0
- package/dist/tokens/layout.d.ts.map +1 -0
- package/dist/tokens/layout.js +85 -0
- package/dist/tokens/typography.d.ts +97 -0
- package/dist/tokens/typography.d.ts.map +1 -0
- package/dist/tokens/typography.js +67 -0
- package/package.json +45 -0
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* CODECAVE Design System — color and typography foundations
|
|
3
|
+
* ----------------------------------------------------------------------------
|
|
4
|
+
* Source of truth, in order of authority:
|
|
5
|
+
* 1. codecave.pro/src/styles/global.css (production Tailwind 4 @theme)
|
|
6
|
+
* -> context/local-code/codecave.pro/files/src/styles/global.css
|
|
7
|
+
* 2. CodeCavePro/brand docs/tokens/*.css (published brand token files)
|
|
8
|
+
* -> context/github/CodeCavePro-brand/files/docs/tokens/
|
|
9
|
+
* 3. https://codecave.gay (live site, measured steps)
|
|
10
|
+
*
|
|
11
|
+
* Two layers. The raw ramps (brand violet + gray, verbatim from the site's
|
|
12
|
+
* 2026 palette rebuild) exist ONLY so the semantic names have somewhere to
|
|
13
|
+
* point. Consume the SEMANTIC layer. Never hard-code a hex.
|
|
14
|
+
*
|
|
15
|
+
* Link this file and the tokens are live on :root — there is no provider,
|
|
16
|
+
* no theme object, and no component bundle. See SKILL.md.
|
|
17
|
+
* ========================================================================== */
|
|
18
|
+
|
|
19
|
+
/* ---------------------------------------------------------------------------
|
|
20
|
+
* Font faces
|
|
21
|
+
*
|
|
22
|
+
* PROVENANCE NOTE — this differs from production on purpose.
|
|
23
|
+
* codecave.pro ships exactly ONE real cut (Satoshi-Regular.ttf, weight 400)
|
|
24
|
+
* and lets the browser synthesize 300 and 700. This package ships the real
|
|
25
|
+
* Satoshi cuts captured in the brand asset upload, so bound weights render
|
|
26
|
+
* genuinely rather than faux-bolded. Designs built here will look very
|
|
27
|
+
* slightly tighter at 700 than the live site does today.
|
|
28
|
+
* ------------------------------------------------------------------------ */
|
|
29
|
+
|
|
30
|
+
@font-face {
|
|
31
|
+
font-family: Satoshi;
|
|
32
|
+
src: url("./fonts/Satoshi-Light.woff2") format("woff2"),
|
|
33
|
+
url("./fonts/Satoshi-Light.woff") format("woff");
|
|
34
|
+
font-weight: 300;
|
|
35
|
+
font-style: normal;
|
|
36
|
+
font-display: swap;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@font-face {
|
|
40
|
+
font-family: Satoshi;
|
|
41
|
+
src: url("./fonts/Satoshi-Regular.woff2") format("woff2"),
|
|
42
|
+
url("./fonts/Satoshi-Regular.woff") format("woff");
|
|
43
|
+
font-weight: 400;
|
|
44
|
+
font-style: normal;
|
|
45
|
+
font-display: swap;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@font-face {
|
|
49
|
+
font-family: Satoshi;
|
|
50
|
+
src: url("./fonts/Satoshi-Italic.woff2") format("woff2"),
|
|
51
|
+
url("./fonts/Satoshi-Italic.woff") format("woff");
|
|
52
|
+
font-weight: 400;
|
|
53
|
+
font-style: italic;
|
|
54
|
+
font-display: swap;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@font-face {
|
|
58
|
+
font-family: Satoshi;
|
|
59
|
+
src: url("./fonts/Satoshi-Medium.woff2") format("woff2"),
|
|
60
|
+
url("./fonts/Satoshi-Medium.woff") format("woff");
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
font-style: normal;
|
|
63
|
+
font-display: swap;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@font-face {
|
|
67
|
+
font-family: Satoshi;
|
|
68
|
+
src: url("./fonts/Satoshi-Bold.woff2") format("woff2"),
|
|
69
|
+
url("./fonts/Satoshi-Bold.woff") format("woff");
|
|
70
|
+
font-weight: 700;
|
|
71
|
+
font-style: normal;
|
|
72
|
+
font-display: swap;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@font-face {
|
|
76
|
+
font-family: Satoshi;
|
|
77
|
+
src: url("./fonts/Satoshi-Black.woff2") format("woff2"),
|
|
78
|
+
url("./fonts/Satoshi-Black.woff") format("woff");
|
|
79
|
+
font-weight: 900;
|
|
80
|
+
font-style: normal;
|
|
81
|
+
font-display: swap;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
:root {
|
|
85
|
+
/* =========================================================================
|
|
86
|
+
* RAW RAMPS — verbatim from global.css :root after the 2026 palette
|
|
87
|
+
* rebuild. A violet brand ramp, a gray ramp, and single-use accents.
|
|
88
|
+
* Do not consume directly except where noted (glow-25 is the glow-button
|
|
89
|
+
* fill; progress-0 is the gradient mid; shadow-0 seeds the section glow).
|
|
90
|
+
* ====================================================================== */
|
|
91
|
+
|
|
92
|
+
/* ---- Brand (violet) ---- */
|
|
93
|
+
--color-brand-25: #E8E6F0; /* near-white tint — gradient endpoint */
|
|
94
|
+
--color-brand-50: #BBB9CB;
|
|
95
|
+
--color-brand-100: #B8AFDB;
|
|
96
|
+
--color-brand-200: #B19AFE; /* hover foreground */
|
|
97
|
+
--color-brand-300: #735CAB;
|
|
98
|
+
--color-brand-400: #5F3ABD; /* feature-card ring mid-stop (was cyan) */
|
|
99
|
+
--color-brand-500: #5F20FE; /* ACTION — links, borders, eyebrows */
|
|
100
|
+
--color-brand-600: #4705ED;
|
|
101
|
+
--color-brand-700: #4004AF; /* primary button hover; tertiary border hover */
|
|
102
|
+
--color-brand-800: #1B0D4E; /* text ON the glow button; outline hover */
|
|
103
|
+
--color-brand-900: #1E113B; /* primary button active; secondary fill */
|
|
104
|
+
--color-brand-950: #0A0A0B; /* THE PAGE — a neutral black kept in the
|
|
105
|
+
brand ramp; see WEBSITE-REVIEW.md sect. 5 */
|
|
106
|
+
|
|
107
|
+
/* ---- Gray ---- */
|
|
108
|
+
--color-gray-50: #F4F4F6; /* headings, body text */
|
|
109
|
+
--color-gray-100: #DCDCE5;
|
|
110
|
+
--color-gray-200: #D5D5DD; /* long-form body copy */
|
|
111
|
+
--color-gray-300: #C7C7DB; /* secondary body, lighter */
|
|
112
|
+
--color-gray-400: #A3A3C2;
|
|
113
|
+
--color-gray-500: #9595BB; /* secondary body, placeholders */
|
|
114
|
+
--color-gray-600: #6B6699; /* neutral — muted metadata, LARGE TEXT ONLY */
|
|
115
|
+
--color-gray-700: #2B2848; /* borders / quaternary surface */
|
|
116
|
+
--color-gray-800: #232339; /* surface hover */
|
|
117
|
+
--color-gray-900: #1C1C27; /* tertiary surface */
|
|
118
|
+
--color-gray-950: #15151E;
|
|
119
|
+
--color-gray-1000: #050505; /* one dark section ground ("failureproof") */
|
|
120
|
+
--color-gray-1100: #0F0F15; /* card surface — NOTE: lighter than gray-1000;
|
|
121
|
+
the ramp is non-monotonic past 950 */
|
|
122
|
+
|
|
123
|
+
/* ---- Single-use accents (global.css calls them "once used") ---- */
|
|
124
|
+
--color-glow-25: #9980FF; /* GLOW BUTTON FILL (takes dark text) */
|
|
125
|
+
--color-shadow-0: #281470; /* the upward section glow */
|
|
126
|
+
--color-progress-0: #8252FC; /* gradient mid — .rule / .progress */
|
|
127
|
+
|
|
128
|
+
/* ---- Technology-card wash — the only place cyan survives ---- */
|
|
129
|
+
--color-technology-gradient-0: #077689; /* deep cyan */
|
|
130
|
+
--color-technology-gradient-25: #1A0452;
|
|
131
|
+
--color-technology-gradient-50: #070312;
|
|
132
|
+
|
|
133
|
+
/* ---- Error ramp ---- */
|
|
134
|
+
--color-error-100: #FE9A9A;
|
|
135
|
+
--color-error-200: #FE2020;
|
|
136
|
+
--color-error-300: #B42318;
|
|
137
|
+
--color-error-400: #CA1400;
|
|
138
|
+
|
|
139
|
+
/* =========================================================================
|
|
140
|
+
* SEMANTIC LAYER — use these. Mapping verbatim from the production @theme.
|
|
141
|
+
* ====================================================================== */
|
|
142
|
+
--color-heading: var(--color-gray-50);
|
|
143
|
+
--color-action: var(--color-brand-500);
|
|
144
|
+
--color-neutral: var(--color-gray-600);
|
|
145
|
+
--color-hovered: var(--color-brand-200);
|
|
146
|
+
--color-outline-primary-hover: var(--color-brand-800);
|
|
147
|
+
|
|
148
|
+
--color-body-primary: var(--color-gray-50);
|
|
149
|
+
--color-text-body-primary: var(--color-gray-200); /* long-form copy */
|
|
150
|
+
--color-body-secondary: var(--color-gray-500);
|
|
151
|
+
--color-body-secondary-lighter:var(--color-gray-300);
|
|
152
|
+
|
|
153
|
+
--color-surface-primary: var(--color-brand-950);
|
|
154
|
+
--color-surface-primary-transparent: hsl(from var(--color-surface-primary) h s l / 0.3);
|
|
155
|
+
--color-surface-primary-hover: var(--color-gray-800);
|
|
156
|
+
--color-surface-secondary: var(--color-gray-1100);
|
|
157
|
+
--color-surface-tertiary: var(--color-gray-900);
|
|
158
|
+
--color-surface-quaternary: var(--color-gray-700);
|
|
159
|
+
|
|
160
|
+
--color-failureproof-0: var(--color-gray-1000);
|
|
161
|
+
--color-default-transparent: var(--color-gray-100);
|
|
162
|
+
--color-error: var(--color-error-300);
|
|
163
|
+
|
|
164
|
+
/* ---- Brand gradient — violet to near-white, left to right.
|
|
165
|
+
* The mid stop is the once-used progress accent (#8252FC), NOT brand-300:
|
|
166
|
+
* the rebuilt ramp's brand-300 (#735CAB) is a different, muted violet. ---- */
|
|
167
|
+
--gradient-brand: linear-gradient(
|
|
168
|
+
to right,
|
|
169
|
+
var(--color-brand-500) 0%,
|
|
170
|
+
var(--color-progress-0) 60%,
|
|
171
|
+
var(--color-brand-200) 75%,
|
|
172
|
+
var(--color-brand-25) 100%
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
/* =========================================================================
|
|
176
|
+
* TYPOGRAPHY
|
|
177
|
+
* One family. Named heading sizes come from the production @theme block;
|
|
178
|
+
* the remaining steps are measured from the live site.
|
|
179
|
+
* ====================================================================== */
|
|
180
|
+
--font-sans: Satoshi, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
181
|
+
"Helvetica Neue", "Noto Sans", Arial, sans-serif;
|
|
182
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
|
183
|
+
"Liberation Mono", "Courier New", monospace;
|
|
184
|
+
|
|
185
|
+
--text-heading-lg: 3.5rem; /* 56px — hero */
|
|
186
|
+
--text-heading-lg--line-height: 130%;
|
|
187
|
+
--text-heading-md: 2.75rem; /* 44px — section heading */
|
|
188
|
+
--text-heading-md--line-height: 115%;
|
|
189
|
+
--text-heading-sm: 2rem; /* 32px — eyebrow + lead pair */
|
|
190
|
+
--text-heading-sm--line-height: 110%;
|
|
191
|
+
|
|
192
|
+
/* Named for the thing, not for a t-shirt size — the same rule as RADIUS
|
|
193
|
+
* below, and here for the same reason rather than for symmetry.
|
|
194
|
+
*
|
|
195
|
+
* These were `--text-lg`, `--text-sm` and `--text-base`, which are ALSO
|
|
196
|
+
* Tailwind default theme names, at 1.125rem, 0.875rem and 1rem. Tailwind
|
|
197
|
+
* declares its theme inside `@layer theme` and this file declares in a plain
|
|
198
|
+
* :root, and unlayered CSS beats any cascade layer regardless of source
|
|
199
|
+
* order — so the collision was not two declarations racing, it was this file
|
|
200
|
+
* silently and unconditionally winning. Importing the stylesheet into a
|
|
201
|
+
* Tailwind app resized every `text-lg` and `text-sm` in it, with no warning
|
|
202
|
+
* anywhere; codecave.pro alone uses those two utilities 72 times across 37
|
|
203
|
+
* files. CCWEB2-323.
|
|
204
|
+
*
|
|
205
|
+
* The old scale was also offset by one step from Tailwind's, which is what
|
|
206
|
+
* made it dangerous rather than merely wrong: `--text-caption` is 0.875rem,
|
|
207
|
+
* exactly Tailwind's `text-sm`. Every value looked plausible.
|
|
208
|
+
*
|
|
209
|
+
* `md` stays a size word only because Tailwind has no `--text-md` default.
|
|
210
|
+
* That is luck, not design, and `npm run check:collisions` is what notices
|
|
211
|
+
* if it stops being true. */
|
|
212
|
+
--text-stat: 2.25rem; /* 36px / 40px — stat figures */
|
|
213
|
+
--text-subhead: 1.5rem; /* 24px / 32px — subsection heading */
|
|
214
|
+
--text-md: 1.25rem; /* 20px / 28px — intro copy */
|
|
215
|
+
--text-label: 1.125rem; /* 18px / 28px — names, labels */
|
|
216
|
+
--text-body: 1rem; /* 16px / 24px — body, buttons */
|
|
217
|
+
--text-caption: 0.875rem; /* 14px / 20px — captions, form labels */
|
|
218
|
+
|
|
219
|
+
--leading-stat: 2.5rem;
|
|
220
|
+
--leading-subhead: 2rem;
|
|
221
|
+
--leading-md: 1.75rem;
|
|
222
|
+
--leading-label: 1.75rem;
|
|
223
|
+
--leading-body: 1.5rem;
|
|
224
|
+
--leading-caption: 1.25rem;
|
|
225
|
+
|
|
226
|
+
--font-weight-regular: 400;
|
|
227
|
+
--font-weight-bold: 700;
|
|
228
|
+
|
|
229
|
+
/* =========================================================================
|
|
230
|
+
* RADIUS — the loudest brand signal. Small corners read as off-brand.
|
|
231
|
+
* ====================================================================== */
|
|
232
|
+
/* Named for the thing, not for a t-shirt size. `--radius-sm` / `--radius-md`
|
|
233
|
+
* are also Tailwind default theme names at DIFFERENT values (0.25rem and
|
|
234
|
+
* 0.375rem), so a component authored against one system and rendered in the
|
|
235
|
+
* other silently changed shape — see the checkbox note further down. The
|
|
236
|
+
* remaining six radii were already semantic; these two were the only ones
|
|
237
|
+
* that could collide, and now they cannot. */
|
|
238
|
+
--radius-control-sm:0.25rem; /* 4px — the 16px checkbox box ONLY */
|
|
239
|
+
--radius-control: 0.5rem; /* 8px — inputs, small chips */
|
|
240
|
+
--radius-tile: 0.75rem; /* 12px — inline chips, icon tiles */
|
|
241
|
+
--radius-card: 1.5rem; /* 24px — DEFAULT card, dominant sitewide */
|
|
242
|
+
--radius-article: 2.25rem; /* 36px — article / insight cards */
|
|
243
|
+
--radius-custom: 2.75rem; /* 44px — feature cards (from @theme) */
|
|
244
|
+
--radius-section: 4rem; /* 64px — section panel, <768px */
|
|
245
|
+
--radius-section-md:7.5rem; /* 120px — section panel, >=768px */
|
|
246
|
+
--radius-pill: 9999px; /* buttons, pills, avatars */
|
|
247
|
+
|
|
248
|
+
/* =========================================================================
|
|
249
|
+
* LAYOUT AND RHYTHM
|
|
250
|
+
* ====================================================================== */
|
|
251
|
+
--max-width-desktop: 1280px;
|
|
252
|
+
--breakpoint-sm: 457px;
|
|
253
|
+
|
|
254
|
+
--gutter-base: 0.5rem; /* <768px */
|
|
255
|
+
--gutter-md: 1rem; /* >=768px */
|
|
256
|
+
--gutter-xl: 2rem; /* >=1280px */
|
|
257
|
+
|
|
258
|
+
--section-padding-top: 12.5rem; /* 200px */
|
|
259
|
+
--section-padding-bottom: 7.5rem; /* 120px */
|
|
260
|
+
--card-padding: 2rem 2rem 3rem; /* heavier bottom seats a footer action */
|
|
261
|
+
|
|
262
|
+
--control-height: 3rem; /* 48px — every button and pill */
|
|
263
|
+
--input-height: 4rem; /* 64px — floating-label text input */
|
|
264
|
+
|
|
265
|
+
/* =========================================================================
|
|
266
|
+
* ELEVATION — CODECAVE's light comes from BELOW.
|
|
267
|
+
* Never substitute a conventional downward drop shadow for --shadow-section.
|
|
268
|
+
* ====================================================================== */
|
|
269
|
+
--shadow-section:
|
|
270
|
+
0 -64px 64px 0 hsl(from var(--color-shadow-0) h s l / 0.20),
|
|
271
|
+
0 -10px 24px 0 hsl(from var(--color-shadow-0) h s l / 0.10),
|
|
272
|
+
0 -6px 6px 0 hsl(from var(--color-shadow-0) h s l / 0.08);
|
|
273
|
+
|
|
274
|
+
/* The GlowButton's own halo — VERBATIM from common/GlowButton.vue.
|
|
275
|
+
* These three hues are measured production literals and deliberately are NOT
|
|
276
|
+
* ramp steps: #7A58FF and #5B34FA sit between the progress accent and the
|
|
277
|
+
* action violet, and
|
|
278
|
+
* #4F22FF is bluer than the action violet. Rounding them to the nearest token
|
|
279
|
+
* visibly changes the halo, which is the brand's most recognisable edge. */
|
|
280
|
+
--shadow-glow-button:
|
|
281
|
+
0 0 64px 0 #7A58FFA8,
|
|
282
|
+
0 0 16px 0 #4F22FFA6,
|
|
283
|
+
0 0 4px 2px #5B34FA;
|
|
284
|
+
|
|
285
|
+
/* Focused input halo (source: common/InputText.vue) */
|
|
286
|
+
--shadow-input-focus:
|
|
287
|
+
0 0 16px 0 hsl(from var(--color-brand-500) h s l / 0.5),
|
|
288
|
+
0 0 4px 0 hsl(from var(--color-brand-500) h s l / 0.6);
|
|
289
|
+
--shadow-input-error:
|
|
290
|
+
0 0 16px 0 hsl(from var(--color-error-200) h s l / 0.5),
|
|
291
|
+
0 0 4px 0 hsl(from var(--color-error-100) h s l / 0.6);
|
|
292
|
+
|
|
293
|
+
--transition-base: 200ms ease;
|
|
294
|
+
--transition-slow: 500ms ease;
|
|
295
|
+
/* Tailwind's `transition-colors`, which is what every button in production
|
|
296
|
+
* actually carries — 150ms, not the 200ms used elsewhere in this file. */
|
|
297
|
+
--transition-colors: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* ---------------------------------------------------------------------------
|
|
301
|
+
* Baseline — CODECAVE is dark-first. Near-black page, near-white text.
|
|
302
|
+
* ------------------------------------------------------------------------ */
|
|
303
|
+
|
|
304
|
+
html {
|
|
305
|
+
background: var(--color-surface-primary);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
body {
|
|
309
|
+
margin: 0;
|
|
310
|
+
background: var(--color-surface-primary);
|
|
311
|
+
color: var(--color-body-primary);
|
|
312
|
+
font-family: var(--font-sans);
|
|
313
|
+
font-size: var(--text-body);
|
|
314
|
+
line-height: var(--leading-body);
|
|
315
|
+
-webkit-font-smoothing: antialiased;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
h1, h2, h3, h4, h5, h6 {
|
|
319
|
+
color: var(--color-heading);
|
|
320
|
+
font-weight: var(--font-weight-bold);
|
|
321
|
+
margin: 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
h1 { font-size: var(--text-heading-lg); line-height: var(--text-heading-lg--line-height); }
|
|
325
|
+
h2 { font-size: var(--text-heading-md); line-height: var(--text-heading-md--line-height); }
|
|
326
|
+
h3 { font-size: var(--text-heading-sm); line-height: var(--text-heading-sm--line-height); }
|
|
327
|
+
|
|
328
|
+
a { color: var(--color-action); text-underline-offset: 0.2em; }
|
|
329
|
+
a:hover { color: var(--color-hovered); }
|
|
330
|
+
|
|
331
|
+
/* Every focusable element gets a visible ring. */
|
|
332
|
+
:focus-visible {
|
|
333
|
+
outline: 2px solid var(--color-hovered);
|
|
334
|
+
outline-offset: 2px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* ---------------------------------------------------------------------------
|
|
338
|
+
* Layout primitives — verbatim behaviour from the production stylesheet.
|
|
339
|
+
* ------------------------------------------------------------------------ */
|
|
340
|
+
|
|
341
|
+
.page-container {
|
|
342
|
+
width: 100%;
|
|
343
|
+
min-height: 100%;
|
|
344
|
+
margin: 0 auto;
|
|
345
|
+
padding-left: var(--gutter-base);
|
|
346
|
+
padding-right: var(--gutter-base);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@media (min-width: 768px) {
|
|
350
|
+
.page-container {
|
|
351
|
+
padding-left: var(--gutter-md);
|
|
352
|
+
padding-right: var(--gutter-md);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@media (min-width: 1280px) {
|
|
357
|
+
.page-container {
|
|
358
|
+
max-width: var(--max-width-desktop);
|
|
359
|
+
padding-left: var(--gutter-xl);
|
|
360
|
+
padding-right: var(--gutter-xl);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.section-container {
|
|
365
|
+
position: relative;
|
|
366
|
+
z-index: 50;
|
|
367
|
+
background: var(--color-surface-primary);
|
|
368
|
+
border-radius: var(--radius-section);
|
|
369
|
+
box-shadow: var(--shadow-section);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
@media (min-width: 768px) {
|
|
373
|
+
.section-container { border-radius: var(--radius-section-md); }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* ---------------------------------------------------------------------------
|
|
377
|
+
* The eyebrow — the system's one typographic signature.
|
|
378
|
+
* Bold violet line over a larger light lead line. Opens nearly every section.
|
|
379
|
+
* Source: services/heading.astro, homepage/expertise.astro
|
|
380
|
+
* ------------------------------------------------------------------------ */
|
|
381
|
+
|
|
382
|
+
.eyebrow {
|
|
383
|
+
display: block;
|
|
384
|
+
color: var(--color-action);
|
|
385
|
+
font-weight: var(--font-weight-bold);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.lead {
|
|
389
|
+
display: block;
|
|
390
|
+
color: var(--color-heading);
|
|
391
|
+
font-weight: var(--font-weight-bold);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* ---------------------------------------------------------------------------
|
|
395
|
+
* Accessibility — motion is slow and continuous, and always collapses.
|
|
396
|
+
* ------------------------------------------------------------------------ */
|
|
397
|
+
|
|
398
|
+
@media (prefers-reduced-motion: reduce) {
|
|
399
|
+
*, *::before, *::after {
|
|
400
|
+
animation-duration: 0.01ms !important;
|
|
401
|
+
animation-iteration-count: 1 !important;
|
|
402
|
+
transition-duration: 0.01ms !important;
|
|
403
|
+
scroll-behavior: auto !important;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* ===========================================================================
|
|
408
|
+
* COMPONENT LAYER
|
|
409
|
+
* Geometry, color and state pairs transcribed from the production Vue/Astro
|
|
410
|
+
* components captured in source_examples/. Each block names its source file.
|
|
411
|
+
* ======================================================================== */
|
|
412
|
+
|
|
413
|
+
/* ---------------------------------------------------------------------------
|
|
414
|
+
* Buttons — source: source_examples/common/Button.vue
|
|
415
|
+
* Every control is a 48px pill: min-h-12 px-6 py-1 rounded-full font-bold.
|
|
416
|
+
* ONE solid fill per action. Everything else is bordered, ghost or text.
|
|
417
|
+
*
|
|
418
|
+
* min-height, not height, because that is what production does: Button.vue
|
|
419
|
+
* puts `min-h-12` on the shared base class and `h-11` on two variants, and
|
|
420
|
+
* min-height wins — every button is 48px and the h-11 is dead (CCWEB2-319).
|
|
421
|
+
* Transcribing the RESULT rather than the declarations also survives a label
|
|
422
|
+
* that wraps, which a fixed height would clip.
|
|
423
|
+
* ------------------------------------------------------------------------ */
|
|
424
|
+
|
|
425
|
+
.btn {
|
|
426
|
+
display: inline-flex;
|
|
427
|
+
align-items: center;
|
|
428
|
+
justify-content: center;
|
|
429
|
+
box-sizing: border-box;
|
|
430
|
+
width: fit-content;
|
|
431
|
+
max-width: 100%;
|
|
432
|
+
min-height: var(--control-height);
|
|
433
|
+
padding: 0.25rem 1.5rem;
|
|
434
|
+
border: 0;
|
|
435
|
+
border-radius: var(--radius-pill);
|
|
436
|
+
background: none;
|
|
437
|
+
color: var(--color-body-primary);
|
|
438
|
+
font-family: var(--font-sans);
|
|
439
|
+
font-size: var(--text-body);
|
|
440
|
+
font-weight: var(--font-weight-bold);
|
|
441
|
+
line-height: var(--leading-body);
|
|
442
|
+
text-decoration: none;
|
|
443
|
+
cursor: pointer;
|
|
444
|
+
transition: color var(--transition-colors),
|
|
445
|
+
background-color var(--transition-colors),
|
|
446
|
+
border-color var(--transition-colors);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/* Solid violet — form submits and in-card confirmations. */
|
|
450
|
+
.btn-primary { background: var(--color-brand-500); }
|
|
451
|
+
.btn-primary:hover { background: var(--color-brand-700); }
|
|
452
|
+
.btn-primary:active { background: var(--color-brand-900); }
|
|
453
|
+
|
|
454
|
+
/* Deep-violet fill — the quiet neighbour of a primary. Source fills with
|
|
455
|
+
* brand-900 (#1E113B) and hovers to the tertiary surface. */
|
|
456
|
+
.btn-secondary { background: var(--color-brand-900); }
|
|
457
|
+
.btn-secondary:hover { background: var(--color-surface-tertiary); }
|
|
458
|
+
|
|
459
|
+
/* Bordered — the workhorse. "Learn more" everywhere on the site.
|
|
460
|
+
* The border hovers DOWN to brand-700 (#4004AF, ~1.75:1 on the page): the
|
|
461
|
+
* outline recedes rather than brightening. This is what production ships and
|
|
462
|
+
* the label (#F4F4F6) never changes, so text contrast is untouched.
|
|
463
|
+
* WEBSITE-REVIEW.md sect. 4 flags the direction for a designer decision. */
|
|
464
|
+
.btn-tertiary { border: 1px solid var(--color-brand-500); }
|
|
465
|
+
.btn-tertiary:hover { border-color: var(--color-brand-700); }
|
|
466
|
+
|
|
467
|
+
/* Ghost / text / inline link — nav items and tertiary entry points.
|
|
468
|
+
* Only .btn-ghost re-declares font-bold in Button.vue; the text and link
|
|
469
|
+
* variants ride the inherited regular weight. */
|
|
470
|
+
.btn-ghost { height: auto; padding: 0.5rem 1.5rem;
|
|
471
|
+
color: var(--color-body-primary); }
|
|
472
|
+
.btn-ghost:hover,
|
|
473
|
+
.btn-ghost:active { color: var(--color-hovered); }
|
|
474
|
+
|
|
475
|
+
.btn-text { height: auto; padding: 0;
|
|
476
|
+
font-weight: var(--font-weight-regular);
|
|
477
|
+
color: var(--color-body-secondary-lighter); }
|
|
478
|
+
.btn-text:hover,
|
|
479
|
+
.btn-text:active { color: var(--color-hovered); }
|
|
480
|
+
|
|
481
|
+
.btn-link { height: auto; padding: 0;
|
|
482
|
+
font-weight: var(--font-weight-regular);
|
|
483
|
+
color: var(--color-hovered);
|
|
484
|
+
text-decoration: underline; }
|
|
485
|
+
|
|
486
|
+
/* Disabled is the ONLY state permitted to reduce contrast. */
|
|
487
|
+
.btn:disabled,
|
|
488
|
+
.btn[aria-disabled="true"],
|
|
489
|
+
.btn-disabled { opacity: 0.2; cursor: not-allowed; }
|
|
490
|
+
|
|
491
|
+
/* ---------------------------------------------------------------------------
|
|
492
|
+
* Glow button — source: source_examples/common/GlowButton.vue
|
|
493
|
+
* The single highest-intent CTA. Lighter violet field (#9980FF) with the only
|
|
494
|
+
* dark text in the system, wrapped in a violet halo. At most ONE per action:
|
|
495
|
+
* a long page may repeat it once at the end, never twice in a viewport.
|
|
496
|
+
* ------------------------------------------------------------------------ */
|
|
497
|
+
|
|
498
|
+
.btn-glow {
|
|
499
|
+
position: relative;
|
|
500
|
+
overflow: hidden;
|
|
501
|
+
background: var(--color-glow-25);
|
|
502
|
+
color: var(--color-brand-800);
|
|
503
|
+
box-shadow: var(--shadow-glow-button);
|
|
504
|
+
transition: transform var(--transition-slow);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* No hover fill. The fill stays #9980FF in every state — the response to the
|
|
508
|
+
* pointer is the highlight travelling under it, not a colour swap. */
|
|
509
|
+
.btn-glow:active { transform: scale(0.98); }
|
|
510
|
+
|
|
511
|
+
/* The travelling highlight. Production drives x/y with GSAP quickTo on
|
|
512
|
+
* pointermove; here the resting position (20% / 50%) is the default and a host
|
|
513
|
+
* may animate it by writing --glow-x, as brand-kit.html does. */
|
|
514
|
+
.btn-glow::after {
|
|
515
|
+
content: "";
|
|
516
|
+
position: absolute;
|
|
517
|
+
top: -50%;
|
|
518
|
+
bottom: -50%;
|
|
519
|
+
left: var(--glow-x, 20%);
|
|
520
|
+
z-index: 0;
|
|
521
|
+
width: 50%;
|
|
522
|
+
transform: translateX(-50%);
|
|
523
|
+
pointer-events: none;
|
|
524
|
+
filter: blur(12px);
|
|
525
|
+
background: radial-gradient(ellipse at left center,
|
|
526
|
+
#FFFFFF 27.88%,
|
|
527
|
+
rgba(223, 212, 249, 0.76) 51.92%,
|
|
528
|
+
rgba(153, 128, 255, 0) 100%);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.btn-glow > span { position: relative; z-index: 2; user-select: none; }
|
|
532
|
+
|
|
533
|
+
/* The edge bloom. Two blurred radial fields sit OUTSIDE the pill, 8px past each
|
|
534
|
+
* end, which is why the button is clipped (overflow: hidden) but the bloom is
|
|
535
|
+
* not — in GlowButton.vue they are siblings of the <a>, not children. The left
|
|
536
|
+
* one burns at rest and the right one is dark; production cross-fades them from
|
|
537
|
+
* pointer X across the outer 10% of the width. Wrap the button to get them:
|
|
538
|
+
*
|
|
539
|
+
* <span class="btn-glow-field">
|
|
540
|
+
* <a class="btn btn-glow" href="..."><span>Label</span></a>
|
|
541
|
+
* </span>
|
|
542
|
+
*
|
|
543
|
+
* A host may drive --edge-left / --edge-right on the wrapper to reproduce the
|
|
544
|
+
* cross-fade; without JS the pair renders the resting state exactly. */
|
|
545
|
+
.btn-glow-field { position: relative; display: inline-block; }
|
|
546
|
+
|
|
547
|
+
.btn-glow-field::before,
|
|
548
|
+
.btn-glow-field::after {
|
|
549
|
+
content: "";
|
|
550
|
+
position: absolute;
|
|
551
|
+
z-index: 0;
|
|
552
|
+
top: 50%;
|
|
553
|
+
width: 100%;
|
|
554
|
+
height: 80%;
|
|
555
|
+
transform: translateY(-50%);
|
|
556
|
+
pointer-events: none;
|
|
557
|
+
filter: blur(12px);
|
|
558
|
+
background: radial-gradient(55.23% 55.23% at 50% 50%,
|
|
559
|
+
#FFFFFF 27.88%,
|
|
560
|
+
rgba(223, 212, 249, 0.762963) 51.92%,
|
|
561
|
+
rgba(153, 128, 255, 0) 100%);
|
|
562
|
+
transition: opacity var(--transition-base);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.btn-glow-field::before { left: -0.5rem; opacity: var(--edge-left, 1); }
|
|
566
|
+
.btn-glow-field::after { right: -0.5rem; opacity: var(--edge-right, 0); }
|
|
567
|
+
|
|
568
|
+
.btn-glow-field > .btn-glow { position: relative; z-index: 1; }
|
|
569
|
+
|
|
570
|
+
/* ---------------------------------------------------------------------------
|
|
571
|
+
* Cards — sources: homepage/projects.astro, common/ArticlePreview.vue,
|
|
572
|
+
* homepage/technology-card.vue
|
|
573
|
+
* Depth is never built from surface contrast: the card sits ONE narrow step
|
|
574
|
+
* off the page (#0F0F15 on #0A0A0B). Radius and border carry the separation.
|
|
575
|
+
* ------------------------------------------------------------------------ */
|
|
576
|
+
|
|
577
|
+
.card {
|
|
578
|
+
box-sizing: border-box;
|
|
579
|
+
background: var(--color-surface-secondary);
|
|
580
|
+
border: 1px solid var(--color-surface-quaternary);
|
|
581
|
+
border-radius: var(--radius-card);
|
|
582
|
+
padding: var(--card-padding); /* heavier bottom seats a footer action */
|
|
583
|
+
color: var(--color-body-primary);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.card-article { border-radius: var(--radius-article); }
|
|
587
|
+
|
|
588
|
+
/* Feature card — gradient hairline drawn with mask-composite, blurred
|
|
589
|
+
* backdrop, no fill. Source: homepage/technology-card.vue.
|
|
590
|
+
* Since the rebuild brand-400 is violet (#5F3ABD), so the ring reads violet
|
|
591
|
+
* end to end; cyan survives only in the wash tokens above. */
|
|
592
|
+
.card-feature {
|
|
593
|
+
position: relative;
|
|
594
|
+
box-sizing: border-box;
|
|
595
|
+
border: 0;
|
|
596
|
+
border-radius: var(--radius-custom);
|
|
597
|
+
padding: 2rem;
|
|
598
|
+
backdrop-filter: blur(14px);
|
|
599
|
+
background: linear-gradient(
|
|
600
|
+
hsl(from var(--color-technology-gradient-25) h s l / 0.1),
|
|
601
|
+
hsl(from var(--color-technology-gradient-0) h s l / 0.1),
|
|
602
|
+
hsl(from var(--color-technology-gradient-50) h s l / 0.1)) border-box;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.card-feature::before {
|
|
606
|
+
content: "";
|
|
607
|
+
position: absolute;
|
|
608
|
+
inset: 0;
|
|
609
|
+
border-radius: inherit;
|
|
610
|
+
border: 1px solid transparent;
|
|
611
|
+
pointer-events: none;
|
|
612
|
+
background: linear-gradient(
|
|
613
|
+
hsl(from var(--color-brand-500) h s l / 0.35),
|
|
614
|
+
hsl(from var(--color-brand-400) h s l / 0.675),
|
|
615
|
+
var(--color-brand-500)) border-box;
|
|
616
|
+
mask: linear-gradient(#000, #000) border-box,
|
|
617
|
+
linear-gradient(#000, #000) padding-box;
|
|
618
|
+
mask-composite: subtract;
|
|
619
|
+
-webkit-mask-composite: destination-out;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/* Static chip (tag) — source: project/project-chip.astro.
|
|
623
|
+
* The FORM chips are selectable — see .checkbox-chip further down. */
|
|
624
|
+
.chip {
|
|
625
|
+
display: inline-flex;
|
|
626
|
+
align-items: center;
|
|
627
|
+
gap: 0.5rem;
|
|
628
|
+
padding: 0.5rem 0.75rem;
|
|
629
|
+
border-radius: var(--radius-tile);
|
|
630
|
+
background: var(--color-surface-secondary);
|
|
631
|
+
color: var(--color-body-primary);
|
|
632
|
+
font-size: var(--text-caption);
|
|
633
|
+
line-height: var(--leading-caption);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/* ---------------------------------------------------------------------------
|
|
637
|
+
* Text input — source: source_examples/common/InputText.vue
|
|
638
|
+
* 64px tall, 8px radius (one of only two small radii in the system), a bold
|
|
639
|
+
* 14px floating label seated in the top-left, and a violet focus halo instead
|
|
640
|
+
* of an outline. Filled, hovered and focused are all distinct.
|
|
641
|
+
* ------------------------------------------------------------------------ */
|
|
642
|
+
|
|
643
|
+
.field { position: relative; width: 100%; }
|
|
644
|
+
|
|
645
|
+
.field > label {
|
|
646
|
+
position: absolute;
|
|
647
|
+
top: 0.75rem;
|
|
648
|
+
left: 0.75rem;
|
|
649
|
+
display: flex;
|
|
650
|
+
align-items: center;
|
|
651
|
+
gap: 0.125rem;
|
|
652
|
+
color: var(--color-heading);
|
|
653
|
+
font-size: var(--text-caption);
|
|
654
|
+
font-weight: var(--font-weight-bold);
|
|
655
|
+
pointer-events: none;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.field > label .required { color: var(--color-action); }
|
|
659
|
+
|
|
660
|
+
.field input,
|
|
661
|
+
.field textarea {
|
|
662
|
+
box-sizing: border-box;
|
|
663
|
+
width: 100%;
|
|
664
|
+
padding: 1.75rem 0.75rem 0.75rem;
|
|
665
|
+
border: 0;
|
|
666
|
+
border-radius: var(--radius-control);
|
|
667
|
+
background: var(--color-surface-secondary);
|
|
668
|
+
color: var(--color-hovered);
|
|
669
|
+
font-family: var(--font-sans);
|
|
670
|
+
font-size: var(--text-body);
|
|
671
|
+
line-height: var(--leading-body);
|
|
672
|
+
transition: background-color var(--transition-base),
|
|
673
|
+
box-shadow var(--transition-base);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/* No pinned height — InputText.vue has none. --input-height (64px) emerges
|
|
677
|
+
* from the padding: 28px above one 24px text line, 12px below. */
|
|
678
|
+
|
|
679
|
+
.field textarea {
|
|
680
|
+
min-height: 7.5rem;
|
|
681
|
+
padding-top: 2rem;
|
|
682
|
+
resize: vertical;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.field input::placeholder,
|
|
686
|
+
.field textarea::placeholder {
|
|
687
|
+
color: var(--color-body-secondary);
|
|
688
|
+
font-size: var(--text-caption);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/* Chrome autofill would repaint the field; the source component overrides
|
|
692
|
+
* it (InputText.vue scoped style) and so does the port. */
|
|
693
|
+
.field input:-webkit-autofill {
|
|
694
|
+
box-shadow: 0 0 0 1000px var(--color-surface-tertiary) inset;
|
|
695
|
+
-webkit-text-fill-color: var(--color-heading);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.field input:hover,
|
|
699
|
+
.field textarea:hover { background: var(--color-surface-tertiary); }
|
|
700
|
+
|
|
701
|
+
.field input:focus,
|
|
702
|
+
.field textarea:focus {
|
|
703
|
+
outline: none;
|
|
704
|
+
background: var(--color-surface-tertiary);
|
|
705
|
+
box-shadow: var(--shadow-input-focus);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.field.is-error input,
|
|
709
|
+
.field.is-error textarea { color: var(--color-error-100);
|
|
710
|
+
box-shadow: var(--shadow-input-error); }
|
|
711
|
+
|
|
712
|
+
.field .error-message {
|
|
713
|
+
display: block;
|
|
714
|
+
margin-top: 0.375rem;
|
|
715
|
+
color: var(--color-error-100);
|
|
716
|
+
font-size: var(--text-caption);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/* Checkbox — source: source_examples/common/Checkbox.vue
|
|
720
|
+
* 24px, 8px radius, 2px violet-black border that brightens to --color-action. */
|
|
721
|
+
.checkbox {
|
|
722
|
+
display: inline-flex;
|
|
723
|
+
align-items: center;
|
|
724
|
+
gap: 0.75rem;
|
|
725
|
+
color: var(--color-body-primary);
|
|
726
|
+
font-size: var(--text-caption);
|
|
727
|
+
cursor: pointer;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.checkbox input {
|
|
731
|
+
appearance: none;
|
|
732
|
+
-webkit-appearance: none;
|
|
733
|
+
display: grid;
|
|
734
|
+
place-content: center;
|
|
735
|
+
/* The box is a fixed square, so it must not shrink. Checkbox.vue sizes it
|
|
736
|
+
* with `w-6 h-6` and nothing else, and a Tailwind width is not a floor: put
|
|
737
|
+
* a label long enough to wrap next to it and the flex item gives up width
|
|
738
|
+
* while keeping its height, so the box renders as a tall rounded slot
|
|
739
|
+
* instead of a square. Every consumer with a sentence-length label hits it.
|
|
740
|
+
* Filed against the source in WEBSITE-REVIEW.md sect. 4. */
|
|
741
|
+
flex: none;
|
|
742
|
+
width: 1.5rem;
|
|
743
|
+
height: 1.5rem;
|
|
744
|
+
margin: 0;
|
|
745
|
+
border: 2px solid var(--color-outline-primary-hover);
|
|
746
|
+
/* The control radius, same as every other input and small chip. This used to
|
|
747
|
+
* be a 4px literal with a long apology attached: Checkbox.vue read
|
|
748
|
+
* `var(--radius-sm)`, the site declared no such name, and the box quietly
|
|
749
|
+
* took Tailwind's 0.25rem default. Nobody had chosen 4px. The site now
|
|
750
|
+
* declares `--radius-control: 0.5rem` itself (CCWEB2-313), so the specimen
|
|
751
|
+
* and the site agree on one value under one name and the divergence is
|
|
752
|
+
* gone. */
|
|
753
|
+
border-radius: var(--radius-control);
|
|
754
|
+
cursor: pointer;
|
|
755
|
+
transition: border-color var(--transition-base),
|
|
756
|
+
background-color var(--transition-base);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.checkbox input:hover { border-color: var(--color-action); }
|
|
760
|
+
.checkbox input:checked { background: var(--color-action);
|
|
761
|
+
border-color: var(--color-action); }
|
|
762
|
+
|
|
763
|
+
.checkbox input::before {
|
|
764
|
+
content: "";
|
|
765
|
+
width: 0.75em;
|
|
766
|
+
height: 0.75em;
|
|
767
|
+
transform: scale(0);
|
|
768
|
+
transition: transform var(--transition-base) ease-in-out;
|
|
769
|
+
background: var(--color-heading);
|
|
770
|
+
clip-path: polygon(14% 52%, 0 66%, 39% 100%, 100% 20%, 85% 8%, 37% 72%);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.checkbox input:checked::before { transform: scale(1); }
|
|
774
|
+
|
|
775
|
+
/* Chip checkbox — Checkbox.vue variant="secondary" size="small": the services
|
|
776
|
+
* and budget pickers on the contact form. The chip surface IS the label; the
|
|
777
|
+
* box is 16px and swaps its 2px border for an outline that disappears when
|
|
778
|
+
* checked, exactly as the Tailwind classes in source read. */
|
|
779
|
+
.checkbox-chip {
|
|
780
|
+
gap: 0.5rem;
|
|
781
|
+
padding: 0.5rem 0.75rem;
|
|
782
|
+
border-radius: var(--radius-control);
|
|
783
|
+
background: var(--color-surface-secondary);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.checkbox-chip input {
|
|
787
|
+
width: 1rem;
|
|
788
|
+
height: 1rem;
|
|
789
|
+
/* The -sm step, not --radius-control. The box is 16px, so 8px would be
|
|
790
|
+
* exactly half its side: a perfect circle, i.e. a radio button, i.e.
|
|
791
|
+
* pick-one rendering for a pick-any control. The surrounding pill keeps 8px
|
|
792
|
+
* (`rounded-lg` in source), so the chip reads as a rounded tile holding a
|
|
793
|
+
* small square. Checkbox.vue keys the same distinction off `data-size`. */
|
|
794
|
+
border-radius: var(--radius-control-sm);
|
|
795
|
+
border: 0;
|
|
796
|
+
outline: 2px solid var(--color-surface-quaternary);
|
|
797
|
+
transition: outline-color var(--transition-base),
|
|
798
|
+
background-color var(--transition-base);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.checkbox-chip input:hover { outline-color: var(--color-action); }
|
|
802
|
+
.checkbox-chip input:checked { outline: none; background: var(--color-action); }
|
|
803
|
+
.checkbox-chip input::before { width: 0.5em; height: 0.5em; }
|
|
804
|
+
|
|
805
|
+
/* ---------------------------------------------------------------------------
|
|
806
|
+
* Eyebrow + lead pair and stat figures
|
|
807
|
+
* Sources: services/heading.astro, homepage/expertise.astro, workflow/*.astro
|
|
808
|
+
* ------------------------------------------------------------------------ */
|
|
809
|
+
|
|
810
|
+
.eyebrow-lead { display: grid; gap: 0.25rem; }
|
|
811
|
+
.eyebrow-lead .eyebrow { font-size: var(--text-heading-sm);
|
|
812
|
+
line-height: var(--text-heading-sm--line-height); }
|
|
813
|
+
.eyebrow-lead .lead { font-size: var(--text-heading-sm);
|
|
814
|
+
line-height: var(--text-heading-sm--line-height);
|
|
815
|
+
font-weight: var(--font-weight-regular);
|
|
816
|
+
color: var(--color-body-secondary-lighter); }
|
|
817
|
+
|
|
818
|
+
.stat { display: grid; gap: 0.25rem; }
|
|
819
|
+
.stat-figure { color: var(--color-heading);
|
|
820
|
+
font-size: var(--text-stat);
|
|
821
|
+
line-height: var(--leading-stat);
|
|
822
|
+
font-weight: var(--font-weight-bold); }
|
|
823
|
+
.stat-label { color: var(--color-body-secondary-lighter);
|
|
824
|
+
font-size: var(--text-caption);
|
|
825
|
+
line-height: var(--leading-caption); }
|
|
826
|
+
|
|
827
|
+
.divider { height: 1px; border: 0; margin: 0;
|
|
828
|
+
background: var(--color-surface-quaternary); }
|
|
829
|
+
|
|
830
|
+
/* ---------------------------------------------------------------------------
|
|
831
|
+
* Rule and progress — the gradient line
|
|
832
|
+
* Source: CodeCavePro/brand docs/index.html (.rule), carried forward from the
|
|
833
|
+
* previously published brand site. It is the one element the earlier docs had
|
|
834
|
+
* that this system did not, and it is kept deliberately.
|
|
835
|
+
*
|
|
836
|
+
* This is the ONLY place --gradient-brand is permitted as a field rather than
|
|
837
|
+
* as display type: a 4px pill running violet (#5F20FE) to near-white (#E8E6F0)
|
|
838
|
+
* left to right, capped at 320px. The cap matters — stretched across a full
|
|
839
|
+
* container the gradient flattens into a pale wash and stops reading as brand.
|
|
840
|
+
*
|
|
841
|
+
* .rule static accent under a heading; carries no value
|
|
842
|
+
* .progress determinate bar; the fill width comes from --progress
|
|
843
|
+
* ------------------------------------------------------------------------ */
|
|
844
|
+
|
|
845
|
+
.rule {
|
|
846
|
+
height: 4px;
|
|
847
|
+
max-width: 320px;
|
|
848
|
+
border: 0;
|
|
849
|
+
margin: 0;
|
|
850
|
+
border-radius: var(--radius-pill);
|
|
851
|
+
background: var(--gradient-brand);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/* The track is a border-step surface, never a lighter fill — same rule as a
|
|
855
|
+
* card. Depth here comes from the gradient, not from lightening the ground. */
|
|
856
|
+
.progress {
|
|
857
|
+
height: 4px;
|
|
858
|
+
width: 100%;
|
|
859
|
+
max-width: 320px;
|
|
860
|
+
overflow: hidden;
|
|
861
|
+
border-radius: var(--radius-pill);
|
|
862
|
+
background: var(--color-surface-quaternary);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.progress > .progress-value {
|
|
866
|
+
display: block;
|
|
867
|
+
height: 100%;
|
|
868
|
+
width: var(--progress, 0%);
|
|
869
|
+
border-radius: inherit;
|
|
870
|
+
background: var(--gradient-brand);
|
|
871
|
+
/* The gradient is painted at the bar's full width and then revealed, so the
|
|
872
|
+
* violet end stays anchored at 0% and the near-white end always means done.
|
|
873
|
+
* Sizing the gradient to the fill instead would show near-white at 5%. */
|
|
874
|
+
background-size: var(--progress-track-width, 320px) 100%;
|
|
875
|
+
background-repeat: no-repeat;
|
|
876
|
+
transition: width var(--transition-slow);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/* Indeterminate — a 40% band sweeping the track. Use only when the work has no
|
|
880
|
+
* measurable end; a determinate bar is always the better answer. */
|
|
881
|
+
.progress.is-indeterminate > .progress-value {
|
|
882
|
+
width: 40%;
|
|
883
|
+
background-size: 100% 100%;
|
|
884
|
+
animation: progress-sweep 1.6s ease-in-out infinite;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
@keyframes progress-sweep {
|
|
888
|
+
0% { transform: translateX(-100%); }
|
|
889
|
+
100% { transform: translateX(250%); }
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/* The global reduced-motion collapse above freezes animations at 0.01ms, which
|
|
893
|
+
* would strand the sweep off-screen. Fall back to a filled, quieter track. */
|
|
894
|
+
@media (prefers-reduced-motion: reduce) {
|
|
895
|
+
.progress.is-indeterminate > .progress-value {
|
|
896
|
+
width: 100%;
|
|
897
|
+
transform: none;
|
|
898
|
+
opacity: 0.6;
|
|
899
|
+
}
|
|
900
|
+
}
|