Files
runicgateway.com/src/styles/global.css
wtclaude 2d19ee4220
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
feat(marketing): phase 4 — the marketing pages
PLAN.md §13 phase 4: /features/, /architecture/, /modules/, /integrations/, and
/community/ — plus the two scope items the phase table never assigned to anyone.

Six decisions taken by the org lead before coding, recorded in PLAN.md §10 as
D20-D25:

- D20 /features/ is the homepage's list with a `detail` line, not a second list.
  One data file, two renderings, so they cannot disagree about what exists.
- D21 /architecture/ draws reasons, not reference: three new inline SVGs, one per
  boundary. No endpoint tables, no config keys — those are phase 8's and stay
  canonical in docs/.
- D22 The deliberate absences of §2 become one tagged data file, rendered on the
  three pages that promise them.
- D23 Phase 4 absorbs /community/ (specified in §10 and §14 N3, linked from the
  header since phase 1, built by no phase) and checkLinks.mjs.
- D24 `needsModule`: writing the Teams detail exposed a false claim phase 3
  shipped. Teams are module-sourced only — teams.module_id is NOT NULL, there is
  no create route, sync is gated on providerModuleId() — so the Community group
  no longer says a bare core does all of it.
- D25 The per-capability demo affordance brand.json had promised since phase 2 is
  a deep link, filled at boot from data-demo-path.

checkLinks.mjs reads the built HTML rather than src/, because half these links
are assembled from data files and template literals. Its PLANNED_ROUTES list is
checked in both directions, so it cannot rot into a permanent exemption.

applyBrand.mjs gained a pass that recomputes deep links from their immutable
path, making it idempotent and reversible; checkBrand.mjs lifts that pattern out
and runs it against the stock markup so the two cannot drift. Both proved
against a real mount, in both directions.

Fixes a cascade bug the checks could not see: [data-demo-url=''] and a scoped
component class are both specificity 0,1,0, so .demo-link's `display` beat the
hide rule and twelve links to a nonexistent demo rendered, each resolving to the
current page. The rule is now !important.

The four diagrams' shared SVG vocabulary moved to src/styles/diagram.css.

Verified from a clean checkout: npm ci, all five checks, astro check (0 errors),
production build, and a live browser pass at desktop and 390px.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 01:53:11 -05:00

481 lines
12 KiB
CSS

/* ============================================================================
runicgateway.com — the layout shell
============================================================================
Every value here is a token from tokens.css. No colour literal appears below
this comment; `scripts/checkTokens.mjs` fails the build if one does.
-------------------------------------------------------------------------- */
@import '@fontsource-variable/cinzel';
@import '@fontsource-variable/inter';
/* The SVG diagram vocabulary and the figure-beside-prose layout, shared by the
homepage's data path and `/architecture/`'s three. Its own file because it is
a self-contained language rather than part of the shell — see its header for
the two rules every diagram on this site follows. */
@import './diagram.css';
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--sans);
font-size: 1rem;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
h1,
h2,
h3,
h4 {
color: var(--head);
line-height: 1.2;
text-wrap: balance;
}
h1 {
font-family: var(--display);
font-weight: 600;
letter-spacing: 0.01em;
}
p {
text-wrap: pretty;
}
a {
color: var(--accent);
text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent);
text-underline-offset: 0.18em;
}
a:hover {
color: var(--accent-bright);
text-decoration-color: currentColor;
}
:focus-visible {
outline: 2px solid var(--portal-bright);
outline-offset: 3px;
border-radius: var(--radius-input);
}
code,
pre,
kbd,
samp {
font-family: var(--mono);
font-size: 0.9em;
}
hr {
border: 0;
border-top: 1px solid var(--line-soft);
margin: 2.5rem 0;
}
img,
svg {
max-width: 100%;
height: auto;
}
/* ---- Page frame --------------------------------------------------------- */
.page {
width: 100%;
max-width: var(--page-max);
margin-inline: auto;
padding-inline: var(--gutter);
}
.site {
display: flex;
min-height: 100vh;
flex-direction: column;
}
.site > main {
flex: 1;
}
/* The keyboard escape hatch past the header nav. Visible only when focused. */
.skip-link {
position: absolute;
left: var(--gutter);
top: 0;
z-index: 100;
transform: translateY(-140%);
padding: 0.6rem 1rem;
border: 1px solid var(--gold-deep);
border-radius: var(--radius-input);
background: var(--panel-flat);
color: var(--ink);
font-size: 0.9rem;
text-decoration: none;
transition: transform 0.15s ease;
}
.skip-link:focus {
transform: translateY(12px);
}
/* ---- Header ------------------------------------------------------------- */
.site-header {
position: sticky;
top: 0;
z-index: 50;
min-height: var(--header-h);
border-bottom: 1px solid var(--line-soft);
background: color-mix(in srgb, var(--bg-deep) 88%, transparent);
backdrop-filter: blur(10px);
}
.site-header__inner {
display: flex;
min-height: var(--header-h);
align-items: center;
justify-content: space-between;
gap: var(--gutter);
}
.brand-lockup {
display: inline-flex;
align-items: center;
gap: 0.65rem;
color: var(--ink);
text-decoration: none;
}
.brand-lockup__mark {
width: 40px;
height: 40px;
flex: none;
}
.brand-lockup__name {
font-family: var(--display);
font-size: 1.12rem;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--gold);
}
.site-nav {
display: flex;
align-items: center;
gap: 0.25rem;
}
.site-nav a {
padding: 0.45rem 0.7rem;
border-radius: var(--radius-input);
color: var(--muted);
font-size: 0.94rem;
text-decoration: none;
}
.site-nav a:hover {
background: var(--panel-flat);
color: var(--ink);
}
.site-nav a[aria-current='page'] {
color: var(--gold);
}
/* Phase 1 left the mobile nav to phase 3, expecting a disclosure control. It got
a wrap instead, and deliberately: with four links there is nothing to disclose.
The lockup keeps the first row, the links take the second, and the whole thing
stays four keyboard stops with no state, no script and no duplicate markup —
all three of which a hamburger would have cost.
Phase 3 found the bug this fixes by rendering the homepage in a 390px frame:
the four links plus the lockup measured 433px against a 390px viewport, so
every phone got a horizontally scrolling page. Shrinking the type further was
the tempting fix and would only have moved the failure to the next narrow
screen. */
@media (max-width: 720px) {
.site-header__inner {
flex-wrap: wrap;
justify-content: flex-start;
row-gap: 0.15rem;
padding-block: 0.55rem;
min-height: 0;
}
.site-nav {
flex-wrap: wrap;
width: 100%;
gap: 0;
/* Pull the first link's own padding back to the gutter so the row of links
lines up with the lockup above it rather than sitting indented. */
margin-left: -0.45rem;
}
.site-nav a {
padding-inline: 0.45rem;
font-size: 0.86rem;
}
}
/* ---- Footer ------------------------------------------------------------- */
.site-footer {
margin-top: 4rem;
border-top: 1px solid var(--line-soft);
background: var(--bg-deep);
padding-block: 2.5rem 2rem;
color: var(--dim);
font-size: 0.9rem;
}
.site-footer__cols {
display: grid;
gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.site-footer h2 {
margin: 0 0 0.7rem;
color: var(--muted);
font-family: var(--sans);
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.11em;
text-transform: uppercase;
}
.site-footer ul {
margin: 0;
padding: 0;
list-style: none;
}
.site-footer li + li {
margin-top: 0.4rem;
}
.site-footer a {
color: var(--muted);
text-decoration: none;
}
.site-footer a:hover {
color: var(--accent-bright);
text-decoration: underline;
}
.site-footer__legal {
margin-top: 2.25rem;
padding-top: 1.25rem;
border-top: 1px solid var(--line-soft);
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.25rem;
align-items: baseline;
justify-content: space-between;
}
.site-footer__legal p {
margin: 0;
}
/* ---- Panels ------------------------------------------------------------- */
.panel {
border: 1px solid var(--line);
border-radius: var(--radius-panel);
background: var(--panel-grad);
box-shadow: var(--shadow-card);
padding: 1.5rem;
}
.eyebrow {
margin: 0 0 0.75rem;
color: var(--gold);
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.prose {
max-width: var(--measure);
}
/* ---- Status chip -------------------------------------------------------- */
.chip {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.15rem 0.6rem;
border: 1px solid var(--line);
border-radius: var(--radius-pill);
background: var(--panel-flat);
color: var(--muted);
font-size: 0.76rem;
letter-spacing: 0.04em;
white-space: nowrap;
}
.chip--version {
border-color: color-mix(in srgb, var(--gold-deep) 70%, transparent);
color: var(--gold);
}
.chip--draft {
border-color: color-mix(in srgb, var(--mode-maint) 55%, transparent);
color: var(--mode-maint);
}
.chip--live {
border-color: color-mix(in srgb, var(--mode-live) 55%, transparent);
color: var(--mode-live);
}
/* ---- Sections ------------------------------------------------------------
The vertical rhythm every marketing page is built from. Here rather than in
a component because phases 4 to 6 add pages that must sit on the same grid,
and a per-page `padding-block` is how that stops being true. */
.section {
padding-block: clamp(2.5rem, 6vw, 4.5rem);
}
.section + .section {
padding-top: 0;
}
/* ---- Buttons -------------------------------------------------------------
Three variants, all the same box: solid for the one action a page wants,
outlined for the alternatives, and the demo's own below. Anchors, not
buttons — every one of them navigates, and the site runs no client JS. */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.62rem 1.15rem;
border: 1px solid var(--line);
border-radius: var(--radius-input);
background: var(--panel-flat);
color: var(--ink);
font-size: 0.96rem;
font-weight: 500;
line-height: 1.3;
text-decoration: none;
transition:
border-color 0.15s ease,
background-color 0.15s ease,
color 0.15s ease;
}
.btn:hover {
border-color: var(--gold-deep);
color: var(--ink);
}
/* The primary action reads as gold-on-dark rather than a filled gold slab: at
7.91:1 the token is a text colour, and `--gold-deep` is explicitly annotated
"rules, borders, UI edges. NEVER text." — so it carries the edge and the
wash, and the gold carries the label. */
.btn--primary {
border-color: var(--gold);
background: color-mix(in srgb, var(--gold-deep) 18%, transparent);
color: var(--gold-bright);
}
.btn--primary:hover {
background: color-mix(in srgb, var(--gold-deep) 30%, transparent);
color: var(--gold-bright);
}
.btn--ghost {
background: transparent;
color: var(--muted);
}
.btn--ghost:hover {
color: var(--ink);
}
/* ---- The demo slot -------------------------------------------------------
PLAN.md §15 / D12. A public demo instance is planned and out of scope, but
the site is built so that gaining one is a line in the mounted brand.json
rather than a rebuild — the same class of change as swapping a logo (§7).
Anything carrying `data-demo-url` is hidden while that attribute is empty,
which is the state a stock build ships in. `scripts/applyBrand.mjs` fills
both the attribute and the adjacent empty `href` at boot when the mount sets
`demoUrl`, and the element appears. The markup contract is:
<a class="demo-cta" href="" data-demo-url="">See it running</a>
Written here, before phase 3 writes that markup, because the rule and the
rewrite have to agree and they live in different files. */
/* `!important`, and it is earning its keep rather than papering over something.
This selector is specificity 0,1,0. So is a class — including the scoped class an
Astro component puts on the very same element — and a component's styles are emitted
AFTER this file, so any component that gives one of these elements a `display` wins on
source order alone. Phase 4 did exactly that: `/features/`'s `.demo-link` set
`display: inline-flex` for its arrow, and twelve links to a demo that does not exist
appeared on the page, each one pointing at `href=""` — which a browser resolves to the
page it is already on.
Nothing caught it. checkBrand.mjs verifies the ATTRIBUTES, and they were perfect; the
defect was three files away in the cascade. It was found by looking at the rendered
page, which is not a mechanism.
So the rule is stated as one: while there is no demo, these elements do not render, and
no component style may overrule that by accident. A component that genuinely needs to
lay one of these out sets every property except `display`. */
[data-demo-url=''] {
display: none !important;
}
/* Phase 3 writes that markup as `class="btn demo-cta"`, so the slot is a button
like its neighbours and takes the portal colour — the live signal, for the
one link on the site that leads to something actually running. */
.demo-cta {
border-color: var(--portal);
background: color-mix(in srgb, var(--portal-deep) 22%, transparent);
color: var(--portal-bright);
}
.demo-cta:hover {
border-color: var(--portal-bright);
background: color-mix(in srgb, var(--portal-deep) 34%, transparent);
color: var(--portal-bright);
}