All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
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>
481 lines
12 KiB
CSS
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);
|
|
}
|