a97c2a4034
`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>
1201 lines
32 KiB
TypeScript
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>
|