All checks were successful
PR checks / checks (pull_request) Successful in 2m37s
PLAN.md §16 phase 17, built on the org lead's answers of 2026-10-22, recorded as D75-D79 (count of record 79). - D75: core marketing pages describe what a game module adds in core's words. "Game intelligence" became "What a game module adds"; each game's own list moved to src/data/games/ and renders only on the new /modules/uo/ and /modules/rust/. /modules/ is a card per game. - The page-side capability check is per game (assertGameCoverage), the item phase 13 moved here. - D76: the homepage keeps its game screenshot, labelled. screens.mjs entries may carry `game` (D67); Screenshot.astro and the app grid print the game's name and mark the figure data-game. - D77: /app/ describes the Rust screens (released the same day). - D78: /runicnpc/, the standalone page, is in the header and footer; the narrow header was re-measured so five links keep two rows. - D79: checkTerms.mjs --built reads every built page's visible text after the build; game routes and data-game elements allow their game's words. Proven against ten plants. Still the twelfth check. - Re-read fixes: /architecture/'s "two machines is also the maximum", Discord "guild" on /integrations/, and collection.mjs against what Steam sign-in and the Rust module keep (PLAY_DATA_SAFETY.md regenerated; the Play answers are unchanged). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
500 lines
13 KiB
CSS
500 lines
13 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;
|
|
column-gap: 0.5rem;
|
|
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.3rem;
|
|
}
|
|
|
|
/* Five links since phase 17 (D78, RunicNPC). Measured at 390px, the fifth pushed Search
|
|
onto a third row: the links ended at 326px, and the row's 24px gap plus the 32px
|
|
button needed 382 of the 366 available. A tighter gap, padding and type put all of it
|
|
back on the second row with room to spare, and every link still clears a 24px target. */
|
|
.site-nav a {
|
|
padding-inline: 0.3rem;
|
|
font-size: 0.84rem;
|
|
}
|
|
|
|
/* Search (D47) shares the second row with the links rather than taking a third. `order`
|
|
was the obvious way to lift it beside the lockup instead, and it was rejected: the tab
|
|
order follows the DOM, so a keyboard user would tab from the lockup down to the links
|
|
and back up to a button above them. Keeping the visual order the same as the focus
|
|
order is worth more here than the row it saves. */
|
|
.site-nav {
|
|
width: auto;
|
|
flex: 1 1 auto;
|
|
}
|
|
|
|
.site-search {
|
|
margin-left: auto;
|
|
}
|
|
}
|
|
|
|
/* ---- 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);
|
|
}
|