Files
ai-for-dummies/src/components/islands/RulesInteractive.astro
T
Marcos Paulo a97c2a4034
verify-and-publish / gate (push) Successful in 16m6s
verify-and-publish / publish (push) Has been skipped
fix(rules): stop rendering the skills paragraph's markup as text
`skillsText` is written into the page with `set:html`, because its copy
carries a `<code>.agents/skills/</code>`. It was missing from the island's
HTML_KEYS list, so the language pass rewrote the node with `textContent`
on load -- and every visitor to /rules/ read a literal `<code>` tag in
the middle of the sentence.

It is the only key with this mismatch: cross-checking every copy value
containing markup against HTML_KEYS turns up `skillsText` and nothing
else. Three keys are declared but carry no markup (navPipeline,
navSkills, navExamples), which is harmless.

Also teaches rendered-text-diff.mjs about the landing page, which lives
at the repository root rather than in a directory. It was requesting
/index/index.html and diffing against a 404, which reported a clean four
spans. With the path fixed the landing page really is clean, 36 of 36.

All eight routes now report zero missing spans except /full-guide/,
which is task 15f.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:53:14 +00:00

1201 lines
32 KiB
TypeScript

---
// RulesInteractive — single island for the /rules/ page. Hosts five
// distinct interactions that all share a single piece of state: the
// active language.
//
// - language toggle (topbar buttons)
// - stage tabs (pipeline row)
// - skill tabs (skills list)
// - copy-prompt button (copy-lab card)
// - scroll progress bar (top edge)
//
// Why one island: every interaction reads or writes the active language.
// Splitting them into separate islands would either duplicate the language
// state or require a shared module, both of which cost more JS budget than
// one cohesive island. The total client payload here matches the legacy
// rules/app.js (16 KB) byte-for-byte after gzip.
//
// Hydration: client:idle. The topbar's EN/PT buttons are above the fold
// but the toggle is a low-priority idle action — see .agents/rules/astro.md
// for the hydration preference order.
import copy from '../../content/rules/copy.json';
import stages from '../../content/rules/stages.json';
import skills from '../../content/rules/skills.json';
import prompts from '../../content/rules/prompts.json';
// The page server-renders the active language (English by default; the
// island picks up any stored preference on hydrate). Static markup carries
// only English strings; the script rewrites them when the user toggles.
---
<div class="rules-root" data-rules-root>
<!-- Progress bar: top edge. JS only writes `width`. -->
<div class="progress" aria-hidden="true"><span data-progress></span></div>
<!-- Topbar. Three-cell layout: back | nav | language toggle. -->
<header class="topbar">
<a class="back" href={`${import.meta.env.BASE_URL}`}>
<b>A</b><span data-copy="back">{copy.en.back}</span>
</a>
<nav aria-label="Page sections">
<a href="#pipeline" data-copy="navPipeline">{copy.en.navPipeline}</a>
<a href="#skills" data-copy="navSkills">{copy.en.navSkills}</a>
<a href="#examples" data-copy="navExamples">{copy.en.navExamples}</a>
</nav>
<div class="languages" aria-label="Language">
<button class="active" data-lang="en" aria-pressed="true">EN</button>
<span>/</span>
<button data-lang="pt" aria-pressed="false">PT</button>
</div>
</header>
<main>
<section class="hero">
<p class="eyebrow" data-copy="heroEyebrow">{copy.en.heroEyebrow}</p>
<h1 data-copy="heroTitle" set:html={copy.en.heroTitle} />
<div class="hero-foot">
<p data-copy="heroText">{copy.en.heroText}</p>
<aside>
<span>CASE / NETCRACKER</span>
<strong>interview</strong>
<small data-copy="heroAside">{copy.en.heroAside}</small>
</aside>
</div>
</section>
<section class="thesis">
<span data-copy="thesisLabel">{copy.en.thesisLabel}</span>
<strong data-copy="thesis">{copy.en.thesis}</strong>
</section>
<section class="pipeline-section" id="pipeline">
<div class="section-label">
<span data-copy="pipelineLabel">{copy.en.pipelineLabel}</span>
<span data-copy="pipelineMeta">{copy.en.pipelineMeta}</span>
</div>
<div class="intro">
<div>
<p class="eyebrow" data-copy="pipelineEyebrow">{copy.en.pipelineEyebrow}</p>
<h2 data-copy="pipelineTitle" set:html={copy.en.pipelineTitle} />
</div>
<p data-copy="pipelineText">{copy.en.pipelineText}</p>
</div>
<div class="pipeline" role="tablist" aria-label="Rule enforcement stages">
<button class="active" data-stage="context" role="tab" aria-selected="true">
<span>01</span>
<strong data-copy="stageContext">{copy.en.stageContext}</strong>
<small>AGENTS.md</small>
</button>
<i></i>
<button data-stage="skills" role="tab" aria-selected="false">
<span>02</span>
<strong>SKILLS</strong>
<small>.agents/skills</small>
</button>
<i></i>
<button data-stage="cli" role="tab" aria-selected="false">
<span>03</span>
<strong>CLI</strong>
<small>check:ui</small>
</button>
<i></i>
<button data-stage="commit" role="tab" aria-selected="false">
<span>04</span>
<strong>COMMIT</strong>
<small>Husky</small>
</button>
<i></i>
<button data-stage="review" role="tab" aria-selected="false">
<span>05</span>
<strong data-copy="stageReview">{copy.en.stageReview}</strong>
<small>pragent</small>
</button>
</div>
<article class="stage-detail" id="stage-detail" aria-live="polite"></article>
</section>
<section class="skill-section" id="skills">
<div class="section-label">
<span data-copy="skillsLabel">{copy.en.skillsLabel}</span>
<span data-copy="skillsMeta">{copy.en.skillsMeta}</span>
</div>
<div class="intro">
<div>
<p class="eyebrow" data-copy="skillsEyebrow">{copy.en.skillsEyebrow}</p>
<h2 data-copy="skillsTitle" set:html={copy.en.skillsTitle} />
</div>
<p data-copy="skillsText" set:html={copy.en.skillsText} />
</div>
<div class="skill-console">
<div class="skill-list" role="tablist" aria-label="Repository skills">
<button class="active" data-skill="gate" role="tab" aria-selected="true">
<strong>gate-discipline</strong>
<small data-copy="skillGate">{copy.en.skillGate}</small>
</button>
<button data-skill="parallel" role="tab" aria-selected="false">
<strong>parallel-agents</strong>
<small data-copy="skillParallel">{copy.en.skillParallel}</small>
</button>
<button data-skill="repo" role="tab" aria-selected="false">
<strong>repo-db</strong>
<small data-copy="skillRepo">{copy.en.skillRepo}</small>
</button>
<button data-skill="debt" role="tab" aria-selected="false">
<strong>tech-debt</strong>
<small data-copy="skillDebt">{copy.en.skillDebt}</small>
</button>
<button data-skill="writer" role="tab" aria-selected="false">
<strong>skill-writer</strong>
<small data-copy="skillWriter">{copy.en.skillWriter}</small>
</button>
<button data-skill="area" role="tab" aria-selected="false">
<strong>frontend / go-api</strong>
<small data-copy="skillArea">{copy.en.skillArea}</small>
</button>
</div>
<article class="skill-detail" id="skill-detail" aria-live="polite"></article>
</div>
</section>
<section class="examples" id="examples">
<div class="section-label">
<span data-copy="examplesLabel">{copy.en.examplesLabel}</span>
<span data-copy="examplesMeta">{copy.en.examplesMeta}</span>
</div>
<div class="example-grid">
<article>
<span data-copy="ratchetLabel">{copy.en.ratchetLabel}</span>
<h3 data-copy="ratchetTitle" set:html={copy.en.ratchetTitle} />
<pre><code>pnpm check:ui
# raw buttons
# swallowed catches
# pages without h1
# hardcoded colours</code></pre>
<a
data-copy="readChecker"
data-source="ratchet"
href="https://git.marcospaulo.dev.br/netcracker/interview/src/branch/main/scripts/check-ui-contract.mjs"
>{copy.en.readChecker}</a
>
</article>
<article>
<span>HUSKY / PRE-COMMIT</span>
<h3 data-copy="hookTitle">{copy.en.hookTitle}</h3>
<pre><code>pnpm exec lint-staged
node scripts/check-ui-contract.mjs</code></pre>
<a
data-copy="readHook"
data-source="hooks"
href="https://git.marcospaulo.dev.br/netcracker/interview/src/branch/main/.husky/pre-commit"
>{copy.en.readHook}</a
>
</article>
<article>
<span data-copy="commitLabel">{copy.en.commitLabel}</span>
<h3 data-copy="commitTitle">{copy.en.commitTitle}</h3>
<pre><code>pnpm exec commitlint --edit $1
feat: add interview timer
fix(api): scope session query</code></pre>
<a
data-copy="readCommit"
data-source="commit"
href="https://git.marcospaulo.dev.br/netcracker/interview/src/branch/main/commitlint.config.cjs"
>{copy.en.readCommit}</a
>
</article>
<article>
<span data-copy="reviewLabel">{copy.en.reviewLabel}</span>
<h3 data-copy="reviewTitle">{copy.en.reviewTitle}</h3>
<pre><code>.pr-review.json
focus
exclude_paths
languages
instructions</code></pre>
<a
data-copy="readReview"
data-source="review"
href="https://git.marcospaulo.dev.br/netcracker/interview/src/branch/main/.pr-review.json"
>{copy.en.readReview}</a
>
</article>
</div>
</section>
<section class="copy-lab">
<div>
<span data-copy="copyLabel">{copy.en.copyLabel}</span>
<h2 data-copy="copyTitle">{copy.en.copyTitle}</h2>
<p data-copy="copyText">{copy.en.copyText}</p>
</div>
<article>
<button data-copy-prompt>
<span data-copy="copyButton">{copy.en.copyButton}</span>
<b></b>
</button>
<pre><code id="explore-prompt" /></pre>
<p id="copy-status" role="status" aria-live="polite"></p>
</article>
</section>
<section class="deeper">
<div>
<span data-copy="deeperLabel">{copy.en.deeperLabel}</span>
<strong data-copy="deeperTitle">{copy.en.deeperTitle}</strong>
</div>
<nav>
<a href="https://git.marcospaulo.dev.br/netcracker/interview/src/branch/main/AGENTS.md">
<span>01</span>
<b data-copy="deepContext">{copy.en.deepContext}</b>
<small>AGENTS.md</small>
</a>
<a
href="https://git.marcospaulo.dev.br/netcracker/interview/src/branch/main/.agents/skills"
>
<span>02</span>
<b data-copy="deepSkills">{copy.en.deepSkills}</b>
<small>.agents/skills/</small>
</a>
<a href={`${import.meta.env.BASE_URL}skills/`}>
<span>03</span>
<b>Design skills</b>
<small>skills/</small>
</a>
<a
href="https://git.marcospaulo.dev.br/netcracker/interview/src/branch/main/.claude/agents"
>
<span>03</span>
<b data-copy="deepAgents">{copy.en.deepAgents}</b>
<small>.claude/agents/</small>
</a>
<a
href="https://git.marcospaulo.dev.br/netcracker/interview/src/branch/main/.lintstagedrc.cjs"
>
<span>04</span>
<b data-copy="deepPolicy">{copy.en.deepPolicy}</b>
<small>.lintstagedrc.cjs</small>
</a>
</nav>
</section>
</main>
<!--
Runtime data. The page renders English markup server-side; the script
below rewrites it when the user toggles language or switches tabs. Data
lives in src/content/rules/* and is JSON-imported via Astro.
-->
<script type="application/json" data-rules-copy set:html={JSON.stringify(copy)} />
<script type="application/json" data-rules-stages set:html={JSON.stringify(stages)} />
<script type="application/json" data-rules-skills set:html={JSON.stringify(skills)} />
<script type="application/json" data-rules-prompts set:html={JSON.stringify(prompts)} />
</div>
<script is:inline>
// Rules island — language toggle, stage tabs, skill tabs, copy prompt,
// scroll progress. Reads bilingual data from <script type="application/json">
// siblings. Plain JS so the browser can run it without a build step.
(function () {
const root = document.querySelector('[data-rules-root]');
if (!root) return;
function parse(sel) {
const el = root.querySelector(sel);
if (!el) throw new Error('rules island: missing ' + sel);
return JSON.parse(el.textContent || '{}');
}
const copy = parse('[data-rules-copy]');
const stages = parse('[data-rules-stages]');
const skills = parse('[data-rules-skills]');
const prompts = parse('[data-rules-prompts]');
function getCopy(lang, key) {
return copy[lang] && copy[lang][key];
}
function stageLinkText(lang) {
return getCopy(lang, 'stageOpenSource') || 'OPEN SOURCE ↗';
}
function skillLinkText(lang) {
return getCopy(lang, 'skillReadSkill') || 'READ SKILL ↗';
}
function triggerLabel(lang) {
return getCopy(lang, 'skillTriggerLabel') || 'TRIGGER';
}
function statusOk(lang) {
return getCopy(lang, 'copyStatus') || 'Prompt copied.';
}
function statusFail(lang) {
return getCopy(lang, 'copyFallback') || 'Select the text manually.';
}
const stored = localStorage.getItem('rules-language');
let language = stored === 'pt' ? 'pt' : 'en';
function select(selector, value, key) {
root.querySelectorAll(selector).forEach(function (button) {
const active = button.dataset[key] === value;
button.classList.toggle('active', active);
if (button.hasAttribute('aria-selected')) {
button.setAttribute('aria-selected', active ? 'true' : 'false');
}
if (button.hasAttribute('aria-pressed')) {
button.setAttribute('aria-pressed', active ? 'true' : 'false');
}
});
}
function renderStage(id) {
const item = stages[id];
if (!item) return;
const target = root.querySelector('#stage-detail');
if (!target) return;
target.innerHTML =
'<div class="stage-number">' +
item.number +
'</div>' +
'<div><span>' +
item.file +
'</span><h3>' +
item.title[language] +
'</h3>' +
'<p>' +
item.text[language] +
'</p>' +
'<a href="' +
item.link +
'">' +
stageLinkText(language) +
'</a></div>' +
'<pre><code>' +
item.code +
'</code></pre>';
select('[data-stage]', id, 'stage');
}
function renderSkill(id) {
const item = skills[id];
if (!item) return;
const target = root.querySelector('#skill-detail');
if (!target) return;
target.innerHTML =
'<header><span>' +
triggerLabel(language) +
'</span>' +
'<strong>' +
item.trigger[language] +
'</strong></header>' +
'<h3>' +
item.name +
'</h3>' +
'<p>' +
item.lesson[language] +
'</p>' +
'<pre><code>' +
item.example +
'</code></pre>' +
'<a href="https://git.marcospaulo.dev.br/netcracker/interview/src/branch/main/' +
item.path +
'">' +
skillLinkText(language) +
'</a>';
select('[data-skill]', id, 'skill');
}
// Keys whose copy carries markup, so the swap must write innerHTML.
// `skillsText` was missing while the markup renders it with `set:html`:
// the island rewrote it as text on load and every visitor saw a literal
// `<code>.agents/skills/</code>` in the paragraph.
const HTML_KEYS = {
heroTitle: 1,
skillsText: 1,
pipelineTitle: 1,
skillsTitle: 1,
ratchetTitle: 1,
navPipeline: 1,
navSkills: 1,
navExamples: 1,
};
function renderLanguage(next) {
language = next;
document.documentElement.lang = language === 'pt' ? 'pt-BR' : 'en';
root.querySelectorAll('[data-copy]').forEach(function (node) {
const key = node.dataset.copy;
if (!key) return;
const value = getCopy(language, key);
if (value === undefined) return;
if (HTML_KEYS[key]) {
node.innerHTML = value;
} else {
node.textContent = value;
}
});
const promptEl = root.querySelector('#explore-prompt');
if (promptEl) promptEl.textContent = prompts[language];
select('[data-lang]', language, 'lang');
const activeStage = root.querySelector('[data-stage].active');
renderStage(activeStage ? activeStage.dataset.stage : 'context');
const activeSkill = root.querySelector('[data-skill].active');
renderSkill(activeSkill ? activeSkill.dataset.skill : 'gate');
localStorage.setItem('rules-language', language);
}
function copyPrompt() {
const value = prompts[language];
const status = root.querySelector('#copy-status');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(value).then(
function () {
if (status) status.textContent = statusOk(language);
},
function () {
if (status) status.textContent = statusFail(language);
},
);
} else if (status) {
status.textContent = statusFail(language);
}
}
root.querySelectorAll('[data-stage]').forEach(function (button) {
button.addEventListener('click', function () {
renderStage(button.dataset.stage || 'context');
});
});
root.querySelectorAll('[data-skill]').forEach(function (button) {
button.addEventListener('click', function () {
renderSkill(button.dataset.skill || 'gate');
});
});
root.querySelectorAll('[data-lang]').forEach(function (button) {
button.addEventListener('click', function () {
renderLanguage(button.dataset.lang || 'en');
});
});
const copyBtn = root.querySelector('[data-copy-prompt]');
if (copyBtn) copyBtn.addEventListener('click', copyPrompt);
const progress = root.querySelector('[data-progress]');
if (progress) {
function update() {
const height = document.documentElement.scrollHeight - window.innerHeight;
progress.style.width = (height > 0 ? (window.scrollY / height) * 100 : 0) + '%';
}
window.addEventListener('scroll', update, { passive: true });
update();
}
renderLanguage(language);
})();
</script>
<style>
/* rules-root is the only element Astro scopes styles to. The legacy
page's rules/styles.css put everything on body-level selectors, so we
use :global() to reach them. */
.rules-root :global(*) {
box-sizing: border-box;
}
.rules-root :global(html) {
scroll-behavior: smooth;
}
.rules-root :global(body) {
min-width: 320px;
margin: 0;
color: var(--ink);
background: var(--paper);
font-family: Manrope, Arial, sans-serif;
}
.progress {
position: fixed;
z-index: 20;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: transparent;
}
.progress span {
display: block;
width: 0;
height: 100%;
background: var(--gold);
}
.topbar {
position: sticky;
z-index: 10;
top: 0;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
min-height: 72px;
padding: 0 clamp(24px, 5vw, 90px);
border-bottom: 1px solid var(--line);
background: color-mix(in srgb, var(--paper) 94%, transparent);
backdrop-filter: blur(14px);
}
.back {
display: flex;
align-items: center;
gap: 11px;
color: var(--ink);
font:
700 10px 'DM Mono',
monospace;
text-decoration: none;
text-transform: uppercase;
}
.back b {
display: grid;
place-items: center;
width: 30px;
height: 30px;
color: var(--paper);
background: var(--accent);
font-size: var(--step-1);
}
.topbar nav {
display: flex;
gap: 28px;
}
.topbar nav a,
.languages {
color: var(--muted);
font:
600 9px 'DM Mono',
monospace;
letter-spacing: 0.08em;
text-decoration: none;
text-transform: uppercase;
}
.languages {
justify-self: end;
display: flex;
align-items: center;
gap: 7px;
}
.languages button {
padding: 7px;
border: 0;
color: var(--muted);
background: transparent;
cursor: pointer;
}
.languages button.active {
color: var(--paper);
background: var(--blue);
}
main {
max-width: 1920px;
margin: auto;
padding: 0 clamp(24px, 5vw, 90px);
}
.hero {
min-height: 700px;
padding: 130px 0 80px;
}
.eyebrow,
.section-label,
.thesis > span {
color: var(--accent);
font:
600 9px 'DM Mono',
monospace;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.hero h1,
.intro h2 {
margin: 22px 0 50px;
font-size: clamp(64px, 8.3vw, 148px);
line-height: 0.82;
letter-spacing: -0.075em;
}
.hero h1 em,
.intro h2 em {
color: var(--blue);
font-family: Georgia, serif;
font-weight: 400;
}
.hero-foot {
display: grid;
grid-template-columns: 1.3fr 0.7fr;
gap: 70px;
align-items: end;
}
.hero-foot > p {
max-width: 750px;
margin: 0;
color: var(--muted);
font-size: var(--step-16);
line-height: 1.75;
}
.hero-foot aside {
display: grid;
gap: 8px;
padding: 23px;
border-left: 6px solid var(--gold);
color: var(--paper);
background: var(--deep);
}
.hero-foot aside span,
.hero-foot aside small {
color: var(--gold);
font:
500 8px 'DM Mono',
monospace;
letter-spacing: 0.08em;
}
.hero-foot aside strong {
font-size: var(--step-32);
}
.thesis {
display: grid;
grid-template-columns: 200px 1fr;
gap: 35px;
align-items: center;
margin: 0 calc(clamp(24px, 5vw, 90px) * -1) 130px;
padding: 35px clamp(24px, 5vw, 90px);
color: var(--paper);
background: var(--ink);
}
.thesis strong {
font-size: clamp(20px, 2.4vw, 38px);
line-height: 1.2;
}
.section-label {
display: flex;
justify-content: space-between;
padding-bottom: 15px;
border-bottom: 1px solid var(--line);
}
.section-label span:last-child {
color: var(--muted);
}
.pipeline-section,
.skill-section,
.examples,
.copy-lab {
margin-bottom: 140px;
padding-top: 30px;
}
.intro {
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: 70px;
align-items: end;
margin: 45px 0;
}
.intro h2 {
margin: 15px 0 0;
font-size: clamp(48px, 5.6vw, 92px);
}
.intro > p {
max-width: 650px;
margin: 0;
color: var(--muted);
font-size: var(--step-14);
line-height: 1.75;
}
.pipeline {
display: grid;
grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr auto 1fr;
align-items: stretch;
border: 1px solid var(--line);
background: var(--paper);
}
.pipeline button {
display: grid;
gap: 8px;
min-height: 126px;
padding: 20px;
border: 0;
color: var(--ink);
background: transparent;
text-align: left;
cursor: pointer;
}
.pipeline button span {
color: var(--accent);
font:
600 9px 'DM Mono',
monospace;
}
.pipeline button strong {
font:
700 13px 'DM Mono',
monospace;
}
.pipeline button small {
color: var(--muted);
font:
10px 'DM Mono',
monospace;
}
.pipeline button.active {
color: var(--paper);
background: var(--blue);
box-shadow: inset 0 -6px var(--gold);
}
.pipeline button.active span,
.pipeline button.active small {
color: var(--gold);
}
.pipeline > i {
align-self: center;
color: var(--gold);
font-style: normal;
}
.stage-detail {
display: grid;
grid-template-columns: 120px 1.3fr 0.7fr;
gap: 35px;
min-height: 330px;
padding: 42px;
color: var(--paper);
background: var(--deep);
}
.stage-number {
color: var(--white-14);
font:
500 76px 'DM Mono',
monospace;
}
.stage-detail > div:nth-child(2) > span,
.skill-detail header span {
color: var(--gold);
font:
500 9px 'DM Mono',
monospace;
letter-spacing: 0.08em;
}
.stage-detail h3,
.skill-detail h3 {
margin: 15px 0;
font-size: clamp(28px, 3vw, 48px);
letter-spacing: -0.05em;
}
.stage-detail p,
.skill-detail p {
max-width: 650px;
color: var(--ink-line);
font-size: var(--step-13);
line-height: 1.7;
}
.stage-detail a,
.skill-detail a,
.example-grid a {
display: inline-block;
margin-top: 15px;
color: var(--gold);
font:
600 9px 'DM Mono',
monospace;
letter-spacing: 0.07em;
}
.stage-detail pre,
.skill-detail pre,
.example-grid pre,
.copy-lab pre {
margin: 0;
overflow: auto;
background: var(--deep);
}
.stage-detail pre {
align-self: stretch;
padding: 28px;
}
.stage-detail code,
.skill-detail code,
.example-grid code,
.copy-lab code {
color: var(--ink-code);
font:
11px/1.7 'DM Mono',
monospace;
}
.skill-console {
display: grid;
grid-template-columns: minmax(240px, 0.7fr) minmax(0, 1.5fr);
min-height: 570px;
border: 1px solid var(--line);
background: var(--line);
gap: 1px;
}
.skill-list {
display: grid;
grid-template-rows: repeat(6, 1fr);
gap: 1px;
background: var(--line);
}
.skill-list button {
display: grid;
align-content: center;
gap: 7px;
padding: 18px 22px;
border: 0;
color: var(--ink);
background: var(--paper);
text-align: left;
cursor: pointer;
}
.skill-list button strong {
font:
700 12px 'DM Mono',
monospace;
}
.skill-list button small {
color: var(--muted);
font-size: var(--step-00);
}
.skill-list button.active {
color: var(--paper);
background: var(--accent);
box-shadow: inset 6px 0 var(--gold);
}
.skill-list button.active small {
color: var(--accent-paper-active);
}
.skill-detail {
display: grid;
align-content: center;
padding: 50px clamp(30px, 5vw, 80px);
color: var(--paper);
background: var(--accent-surface);
}
.skill-detail header {
display: grid;
gap: 9px;
padding-bottom: 22px;
border-bottom: 1px solid var(--white-23);
}
.skill-detail header strong {
font-size: var(--step-12);
}
.skill-detail pre {
margin: 20px 0 0;
padding: 18px;
}
.example-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
margin-top: 45px;
background: var(--line);
}
.example-grid article {
display: grid;
grid-template-rows: auto auto 1fr auto;
min-height: 390px;
padding: 35px;
background: var(--paper);
}
.example-grid article > span {
color: var(--accent);
font:
600 9px 'DM Mono',
monospace;
letter-spacing: 0.08em;
}
.example-grid h3 {
margin: 22px 0;
font-size: clamp(26px, 3vw, 42px);
line-height: 1.05;
letter-spacing: -0.045em;
}
.example-grid pre {
padding: 22px;
}
.example-grid a {
color: var(--blue);
}
.copy-lab {
display: grid;
grid-template-columns: 0.75fr 1.25fr;
gap: 1px;
color: var(--paper);
background: var(--line);
}
.copy-lab > div {
padding: 45px;
background: var(--accent);
}
.copy-lab > div span {
color: var(--gold);
font:
600 9px 'DM Mono',
monospace;
}
.copy-lab h2 {
margin: 20px 0;
font-size: clamp(30px, 3.4vw, 56px);
line-height: 1;
}
.copy-lab > div p {
max-width: 420px;
color: var(--accent-paper);
line-height: 1.6;
}
.copy-lab article {
display: grid;
grid-template-rows: auto 1fr auto;
min-width: 0;
background: var(--deep);
}
.copy-lab button {
justify-self: end;
display: flex;
gap: 20px;
margin: 20px;
padding: 11px 14px;
border: 1px solid var(--white-31);
color: var(--paper);
background: transparent;
font:
600 9px 'DM Mono',
monospace;
cursor: pointer;
}
.copy-lab pre {
padding: 26px;
white-space: pre-wrap;
}
.copy-lab article > p {
min-height: 20px;
margin: 12px 24px;
color: var(--gold);
font:
600 9px 'DM Mono',
monospace;
}
.deeper {
margin: 0 calc(clamp(24px, 5vw, 90px) * -1);
padding: 60px clamp(24px, 5vw, 90px) 100px;
background: var(--gold);
}
.deeper > div {
display: flex;
justify-content: space-between;
gap: 30px;
margin-bottom: 35px;
}
.deeper > div span {
color: var(--accent);
font:
600 9px 'DM Mono',
monospace;
}
.deeper > div strong {
max-width: 700px;
font-size: clamp(24px, 3vw, 44px);
line-height: 1.1;
}
.deeper nav {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
background: var(--ink);
}
.deeper nav a {
display: grid;
gap: 9px;
min-height: 160px;
padding: 25px;
color: var(--paper);
background: var(--ink);
text-decoration: none;
}
.deeper nav span {
color: var(--accent);
font:
600 10px 'DM Mono',
monospace;
}
.deeper nav b {
font-size: var(--step-1);
}
.deeper nav small {
color: var(--ink-muted);
font:
9px 'DM Mono',
monospace;
}
.topbar a:focus-visible,
.topbar button:focus-visible,
.pipeline button:focus-visible,
.skill-list button:focus-visible,
.copy-lab button:focus-visible,
.deeper a:focus-visible {
outline: 3px solid var(--gold);
outline-offset: -3px;
}
@media (min-width: 2200px) {
main {
max-width: 2880px;
padding-inline: clamp(150px, 7vw, 300px);
}
.hero {
min-height: 900px;
}
.hero h1 {
font-size: clamp(150px, 7vw, 220px);
}
.pipeline button {
min-height: 170px;
}
.stage-detail {
min-height: 440px;
}
.skill-console {
min-height: 720px;
}
}
@media (max-width: 800px) {
.topbar {
grid-template-columns: 1fr auto;
}
.topbar nav {
display: none;
}
.hero {
min-height: auto;
}
.hero-foot,
.intro,
.copy-lab {
grid-template-columns: 1fr;
}
.pipeline {
grid-template-columns: 1fr 1fr 1fr;
}
.pipeline > i {
display: none;
}
.pipeline button {
min-height: 100px;
}
.stage-detail {
grid-template-columns: 80px 1fr;
}
.stage-detail pre {
grid-column: 1 / -1;
}
.skill-console {
grid-template-columns: 1fr;
}
.skill-list {
grid-template-columns: 1fr 1fr;
grid-template-rows: none;
}
.example-grid {
grid-template-columns: 1fr;
}
.deeper nav {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 560px) {
main {
padding-inline: 18px;
}
.topbar {
padding-inline: 18px;
}
.hero {
padding-top: 80px;
}
.hero h1 {
font-size: clamp(54px, 17vw, 78px);
}
.hero-foot {
gap: 30px;
}
.thesis {
grid-template-columns: 1fr;
margin-inline: -18px;
padding-inline: 18px;
}
.section-label {
gap: 20px;
}
.section-label span:last-child {
text-align: right;
}
.pipeline {
grid-template-columns: 1fr;
}
.pipeline button {
grid-template-columns: 38px 1fr;
grid-template-rows: auto auto;
min-height: 82px;
}
.pipeline button span {
grid-row: 1 / -1;
align-self: center;
}
.stage-detail {
grid-template-columns: 1fr;
padding: 28px 22px;
}
.stage-number {
font-size: var(--step-48);
}
.stage-detail pre {
grid-column: auto;
}
.skill-list {
grid-template-columns: 1fr;
}
.skill-detail {
padding: 34px 24px;
}
.example-grid article {
min-height: 340px;
padding: 25px;
}
.copy-lab > div {
padding: 30px 24px;
}
.deeper {
margin-inline: -18px;
padding-inline: 18px;
}
.deeper > div {
display: grid;
}
.deeper nav {
grid-template-columns: 1fr;
}
.deeper nav a {
min-height: 120px;
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
.progress span {
transition: none;
}
}
</style>