Phases C+D+E — pull-arrows, interlude, anti-patterns section, chapter rail

Three phases of editorial-spine moves landed together:

PHASE C — Pacing.

· End-of-section pull-arrows. Each major section now closes with a
  small italic-serif arrow link pulling the eye to the next
  section: "See the verbs →" (after Section 02), "See the named
  tells →" (after 03), "Read the rules →" (after the new 04),
  "Install in your editor →" (after 05). Hairline rule above each
  arrow. Hover nudges the arrow forward, recolours to accent.
  Theme-aware: editorial themes get italic serif; brutal /
  manifesto / sport / terminal etc. switch to body sans uppercase.

· Mid-scroll typographic interlude between Sections III and IV.
  Single declarative phrase set in the active theme's display
  face on a contrasting paper band: "The page is the demo."
  ("The page" in italic accent, rest in roman ink). Full-bleed,
  centred, generous padding. Pure type, zero motion. Resets
  attention before the anti-patterns section.

PHASE D — Slop, by name.

· Replaced Section 04 (Without/With) with a new Anti-patterns
  section. Five named tells, each shown as: a number (mono), the
  tell's name in italic display with a strike-through (the slop,
  visually struck), a description (italic serif muted), and then
  the Hallmark fix in clean ink with an accent label. Side-by-
  side typography so the contrast IS the design.

  The five tells: purple-gradient hero, Inter-as-display, centred
  everything, icon-tile feature card, the AI nav.

· Original Section 04 markup preserved verbatim inside a
  <template id="section-04-archive-without-with"> wrapper. The
  template element doesn't render but keeps the content in the
  file for revertibility. To restore the original section: change
  the opening <template> to <section class="section reveal"
  style="--i:3" aria-labelledby="s4"> and the closing </template>
  to </section>, then delete the new Anti-patterns section. (The
  archived content uses the vs--notes / vs__panel--dash etc class
  names which contain "--" sequences invalid inside HTML
  comments, hence the template wrapper instead.)

PHASE E — Chapter rail.

· New <aside class="chapter-rail"> pinned to the left margin on
  viewports ≥80rem (1280px). Lists II Examples · III Skills · IV
  Anti-patterns · V Foundations · VI Install. Italic serif Roman
  numeral + small-caps mono name. Hidden below 80rem so it never
  competes with content on narrower screens.

· IntersectionObserver in main.js tracks the topmost visible
  section and adds .is-current to the matching link. Active link
  brightens to ink colour with accent numeral; inactive links
  fade to rule-2 muted. Smooth (one CSS-token transition).

Net editorial gravity:
  Banner → 01/23 indicator + dropdown (clean chrome)
  Section 02 → "Different briefs. Different bones." + larger cards
  Section 03 → unchanged (already strong)
  Mid-scroll → "The page is the demo." interlude
  Section 04 → Anti-patterns ("Slop, by name.") replacing W/W
  Pull-arrows between every section
  Chapter rail on wide screens
  Footer → colophon (publisher's signature page)

Files changed: index.html (markup) · components.css (chapter rail
+ anti-patterns + interlude theme overrides) · sections.css
(anti-patterns layout + pull-arrow + interlude) · main.js
(chapter rail observer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Youssef
2026-05-06 12:14:46 +01:00
co-authored by Claude Opus 4.7
parent 5d9354f34d
commit 58ee2deade
4 changed files with 541 additions and 3 deletions
+83
View File
@@ -452,6 +452,89 @@
.banner__theme-indicator .banner__theme { display: none; }
}
/* — Chapter rail — sticky outline on wide viewports ————————
Vertical list of section coordinates pinned to the left margin.
Highlights the current section as you scroll. Hidden below 80rem
(1280px) where there's no margin space for it. */
.chapter-rail {
position: fixed;
top: 50%;
left: max(var(--space-sm), calc((100vw - 76rem) / 2 - 7rem));
transform: translateY(-50%);
z-index: 5;
pointer-events: auto;
}
.chapter-rail__list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.55rem;
}
.chapter-rail__list a {
display: grid;
grid-template-columns: 1.6em 1fr;
align-items: baseline;
gap: 0.55rem;
text-decoration: none;
color: var(--color-rule-2);
padding: 0.15rem 0.4rem 0.15rem 0;
font-family: var(--font-label);
font-size: 0.625rem;
letter-spacing: var(--tracking-label);
text-transform: uppercase;
transition: color var(--dur-micro) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
.chapter-rail__list a:hover {
color: var(--color-ink-2);
}
}
.chapter-rail__list a:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
.chapter-rail__list a.is-current {
color: var(--color-ink);
}
.chapter-rail__num {
font-family: var(--font-serif);
font-style: italic;
text-transform: none;
font-size: 0.8125rem;
font-weight: 500;
letter-spacing: 0;
color: var(--color-rule-2);
text-align: end;
transition: color var(--dur-micro) var(--ease-out);
}
.chapter-rail__list a.is-current .chapter-rail__num {
color: var(--color-accent);
}
.chapter-rail__name {
/* The name itself is small mono — only the active section's name
gets visible weight; the others fade. */
opacity: 0.6;
transition: opacity var(--dur-micro) var(--ease-out);
}
.chapter-rail__list a.is-current .chapter-rail__name,
.chapter-rail__list a:hover .chapter-rail__name {
opacity: 1;
}
@media (max-width: 80rem) {
.chapter-rail { display: none; }
}
/* — Section label (01 / FOUNDATIONS) —————————————————— */
.section-label {
font-family: var(--font-label);
+276
View File
@@ -1362,6 +1362,282 @@ html[data-theme="almanac"] body {
color: var(--color-accent-ink);
}
/* — 04 · Anti-patterns — Slop, by name ——————————————————
Five-row editorial list: number · slop name (struck-through,
muted) · fix label + body (clean, ink). Side-by-side typography
so the contrast IS the design. Hairline rules between rows. */
.anti__intro {
margin: 0 0 var(--space-2xl);
font-family: var(--font-serif);
font-style: italic;
font-size: var(--text-md);
line-height: var(--lh-snug);
color: var(--color-ink-2);
max-width: 60ch;
}
[data-theme="brutal"] .anti__intro,
[data-theme="manifesto"] .anti__intro,
[data-theme="terminal"] .anti__intro,
[data-theme="sport"] .anti__intro,
[data-theme="almanac"] .anti__intro,
[data-theme="quiet"] .anti__intro,
[data-theme="bloom"] .anti__intro,
[data-theme="riso"] .anti__intro {
font-family: var(--font-body);
font-style: normal;
}
.anti__intro code {
font-family: var(--font-mono);
font-style: normal;
font-size: 0.875em;
background: var(--color-paper-2);
padding: 0.05em 0.35em;
border: var(--rule-hair) solid var(--color-rule);
color: var(--color-ink);
}
.anti-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0;
}
.anti-row {
display: grid;
grid-template-columns: 3.5rem minmax(0, 1.3fr) minmax(0, 1fr);
gap: var(--space-xl);
padding-block: var(--space-xl);
border-block-start: var(--rule-hair) solid var(--color-rule);
align-items: start;
}
.anti-row:first-child {
border-block-start: 0;
padding-block-start: 0;
}
@media (max-width: 60rem) {
.anti-row {
grid-template-columns: 1fr;
gap: var(--space-md);
}
}
.anti-row__num {
margin: 0;
font-family: var(--font-mono);
font-size: var(--text-xs);
letter-spacing: var(--tracking-label);
text-transform: uppercase;
color: var(--color-muted);
padding-block-start: 0.45em;
font-feature-settings: "tnum" 1;
}
.anti-row__name {
margin: 0 0 var(--space-sm) 0;
font-family: var(--font-display);
font-style: italic;
font-weight: var(--display-weight, 500);
font-size: clamp(1.25rem, 1.5vw + 0.75rem, 1.75rem);
line-height: var(--lh-tight);
letter-spacing: var(--tracking-tight);
color: var(--color-rule-2);
text-decoration: line-through;
text-decoration-thickness: 1px;
text-decoration-color: var(--color-rule-2);
font-optical-sizing: auto;
}
[data-theme="brutal"] .anti-row__name,
[data-theme="manifesto"] .anti-row__name,
[data-theme="sport"] .anti-row__name {
text-transform: uppercase;
font-style: normal;
}
[data-theme="terminal"] .anti-row__name {
font-family: var(--font-mono);
font-style: normal;
}
.anti-row__desc {
margin: 0;
font-family: var(--font-serif);
font-style: italic;
font-size: 0.9375rem;
line-height: var(--lh-relaxed);
color: var(--color-muted);
max-width: 50ch;
}
[data-theme="brutal"] .anti-row__desc,
[data-theme="manifesto"] .anti-row__desc,
[data-theme="terminal"] .anti-row__desc,
[data-theme="sport"] .anti-row__desc,
[data-theme="almanac"] .anti-row__desc,
[data-theme="quiet"] .anti-row__desc,
[data-theme="bloom"] .anti-row__desc {
font-family: var(--font-body);
font-style: normal;
}
.anti-row__fix-label {
margin: 0 0 var(--space-2xs) 0;
font-family: var(--font-label);
font-size: var(--text-xs);
letter-spacing: var(--tracking-label);
text-transform: uppercase;
color: var(--color-accent);
font-feature-settings: "tnum";
}
.anti-row__fix-body {
margin: 0;
font-family: var(--font-body);
font-size: var(--text-base);
line-height: var(--lh-relaxed);
color: var(--color-ink);
max-width: 45ch;
}
/* — Section pull-arrow ————————————————————————————————————
Editorial "continued on p. 12" cue at the end of each section.
Italic serif label + arrow, biased left or right per section
for rhythm. Hover nudges the arrow forward. Pulls the eye into
the next section instead of stopping cold. */
.section__pull {
display: inline-flex;
align-items: baseline;
gap: 0.5em;
margin-block-start: var(--space-xl);
padding-block: var(--space-sm);
font-family: var(--font-serif);
font-style: italic;
font-size: var(--text-md);
line-height: 1.2;
color: var(--color-ink-2);
text-decoration: none;
border-block-start: var(--rule-hair) solid var(--color-rule);
width: fit-content;
transition: color var(--dur-micro) var(--ease-out);
}
.section__pull--right {
margin-inline-start: auto;
text-align: end;
}
.section__pull-arrow {
font-style: normal;
font-weight: 400;
color: var(--color-rule-2);
font-feature-settings: "tnum";
transition: transform var(--dur-short) var(--ease-out),
color var(--dur-micro) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
.section__pull:hover { color: var(--color-ink); }
.section__pull:hover .section__pull-arrow {
transform: translateX(4px);
color: var(--color-accent);
}
}
.section__pull:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 4px;
color: var(--color-ink);
}
[data-theme="brutal"] .section__pull,
[data-theme="manifesto"] .section__pull,
[data-theme="terminal"] .section__pull,
[data-theme="sport"] .section__pull,
[data-theme="almanac"] .section__pull,
[data-theme="quiet"] .section__pull,
[data-theme="bloom"] .section__pull {
font-family: var(--font-body);
font-style: normal;
text-transform: uppercase;
font-size: var(--text-sm);
letter-spacing: 0.04em;
}
[data-theme="terminal"] .section__pull-arrow::before {
content: ">";
margin-inline-end: 0.4em;
color: var(--color-accent);
}
/* — Interlude — full-bleed typographic moment between sections —
One declarative phrase set in the active theme's display face,
centered on a contrasting paper band. Punctuates the scroll;
resets attention before the next section. Zero motion, pure type. */
.interlude {
margin-inline: calc(-1 * var(--page-gutter));
padding-block: var(--space-3xl);
padding-inline: var(--page-gutter);
background: var(--color-paper-2);
border-block: var(--rule-hair) solid var(--color-rule);
display: grid;
place-items: center;
text-align: center;
}
.interlude__phrase {
margin: 0;
font-family: var(--font-display);
font-weight: var(--display-weight, 500);
font-style: var(--display-style, normal);
font-size: clamp(2.25rem, 6vw + 1rem, 5rem);
line-height: var(--lh-tight);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
font-optical-sizing: auto;
max-width: 22ch;
/* Long-word safety inherited from base.css */
}
.interlude__phrase em {
font-style: italic;
color: var(--color-accent-ink, var(--color-accent));
font-weight: 500;
}
[data-theme="brutal"] .interlude__phrase,
[data-theme="manifesto"] .interlude__phrase,
[data-theme="sport"] .interlude__phrase {
text-transform: uppercase;
letter-spacing: -0.01em;
}
[data-theme="brutal"] .interlude__phrase em,
[data-theme="manifesto"] .interlude__phrase em,
[data-theme="sport"] .interlude__phrase em {
font-style: normal;
color: var(--color-accent);
}
[data-theme="terminal"] .interlude__phrase {
font-family: var(--font-mono);
}
[data-theme="terminal"] .interlude__phrase em {
font-style: normal;
color: var(--color-accent);
}
@media (max-width: 48rem) {
.interlude { padding-block: var(--space-2xl); }
}
/* ──────────────────────────────────────────────────────────────
Mobile collapse — section-head one-column on narrow viewports.
Many themes override `.section__head`'s `grid-template-columns`
+137 -3
View File
@@ -139,6 +139,18 @@
</aside>
</header>
<!-- CHAPTER RAIL — sticky outline visible on wide viewports only.
Highlights current section as you scroll. Hidden below 80rem. -->
<aside class="chapter-rail" aria-label="Page outline">
<ol class="chapter-rail__list">
<li><a href="#examples" data-chapter-link="examples"><span class="chapter-rail__num">II</span><span class="chapter-rail__name">Examples</span></a></li>
<li><a href="#skills" data-chapter-link="skills"><span class="chapter-rail__num">III</span><span class="chapter-rail__name">Skills</span></a></li>
<li><a href="#anti-patterns" data-chapter-link="anti-patterns"><span class="chapter-rail__num">IV</span><span class="chapter-rail__name">Anti-patterns</span></a></li>
<li><a href="#s5-foundations" data-chapter-link="foundations"><span class="chapter-rail__num">V</span><span class="chapter-rail__name">Foundations</span></a></li>
<li><a href="#install" data-chapter-link="install"><span class="chapter-rail__num">VI</span><span class="chapter-rail__name">Install</span></a></li>
</ol>
</aside>
<main class="page">
<!-- 01 · HERO SLOT — archetype swaps per theme -->
@@ -312,6 +324,11 @@
</a>
</div>
<a class="section__pull" href="#skills" aria-label="Continue to Skills section">
<span class="section__pull-label">See the verbs</span>
<span class="section__pull-arrow" aria-hidden="true"></span>
</a>
</section>
<!-- 03 · SKILLS — five things Hallmark does, one ruleset underneath -->
@@ -523,10 +540,116 @@
</li>
</ol>
<a class="section__pull section__pull--right" href="#anti-patterns" aria-label="Continue to Anti-patterns section">
<span class="section__pull-label">See the named tells</span>
<span class="section__pull-arrow" aria-hidden="true"></span>
</a>
</section>
<!-- 04 · WITHOUT / WITH — same copy, two design languages, four examples -->
<section class="section reveal" style="--i:3" aria-labelledby="s4">
<!-- INTERLUDE — full-bleed typographic moment between III and IV.
Mid-scroll punctuation: a single declarative phrase set in
the active theme's display face, on a contrasting paper band.
Reprise of the marketing pitch in oversized type. -->
<aside class="interlude reveal" style="--i:3" aria-hidden="true">
<p class="interlude__phrase"><em>The page</em> is the demo.</p>
</aside>
<!-- 04 · ANTI-PATTERNS — Slop, by name. Five named AI-UI tells, each
with the Hallmark fix beside it. Replaces the original Without/
With section (preserved as a <template> below). -->
<section class="section reveal" style="--i:3" id="anti-patterns" aria-labelledby="s-anti">
<header class="section__head">
<p class="section-label">
<span class="num">04</span><span class="divider"></span><span>Anti-patterns</span>
</p>
<h2 class="section__title" id="s-anti">
Slop, by name.
</h2>
</header>
<p class="anti__intro">
Five tells the LLM reaches for by default. The Hallmark fix beside each. <code>hallmark audit</code> flags every one of these on existing code.
</p>
<ol class="anti-list">
<li class="anti-row">
<p class="anti-row__num tnum">01</p>
<div class="anti-row__slop">
<h3 class="anti-row__name">The purple-gradient hero.</h3>
<p class="anti-row__desc">A hero with a background gradient from purple to blue or purple to pink, white centred text. The single most-recognised AI aesthetic.</p>
</div>
<div class="anti-row__fix">
<p class="anti-row__fix-label">Hallmark</p>
<p class="anti-row__fix-body">Pick a single anchor hue. One accent. No gradient backgrounds on heroes. If you want warmth, tint the neutrals.</p>
</div>
</li>
<li class="anti-row">
<p class="anti-row__num tnum">02</p>
<div class="anti-row__slop">
<h3 class="anti-row__name">Inter as display.</h3>
<p class="anti-row__desc">Inter (or Roboto, or Open Sans) used as both display and body, no pairing face. A one-font page is a template page.</p>
</div>
<div class="anti-row__fix">
<p class="anti-row__fix-label">Hallmark</p>
<p class="anti-row__fix-body">Pair a distinctive display face with a refined body. Two faces minimum, never the same family doing both jobs.</p>
</div>
</li>
<li class="anti-row">
<p class="anti-row__num tnum">03</p>
<div class="anti-row__slop">
<h3 class="anti-row__name">Centred everything.</h3>
<p class="anti-row__desc">Headline centred, body centred, button centred, section after section of centred columns. Symmetry as default.</p>
</div>
<div class="anti-row__fix">
<p class="anti-row__fix-label">Hallmark</p>
<p class="anti-row__fix-body">Bias the layout. Wide left margin, narrow right — or the reverse. Breaking symmetry once is enough to register intent.</p>
</div>
</li>
<li class="anti-row">
<p class="anti-row__num tnum">04</p>
<div class="anti-row__slop">
<h3 class="anti-row__name">The icon-tile feature card.</h3>
<p class="anti-row__desc">Rounded rectangle, icon in a coloured square top-left, two-line heading, three-line copy, optional "Learn more →". The universal template.</p>
</div>
<div class="anti-row__fix">
<p class="anti-row__fix-label">Hallmark</p>
<p class="anti-row__fix-body">If you need feature cards, let them be asymmetric — vary sizes, vary alignments, pull the icon inline. Or drop the icon and lead with type.</p>
</div>
</li>
<li class="anti-row">
<p class="anti-row__num tnum">05</p>
<div class="anti-row__slop">
<h3 class="anti-row__name">The AI nav.</h3>
<p class="anti-row__desc">Wordmark hard-left, four inline links centred, CTA button hard-right, sticky on scroll, hairline border-bottom. The shape every LLM ships.</p>
</div>
<div class="anti-row__fix">
<p class="anti-row__fix-label">Hallmark</p>
<p class="anti-row__fix-body">Pick a nav archetype that matches the page's genre — newspaper masthead, terminal command bar, edge-aligned minimal. The nav should tell you what kind of site you're on.</p>
</div>
</li>
</ol>
<a class="section__pull section__pull--right" href="#s5-foundations" aria-label="Continue to Foundations section">
<span class="section__pull-label">Read the rules</span>
<span class="section__pull-arrow" aria-hidden="true"></span>
</a>
</section>
<!-- DEPRECATED: original Section 04 (Without / with) — replaced by
the Gallery of Shame above. Wrapped in <template> so the markup
stays in this file but doesn't render. To revert: change the
opening <template> tag to <section class="section reveal"
style="--i:3" aria-labelledby="s4"> and the closing </template>
to </section>, then remove the Anti-patterns section above. -->
<template id="section-04-archive-without-with">
<section class="section reveal" style="--i:3" aria-labelledby="s4">
<header class="section__head">
<p class="section-label">
<span class="num">04</span><span class="divider"></span><span>Without / with</span>
@@ -695,7 +818,13 @@
</div>
</div>
</section>
<a class="section__pull" href="#s5-foundations" aria-label="Continue to Foundations section">
<span class="section__pull-label">Read the rules</span>
<span class="section__pull-arrow" aria-hidden="true"></span>
</a>
</section>
</template>
<!-- 05 · FOUNDATIONS — vertical list + active panel (CSS-only tabs) -->
<section class="section reveal" style="--i:4" aria-labelledby="s5-foundations">
@@ -818,6 +947,11 @@
</div>
</div>
<a class="section__pull section__pull--right" href="#install" aria-label="Continue to Install section">
<span class="section__pull-label">Install in your editor</span>
<span class="section__pull-arrow" aria-hidden="true"></span>
</a>
</section>
<!-- 06 · INSTALL — one command, every harness -->
+45
View File
@@ -1069,6 +1069,51 @@ if (statesBtn && statesReadout) {
});
}
/* Chapter rail IntersectionObserver to highlight current section
The rail is hidden via CSS below 80rem; we still wire the observer
regardless so the highlight is correct if the user resizes up. */
(() => {
const railLinks = document.querySelectorAll(".chapter-rail [data-chapter-link]");
if (!railLinks.length || !("IntersectionObserver" in window)) return;
const sections = Array.from(railLinks).map((a) => {
const id = a.getAttribute("href").slice(1);
return { link: a, section: document.getElementById(id) };
}).filter((x) => x.section);
const visible = new Set();
const setCurrent = () => {
let activeId = null;
let topMost = Infinity;
for (const id of visible) {
const sec = document.getElementById(id);
if (!sec) continue;
const top = sec.getBoundingClientRect().top;
if (top < topMost) { topMost = top; activeId = id; }
}
railLinks.forEach((a) => a.classList.remove("is-current"));
if (activeId) {
const link = document.querySelector(`.chapter-rail a[href="#${activeId}"]`);
if (link) link.classList.add("is-current");
}
};
const railIO = new IntersectionObserver((entries) => {
for (const e of entries) {
const id = e.target.id;
if (e.isIntersecting) visible.add(id);
else visible.delete(id);
}
setCurrent();
}, {
rootMargin: "-25% 0px -55% 0px",
threshold: 0,
});
sections.forEach((s) => railIO.observe(s.section));
})();
/* Tab-click scroll-jump fix
The CSS-only radio tab pattern in Section 04 (Without/With) and
Section 05 (Foundations) places the radio inputs at top:0 of their