Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 209 additions & 1 deletion src/loregraph/web/marketing-patch.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,100 @@ const CORRECTIONS = `
* separating and the four links ran together. */
.lang-switch { gap: 14px; }

/* The switcher now lives in the header instead of on its own strip. It has to
* earn its 211px there, and the header was already tight: at 1281 the brand's
* second line reappears and the row needs 1345px against 1201px of content.
*
* The second line is what goes. "Apache-2.0 / evidence-anchored graphs" repeats
* the hero label sitting 120px below it and the licence line in the footer, and
* it is the single widest thing in the header at 227px. Without it the row needs
* 1100px, which clears from about 1180 up; between 1081 and 1180 the repository
* pill stands down too, the same way it already does below 1080. */
.brand-meta { display: none; }
@media (max-width: 1240px) {
.nav-cta { display: none; }
}
.lang-switch {
display: inline-flex;
align-items: center;
font-family: var(--sans);
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-mute);
white-space: nowrap;
flex-shrink: 0;
}
.lang-switch b { color: var(--ink); font-weight: 600; }

/* Nothing declares scroll-margin-top, and the header is sticky, so every anchor
* jump lands the target's top at viewport 0 — behind the header. On a phone all
* four section links bury the section's own rule and label under 158px of
* chrome; on desktop it is "back to top" that lands 60px under. The links were
* hidden below 1080 until this pass, so the phone case never showed.
*
* The offsets are the header's own measured heights plus 15px of air: 85px above
* 1080, 140 for the two-row band, 158 for the three-row one. */
:target,
section[id],
[id='top'],
[id='agents'],
[id='labs'],
[id='contact'] { scroll-margin-top: 100px; }
@media (max-width: 1080px) {
:target,
section[id],
[id='top'],
[id='agents'],
[id='labs'],
[id='contact'] { scroll-margin-top: 156px; }
}
@media (max-width: 620px) {
:target,
section[id],
[id='top'],
[id='agents'],
[id='labs'],
[id='contact'] { scroll-margin-top: 174px; }
}

/* Below 1080 the header is already two rows — brand and actions, then the
* section links. The switcher needs 228px of its own, which at 390 leaves 96px
* for the links if it shares their row and nothing at all if it shares the
* first. So it takes a third row from 620 down, right-aligned under the links.
* Three rows, one job each, and still one band fewer than the strip it
* replaced — that was two rows of its own on a phone, plus the header's. */
@media (max-width: 1080px) {
.nav-inner {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-areas:
'brand . side'
'links links lang';
align-items: center;
row-gap: 12px;
}
.nav-inner > .brand { grid-area: brand; }
.nav-inner > .nav-side { grid-area: side; }
.nav-inner > nav { grid-area: links; min-width: 0; }
.nav-inner > .lang-switch { grid-area: lang; justify-self: end; padding-top: 11px; }
}
@media (max-width: 620px) {
.nav-inner {
grid-template-areas:
'brand . side'
'links links links'
'lang lang lang';
}
.nav-inner > .lang-switch { justify-self: start; padding-top: 0; }
/* Three rows at 22px of padding is a 178px sticky header on a 844px phone,
* 48px more sticky chrome than the two bands this replaced. The rows are the
* price of four languages and a real call to action on a 342px line; the
* padding is not. */
.nav { padding: 12px 0; }
.nav-inner { row-gap: 10px; }
}

/* The corpus section's 90px against every other section's 130px is not an
* inconsistency and is deliberately left alone: it wraps the dark panel, which
* carries 110px of its own, so 90 + 110 comes out ahead of a plain 130. The
Expand Down Expand Up @@ -299,7 +393,6 @@ const CORRECTIONS = `
* content, not ornament — .about-caption is the museum credit for the plate and
* .about-side-note states the closed-world rule. Same floor everywhere. */
.topbar-inner,
.nav-links a .num,
.about-caption,
.card .num .tag,
.lab-img .badge,
Expand Down Expand Up @@ -327,6 +420,13 @@ const CORRECTIONS = `
.work-card .small-label,
.work-rule { font-size: 11px; }

/* .nav-links a .num is NOT in that list, and was wrongly put there once. It is
* an absolutely-positioned superscript counter at top:-7px right:-16px — pure
* ornament, not running copy — and those offsets are drawn around a 9px glyph.
* At 11px the counter grew past them and printed into the neighbouring link.
* The floor is for text a reader has to read; this is a tick mark. */
.nav-links a .num { font-size: 9px; }

/* Seven section headlines came out at seven sizes — 64, 66, 68, 72, 74.9, 77.8
* and 95px at 1440 — because each one got its own clamp() ramp: three of them
* differ only in the vw coefficient with identical endpoints. The 95px one is a
Expand Down Expand Up @@ -397,6 +497,53 @@ html[lang='ja'] .work-copy h2 { line-height: 1.2; }
* numeral to the caption's first baseline puts it back on a line of text. */
.sec-rule { align-items: baseline; column-gap: 18px; }

/* The rule is three unequal items in a space-between row — numeral, plate
* caption, counter — so the caption lands wherever the two ends leave it: 26px
* left of the band's centre at 1150, and a different amount at every width. It
* is neither centred nor on a column edge, six times down the page. A three
* column grid with equal outer tracks puts it on the centre line and keeps it
* there. The outer tracks are minmax(0, 1fr) so a long caption can still push
* into them rather than overflow.
*
* Only from 700 up. Below that the caption is wider than the third of the band
* a centred layout would leave it, the outer minmax(0, 1fr) tracks collapse to
* nothing, and the counter ends up printed over "THE MET". There is no centre
* to find on a phone — the export's own space-between, with the caption allowed
* to wrap, is the right behaviour there. */
@media (min-width: 700px) {
.sec-rule {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
}
.sec-rule > :first-child { justify-self: start; }
.sec-rule > :last-child { justify-self: end; }
.sec-rule .meta-grp { justify-self: center; }
}

/* This row summarises the whole section, but it sits inside the left column of
* the section's two-column grid — 495px, and its three children need exactly
* that, so every one of them is at its minimum width. The four claims wrap to
* two ragged lines and "One rule / ≥ 95%" is crushed into 58px, which is three
* words over five lines. Letting the row wrap gives the claims their own line,
* and the stamp keeps its two parts whole. */
.about .footer-row { flex-wrap: wrap; row-gap: 16px; }
.about .footer-row > span:not(.mark) { letter-spacing: 0.08em; }
/* The stamp is a two-line stack by design — "One rule" over "≥ 95%" — but at
* 0.18em it needed 90px for the first line inside the 57px the squeezed row
* gave it, so that line broke too and the stamp came out as three. It keeps its
* two lines and stops shrinking. */
.about .footer-row .stamp { flex: 0 0 auto; white-space: nowrap; }

/* The five section links were never given nowrap, so whenever the header runs
* short of room they break inside the word: "Reading / room", "Quick / start".
* Above 1280 the brand's second line reappears and takes the room, which is the
* band where it showed. Links are labels, not copy — they do not wrap. The 38px
* gap pays for it; 28px is still twice the word space. */
.nav-links a { white-space: nowrap; }
@media (min-width: 1081px) {
.nav-links { gap: 28px; }
}

/* The five filter pills need 522px and their grid column was 488px, so
* "Catalogue" wrapped alone onto a second row while 684px of headline column
* sat half empty beside it. The headline is smaller now and needs less of the
Expand Down Expand Up @@ -751,6 +898,67 @@ const PATCHES = [
return { html: out, count };
},
},
{
name: "fold-topbar-into-header",
why: "two beige bands with one hairline between them read as one 124px block, not as a utility rail plus a header",
// The top bar carried four things. Three of them already appear elsewhere
// within two rows: the wordmark (the nav's own brand mark, one row down),
// "v0.1.0-dev" and "Alpha" (the same fact stated twice, and again in the
// footer), and "Apache-2.0 / Python 3.11+" (also the brand meta, also the
// footer). The fourth, the language switcher, is the only thing on the strip
// that exists nowhere else — so the strip goes and the switcher moves into
// the header, between the section links and the actions.
//
// That is one band fewer above the fold and 39px less before the headline.
// The cost, accepted deliberately: the release link behind "Alpha" and the
// version number no longer appear above the fold. Both are still in the
// footer.
run(html) {
// The switcher is one balanced <span>; the bar is one balanced <div>.
const balanced = (src, at, tag) => {
const open = `<${tag}`, close = `</${tag}>`;
let depth = 0, p = at;
while (p < src.length) {
if (src.startsWith(open, p)) depth++;
else if (src.startsWith(close, p)) {
depth--;
if (depth === 0) return p + close.length;
}
p++;
}
return -1;
};

const barAt = html.indexOf("<div class='topbar'");
const langAt = html.indexOf("<span class='lang-switch'>");
const sideAt = html.indexOf("<div class='nav-side'>");
if (barAt < 0 || langAt < 0 || sideAt < 0) return { html, count: 0 };

const barEnd = balanced(html, barAt, "div");
const langEnd = balanced(html, langAt, "span");
if (barEnd < 0 || langEnd < 0) return { html, count: 0 };
// The switcher has to be inside the bar for this to be the right surgery.
if (langAt < barAt || langEnd > barEnd) return { html, count: 0 };

// The switcher's four items were separated by " · ". drop-dot-chains would
// turn each into two non-breaking spaces, and in the header the switcher
// is an inline-flex row with a real 14px gap — so those spaces become flex
// items of their own and the group pays for both separators: 114px of gap
// and padding around 99px of text, 241px in a header that has none to
// spare. The gap is enough on its own.
const lang = html
.slice(langAt, langEnd)
.split(" · ")
.join("")
.split("&nbsp;·&nbsp;")
.join("");
let out = html.slice(0, barAt) + html.slice(barEnd);
const shift = barEnd - barAt;
const side = sideAt > barAt ? sideAt - shift : sideAt;
out = out.slice(0, side) + lang + "\n " + out.slice(side);
return { html: out, count: 2 };
},
},
{
name: "corrections-stylesheet",
why: "the export has no working phone layout — the document floors at 541px wide",
Expand Down
15 changes: 15 additions & 0 deletions src/loregraph/web/marketing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ the measurements behind each one. In short:
- **Navigation on a phone.** The header dropped all five section links below
1080px with nothing in their place, and the footer hid two whole columns —
the repository, the issues, the changelog and the licence.
- **One header, not two bands.** The top bar and the header had the same
background, the same width and the same 11px tracked caps, separated by one
hairline, so they read as a single 124px beige block with a stray line through
it. Three of the bar's four items already appeared within two rows: the
wordmark (the nav's own brand mark), "v0.1.0-dev" and "Alpha" (one fact twice,
and again in the footer), "Apache-2.0 / Python 3.11+" (also the brand meta,
also the footer). The fourth, the language switcher, moved into the header and
the bar went.
- **Furniture that fits its column.** Each section rule centred its plate
caption by accident — `space-between` across three unequal items, landing 26px
off the centre line at 1150 and somewhere else at every other width, six times
down the page. And the section-summary row was placed inside the left grid
column, whose 495px is exactly its three children's minimum, so the four
claims wrapped to two ragged lines and "One rule / ≥ 95%" was crushed into
57px — three words over five lines.
- **The star count.** The live count replaced the whole button label, so
"在 GitHub 上 Star" became "Star · 5" a moment after load on three of the four
pages.
Expand Down
Loading