Files
runicgateway.com/src/styles/global.css
wtclaude 556dee7355
All checks were successful
PR checks / checks (pull_request) Successful in 49s
feat(home): phase 3 — the homepage
Replaces phase 1's scaffold with the real homepage: hero, the data path as
inline SVG, the self-hosted argument, all five capability groups, and the
get-started CTA. Three decisions the org lead took first are recorded in
PLAN.md as D17-D19.

The data path is drawn generically and captioned specifically (D17): the nodes
say "your game server" and "sidecar", the sub-labels and caption name ServUO and
uo-link. The SVG is aria-hidden because the four numbered steps beside it carry
the same path in prose — one telling, not two.

The capability list is data with a check behind it (D18). Every Game-intelligence
item names the module-uo capability slug it comes from, and the build fails if
the page and platform.json disagree either way. That needed a fifteenth fact in
checkFacts.mjs: §12 named the capability list as an externally-sourced fact and
nothing re-read it, so the chain rested on someone remembering. It also found
that the site was omitting two of the module's eight capabilities — guilds and
city governors are now listed, in the page and in §10.

The hero leads with the emblem (D19), derived from whichever logo.png is in
force so one file still changes the hero, header, tab icon and app icon
together.

Also here, both found by standing the build up rather than by review:

  - checkBrand.mjs now enforces the demo slot's markup contract. applyBrand.mjs
    reveals the demo link by replacing an exact pair of empty attributes; an
    attribute inserted between them produces a build where the mount sets a demo
    URL, the boot log says nothing and the link never appears. Both halves are
    checked and the literal is derived from the expression applyBrand.mjs uses,
    so they cannot drift.

  - The header nav overflowed at 390px — four links plus the lockup measured
    433px against a 390px viewport, so every phone got a horizontally scrolling
    page. Phase 1 left this to phase 3 expecting a disclosure control; it got a
    wrap instead, because with four links there is nothing to disclose and a
    hamburger costs state, script and duplicate markup.

Verified on a clean checkout of this commit: all four checks, astro check, a
production build, a live /brand/* smoke, and a demo URL mounted and reverted.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 00:03:04 -05:00

458 lines
10 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';
*,
*::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. */
[data-demo-url=''] {
display: none;
}
/* 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);
}