/* ============================================================================ 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: See it running 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); }