| 1 |
:root { |
| 2 |
|
| 3 |
loads before this file. |
| 4 |
|
| 5 |
`--font-mono` and `--font-sans` are the house slots and `--font-display` |
| 6 |
is MNW's own, an override declared in build.rs. Do not redefine any of the |
| 7 |
three here -- that is the divergence the unification deleted, and this |
| 8 |
file is the one that would win. A product face belongs in the override, |
| 9 |
where the whole tree can read it. |
| 10 |
|
| 11 |
|
| 12 |
INTENT LAYER, the shared theme vocabulary (themeable) |
| 13 |
|
| 14 |
These are the intent tokens emitted by theme_common::intent_css_vars(): |
| 15 |
colors declared by ROLE (surface/content/action/status/line/category), |
| 16 |
plus the derived interactive states (hover/active/selection/row-stripe/ |
| 17 |
contrast) the crate computes once for every product. The values below |
| 18 |
are the platform default (shared/themes/makenotwork.toml resolved) so |
| 19 |
non-creator pages render without injection; creator profile/project/item |
| 20 |
pages inject a <style> that supersedes this block with the chosen theme. |
| 21 |
Generated. Keep in sync with `cargo run --example dump_intents makenotwork`. |
| 22 |
-------------------------------------------------------------------- |
| 23 |
--surface-page: #ddd6c9; |
| 24 |
--surface-raised: #efe9e0; |
| 25 |
--surface-sunken: #cdc5b5; |
| 26 |
--surface-overlay: #efe9e0; |
| 27 |
|
| 28 |
container rather than as bands on a panel. Derived by makeover from |
| 29 |
surface-raised, away from resolved content, so it goes lighter on a light |
| 30 |
theme and darker on a dark one. |
| 31 |
--surface-well: #fffef5; |
| 32 |
|
| 33 |
surface, so they arrive with the rest of the intent set through |
| 34 |
intent_css_vars() and every creator theme gains them automatically. |
| 35 |
--bevel-light: #fffef5; |
| 36 |
--bevel-dark: #b5afa6; |
| 37 |
--content: #3d3530; |
| 38 |
--content-secondary: #5c554f; |
| 39 |
--content-muted: #8a8480; |
| 40 |
--content-on-action: #ffffff; |
| 41 |
--action: #6c5ce7; |
| 42 |
--action-hover: #7a6cf8; |
| 43 |
--danger: #c0392b; |
| 44 |
--success: #27ae60; |
| 45 |
--warning: #f59e0b; |
| 46 |
--info: #17a2b8; |
| 47 |
--border: #b3ab97; |
| 48 |
--border-strong: #a39c88; |
| 49 |
--focus-ring: #6c5ce7; |
| 50 |
--hover-surface: #cdc5b5; |
| 51 |
--category-one: #c0392b; |
| 52 |
--category-two: #27ae60; |
| 53 |
--category-three: #6c5ce7; |
| 54 |
--category-four: #f59e0b; |
| 55 |
--category-five: #a29bfe; |
| 56 |
--category-six: #17a2b8; |
| 57 |
--overlay: rgba(3, 1, 0, 0.5); |
| 58 |
|
| 59 |
scrim above and pinned near-black on every theme, because a shadow is |
| 60 |
absence of light rather than a tint. layout.css composes the geometry |
| 61 |
into --elevation-overlay; a consumer that needs a different geometry |
| 62 |
builds its own from this token rather than from a literal. |
| 63 |
--elevation: rgba(3, 1, 0, 0.18); |
| 64 |
|
| 65 |
|
| 66 |
APP-LOCAL CONSTANTS, not theme colors, not derivable from intents. |
| 67 |
|
| 68 |
Component CSS references the intent tokens above directly (no brand |
| 69 |
aliases). Status backgrounds derive from the intents inline via |
| 70 |
color-mix(); these few stay fixed: |
| 71 |
- pure black/white contrast anchors |
| 72 |
- Stripe's brand color (an external constant) |
| 73 |
- diff add/del, a hard readability convention that must not re-theme |
| 74 |
-------------------------------------------------------------------- |
| 75 |
--primary-dark: #000000; |
| 76 |
--primary-light: #ffffff; |
| 77 |
--stripe: #635BFF; |
| 78 |
|
| 79 |
|
| 80 |
Derived from --action, so it re-tints with the creator's theme. Named |
| 81 |
because the charter names it and because a dozen rules read it. |
| 82 |
--highlight-faint: color-mix(in oklch, var(--action) 8%, transparent); |
| 83 |
|
| 84 |
|
| 85 |
--diff-add: #2d7d2d; |
| 86 |
--diff-add-bg: rgba(45, 125, 45, 0.08); |
| 87 |
--diff-del: #a83232; |
| 88 |
--diff-del-bg: rgba(168, 50, 50, 0.08); |
| 89 |
|
| 90 |
|
| 91 |
--radius-square / -fine / -control / -panel / -round. Containers are |
| 92 |
square and controls keep a corner, so the rounding survives only where a |
| 93 |
thing is meant to be pressed. |
| 94 |
|
| 95 |
|
| 96 |
whole rather than assembled colour by colour. It stays fixed across all |
| 97 |
31 themes on purpose, the same argument as the diff add/del pair above: |
| 98 |
a reader recognises a highlighting palette, and re-tinting it per theme |
| 99 |
costs that recognition to gain nothing. Named so it is legible as one |
| 100 |
palette instead of seven hex literals scattered through the git rules. |
| 101 |
--syntax-keyword: #8959a8; |
| 102 |
--syntax-string: #718c00; |
| 103 |
--syntax-comment: #8e908c; |
| 104 |
--syntax-constant: #f5871f; |
| 105 |
--syntax-entity: #4271ae; |
| 106 |
--syntax-variable: #c82829; |
| 107 |
--syntax-support: #3e999f; |
| 108 |
|
| 109 |
|
| 110 |
generated: this is a claim about MNW's own furniture, not an invariant |
| 111 |
like spacing. The numbers are spaced so a value can be slotted between |
| 112 |
two of them without renumbering, and nothing outside this block should |
| 113 |
write a z-index literal. Anything not listed here does not float. |
| 114 |
--z-raised: 1; |
| 115 |
--z-nav: 10; |
| 116 |
--z-header: 11; |
| 117 |
which have to stay above the panel they open |
| 118 |
--z-dropdown: 20; |
| 119 |
--z-picker: 30; |
| 120 |
--z-context: 40; |
| 121 |
--z-overlay: 50; |
| 122 |
--z-modal: 60; |
| 123 |
--z-toast: 70; |
| 124 |
--z-banner: 80; |
| 125 |
--z-skip-link: 90; |
| 126 |
|
| 127 |
|
| 128 |
|
| 129 |
A surface IN the page takes a bevel: an edge, not a distance. That is |
| 130 |
--shadow-raised/-card/-inset, aliasing the pair layout.css derives from |
| 131 |
each theme's own raised surface. --shadow-raised and --shadow-card |
| 132 |
collapse onto one value because an edge has no heavier and lighter |
| 133 |
version to pick between. |
| 134 |
|
| 135 |
A surface floating OVER the page takes --elevation-overlay, also from |
| 136 |
layout.css, derived from the theme's page colour. Menus, toasts, |
| 137 |
popovers, dropdowns and pickers float. A card, a plate and a framed |
| 138 |
image do not; they are .raised. |
| 139 |
|
| 140 |
There used to be a third kind, --shadow-1/-2/-3, a blurred scale of |
| 141 |
rgba-black literals picked against parchment and then applied to all 31 |
| 142 |
themes a creator can select. It was the same defect the bevel trio was |
| 143 |
rewritten to undo, one scale later, and the elevation intent is what |
| 144 |
replaced it. No literal survives here now, so every consumer re-themes |
| 145 |
without being opened. Geometry stays consumer-side: a site that needs a |
| 146 |
different blur composes it from var(--elevation), never from a literal. |
| 147 |
--shadow-raised: var(--bevel-raised); |
| 148 |
--shadow-card: var(--bevel-raised); |
| 149 |
--shadow-inset: var(--bevel-inset); |
| 150 |
} |
| 151 |
|
| 152 |
|
| 153 |
* { |
| 154 |
margin: 0; |
| 155 |
padding: 0; |
| 156 |
box-sizing: border-box; |
| 157 |
} |
| 158 |
|
| 159 |
|
| 160 |
body { |
| 161 |
margin: 0; |
| 162 |
background-color: var(--surface-page); |
| 163 |
color: var(--content); |
| 164 |
font-family: var(--font-sans); |
| 165 |
line-height: 1.6; |
| 166 |
} |
| 167 |
|
| 168 |
|
| 169 |
.padded-page { |
| 170 |
padding: var(--gap-pane); |
| 171 |
} |
| 172 |
|
| 173 |
|
| 174 |
.centered-page { |
| 175 |
display: flex; |
| 176 |
flex-direction: column; |
| 177 |
justify-content: center; |
| 178 |
align-items: center; |
| 179 |
min-height: 100vh; |
| 180 |
gap: var(--gap-page); |
| 181 |
} |
| 182 |
|
| 183 |
.centered-page h1 { |
| 184 |
font-size: var(--text-hero); |
| 185 |
} |
| 186 |
|
| 187 |
.message-container { |
| 188 |
max-width: 500px; |
| 189 |
text-align: center; |
| 190 |
} |
| 191 |
|
| 192 |
.centered-wrapper { |
| 193 |
justify-content: center; |
| 194 |
text-align: center; |
| 195 |
} |
| 196 |
|
| 197 |
h1 { |
| 198 |
font-family: var(--font-display); |
| 199 |
font-weight: normal; |
| 200 |
color: var(--content); |
| 201 |
text-align: center; |
| 202 |
font-size: var(--text-display); |
| 203 |
} |
| 204 |
|
| 205 |
h2, |
| 206 |
h3 { |
| 207 |
font-family: var(--font-mono); |
| 208 |
font-weight: normal; |
| 209 |
color: var(--content); |
| 210 |
} |
| 211 |
|
| 212 |
|
| 213 |
template must carry one of these so the role is explicit). |
| 214 |
|
| 215 |
.brand-h1: the "Makenot.work" wordmark on auth/wizard pages. |
| 216 |
.page-title: page-level h1 (Young Serif, centered). |
| 217 |
.subtitle-h2: page subtitle h2 used under .brand-h1 in auth/wizards. |
| 218 |
.subsection-title: h2 inside dashboards, tabs, and prose partials |
| 219 |
(mono, no border, the default-h2 role made explicit). |
| 220 |
.section-header: h2 prose sub-section heading with bottom border |
| 221 |
(existing rule, see SECTIONS block). |
| 222 |
.brand-h1, |
| 223 |
.page-title { |
| 224 |
font-family: var(--font-display); |
| 225 |
font-weight: normal; |
| 226 |
color: var(--content); |
| 227 |
text-align: center; |
| 228 |
font-size: var(--text-display); |
| 229 |
} |
| 230 |
|
| 231 |
.subtitle-h2 { |
| 232 |
font-family: var(--font-mono); |
| 233 |
font-weight: normal; |
| 234 |
color: var(--content); |
| 235 |
text-align: center; |
| 236 |
font-size: var(--text-head); |
| 237 |
} |
| 238 |
|
| 239 |
.subsection-title { |
| 240 |
font-family: var(--font-mono); |
| 241 |
font-weight: normal; |
| 242 |
color: var(--content); |
| 243 |
} |
| 244 |
|
| 245 |
p { |
| 246 |
font-family: var(--font-sans); |
| 247 |
color: var(--content); |
| 248 |
} |
| 249 |
|
| 250 |
a { |
| 251 |
color: var(--content); |
| 252 |
text-decoration: none; |
| 253 |
} |
| 254 |
|
| 255 |
a:hover { |
| 256 |
text-decoration: underline; |
| 257 |
} |
| 258 |
|
| 259 |
|
| 260 |
.dot { |
| 261 |
color: var(--action); |
| 262 |
} |
| 263 |
|
| 264 |
|
| 265 |
or comma-grouped page-scoped overrides below for body-class-based control. |
| 266 |
.container { |
| 267 |
max-width: 1200px; |
| 268 |
margin: 0 auto; |
| 269 |
padding: var(--gap-page); |
| 270 |
} |
| 271 |
|
| 272 |
|
| 273 |
per width tier; new pages should add their body class here rather than |
| 274 |
defining a new .foo-page .container rule. |
| 275 |
.delete-account-page .container, |
| 276 |
.receipt-page .container { max-width: 600px; margin: 0 auto; } |
| 277 |
|
| 278 |
|
| 279 |
element the renderer draws the page into. The width is this page's own and |
| 280 |
the rest of the block was the design system's. |
| 281 |
.user-page main { max-width: 600px; } |
| 282 |
|
| 283 |
.import-page .container, |
| 284 |
.export-page .container { max-width: 800px; margin: 0 auto; } |
| 285 |
|
| 286 |
.fan-plus-page main, |
| 287 |
.creators-page main { max-width: 900px; margin: 0 auto; } |
| 288 |
|
| 289 |
|
| 290 |
than .container and with tighter horizontal padding, so it is its own |
| 291 |
primitive rather than a .container width tier. |
| 292 |
|
| 293 |
These four wrappers were four identical rules until 2026-08-16. They are |
| 294 |
grouped rather than replaced by one class because the charter treats a new |
| 295 |
class as a smell, and because grouping keeps each selector's own |
| 296 |
specificity intact. A fifth long-form surface joins this list; it does not |
| 297 |
copy the recipe. |
| 298 |
|
| 299 |
.media-container's rule used to live in media-player.css. It is here now so |
| 300 |
the measure has one definition; that sheet keeps its descendant rules. |
| 301 |
.article-page .article-container, |
| 302 |
.doc-container, |
| 303 |
.docs-index, |
| 304 |
.media-container { |
| 305 |
max-width: 680px; |
| 306 |
margin: 0 auto; |
| 307 |
padding: var(--gap-page) var(--gap-pane) var(--gap-page); |
| 308 |
} |
| 309 |
|
| 310 |
|
| 311 |
BUTTONS |
| 312 |
=========================================== |
| 313 |
|
| 314 |
button { |
| 315 |
color: var(--content); |
| 316 |
background: var(--surface-overlay); |
| 317 |
padding: 6px 12px; |
| 318 |
border: 1px solid var(--content); |
| 319 |
font-family: inherit; |
| 320 |
cursor: pointer; |
| 321 |
box-shadow: var(--shadow-raised); |
| 322 |
transition: box-shadow 0.1s ease, background 0.2s ease; |
| 323 |
} |
| 324 |
|
| 325 |
button:hover { |
| 326 |
background: var(--hover-surface); |
| 327 |
} |
| 328 |
|
| 329 |
button:active { |
| 330 |
box-shadow: var(--shadow-inset); |
| 331 |
} |
| 332 |
|
| 333 |
|
| 334 |
above sets the same surface, bevel and tone the generated class does. This |
| 335 |
file is in @layer components, which the order statement puts after |
| 336 |
`makeover`, so the element rule wins whatever the specificity. Without the |
| 337 |
handoff below a described act renders in --surface-overlay with |
| 338 |
--shadow-raised and, worse, in --content rather than its tone: a |
| 339 |
destructive act looks identical to an ordinary one. Same mechanism and same |
| 340 |
remedy as `.tab.chosen` further down; read the comment there. |
| 341 |
|
| 342 |
The split is deliberate. The app keeps padding, border and font from the |
| 343 |
element rule, makeover keeps surface, bevel and tone. That is the |
| 344 |
arrangement that already makes `.field` work, where the app's input rule |
| 345 |
happens to set only geometry and typography. |
| 346 |
|
| 347 |
Tone is handed back on `[data-tone]` rather than on `.button`, because an |
| 348 |
untoned button has no colour in the layer to roll back to and would land on |
| 349 |
the UA default instead of --content. |
| 350 |
.button, |
| 351 |
.chip, |
| 352 |
.segment, |
| 353 |
.toggle { |
| 354 |
|
| 355 |
background: revert-layer; |
| 356 |
box-shadow: revert-layer; |
| 357 |
} |
| 358 |
|
| 359 |
.button:hover, |
| 360 |
.chip:hover, |
| 361 |
.segment:hover, |
| 362 |
.toggle:hover { |
| 363 |
|
| 364 |
background: revert-layer; |
| 365 |
} |
| 366 |
|
| 367 |
.button:active, |
| 368 |
.chip:active, |
| 369 |
.segment:active, |
| 370 |
.toggle:active { |
| 371 |
|
| 372 |
background: revert-layer; |
| 373 |
box-shadow: revert-layer; |
| 374 |
} |
| 375 |
|
| 376 |
.button[data-tone] { |
| 377 |
|
| 378 |
color: revert-layer; |
| 379 |
} |
| 380 |
|
| 381 |
|
| 382 |
`a { color: var(--content); text-decoration: none }` renders a described |
| 383 |
link as body text -- the reader cannot tell it is a link at all, which is |
| 384 |
the same defect as the lost tone above and worse for being silent. |
| 385 |
|
| 386 |
`button.link` is the second arm: a link that posts is a button in the |
| 387 |
markup, and makeover already flattens it (`background: none`, no border, no |
| 388 |
padding, `font: inherit`). The element rule's chrome beat all four, so it |
| 389 |
rendered as an ordinary raised button. Only the properties makeover sets on |
| 390 |
this class are handed back; `font` is handed back on the button arm alone, |
| 391 |
because an `a.link` has no lower-layer font to roll back to and would land |
| 392 |
on the UA default face. |
| 393 |
.link { |
| 394 |
|
| 395 |
color: revert-layer; |
| 396 |
text-decoration: revert-layer; |
| 397 |
} |
| 398 |
|
| 399 |
button.link { |
| 400 |
|
| 401 |
background: revert-layer; |
| 402 |
border: revert-layer; |
| 403 |
padding: revert-layer; |
| 404 |
font: revert-layer; |
| 405 |
box-shadow: revert-layer; |
| 406 |
cursor: revert-layer; |
| 407 |
} |
| 408 |
|
| 409 |
|
| 410 |
names the element-chrome reset, and a facet control withdraws the button's |
| 411 |
fill, stroke and bevel with it: a value reads flat until it is hovered, |
| 412 |
pressed or in force. The element rule above supplies all three, so without |
| 413 |
this a described facet renders as an ordinary raised button and |
| 414 |
`[aria-pressed="true"]` is its only correct state -- which is the whole of |
| 415 |
what the panel had to say and the one thing it could not. |
| 416 |
|
| 417 |
Wider than the `.button` group above by one property. That group keeps the |
| 418 |
app's border deliberately, because a described act is still a button. Here |
| 419 |
the flat rest look IS the withdrawal of the border, so it is handed back |
| 420 |
with the other two. |
| 421 |
.facet-take, |
| 422 |
.facet-prune { |
| 423 |
|
| 424 |
background: revert-layer; |
| 425 |
border: revert-layer; |
| 426 |
box-shadow: revert-layer; |
| 427 |
} |
| 428 |
|
| 429 |
.facet-take:hover, |
| 430 |
.facet-prune:hover { |
| 431 |
|
| 432 |
background: revert-layer; |
| 433 |
} |
| 434 |
|
| 435 |
.facet-take:active, |
| 436 |
.facet-prune:active, |
| 437 |
.facet-take[aria-pressed="true"], |
| 438 |
.facet-prune[aria-pressed="true"] { |
| 439 |
|
| 440 |
background: revert-layer; |
| 441 |
box-shadow: revert-layer; |
| 442 |
} |
| 443 |
|
| 444 |
|
| 445 |
sets padding and font only, which is why the shipped `.field` precedents |
| 446 |
survived; these two also set `background` and `box-shadow`, so a described |
| 447 |
field on either renders in --surface-sunken rather than the well. The focus |
| 448 |
ring is handed back with them: makeover insets it (`calc(-1 * 2px)`) so it |
| 449 |
does not collide with the bevel, and the app's outer 2px was winning. |
| 450 |
.field { |
| 451 |
|
| 452 |
background: revert-layer; |
| 453 |
box-shadow: revert-layer; |
| 454 |
} |
| 455 |
|
| 456 |
.field:focus-visible { |
| 457 |
|
| 458 |
outline-offset: revert-layer; |
| 459 |
} |
| 460 |
|
| 461 |
.btn-primary { |
| 462 |
background: var(--primary-dark); |
| 463 |
color: var(--primary-light); |
| 464 |
border: none; |
| 465 |
padding: var(--gap-section) var(--gap-pane); |
| 466 |
font-family: inherit; |
| 467 |
font-size: var(--text-body); |
| 468 |
cursor: pointer; |
| 469 |
box-shadow: var(--shadow-raised); |
| 470 |
transition: box-shadow 0.1s ease, background 0.2s ease; |
| 471 |
display: inline-block; |
| 472 |
text-decoration: none; |
| 473 |
} |
| 474 |
|
| 475 |
|
| 476 |
The charter's hover rule is "one step lighter than the resting fill", and on |
| 477 |
a neutral control that step is --hover-surface; on this one it is not, since |
| 478 |
the label is --primary-light and a parchment fill under it is unreadable. |
| 479 |
.btn-primary:hover { |
| 480 |
background: color-mix(in oklch, var(--primary-dark) 85%, var(--primary-light)); |
| 481 |
text-decoration: none; |
| 482 |
} |
| 483 |
|
| 484 |
.btn-primary:active { |
| 485 |
box-shadow: var(--shadow-inset); |
| 486 |
} |
| 487 |
|
| 488 |
.btn-secondary { |
| 489 |
background: var(--surface-sunken); |
| 490 |
color: var(--content); |
| 491 |
border: none; |
| 492 |
padding: var(--gap-section) var(--gap-pane); |
| 493 |
font-family: var(--font-mono); |
| 494 |
font-size: var(--text-body); |
| 495 |
cursor: pointer; |
| 496 |
box-shadow: var(--shadow-raised); |
| 497 |
transition: box-shadow 0.1s ease, background 0.2s ease; |
| 498 |
display: inline-block; |
| 499 |
text-decoration: none; |
| 500 |
} |
| 501 |
|
| 502 |
.btn-secondary:hover { |
| 503 |
background: var(--hover-surface); |
| 504 |
text-decoration: none; |
| 505 |
} |
| 506 |
|
| 507 |
.btn-secondary:active { |
| 508 |
box-shadow: var(--shadow-inset); |
| 509 |
} |
| 510 |
|
| 511 |
.btn-danger { |
| 512 |
background: var(--danger); |
| 513 |
color: var(--primary-light); |
| 514 |
border: none; |
| 515 |
padding: var(--gap-section) var(--gap-pane); |
| 516 |
font-family: var(--font-mono); |
| 517 |
font-size: var(--text-body); |
| 518 |
cursor: pointer; |
| 519 |
box-shadow: var(--shadow-raised); |
| 520 |
transition: box-shadow 0.1s ease, background 0.2s ease; |
| 521 |
display: inline-block; |
| 522 |
text-decoration: none; |
| 523 |
} |
| 524 |
|
| 525 |
|
| 526 |
.btn-danger:hover { |
| 527 |
background: color-mix(in oklch, var(--danger) 85%, var(--primary-light)); |
| 528 |
text-decoration: none; |
| 529 |
} |
| 530 |
|
| 531 |
.btn-danger:active { |
| 532 |
box-shadow: var(--shadow-inset); |
| 533 |
} |
| 534 |
|
| 535 |
|
| 536 |
Applies whether disabled via attribute or aria-disabled. |
| 537 |
button:disabled, |
| 538 |
button[aria-disabled="true"], |
| 539 |
.btn-primary:disabled, |
| 540 |
.btn-secondary:disabled, |
| 541 |
.btn-danger:disabled { |
| 542 |
opacity: 0.5; |
| 543 |
cursor: not-allowed; |
| 544 |
box-shadow: none; |
| 545 |
transform: none; |
| 546 |
} |
| 547 |
|
| 548 |
|
| 549 |
Use these on top of .btn-primary / -secondary / -danger. |
| 550 |
|
| 551 |
.btn--large: large CTA padding bump. |
| 552 |
.btn--icon: square icon-only / micro button (small padding, |
| 553 |
tight line-height). |
| 554 |
.btn--link: visually a link, semantically a button (no bg/ |
| 555 |
border, opacity-fade hover, mono). |
| 556 |
|
| 557 |
Some surfaces have tuned button variants that are NOT compositions |
| 558 |
of these modifiers and keep their own classes: |
| 559 |
.big-button: Young Serif 200×60 anchor on splash pages. |
| 560 |
.order-btn: list reorder up/down (with active flash). |
| 561 |
.play-button: circular media-player play control. |
| 562 |
.speed-button: segment in media-player speed group. |
| 563 |
.shortcuts-help-btn: 1.5rem square help glyph in toolbars. |
| 564 |
.toast-retry-btn: inline bordered button inside a toast, |
| 565 |
uses currentColor to inherit toast tone. |
| 566 |
These are documented variants, not deprecation targets. |
| 567 |
.btn--large { |
| 568 |
padding: var(--gap-section) var(--gap-page); |
| 569 |
font-size: var(--text-lead); |
| 570 |
} |
| 571 |
|
| 572 |
.btn--icon { |
| 573 |
padding: var(--gap-bound) var(--gap-peer); |
| 574 |
font-size: var(--text-note); |
| 575 |
line-height: 1; |
| 576 |
min-width: 1.75rem; |
| 577 |
} |
| 578 |
|
| 579 |
.btn--link { |
| 580 |
background: none; |
| 581 |
color: var(--content); |
| 582 |
border: none; |
| 583 |
padding: 0; |
| 584 |
font-family: var(--font-mono); |
| 585 |
font-size: var(--text-body); |
| 586 |
cursor: pointer; |
| 587 |
|
| 588 |
otherwise cascade from `button` / `form button` when a .btn--link |
| 589 |
happens to be a <button> inside a <form> (e.g. logout). |
| 590 |
box-shadow: none; |
| 591 |
transition: opacity 0.2s ease; |
| 592 |
} |
| 593 |
|
| 594 |
.btn--link:hover { |
| 595 |
opacity: 0.6; |
| 596 |
box-shadow: none; |
| 597 |
} |
| 598 |
|
| 599 |
.btn--link:active { |
| 600 |
box-shadow: var(--shadow-inset); |
| 601 |
} |
| 602 |
|
| 603 |
|
| 604 |
filter panel, expander, or popover. They sit flat rather than raised. |
| 605 |
Add new opener selectors here as they appear. |
| 606 |
.context-menu-btn, |
| 607 |
.discover-filter-toggle, |
| 608 |
.bundle-toggle { |
| 609 |
box-shadow: none; |
| 610 |
} |
| 611 |
|
| 612 |
.context-menu-btn:hover, |
| 613 |
.discover-filter-toggle:hover, |
| 614 |
.bundle-toggle:hover { |
| 615 |
box-shadow: none; |
| 616 |
} |
| 617 |
|
| 618 |
.context-menu-btn:active, |
| 619 |
.discover-filter-toggle:active, |
| 620 |
.bundle-toggle:active { |
| 621 |
box-shadow: var(--shadow-inset); |
| 622 |
} |
| 623 |
|
| 624 |
|
| 625 |
FORMS |
| 626 |
=========================================== |
| 627 |
|
| 628 |
form { |
| 629 |
display: flex; |
| 630 |
flex-direction: column; |
| 631 |
gap: var(--gap-section); |
| 632 |
width: 100%; |
| 633 |
} |
| 634 |
|
| 635 |
.form-container { |
| 636 |
max-width: 400px; |
| 637 |
background: var(--surface-overlay); |
| 638 |
padding: var(--gap-page); |
| 639 |
} |
| 640 |
|
| 641 |
label { |
| 642 |
font-family: var(--font-mono); |
| 643 |
color: var(--content); |
| 644 |
font-size: var(--text-note); |
| 645 |
} |
| 646 |
|
| 647 |
input[type="text"], |
| 648 |
input[type="email"], |
| 649 |
input[type="password"], |
| 650 |
input[type="number"], |
| 651 |
input[type="date"], |
| 652 |
textarea, |
| 653 |
select { |
| 654 |
width: 100%; |
| 655 |
background: var(--surface-sunken); |
| 656 |
border: 1px solid var(--border); |
| 657 |
padding: var(--gap-section); |
| 658 |
font-family: var(--font-mono); |
| 659 |
font-size: var(--text-note); |
| 660 |
color: var(--content); |
| 661 |
box-shadow: var(--shadow-inset); |
| 662 |
} |
| 663 |
|
| 664 |
input::placeholder, |
| 665 |
textarea::placeholder { |
| 666 |
opacity: 0.5; |
| 667 |
} |
| 668 |
|
| 669 |
input[type="text"]:focus, |
| 670 |
input[type="email"]:focus, |
| 671 |
input[type="password"]:focus, |
| 672 |
input[type="number"]:focus, |
| 673 |
textarea:focus, |
| 674 |
select:focus { |
| 675 |
outline: none; |
| 676 |
border: 1px solid var(--action); |
| 677 |
} |
| 678 |
|
| 679 |
textarea { |
| 680 |
min-height: 100px; |
| 681 |
resize: vertical; |
| 682 |
} |
| 683 |
|
| 684 |
select { |
| 685 |
cursor: pointer; |
| 686 |
} |
| 687 |
|
| 688 |
input[type="submit"], |
| 689 |
form button { |
| 690 |
font-family: var(--font-display); |
| 691 |
font-size: var(--text-body); |
| 692 |
color: var(--content); |
| 693 |
background: var(--surface-overlay); |
| 694 |
padding: 12px 24px; |
| 695 |
border: 1px solid var(--content); |
| 696 |
cursor: pointer; |
| 697 |
box-shadow: var(--shadow-raised); |
| 698 |
transition: box-shadow 0.1s ease, background 0.2s ease; |
| 699 |
} |
| 700 |
|
| 701 |
input[type="submit"]:hover, |
| 702 |
form button:hover { |
| 703 |
background: var(--hover-surface); |
| 704 |
} |
| 705 |
|
| 706 |
input[type="submit"]:active, |
| 707 |
form button:active { |
| 708 |
box-shadow: var(--shadow-inset); |
| 709 |
} |
| 710 |
|
| 711 |
.form-group { |
| 712 |
margin-bottom: var(--gap-pane); |
| 713 |
} |
| 714 |
|
| 715 |
.form-group label { |
| 716 |
display: block; |
| 717 |
margin-bottom: var(--gap-peer); |
| 718 |
font-size: var(--text-note); |
| 719 |
} |
| 720 |
|
| 721 |
.form-group input, |
| 722 |
.form-group textarea, |
| 723 |
.form-group select { |
| 724 |
width: 100%; |
| 725 |
} |
| 726 |
|
| 727 |
.form-row { |
| 728 |
display: grid; |
| 729 |
grid-template-columns: 1fr 1fr; |
| 730 |
gap: var(--gap-section); |
| 731 |
} |
| 732 |
|
| 733 |
.hint { |
| 734 |
font-size: var(--text-note); |
| 735 |
opacity: 0.6; |
| 736 |
margin-top: var(--gap-peer); |
| 737 |
} |
| 738 |
|
| 739 |
|
| 740 |
Canonical: `partials/_ui.html` ui::form_field macro. |
| 741 |
.form-group--error label { |
| 742 |
color: var(--danger); |
| 743 |
} |
| 744 |
|
| 745 |
.form-group--error input, |
| 746 |
.form-group--error textarea, |
| 747 |
.form-group--error select { |
| 748 |
border-color: var(--danger); |
| 749 |
} |
| 750 |
|
| 751 |
.field-error { |
| 752 |
font-size: var(--text-note); |
| 753 |
color: var(--danger); |
| 754 |
margin-top: var(--gap-peer); |
| 755 |
} |
| 756 |
|
| 757 |
|
| 758 |
.checkbox-group { |
| 759 |
display: flex; |
| 760 |
align-items: start; |
| 761 |
gap: var(--gap-section); |
| 762 |
cursor: pointer; |
| 763 |
transition: background 0.2s ease; |
| 764 |
margin-bottom: var(--gap-peer); |
| 765 |
} |
| 766 |
|
| 767 |
.checkbox-group input[type="checkbox"] { |
| 768 |
width: auto; |
| 769 |
margin-top: var(--gap-bound); |
| 770 |
margin-bottom: var(--gap-bound); |
| 771 |
cursor: pointer; |
| 772 |
} |
| 773 |
|
| 774 |
.radio-group { |
| 775 |
display: flex; |
| 776 |
flex-direction: column; |
| 777 |
gap: var(--gap-section); |
| 778 |
margin-top: var(--gap-peer); |
| 779 |
} |
| 780 |
|
| 781 |
TABS |
| 782 |
=========================================== |
| 783 |
|
| 784 |
.tabs { |
| 785 |
display: flex; |
| 786 |
flex-wrap: nowrap; |
| 787 |
gap: 0; |
| 788 |
margin-bottom: 0; |
| 789 |
} |
| 790 |
|
| 791 |
|
| 792 |
`:hover` steps to --hover-surface on a fine pointer, `:active` insets. This |
| 793 |
rule used to restate the sunken background verbatim and then carry a second, |
| 794 |
redundant selection signal on top of it — opacity 0.6 unselected, 1 selected |
| 795 |
— so a tab said "not chosen" twice and the generated surface step did |
| 796 |
nothing. The opacity idiom is gone and the surface contrast is the signal. |
| 797 |
.tab { |
| 798 |
color: var(--content); |
| 799 |
border: none; |
| 800 |
padding: var(--gap-section) var(--gap-page); |
| 801 |
font-family: var(--font-mono); |
| 802 |
font-size: var(--text-body); |
| 803 |
cursor: pointer; |
| 804 |
|
| 805 |
`button` shadow if a tab is a <button>. |
| 806 |
box-shadow: none; |
| 807 |
transition: background 0.2s ease; |
| 808 |
white-space: nowrap; |
| 809 |
flex-shrink: 0; |
| 810 |
} |
| 811 |
|
| 812 |
|
| 813 |
This file used to carry a `.tab.is-selected` rule setting the same two |
| 814 |
declarations byte for byte, on the argument that `.is-selected` is the |
| 815 |
site's own idiom across git nav links, editor tabs, section tabs, issue tabs |
| 816 |
and filter buttons, so renaming the tabs alone would split it. That is true |
| 817 |
and it is still the wrong trade: a second name for a state the design system |
| 818 |
already names is invisible only while both say the same thing, and stops |
| 819 |
being invisible the moment makeover changes what a chosen tab looks like. |
| 820 |
The other components keep `.is-selected`, because makeover has no primitive |
| 821 |
for a git nav link; the tab has one. |
| 822 |
|
| 823 |
The chosen arm's box-shadow comes back through the layer rather than being |
| 824 |
restated. `.tab` below zeroes the base `button` shadow, and this file is in |
| 825 |
@layer components, which the order statement puts after `makeover`, so it |
| 826 |
outranks it whatever the specificity: without the revert, the generated |
| 827 |
bevel never applies and a chosen tab reads flat. |
| 828 |
`revert-layer` hands the property back instead of naming a value, which is |
| 829 |
the difference between deferring to the design system and copying it. |
| 830 |
.tab.chosen { |
| 831 |
|
| 832 |
box-shadow: revert-layer; |
| 833 |
} |
| 834 |
|
| 835 |
.tab-more-wrap { |
| 836 |
position: relative; |
| 837 |
flex-shrink: 0; |
| 838 |
} |
| 839 |
|
| 840 |
.tab-overflow-menu { |
| 841 |
position: absolute; |
| 842 |
top: 100%; |
| 843 |
right: 0; |
| 844 |
z-index: var(--z-dropdown); |
| 845 |
background: var(--surface-page); |
| 846 |
border: 1px solid var(--border); |
| 847 |
min-width: 180px; |
| 848 |
box-shadow: var(--elevation-overlay); |
| 849 |
} |
| 850 |
|
| 851 |
|
| 852 |
aligned, tighter. It carried the same opacity pair the strip did and it goes |
| 853 |
for the same reason; hover and selection are the generated surface steps. |
| 854 |
.tab-overflow-menu .tab { |
| 855 |
display: block; |
| 856 |
width: 100%; |
| 857 |
text-align: left; |
| 858 |
padding: var(--gap-group) var(--gap-section); |
| 859 |
} |
| 860 |
|
| 861 |
|
| 862 |
.tab-content { |
| 863 |
display: none; |
| 864 |
background: var(--surface-overlay); |
| 865 |
padding: var(--gap-page); |
| 866 |
} |
| 867 |
|
| 868 |
.tab-content.active { |
| 869 |
display: block; |
| 870 |
} |
| 871 |
|
| 872 |
|
| 873 |
TABLES |
| 874 |
=========================================== |
| 875 |
|
| 876 |
.data-table { |
| 877 |
width: 100%; |
| 878 |
border-collapse: collapse; |
| 879 |
font-size: var(--text-note); |
| 880 |
font-family: var(--font-sans); |
| 881 |
margin-bottom: var(--gap-section); |
| 882 |
} |
| 883 |
|
| 884 |
.data-table thead { |
| 885 |
background: var(--surface-sunken); |
| 886 |
} |
| 887 |
|
| 888 |
.data-table th { |
| 889 |
text-align: left; |
| 890 |
padding: var(--gap-section); |
| 891 |
font-weight: normal; |
| 892 |
} |
| 893 |
|
| 894 |
|
| 895 |
nothing in templates/ ever put `sortable` on a `th`. The one sortable table |
| 896 |
in the app is templates/partials/tabs/project_content.html, which used |
| 897 |
`.sortable-th` further down this file. Both are gone; the affordance and the |
| 898 |
caret are `.table-heading[data-sortable]` in the generated static/layout.css. |
| 899 |
See the block that replaced `.sortable-th` for what stayed behind. |
| 900 |
|
| 901 |
.data-table tbody tr { |
| 902 |
transition: background 0.1s ease; |
| 903 |
} |
| 904 |
|
| 905 |
|
| 906 |
set --surface-overlay — the same value, so the stripe never existed and the |
| 907 |
effect was every row painted the panel's own colour. The table is a .well |
| 908 |
now and the rows sit on it unpainted, which is what the banding was |
| 909 |
reaching for. makeover derives no row-stripe intent, so if one is ever |
| 910 |
wanted it is a makeover-side question, not a literal here. |
| 911 |
|
| 912 |
.data-table tbody tr:hover { |
| 913 |
background: var(--surface-sunken); |
| 914 |
} |
| 915 |
|
| 916 |
.data-table td { |
| 917 |
padding: var(--gap-section); |
| 918 |
border-top: 1px solid var(--border); |
| 919 |
} |
| 920 |
|
| 921 |
|
| 922 |
tables. Lived in the pricing block until the fee calculator replaced it. |
| 923 |
.data-table .highlight { |
| 924 |
background: var(--surface-overlay); |
| 925 |
border-left: 3px solid var(--action); |
| 926 |
} |
| 927 |
|
| 928 |
|
| 929 |
COMPACT TABLES (dashboard/admin, lighter than .data-table) |
| 930 |
=========================================== |
| 931 |
|
| 932 |
.compact-table { |
| 933 |
width: 100%; |
| 934 |
border-collapse: collapse; |
| 935 |
} |
| 936 |
|
| 937 |
.compact-table thead tr { |
| 938 |
font-size: var(--text-note); |
| 939 |
opacity: 0.7; |
| 940 |
} |
| 941 |
|
| 942 |
.compact-table th { |
| 943 |
padding: var(--gap-peer) var(--gap-section); |
| 944 |
text-align: left; |
| 945 |
} |
| 946 |
|
| 947 |
.compact-table td { |
| 948 |
padding: var(--gap-section); |
| 949 |
} |
| 950 |
|
| 951 |
.compact-table tbody tr { |
| 952 |
border-bottom: 1px solid var(--border); |
| 953 |
} |
| 954 |
|
| 955 |
|
| 956 |
UTILITIES |
| 957 |
=========================================== |
| 958 |
|
| 959 |
.text-sm { font-size: var(--text-note); } |
| 960 |
.text-xs { font-size: var(--text-fine); } |
| 961 |
.muted { opacity: 0.7; } |
| 962 |
.dimmed { opacity: 0.6; } |
| 963 |
.dimmer { opacity: 0.5; } |
| 964 |
.meta { font-size: var(--text-note); opacity: 0.7; } |
| 965 |
.nowrap { white-space: nowrap; } |
| 966 |
.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; } |
| 967 |
.text-center { text-align: center; } |
| 968 |
.text-right { text-align: right; } |
| 969 |
.fw-bold { font-weight: bold; } |
| 970 |
.unstyled-link { text-decoration: none; color: inherit; } |
| 971 |
.square-cover { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; } |
| 972 |
|
| 973 |
|
| 974 |
mt = margin-top, mb = margin-bottom, my = vertical, py = vertical padding. |
| 975 |
Named for the relationship, same vocabulary as the --gap-* tokens they |
| 976 |
read. Use sparingly; prefer parent layout primitives. |
| 977 |
.m-0 { margin: 0; } |
| 978 |
.mt-0 { margin-top: 0; } |
| 979 |
.mb-0 { margin-bottom: 0; } |
| 980 |
.mt-peer { margin-top: var(--gap-peer); } |
| 981 |
.mt-section { margin-top: var(--gap-section); } |
| 982 |
.mt-pane { margin-top: var(--gap-pane); } |
| 983 |
.mt-page { margin-top: var(--gap-page); } |
| 984 |
.mb-peer { margin-bottom: var(--gap-peer); } |
| 985 |
.mb-section { margin-bottom: var(--gap-section); } |
| 986 |
.mb-pane { margin-bottom: var(--gap-pane); } |
| 987 |
.my-peer { margin-block: var(--gap-peer); } |
| 988 |
.my-section { margin-block: var(--gap-section); } |
| 989 |
.py-section { padding-block: var(--gap-section); } |
| 990 |
|
| 991 |
|
| 992 |
.minw-300 { min-width: 300px; } |
| 993 |
.minw-400 { min-width: 400px; } |
| 994 |
.minw-500 { min-width: 500px; } |
| 995 |
.minw-600 { min-width: 600px; } |
| 996 |
.minw-700 { min-width: 700px; } |
| 997 |
.minw-800 { min-width: 800px; } |
| 998 |
|
| 999 |
|
| 1000 |
The tiny / row-btn / cart-row-btn aliases are visually equivalent, within |
| 1001 |
sub-pixel of each other in the original templates, and folded into .small. |
| 1002 |
.small { padding: var(--gap-bound) var(--gap-group); font-size: var(--text-fine); } |
| 1003 |
|
| 1004 |
|
| 1005 |
Markup: <div class="empty-state">…</div> (optionally with .empty-state--compact |
| 1006 |
inside dropdowns or .empty-state--chart inside a fixed-height chart slot). |
| 1007 |
.empty-state { text-align: center; padding: var(--gap-page); opacity: 0.6; } |
| 1008 |
.empty-state--compact { padding: var(--gap-section); font-size: var(--text-note); } |
| 1009 |
.empty-state--chart { height: 200px; padding: 0; display: flex; align-items: center; justify-content: center; opacity: 0.5; } |
| 1010 |
.empty-state-hint { font-family: var(--font-mono); font-size: var(--text-fine); margin-top: var(--gap-peer); opacity: 0.6; } |
| 1011 |
|
| 1012 |
.tab-docs { |
| 1013 |
display: flex; |
| 1014 |
justify-content: flex-end; |
| 1015 |
margin-bottom: var(--gap-section); |
| 1016 |
} |
| 1017 |
|
| 1018 |
.tab-docs a { |
| 1019 |
font-family: var(--font-mono); |
| 1020 |
font-size: var(--text-fine); |
| 1021 |
opacity: 0.5; |
| 1022 |
transition: opacity 0.2s ease; |
| 1023 |
} |
| 1024 |
|
| 1025 |
.tab-docs a:hover { |
| 1026 |
opacity: 1; |
| 1027 |
} |
| 1028 |
|
| 1029 |
|
| 1030 |
BADGES |
| 1031 |
=========================================== |
| 1032 |
|
| 1033 |
|
| 1034 |
text on the page, and its status colour comes from `data-tone` — see |
| 1035 |
src/types/badge.rs, which maps every status enum onto a tone name. |
| 1036 |
|
| 1037 |
This used to be a filled pill (near-black ground, --primary-light label) |
| 1038 |
with a local `.badge--live/pending/failed` set painting the fill per |
| 1039 |
status. All of it went in wave 2 tier B2: the fill was carrying exactly |
| 1040 |
what the generated `color` carries, so it was the same thing said in a |
| 1041 |
different channel, and keeping it meant the tone attribute could never do |
| 1042 |
anything. |
| 1043 |
.badge { |
| 1044 |
display: inline-block; |
| 1045 |
padding: var(--gap-bound) var(--gap-peer); |
| 1046 |
font-size: var(--text-fine); |
| 1047 |
font-family: var(--font-mono); |
| 1048 |
} |
| 1049 |
|
| 1050 |
|
| 1051 |
Colours track the brand palette: violet for the cleanest case, warm-tan for |
| 1052 |
disclosed AI use, charcoal for primarily generated. |
| 1053 |
|
| 1054 |
respec-ok: this is a taxonomy makeover has no vocabulary for. Its four |
| 1055 |
tones are info / success / warning / danger, which all read as alarm, and |
| 1056 |
disclosure is not alarm — a handmade item is not a "success" and a |
| 1057 |
generated one is not a "danger". So the three hues stay local. They set |
| 1058 |
`color` rather than a background, because the badge form is the generated |
| 1059 |
one now and only the hue is ours. |
| 1060 |
.badge.ai-tier { |
| 1061 |
letter-spacing: 0.02em; |
| 1062 |
} |
| 1063 |
.badge.ai-tier-handmade { |
| 1064 |
color: var(--action); |
| 1065 |
} |
| 1066 |
.badge.ai-tier-assisted { |
| 1067 |
color: var(--warning); |
| 1068 |
} |
| 1069 |
.badge.ai-tier-generated { |
| 1070 |
color: var(--content); |
| 1071 |
} |
| 1072 |
|
| 1073 |
.item-ai-tier { |
| 1074 |
margin: var(--gap-bound) 0 var(--gap-section); |
| 1075 |
} |
| 1076 |
|
| 1077 |
.ai-disclosure { |
| 1078 |
margin: var(--gap-section) 0; |
| 1079 |
padding: var(--gap-section); |
| 1080 |
background: var(--surface-sunken); |
| 1081 |
border-left: 3px solid var(--warning); |
| 1082 |
} |
| 1083 |
.ai-disclosure-label { |
| 1084 |
font-family: var(--font-mono); |
| 1085 |
font-size: var(--text-fine); |
| 1086 |
text-transform: uppercase; |
| 1087 |
letter-spacing: 0.05em; |
| 1088 |
color: var(--content-muted); |
| 1089 |
margin-bottom: var(--gap-bound); |
| 1090 |
} |
| 1091 |
.ai-disclosure-text { |
| 1092 |
font-size: var(--text-note); |
| 1093 |
color: var(--content); |
| 1094 |
} |
| 1095 |
|
| 1096 |
|
| 1097 |
no pill in the row, so fans skim the listing without disclosure noise |
| 1098 |
and use the explicit filter when they care. |
| 1099 |
.discover-row-ai-tier { |
| 1100 |
font-family: var(--font-mono); |
| 1101 |
font-size: var(--text-fine); |
| 1102 |
color: var(--content-muted); |
| 1103 |
} |
| 1104 |
|
| 1105 |
|
| 1106 |
TAGS |
| 1107 |
=========================================== |
| 1108 |
|
| 1109 |
.tag { |
| 1110 |
display: inline-block; |
| 1111 |
background: var(--primary-dark); |
| 1112 |
color: var(--primary-light); |
| 1113 |
padding: var(--gap-bound) var(--gap-peer); |
| 1114 |
font-size: var(--text-fine); |
| 1115 |
margin-right: var(--gap-bound); |
| 1116 |
} |
| 1117 |
|
| 1118 |
.tag-input { |
| 1119 |
background: var(--surface-sunken); |
| 1120 |
padding: var(--gap-peer); |
| 1121 |
margin-bottom: var(--gap-peer); |
| 1122 |
display: flex; |
| 1123 |
flex-wrap: wrap; |
| 1124 |
gap: var(--gap-peer); |
| 1125 |
min-height: 40px; |
| 1126 |
} |
| 1127 |
|
| 1128 |
.tag-input .tag { |
| 1129 |
display: flex; |
| 1130 |
align-items: center; |
| 1131 |
gap: var(--gap-peer); |
| 1132 |
padding: var(--gap-bound) var(--gap-peer); |
| 1133 |
font-size: var(--text-note); |
| 1134 |
} |
| 1135 |
|
| 1136 |
.tag-input .tag button { |
| 1137 |
background: none; |
| 1138 |
border: none; |
| 1139 |
color: var(--primary-light); |
| 1140 |
cursor: pointer; |
| 1141 |
padding: 0; |
| 1142 |
font-size: var(--text-body); |
| 1143 |
} |
| 1144 |
|
| 1145 |
|
| 1146 |
CARDS |
| 1147 |
=========================================== |
| 1148 |
|
| 1149 |
|
| 1150 |
Variants: |
| 1151 |
.card: filled, hover-step (default content card). |
| 1152 |
.card.card-muted: surface-muted fill, no hover (dashboard stat / analytics blocks). |
| 1153 |
.card.card--bordered: bordered, padded, no fill (marketing cards: tier, fork). |
| 1154 |
.card.card--selectable: radio-card pattern; pair with .is-selected. |
| 1155 |
.card.card--grid: grid-cell card (discover grid). |
| 1156 |
No aliases. A template names the modifier it wants and adds its own |
| 1157 |
component class beside it; a recipe reached only through a per-page name is |
| 1158 |
a recipe nobody can find from the charter. |
| 1159 |
|
| 1160 |
is `--surface-raised` plus `--bevel-raised`, and `:hover` goes to |
| 1161 |
`--hover-surface` on a fine pointer only. Declaring either here would take |
| 1162 |
them back, since style.css is in @layer components, which the order |
| 1163 |
statement puts after `makeover`, whatever the specificity. The box-shadow this used to carry was |
| 1164 |
`--shadow-card`, which already aliases `--bevel-raised`, so only the fill |
| 1165 |
actually moved: `--surface-overlay` to `--surface-raised`. Those are equal on |
| 1166 |
the platform default and a theme may set them apart. |
| 1167 |
.card { |
| 1168 |
padding: var(--gap-pane); |
| 1169 |
margin-bottom: var(--gap-section); |
| 1170 |
text-decoration: none; |
| 1171 |
color: var(--content); |
| 1172 |
display: block; |
| 1173 |
border: 1px solid var(--border); |
| 1174 |
transition: background 0.2s ease; |
| 1175 |
} |
| 1176 |
|
| 1177 |
|
| 1178 |
.card-muted { |
| 1179 |
background: var(--surface-sunken); |
| 1180 |
padding: var(--gap-pane); |
| 1181 |
} |
| 1182 |
|
| 1183 |
component class of their own (.tier-card, .fork-card), so the name the |
| 1184 |
charter teaches is the name in the markup. A described screen names none of |
| 1185 |
them: a card there is a region, and the design system draws it. |
| 1186 |
.card--bordered { |
| 1187 |
background: transparent; |
| 1188 |
padding: var(--gap-section); |
| 1189 |
border: 1px solid var(--border); |
| 1190 |
margin-bottom: 0; |
| 1191 |
} |
| 1192 |
.fork-card { padding: var(--gap-pane); display: flex; flex-direction: column; } |
| 1193 |
|
| 1194 |
.card-title { |
| 1195 |
font-size: var(--text-subhead); |
| 1196 |
margin-bottom: var(--gap-peer); |
| 1197 |
font-family: var(--font-display); |
| 1198 |
} |
| 1199 |
|
| 1200 |
.card-meta { |
| 1201 |
font-size: var(--text-note); |
| 1202 |
color: var(--content-muted); |
| 1203 |
margin-bottom: var(--gap-peer); |
| 1204 |
font-family: var(--font-mono); |
| 1205 |
} |
| 1206 |
|
| 1207 |
.card-description { |
| 1208 |
font-size: var(--text-note); |
| 1209 |
opacity: 0.8; |
| 1210 |
font-family: var(--font-sans); |
| 1211 |
} |
| 1212 |
|
| 1213 |
|
| 1214 |
STATS |
| 1215 |
=========================================== |
| 1216 |
|
| 1217 |
.stats-grid { |
| 1218 |
display: grid; |
| 1219 |
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| 1220 |
gap: var(--gap-section); |
| 1221 |
margin-bottom: var(--gap-pane); |
| 1222 |
} |
| 1223 |
|
| 1224 |
.stats-row { |
| 1225 |
display: flex; |
| 1226 |
gap: var(--gap-pane); |
| 1227 |
margin: var(--gap-pane) 0; |
| 1228 |
} |
| 1229 |
|
| 1230 |
.stat-box { |
| 1231 |
background: var(--surface-overlay); |
| 1232 |
padding: var(--gap-section) var(--gap-pane); |
| 1233 |
flex: 1; |
| 1234 |
text-align: center; |
| 1235 |
} |
| 1236 |
|
| 1237 |
.stat-box .number { |
| 1238 |
font-family: var(--font-display); |
| 1239 |
font-weight: bold; |
| 1240 |
font-size: var(--text-head); |
| 1241 |
} |
| 1242 |
|
| 1243 |
.stat-box .label { |
| 1244 |
font-size: var(--text-fine); |
| 1245 |
opacity: 0.7; |
| 1246 |
} |
| 1247 |
|
| 1248 |
|
| 1249 |
|
| 1250 |
.stat-label { |
| 1251 |
font-size: var(--text-note); |
| 1252 |
opacity: 0.7; |
| 1253 |
margin-bottom: var(--gap-bound); |
| 1254 |
} |
| 1255 |
|
| 1256 |
.stat-value { |
| 1257 |
font-size: var(--text-head); |
| 1258 |
} |
| 1259 |
|
| 1260 |
.stat-change { |
| 1261 |
font-size: var(--text-note); |
| 1262 |
opacity: 0.6; |
| 1263 |
} |
| 1264 |
|
| 1265 |
.stat-change.positive { |
| 1266 |
color: var(--success); |
| 1267 |
} |
| 1268 |
|
| 1269 |
.stat-change.negative { |
| 1270 |
color: var(--danger); |
| 1271 |
} |
| 1272 |
|
| 1273 |
|
| 1274 |
FILTER BAR |
| 1275 |
=========================================== |
| 1276 |
|
| 1277 |
.filter-bar { |
| 1278 |
display: flex; |
| 1279 |
gap: var(--gap-peer); |
| 1280 |
margin: var(--gap-pane) 0; |
| 1281 |
flex-wrap: wrap; |
| 1282 |
align-items: center; |
| 1283 |
} |
| 1284 |
|
| 1285 |
.filter-bar > .label { |
| 1286 |
font-size: var(--text-note); |
| 1287 |
opacity: 0.7; |
| 1288 |
margin-right: var(--gap-peer); |
| 1289 |
} |
| 1290 |
|
| 1291 |
.filter-bar button { |
| 1292 |
padding: var(--gap-bound) var(--gap-section); |
| 1293 |
font-size: var(--text-note); |
| 1294 |
} |
| 1295 |
|
| 1296 |
|
| 1297 |
DISCUSSION SECTION |
| 1298 |
=========================================== |
| 1299 |
|
| 1300 |
|
| 1301 |
margin: 2rem auto, which looked indented on wide pages (.library-page, |
| 1302 |
.item-page) where every neighbouring card filled the container. Let the |
| 1303 |
parent dictate width, article-page already constrains via |
| 1304 |
.article-container; player pages have their own narrow layout. |
| 1305 |
.discussion-section { |
| 1306 |
margin: var(--gap-page) 0; |
| 1307 |
padding: var(--gap-pane); |
| 1308 |
border: 1px solid var(--border); |
| 1309 |
font-family: var(--font-mono); |
| 1310 |
} |
| 1311 |
|
| 1312 |
.discussion-section h3 { |
| 1313 |
font-family: var(--font-display); |
| 1314 |
font-size: var(--text-subhead); |
| 1315 |
margin: 0 0 var(--gap-peer); |
| 1316 |
} |
| 1317 |
|
| 1318 |
.discussion-section p { |
| 1319 |
margin: 0 0 var(--gap-section); |
| 1320 |
color: var(--content-muted); |
| 1321 |
font-size: var(--text-note); |
| 1322 |
} |
| 1323 |
|
| 1324 |
.discussion-section a { |
| 1325 |
color: var(--action); |
| 1326 |
text-decoration: none; |
| 1327 |
font-size: var(--text-note); |
| 1328 |
} |
| 1329 |
|
| 1330 |
|
| 1331 |
NOTIFICATION SURFACES (info-box, warning-box, error-message) |
| 1332 |
=========================================== |
| 1333 |
|
| 1334 |
|
| 1335 |
Five distinct components. Keep them distinct, don't merge: |
| 1336 |
.toast: transient, JS-dismissible, bottom-right corner. |
| 1337 |
.banner: full-bleed page-top notice (sandbox, restart, |
| 1338 |
founder pricing). One short sentence + optional link. |
| 1339 |
.alert: inline directive callout with uppercase mono title |
| 1340 |
(NOTE / TIP / IMPORTANT / WARNING / CAUTION). Used in |
| 1341 |
docs and longer-form bodies. |
| 1342 |
.info-box: informational headed block with h3 + bullet list |
| 1343 |
inside forms / wizards (distinct register from .alert). |
| 1344 |
.warning-box: loud yellow-on-yellow attention-grabber for |
| 1345 |
page-level warnings (delete confirms, account |
| 1346 |
warnings). Louder than .alert-warning intentionally. |
| 1347 |
.error-message: inline form-field error; hidden by default, shown |
| 1348 |
with .is-active. Distinct from page-level .alert. |
| 1349 |
.info-box { |
| 1350 |
background: var(--surface-sunken); |
| 1351 |
padding: var(--gap-section); |
| 1352 |
margin-bottom: var(--gap-section); |
| 1353 |
font-size: var(--text-note); |
| 1354 |
font-family: var(--font-sans); |
| 1355 |
} |
| 1356 |
|
| 1357 |
.info-box h3 { |
| 1358 |
font-size: var(--text-body); |
| 1359 |
font-weight: normal; |
| 1360 |
margin-bottom: var(--gap-peer); |
| 1361 |
} |
| 1362 |
|
| 1363 |
.info-box ul { |
| 1364 |
list-style: none; |
| 1365 |
margin-top: var(--gap-peer); |
| 1366 |
} |
| 1367 |
|
| 1368 |
.info-box li { |
| 1369 |
padding: var(--gap-bound) 0; |
| 1370 |
} |
| 1371 |
|
| 1372 |
.info-box li::before { |
| 1373 |
content: "- "; |
| 1374 |
opacity: 0.5; |
| 1375 |
} |
| 1376 |
|
| 1377 |
.warning-box { |
| 1378 |
background: var(--warning); |
| 1379 |
color: var(--primary-light); |
| 1380 |
padding: var(--gap-section); |
| 1381 |
margin-bottom: var(--gap-section); |
| 1382 |
font-size: var(--text-note); |
| 1383 |
font-family: var(--font-sans); |
| 1384 |
} |
| 1385 |
|
| 1386 |
.error-message { |
| 1387 |
font-family: var(--font-mono); |
| 1388 |
color: var(--danger); |
| 1389 |
background: color-mix(in oklch, var(--danger) 10%, var(--surface-page)); |
| 1390 |
padding: 12px; |
| 1391 |
border: 1px solid var(--danger); |
| 1392 |
border-radius: var(--radius-fine); |
| 1393 |
text-align: center; |
| 1394 |
font-size: var(--text-note); |
| 1395 |
display: none; |
| 1396 |
} |
| 1397 |
|
| 1398 |
|
| 1399 |
ERROR PAGE |
| 1400 |
=========================================== |
| 1401 |
|
| 1402 |
|
| 1403 |
the inline `.error-message` form-error class above; named to avoid |
| 1404 |
collision. |
| 1405 |
.error-page { |
| 1406 |
min-height: 100vh; |
| 1407 |
display: flex; |
| 1408 |
align-items: center; |
| 1409 |
justify-content: center; |
| 1410 |
padding: var(--gap-page); |
| 1411 |
background: var(--surface-page); |
| 1412 |
} |
| 1413 |
|
| 1414 |
.error-container { |
| 1415 |
text-align: center; |
| 1416 |
max-width: 560px; |
| 1417 |
} |
| 1418 |
|
| 1419 |
.error-status { |
| 1420 |
font-size: var(--text-note); |
| 1421 |
font-weight: 600; |
| 1422 |
letter-spacing: 0.08em; |
| 1423 |
text-transform: uppercase; |
| 1424 |
color: var(--content-muted); |
| 1425 |
margin-bottom: var(--gap-section); |
| 1426 |
} |
| 1427 |
|
| 1428 |
.error-page-message { |
| 1429 |
font-size: var(--text-head); |
| 1430 |
line-height: 1.4; |
| 1431 |
color: var(--content); |
| 1432 |
margin-bottom: var(--gap-page); |
| 1433 |
} |
| 1434 |
|
| 1435 |
.error-actions { |
| 1436 |
display: flex; |
| 1437 |
gap: var(--gap-section); |
| 1438 |
justify-content: center; |
| 1439 |
} |
| 1440 |
|
| 1441 |
|
| 1442 |
|
| 1443 |
BUY PAGE (standalone direct purchase) |
| 1444 |
=========================================== |
| 1445 |
|
| 1446 |
|
| 1447 |
page used for link-in-bio sharing, no site chrome, just a card. |
| 1448 |
Scoped under .buy-page so the rules don't bleed into the rest of |
| 1449 |
the app. Tokenized; previously lived as a page-isolated <style>. |
| 1450 |
.buy-page { |
| 1451 |
min-height: 100vh; |
| 1452 |
display: flex; |
| 1453 |
align-items: center; |
| 1454 |
justify-content: center; |
| 1455 |
padding: var(--gap-page); |
| 1456 |
} |
| 1457 |
|
| 1458 |
|
| 1459 |
page's content rather than something laid over it, so it takes .raised |
| 1460 |
from layout.css for its surface and its edge, and keeps only the geometry |
| 1461 |
the generated primitive does not carry. |
| 1462 |
.buy-page .buy-card { |
| 1463 |
border-radius: var(--radius-panel); |
| 1464 |
padding: var(--gap-page); |
| 1465 |
max-width: 420px; |
| 1466 |
width: 100%; |
| 1467 |
} |
| 1468 |
|
| 1469 |
.buy-page .cover, |
| 1470 |
.buy-page .no-cover { |
| 1471 |
width: 100%; |
| 1472 |
aspect-ratio: 1; |
| 1473 |
max-height: 280px; |
| 1474 |
border-radius: var(--radius-panel); |
| 1475 |
margin-bottom: var(--gap-pane); |
| 1476 |
background: var(--surface-raised); |
| 1477 |
} |
| 1478 |
|
| 1479 |
.buy-page .cover { |
| 1480 |
object-fit: cover; |
| 1481 |
} |
| 1482 |
|
| 1483 |
.buy-page .no-cover { |
| 1484 |
display: flex; |
| 1485 |
align-items: center; |
| 1486 |
justify-content: center; |
| 1487 |
font-size: var(--text-hero); |
| 1488 |
opacity: 0.2; |
| 1489 |
} |
| 1490 |
|
| 1491 |
.buy-page h1 { |
| 1492 |
text-align: left; |
| 1493 |
font-size: var(--text-head); |
| 1494 |
margin-bottom: var(--gap-bound); |
| 1495 |
} |
| 1496 |
|
| 1497 |
.buy-page .creator { |
| 1498 |
font-family: var(--font-mono); |
| 1499 |
font-size: var(--text-note); |
| 1500 |
opacity: 0.6; |
| 1501 |
margin-bottom: var(--gap-pane); |
| 1502 |
} |
| 1503 |
|
| 1504 |
.buy-page .creator a { |
| 1505 |
color: inherit; |
| 1506 |
} |
| 1507 |
|
| 1508 |
.buy-page .price { |
| 1509 |
font-family: var(--font-mono); |
| 1510 |
font-size: var(--text-subhead); |
| 1511 |
margin-bottom: var(--gap-pane); |
| 1512 |
} |
| 1513 |
|
| 1514 |
.buy-page .description { |
| 1515 |
font-size: var(--text-note); |
| 1516 |
line-height: 1.5; |
| 1517 |
opacity: 0.8; |
| 1518 |
margin-bottom: var(--gap-pane); |
| 1519 |
max-height: 4.5em; |
| 1520 |
overflow: hidden; |
| 1521 |
} |
| 1522 |
|
| 1523 |
.buy-page .buy-btn { |
| 1524 |
display: block; |
| 1525 |
width: 100%; |
| 1526 |
padding: 14px; |
| 1527 |
background: var(--action); |
| 1528 |
color: var(--primary-light); |
| 1529 |
border: none; |
| 1530 |
border-radius: var(--radius-panel); |
| 1531 |
font-size: var(--text-body); |
| 1532 |
font-weight: 600; |
| 1533 |
cursor: pointer; |
| 1534 |
text-align: center; |
| 1535 |
text-decoration: none; |
| 1536 |
transition: opacity 0.2s ease; |
| 1537 |
} |
| 1538 |
|
| 1539 |
.buy-page .buy-btn:hover { |
| 1540 |
opacity: 0.85; |
| 1541 |
} |
| 1542 |
|
| 1543 |
.buy-page .buy-btn:disabled { |
| 1544 |
opacity: 0.6; |
| 1545 |
cursor: wait; |
| 1546 |
} |
| 1547 |
|
| 1548 |
.buy-page .pwyw-input { |
| 1549 |
display: flex; |
| 1550 |
align-items: center; |
| 1551 |
gap: var(--gap-bound); |
| 1552 |
margin-bottom: var(--gap-section); |
| 1553 |
} |
| 1554 |
|
| 1555 |
.buy-page .pwyw-input input { |
| 1556 |
width: 100px; |
| 1557 |
padding: 8px; |
| 1558 |
border: 1px solid var(--border); |
| 1559 |
border-radius: var(--radius-control); |
| 1560 |
font-size: var(--text-body); |
| 1561 |
} |
| 1562 |
|
| 1563 |
.buy-page .note { |
| 1564 |
font-size: var(--text-fine); |
| 1565 |
opacity: 0.5; |
| 1566 |
text-align: center; |
| 1567 |
margin-top: var(--gap-section); |
| 1568 |
} |
| 1569 |
|
| 1570 |
.buy-page .note a { |
| 1571 |
color: var(--action); |
| 1572 |
} |
| 1573 |
|
| 1574 |
.buy-page .footer { |
| 1575 |
text-align: center; |
| 1576 |
margin-top: var(--gap-pane); |
| 1577 |
padding-top: var(--gap-section); |
| 1578 |
border-top: 1px solid var(--border); |
| 1579 |
font-size: var(--text-fine); |
| 1580 |
opacity: 0.5; |
| 1581 |
} |
| 1582 |
|
| 1583 |
.buy-page .footer a { |
| 1584 |
color: inherit; |
| 1585 |
} |
| 1586 |
|
| 1587 |
|
| 1588 |
SECTIONED CONTENT (tabs + panels, shared by three pages) |
| 1589 |
=========================================== |
| 1590 |
|
| 1591 |
|
| 1592 |
library_downloads.html). Tab+panel pattern shared by all three |
| 1593 |
pages, kept global, NOT scoped under .item-page. |
| 1594 |
.section-tabs { |
| 1595 |
display: flex; |
| 1596 |
gap: 0; |
| 1597 |
border-bottom: 1px solid var(--border); |
| 1598 |
margin-bottom: var(--gap-pane); |
| 1599 |
} |
| 1600 |
|
| 1601 |
.section-tab { |
| 1602 |
background: none; |
| 1603 |
border: none; |
| 1604 |
padding: var(--gap-group) var(--gap-section); |
| 1605 |
cursor: pointer; |
| 1606 |
font-size: var(--text-body); |
| 1607 |
opacity: 0.6; |
| 1608 |
border-bottom: 2px solid transparent; |
| 1609 |
font-family: var(--font-sans); |
| 1610 |
color: var(--content); |
| 1611 |
} |
| 1612 |
|
| 1613 |
.section-tab.is-selected { |
| 1614 |
opacity: 1; |
| 1615 |
border-bottom-color: var(--content); |
| 1616 |
} |
| 1617 |
|
| 1618 |
.section-tab:hover { opacity: 0.9; } |
| 1619 |
|
| 1620 |
|
| 1621 |
not a selection, kept as .active. |
| 1622 |
.section-panel { display: none; } |
| 1623 |
.section-panel.active { display: block; } |
| 1624 |
.section-panel p { margin-bottom: var(--gap-section); } |
| 1625 |
.section-panel ul, |
| 1626 |
.section-panel ol { |
| 1627 |
margin-left: var(--gap-pane); |
| 1628 |
margin-bottom: var(--gap-section); |
| 1629 |
} |
| 1630 |
.section-panel li { margin-bottom: var(--gap-peer); } |
| 1631 |
.section-panel h1, |
| 1632 |
.section-panel h2, |
| 1633 |
.section-panel h3 { |
| 1634 |
margin-top: var(--gap-section); |
| 1635 |
margin-bottom: var(--gap-peer); |
| 1636 |
} |
| 1637 |
.section-panel pre { |
| 1638 |
background: var(--surface-sunken); |
| 1639 |
padding: var(--gap-section); |
| 1640 |
overflow-x: auto; |
| 1641 |
margin-bottom: var(--gap-section); |
| 1642 |
} |
| 1643 |
.section-panel code { font-size: var(--text-note); } |
| 1644 |
|
| 1645 |
|