Repository navigation
site: two new designs (Signal, Bulletin), retire Manual, serve from sscsb.dev - #58
Merged
Merged
Conversation
BASE_PATH becomes "/" and SITE_ORIGIN becomes https://sscsb.dev. The site stops being one tool path-routed under a shared tools domain and becomes the whole of its own. The umbrella "Sensible Security Tools" landing retires with the move. It was not optional tidying: build() wrote it to dist/index.html AFTER the design loop, so at BASE_PATH "/" it would have overwritten the SSCSB home with a registry page listing one tool. designs/ledger/landing.ts is deleted; nothing else imported it. build() also wrote every page under a literal "sscsb" directory, so BASE_PATH and the output tree could disagree in silence — exactly what moving to the domain root would have broken. The output directory is derived from BASE_PATH now, and the test that asserts the tree derives it the same way. Each design had four fully-hardcoded absolute URLs (origin, base path and all) in the prefilled issue bodies it puts on other people's trackers, and NOTHING tested them: every one would have kept shipping the old host after this migration with the suite green. They are built from SITE_ORIGIN + BASE_PATH in one new module, designs/share-urls.ts, and 16 tests assert about the ORIGIN the text carries rather than a spelling, so a design that re-hardcodes any host fails whatever it hardcodes. Proven red-first at all three call sites. Two things that test taught, both kept as comments: the nudge body is URL-encoded inside an HTML-escaped attribute, and decoding only & reads ' as a parameter separator and truncates every body at its first apostrophe; and the two nudges are different call sites — the local-scan one renders for a provisional listing, the authenticated-scan one only for an EXTERNAL-lane record — so rendering one fixture covers one of them and a regression in the other survives green. The scan-intake relay is a functional dependency, not a doc edit: sscsb.dev joins the CORS allowlist and the issue footer, and tools.sensiblesecurity.xyz stays DELIBERATELY until the old host stops serving, since pages still being served from it call this endpoint. Removing it is a later, separate step, and a test now pins both origins so that step has to be deliberate. designs/manual/** is untouched on purpose — it is being retired in a separate change and still carries its original literals. Verified: tsc clean; 466 tests pass (450 before, 0 fail); build succeeds; dist/index.html is the SSCSB home with canonical https://sscsb.dev/; dist/CNAME reads sscsb.dev; alternate designs at dist/_d/<id>/ with no /sscsb/ prefix; a dist walk resolved 517 internal href/src refs with 0 broken (checker proven able to fail by planting one). The 22 residual "sensiblesecurity" strings in dist are 12 in the shipping trees, all of them repo.description text inside SIGNED scan records, plus 10 under _d/manual/. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
A poster grid in ink on warm off-white with exactly one hot accent, spent
only on state that matters: a failing grade, a fail segment, a named
contradiction, the active nav item, the one call to action. Hard 2px rules
instead of cards, no shadows, no radii. The numbers that matter — grade,
percent passed, percent answered, control counts — are set at poster scale
in Anton; body is Source Sans 3, data is IBM Plex Mono, each with a real
fallback stack. A strict 12-column grid that collapses to a single column.
It ships its own complete stylesheet (`css`), so it does not ride
public/style.css, and maps its palette onto the shared components' bridge
tokens: `pass` is INK, because a passing check is the page's ordinary
state, and `fail` is the accent, which is the whole reason the accent
exists. Registered LAST in DESIGNS, so Ledger stays the default.
Every honesty rule is rendered, and none of them is this design's to
reword: lane marks resolve through trust.ts (the local stamp is dashed and
unfilled — attributable, and weaker than the action lane); a listing under
the coverage floor names the unanswered checks and the one-line fix from
coverage.ts; a contradiction between evidence sources is named on the row
AND on the detail page from shared-facts.ts; unverified is hatched
everywhere and counted nowhere; A+ is exactly 100%; provisional tags stay.
The shared section renderers supply the facts; only the presentation is new.
MOBILE IS A CORRECTNESS TARGET. Below 760px the directory and the control
table restack into labelled blocks — `td::before` prints the `data-label`
each cell now carries — so the page never scrolls sideways. That works
without touching the shared filter: filter.js only ever sets `display:none`
or `""` on a row, so a shown row falls back to whatever the stylesheet
says. Every id, data-attribute and selector filter.js pins is unchanged.
Tables that cannot restack (the Scorecard comparison, the every-check
index, whose markup is shared) keep a scroller with a VISIBLE affordance:
an always-drawn scrollbar plus an edge shadow that retracts at each end.
Verified in real Chrome with lifecycle live, at 1440 and 390, on all four
pages. Four defects were found that way and fixed:
* "no evidence" is a nowrap label in what was a fixed 5.5ch track. At
390px it pushed 29px past that track, walked up through .prules →
.poster-slab → main, and made the document 403px wide on a 390px
screen — while every element's own rect measured clean, so only
documentElement.scrollWidth showed it. The track sizes to content now.
* A phase with nothing in scope drew "n/a" INSIDE a 10px bar box that
clips: the box that exists to show a length was slicing its own label
in half. It is a dashed baseline now, with no box.
* The phone stylesheet hides the banner's <br>, and with no whitespace
behind it the two clauses rendered as "not runis not a pass".
* The skip link measured 40px against the 44px floor.
Two design-count canaries are bumped from four to five (layout.test.ts,
home.test.ts). They are canaries by design — the literal is what forces
someone to look at the suite when the registry grows — so the count moves
and the assertion does not soften to DESIGNS.length. 450 tests before,
489 after, 0 failing; tsc clean.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
The four-up trial is now three: Manual loses, Ledger stays default. Its directory, the registry entry and the DesignId member go together; the design-count canaries in the tests move when the two new designs land. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
…site-redesign # Conflicts: # site/src/designs/registry.ts # site/src/designs/types.ts
A fifth entry in the design trial, appended last so the Ledger stays the
default. Signal is the "record of evidence" direction: warm paper (#FBFAF7)
and warm ink (#1A1714) with ONE brand accent, blue #2B5FD9.
The accent is blue for a structural reason, not a taste one. This site
encodes control state in colour — pass, fail, gap, unverified — so green,
amber and red are spoken for. A brand accent inside that range would teach
the eye, in the first three seconds, that colour here is decoration, and
the directory then asks that same eye to read colour as meaning. Every
status therefore renders as colour AND shape AND text: a filled disc, a
half-filled disc, a struck ring, a dashed ring, each with its word beside
it. That solves colourblind access without a separate mode.
Three mechanisms borrowed from the reference set, each measured before it
was copied rather than remembered:
- the cocoindex header morph. Single threshold at scrollY > 24, no
hysteresis, class toggled inside requestAnimationFrame on a passive
listener, everything else a CSS transition over 0.42s
cubic-bezier(.2,.8,.2,1). At rest a full-bleed bar owns the glass and
the hairline; past the threshold the bar goes transparent and the inner
element becomes a pill that takes the blur, border and long-throw
shadow with it. Measured at 1440: max-width 100% -> min(1100px, 100% -
24px), radius 0 -> 999px, translateY(0) -> translateY(10px), padding
48px -> 18px. Identical behaviour at 390, where the pill lands at
366px. The header never dissolves and never becomes a hamburger — on a
page with a 200-row table, chrome that hides is chrome that hides
search exactly when it is needed.
- Paperclip's fill-less hairline grid, hardened into a real table: zebra
striping (at fifty rows hairlines alone stop the eye tracking),
tabular numerals, 52px rows, hover a tint and nothing else.
- OpenAI's numbered sticky chapter pills, scrolling sideways on a phone
with a mask at the edge so the cut reads as "more this way".
Deliberately NOT copied: decorative colour, a 16k-pixel scrollytelling
hero, and marketing chrome. The hero has no "Get started" — the directory
search box and a live stat row are the whole of it.
Mobile is a first-class target, and the 44px floor is enforced rather than
inherited: three of the four reference sites fail it.
Verified in Chrome at true 390px layout-viewport reflow
(Emulation.setDeviceMetricsOverride), all four page kinds, both widths: no
horizontal page overflow, no clipped text, all three faces loaded, the
switcher visible with its clearance measured, the filter driven for real
(3 rows -> 1 -> 0 -> 3).
Six defects that only the browser found, and their fixes:
1. overflow:hidden on the table wrapper silently killed the sticky
header. The wrapper became the sticky containing block, so the header
stuck to a box that never scrolls — at scrollY 900 its top read -382.
overflow:clip trims the corners without creating a scroll container.
2. the chapter rail's scroll-spy bound nothing: it ran inline, above the
sections it points at, so every getElementById returned null. Wired
up on DOMContentLoaded instead.
3. the same spy ranked sections by intersectionRatio, which is
intersected-area over ELEMENT-area — a tall section filling the screen
scores lower than a short one peeking in. Replaced with a reading line.
4. that line sat 6px above where a clicked anchor lands (sections carry
scroll-margin 136px), so the rail marked the previous chapter.
5. the wordmark and the chapter pills were under the 44px tap floor
(72x27 and 20x20 for the wordmark; 40px pills). The shared block's
nav-a rule cannot reach the wordmark — it is a sibling of the nav.
6. the hero's last-scan date wrapped mid-token ("2026-09-" / "03"), which
reads as a rendering fault. The stat row now spans the hero.
The two test literals that pin the design roster go 4 -> 5. They are
literals on purpose: comparing them to DESIGNS.length would be
tautological, since both lists are derived from the registry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
…te-redesign # Conflicts: # site/src/designs/registry.ts # site/src/designs/types.ts # site/test/home.test.ts # site/test/layout.test.ts
The two new designs were written before share-urls.ts existed and hand-built their off-site URLs, so the domain move to sscsb.dev left them shipping the old host into other people's issue trackers and footering it on every page. Several smaller correctness defects rode along with them, and three of the fixes turned out to belong in one shared source rather than in the new designs alone. Host honesty - signal/ and bulletin/ directory nudges now build their URLs from ../share-urls (listingShareUrl, METHODOLOGY_SHARE_URL, shareUrl, LOCAL_METHODOLOGY_SHARE_URL), exactly as ledger does. Decoding the rendered nudge bodies for all five designs now yields https://sscsb.dev as the only site origin. - share-urls.test.ts drives the nudge assertions off DESIGNS instead of a hand-kept list of three; the stale "widen once manual is gone" comment is gone. Widened against the unfixed sources first, which failed 4 assertions across signal and bulletin. - config.ts gains SITE_HOST_LABEL, derived from SITE_ORIGIN + BASE_PATH, and all five footers use it — the three that already said sscsb.dev included, so the next move is one line. A new test asserts no rendered page of any registered design names the old host in the site's own voice, scoped by stripping record descriptions (untrusted repo prose that legitimately names it — the live records do), with a control proving the scoping is load-bearing. One source instead of five copies - trust.ts gains LANE_TITLE. Signal and chain described the local lane as "a shorter chain than the authenticated lane"; the contract word, in trust.ts's own header and the methodology, is WEAKER. Every design's lane tooltip now reads from it; visible labels stay each design's voice. - scoring.ts gains PHASE_NAMES beside PHASES. Ledger, console and chain had five-entry maps and rendered a bare "Phase 6"; signal called it "Other checks" where checks.ts and reclassify.ts both say distribution & publishing. - reclassify.ts gains CONTROL_COUNT. Four home pages hardcoded 54, and signal's "one verdict each" denied the third state; they now derive the count and say "each answered or left unanswered". Fragments - bulletin/methodology.ts had no id="trust" while bulletin's directory links methodology/#trust through the shared directoryTermsNote. A new test walks every internal #fragment on every page of every registered design and checks it lands on a real id — it found the same dead anchor in chain and console, both fixed here. Smaller correctness - signal listing rows use gradeBadge(r.score), so the provisional tag rides with the grade as it does on the detail page. - signal's partly-fixable coverage note said "The rest fail elsewhere", turning UNVERIFIED into fail; it now names the rest as unanswered controls a repository scan can see. - signal commits to one light theme, so it now declares color-scheme: light — without it a dark UA paints the directory's native search and select controls dark on a page painted light throughout. - signal's --ink-3 was 4.18:1 on --paper-2 and 3.89:1 on --paper-3, under AA for the 11px lane chips and for all 38 places the token sets text; darkened to #6A635A, which clears 4.5:1 on every surface while staying a clear step below --ink-2 so external and unsigned still read weaker than verified. - data-api / data-fallback are escapeHtml'd in all four designs that were interpolating them raw. - bulletin's masthead said "Edition 0.3.0" with no source behind it; it now shows the methodology and schema versions from config. - bulletin's lead slab showed a provisional grade at poster size with no incomplete note; exemplars.ts now exports incompleteNoteFor as the one source for that sentence and the slab uses it. - relay corsHeadersFor returns Vary: Origin unconditionally. Sending it only on the allowed branch is the wrong way round: the cacheable-and-replayable case is a REFUSED response being served back to an allowed origin. - public/style.css drops the dead .landing-*, .registry-card and .reg-* block left by the deleted domain-root landing — verified unused against all 549 class tokens in the built site. - filter.test.ts's "the shipped value by default" comment was wrong (the shipped prefix is /directory/); reworded to say why the stub keeps the old shape. site: 553 pass / 1 skip / 0 fail (was 504), tsc clean, build green. relay: 60 pass / 0 fail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
Round 1 scored 2-3 against a bar of >=4 on every judge for aesthetics,
delight and seamlessness at 1440 AND 390. Every blocking and major item is
fixed or refused with the measurement that refuses it; the minors that were
cheap and did not fight the register are fixed too. Nothing in "Strengths
noted" was touched: the pill morph, the numbered rail, the four-shape status
vocabulary, the one-accent palette and the zebra table are all as they were.
THE BLOCKING ONE. The directory printed every merge finding in full inside
the Repository cell, so three listings sharing one ~130-word self-report
paragraph made 318/338/337px rows at 1440 and ~700px cards at 390 — the
product's hero surface had stopped being a table. The findings still ride the
row; all but a contradiction ride it folded, counted by their own summary
("2 notes on this record"). A CONTRADICTION is never folded, because types.ts
requires the disagreement to be named on the listing and the detail page, and
a new test pins that split rather than trusting this paragraph. The
description and the disclosure share one line, and the summary buys its 44px
hit area with padding it returns as margin. Measured: 83/96/96 at 1440,
292/331/330 at 390.
WHAT ELSE MOVED, each measured in real Chrome at a true 390px layout viewport
through device-metrics override:
- The 44px floor had no inline half (--tap was applied via min-block-size
only), so the GitHub icon was 41x44 at 1440 and 35x44 at 390 on all four
pages. Now 44x44 at rest and inside the floating pill. Row links, the Order
label, the nine exposure-group links and 18 incident links join it: the only
sub-44 control left on any page is the 20px checkbox the shared block sets,
whose label supplies a 166x44 target.
- Red stopped meaning "caveat". Every merge note wore the fail hue on rows
carrying data-contradictions="0", teaching the eye red before it met a real
FAIL glyph. Contradictions keep it; notes are hairline; the merge-findings
card is accent, not fail.
- 42 "EVIDENCE" labels per repo page had no affordance — `summary
{display:inline-flex}` suppresses the native marker. They carry a drawn
caret that rotates on open and their own line count ("evidence · 3").
- The control grid kept its desktop `auto` chip column at 390, so eleven words
of reason wrapped in a 190px column with a void beside it. The chip takes
its own line; the reason went 190px -> 272px.
- Three methodology tables clipped at 390 with no scroll affordance, hiding
the 4-column comparison's entire payload. They stack as labelled cards the
way .directory already does; scrollOver is 0 on all five.
- The design switcher was a 370x102 two-row slab at 390 and covered the A2
row's status text and the table's SOURCE/SCANNED columns at 1440. It
collapses to 85x56 on a fine pointer and expands on hover or focus; on a
coarse pointer every link stays reachable on ONE row. Covers nothing at 1440
on any page.
- Command blocks wrap at <=560 instead of looking truncated; the two empty
home sections became two compact cards with every word intact; the honesty
chapter stops being the one carded chapter among six; code chips survive the
zebra fill; cards are capped at 920px instead of spanning 1264 with prose
stopping at 700; nine identical disclaimers became one; the glossary
footnote moved up under the marks it defines; a family with no controls in
scope renders no card; search returned to the persistent pill.
- Found while measuring, not on the list: at 320px the header pill pushed the
document sideways (scrollWidth 325). Fixed.
REFUSED, with the reason: inline-in-paragraph links stay at their text height
(WCAG 2.5.8 exempts them, and 44px inline targets would overlap each other and
wreck the prose); "Action" stays out of the phone pill because .sg-nav
measures scrollWidth - clientWidth = 16 at 390 once the GitHub link is at its
44px floor.
tsc clean; 559 pass / 1 skip / 0 fail (baseline 553, +6 new); build green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
Every blocking/major item from the round-1 audit, plus the cheap minors.
Measured at 1440 and at a TRUE 390px layout viewport (device-metrics
override), not at a scaled desktop render.
The switcher (majors 1, 2, 10, 35; minor 29). It wrapped to 370x98 at 390px
and stood over the grade slab and the first listing's score line at every
scroll position, and at 1440 its 407x54 bar overlapped the figure band's
fourth numeral at rest. Now: one full-bleed 44px row docked to the bottom
edge on a phone (five 78px links, label dropped, safe-area inset), and at
desk width a 174px "DESIGN | BULLETIN" chip that opens on hover or keyboard
focus. The four hidden links collapse to zero AREA rather than to display:none,
so they keep their seat in the tab order — measured: focusing the current link
expands the strip to 445px with all five links at 44px. Zero covered text at
scrollY 0 at 1440.
The directory stopped decorating (majors 4, 5, 6). The ~95-word "Two scores
are on this page" explainer was printed verbatim under all three listings
alongside the commit-mismatch note, and the hot rule fired on every row of a
page with no failures. A contradiction still prints in full on the hot rule —
that is a published rule, not a layout choice — and the other three merge
findings fold behind one mono line carrying the facts that actually differ
(local 3cb1290 != scan c8a2349 · self-reported A+ 100% vs DIRECTORY A+ 100%).
The sentences inside the fold are shared-facts.ts verbatim, not a paraphrase.
Rows 340px -> 169-241px at 1440; cards 812-884px -> 565-637px at 390.
The control sheet is a results table again (majors 19, 20, 21, 22; minors 45,
50, 51). The Phase column spent ~60px on one digit in all 54 rows; it is now a
ruled band per phase that doubles as an anchor. Rows share a line with their
verdict, the DETAIL label is gone, evidence is an inline "EVIDENCE (n)" with a
real disclosure marker (the shared layer's summary{display:inline-flex} had
been eating it), and a sticky JUMP TO PHASE strip carries a CSS-only "hide the
13 out-of-scope checks" fold — verified live: 13 rows hidden on check, 13 back
on uncheck. The UNVERIFIED chip paints the hatch as a 16px swatch instead of
as the background behind its own letters, and the repo sheet now carries the
directory's key. Page 20,264px -> 11,817px at 390.
The methodology's tables restack (majors 13, 14, 16; minor 36). Four tables
560-747px wide in a 354px box were hiding the sscsb-mapping column — the
reason that table exists — and cutting questions mid-word. All five are 354px
now, 0 clipped cells. Their markup is shared, so the labels come from column
position and the two shared tables are told apart by data-coverage. The
section index moved out of the page head so it can stick: one scrolling 48px
row with the edge shadow, on a 32,000px page that had no navigation at all.
The accent went back to meaning something (majors 15, 8; minors 31, 34, 37,
40). 155 control ids on the methodology were hot red — 0 now. The figure
band's A+ is ink, matching the design's own grade scale. --rust was #8A4418,
hue 24 beside the accent's 8, which read as a second colour; it is #8F200C.
The poster's one sentence lost its hard <br> for a 13ch measure plus
text-wrap:balance: "A CHECK THAT / COULD NOT RUN / IS NOT A PASS." at both
widths, no orphan.
Also: the two waiting panels sit side by side at >=1000px instead of running
1,400px of blank paper down the middle (major 9); commands wrap instead of
clipping at every width (majors 24, 41, 49, 53); every prose link carries a
44px hit rectangle via inline padding-block, which moves no type (majors 3, 7,
11, 12, 17, 18; minors 26, 30, 39) — 0 sub-44 targets on all four pages at
both widths, against 22 and 15 in round 1; Anton's 400 face is asked for
explicitly so the repo title stops being synthetically emboldened and the
case-sensitive slug stops being uppercased (minors 42, 48); a record scored
under an older methodology says so on the row (minor 52); +local n is defined
in the key (minor 27); the out-of-scope chip no longer breaks over two lines
(minor 46); the phase-label column fits all six names on one line (minor 44);
anchors clear the sticky strips; and the colophon has a way back to the top.
filter.js is untouched and still works against this markup: search filters
3 -> 1, clearing restores 3, sort by name reorders, the coverage checkbox
hides all three. 553 tests pass, tsc clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
Round 2 scored 4/4/3, 4/4/4 and 4/3/4 across three judges. Every change here
answers one of the 45 defects they and the measurement pass recorded, verified
in real Chrome at 1440 and at a TRUE 390px layout viewport (device-metrics
override plus touch emulation, so the coarse-pointer branch is the one that
actually runs), not in the source.
The nine majors:
- The sticky chapter rail was transparent to body text on both long pages at
both widths. Root cause measured, not guessed: `mask-image` on `nav.sg-rail`
(the 28px edge fade) makes that element a BACKDROP ROOT, so the plate's
`backdrop-filter` sampled an empty backdrop and painted nothing — leaving 8%
transparency as the only thing between 16px prose and the rail. The fade is
now a `::after` on a non-scrolling wrapper (it cannot live on the scroller or
it scrolls away with the track), the track is its own element, and the plate
is opaque `--surface`, which is what the brief specified for it. Measured
after: 0 of 157 sample points inside the rail reach the page behind it.
- The floating header pill carried the same 92% plate. It is opaque now; the
resting bar keeps its glass, so the morph still reads glass-bar to solid-pill.
This trades a noted round-1 strength for legibility, deliberately.
- The pill dropped Search AND Action on a phone: five items at 1440, three at
390, and no route to the directory's search from 14,000px down a repo page.
Search is persistent at every width now, icon-only below 560px at its 44px
floor with its aria-label intact. Action stays hidden below 560 — it is the
one item the brief does not call persistent and the row does not fit five.
Below 380px the nav becomes its own scroller rather than dropping an item or
shrinking a target: measured, the head had overrun its pill by 32px and
pushed the document to 339px.
- The design switcher kept all five links expanded on a coarse pointer, and at
390 that 314px bar sat over body text at scrollY 0 on all four pages — 93% of
the home hero's caption. It collapses on a phone too now (84x56, coverage
17.5%), and a tap opens it: CSS alone cannot, because the only focusable
thing in the collapsed state is the current design's own link, so layout.ts
intercepts that first tap. Every link stays reachable and in the tab order.
- `.cmp-none` was written unscoped, so it also hit five BARE `<span>—</span>`
markers with no border-width reset: initial values gave them 3px dashed
`currentColor` with no radius — a tofu box that also borrowed this design's
dashed "nobody could answer" shape for a cell carrying no status. Scoped to
the pill, and the dash itself replaced with the words "No sscsb equivalent",
because this site's rule is colour AND shape AND text.
- Every heading tier below the hero inherited the body's 1.6 leading and h2
never scaled down, so at 390 the ladder read 40/32/28/22 — a 1.14 ratio
between a page title and its section headings against 1.71 at 1440. Now
1.06/1.12/1.20 with a phone-width mid tier: 40/32/22/17.
- The search box promised "type any owner/repo to search the record" and
answered a non-match with a void plus an orphan 43px header row. It says so
now, echoes the query, and hides the table shell. `public/filter.js` is
shared, so this reads back the row visibility that filter.js sets rather than
re-deriving any matching rule; the proper fix is filed as a proposal.
And the minors worth taking: the nine incident disclosures open with a count
(the section's intro promises those primary sources and no reader met them);
one 200-character reason stated once per family instead of six times across a
42-row grid; the glossary as a real definition list built from `glossary.ts`;
zero-count verdict glyphs dropped to muted ink so an all-passing directory
stops spending red and amber on nothing; numeric columns right-aligned on
tabular numerals; card meters bound to their own figures; the listing card
tappable with a chevron; stacked method tables as hairline rows instead of 54
bordered cards (the check table went 11,768px to 6,053px at 390); code blocks
sized to their contents with a scroll shadow that only paints when there is
something to scroll to; and inline identifiers that no longer break mid-token
("...github" / ".com" was a mis-keyable principal in a command a compliance
reader is meant to copy).
Refused, with reasons: every remaining sub-44px target is an inline-in-prose
link (WCAG 2.5.8's inline exception) — 0 non-inline controls are under 44px on
any page at either width; "Action" at 390 stays hidden; and the directory's
52px row is not reachable while the coverage floor's one-line fix rides on the
row, which types.ts requires.
tsc clean, 576 pass / 1 skip / 0 fail (559 before, 17 new), build green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
Every major, plus the minors that were not pure taste. Measured at 1440 and
at a TRUE 390px layout viewport (device-metrics override on a lifecycle-live
instance), not at a scaled desktop render.
THE SWITCHER IS NOT FIXED ANY MORE (majors 1, 2, 7; minors 24, 26). Two rounds
of shrinking a fixed overlay did not stop it painting over body text, because
nothing can: round 1 was two rows standing on the grade slab, round 2 a 174x48
chip that still cut nine text runs out of the Scorecard comparison between
scrollY 8100 and 9900, clipped the last glyph of ALL ANSWERED CHECKS PASSED on
the repo sheet, and at 390 covered "100% passed · coverage 90.9%" outright at
rest. --switcher-clearance reserves room at the END of a document and can do
nothing for the MIDDLE of one. It is set in the colophon now, in the flow,
where a broadsheet prints its apparatus — the label comes back, the box lines
up with the colophon rule above it, and body{padding-block-end} goes to 0
because there is no overlay to clear. Measured after: a 300px-step scroll sweep
over all four pages at both widths reports ZERO fixed overlays and zero
occluded text runs, against 10 events over 9 texts on the methodology page
alone in round 2. The cost is honest: switching design on a 31,000px page now
means reaching the colophon, and the navigation for that (sticky section index,
jump-to-phase strip, the colophon's own back-to-top) is navigation built for
the job rather than review chrome parked over the copy.
THE JUMP STRIP WRAPS INSTEAD OF SCROLLING (majors 13, 25, 34; minor 8). It held
558px in a 358px box, so the one control that changes what the table shows —
worth 13 rows and 1,646px of page — sat 200px off-screen reading "Hide th",
riding the top of an 11,817px page behind a 14px fade. Two rows, 92px of sticky,
label fully visible: measured scrollWidth 358 == clientWidth 358, ctl-hide
right edge 374 inside a 390 viewport. The phase anchors are re-offset to clear
two rows (#phase-3 lands at 96 against a bar bottom of 92).
THE CONTROL SHEET STOPPED REPEATING ITSELF (majors 15, 36; minor 27). Four
rationales covered 25 of 44 rows, two of them three lines at desk width and
five on a phone. Each prints IN FULL, once, as a numbered note under the KEY;
every row it covers keeps its own leading clause, a link, and the whole sentence
in `title`. The rule is structural, not a match on wording — repeated, over 60
characters, and carrying a clause break — so a contradiction, which names the
commits that disagree and is therefore unique to its row, can never be folded
by it. 14 rows now reference 3 notes.
THE ACCENT WENT BACK TO MEANING SOMETHING (major 14, 35). All 27 raw-verdict
chips wore the accent family against 44 rows, including 11 rows already greyed
out as out-of-scope and 6 where raw and final both say pass — louder than the
ink-filled PASS chip beside them. 9 now, only where a verdict actually changed
inside the scored scope. And fail stopped being hue alone: it takes a
counter-diagonal hatch, so a phase bar reads pass = solid, fail = "/",
unverified = "\" in greyscale, in print, and to a protanope — the third state
earned a pattern for exactly this reason and the second one had not.
Also: the nine attack boxes go 2-up at desk width instead of ten ruled panels
each filling half its own measure (major 16); the closing sentence repeated
verbatim under all nine attack groups shows once, as the footnote it is
(minors 3, 9, 17, 28, 37 — the sentence lives in threats-shared.ts, so this is
CSS here and a proposal there); the italic axis is actually requested, so
italics are a loaded face rather than a slanted upright — measured, the two had
an identical 739.516px advance (minor 4); the glossary sets in broadsheet
columns and fills the measure (minor 44); the repo meta rule draws its
separators on the TAIL of each item, so a wrap can no longer open a line with an
orphaned middot (minors 6, 22); phase 6 is advertised AND present — the band,
the jump strip and the hero now agree, and a phase with nothing in scope says
"no checks in scope" rather than "no evidence", which are different facts
(minor 38); the home board prints a key, in plain words, under the bars it
explains (minor 39); the Scorecard table's four columns are allocated rather
than left to auto (minor 20); the formula slab takes 72ch instead of the full
measure (minor 21); the phase block fills its own grid (minor 19); the evidence
disclosure has a fixed home, so all 42 start on the same edge instead of
trailing prose at a different x on every row (minor 30); the section index
says less so more of it fits — 1,394px of links in a 358px strip became 970
(minor 29); the re-verify command sets at 11px on a phone so the 38-character
signing principal fits whole rather than breaking mid-character (minor 43);
the already-listed chips fill their row at both widths with the eyebrow on its
own line (minor 23); Anton is asked for at the one weight it loads (minor 10).
Measured after, all four pages at both widths: 0 fixed overlays, 0 occluded
text runs over a full scroll sweep, documentElement.scrollWidth == viewport
everywhere, 0 sub-44 tap targets (the bare 20x20 coverage checkbox inside its
250x44 label excepted, as in round 1). Repo sheet 8,549 -> 8,106px at 1440.
559 tests pass, tsc clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
Nine majors and fourteen minors, every one re-measured at 1440 and at a true 390px device-metrics viewport rather than read back out of the source. THE TWO COLOURS THE REGISTER FORBIDS, BOTH PAINTED BY THE BROWSER. The search field kept appearance:auto, so Chrome drew its own cancel glyph at #40649F — a system blue — inside the home hero a hundred pixels above the one hot CTA, on the two most-visited pages. The directory's coverage checkbox likewise drew the native control: the only rounded corner, the only pure-white fill and the only 1px border on a page whose stated rule is hard 2px rules, and system blue once checked, while the repo sheet's identical control drew a hard ink square. Both are now this design's own ink; measured, no element on any of the four pages at either width reports a border-radius or a box-shadow. A CONTRACT A READER COULD NOT PARSE. Round 1 stopped the local-lane contract block clipping by wrapping it, which traded a visible defect for a silent one: at 354px every continuation landed flush at the KEY column, so `record-fields schema_version methodology_version repo scanned_at scanner` wrapped to a line reading `request_issue controls score` — a key and a value, two lines under the real key `methodology-version 2`. A hanging indent cannot fix it (text-indent applies to a block's first line; a forced break in a <pre> does not restart it, and each-line is unsupported), so the twelve lines are parsed out of CONTRACT_TEXT — the same bytes both trees pin a digest over — and set as rows: an ink slab at desk width, key above value below 760px. The test pins the substitution from both ends, because a reformat upstream would silently put the old block back. NAVIGATION AT THE WIDTH THAT NEVER GOT IT. The methodology's section index was sticky on a phone and static at every desk width — on an 18,900px document the eight pills appeared once and were then eighteen thousand pixels away, at the width where a reader is most likely to be scanning rather than reading. It is one sticky row at every width now, short labels painted and long titles kept as the accessible name, and it carries a scroll-spy: the current section takes an INK FILL (not the accent — it survives greyscale and does not spend the one hot colour on a position) and the strip scrolls itself to keep it in view. Measured across six sections at both widths, the active link tracks and stays visible, and every anchor jump lands clear of the bar (68px against a 48px bar). The repo sheet's jump strip hid its label below 760px, leaving a bare row reading P1 P2 P3 P4 P5 P6 above a table — six tokens a thumb reads as column headings, explained only by a title attribute touch never fires — and painted its underline transparent until a :hover a phone cannot send. The label is back in its short form, the pills carry a resting hairline and a real accessible name, and both long pages got a way back in the strip that was already sticky rather than a second bar of permanent chrome. FILTERING TO NOTHING NOW SAYS SO. Ticking the coverage box took the listings from three to zero and left the page blank with a 13px count as its only feedback. A dashed strip and a reset now appear at zero rows, driven entirely off the contract public/filter.js documents — it observes the rows and fires the events that file already handles, never reaching into it. THE DEAD COLUMN, AND THE ACCENT. Section 07 ran ~6,000px of prose with nothing beside it and subheads set in the body face. The subheads take the page's own anatomy (Anton 400 over a hard rule) and each prose section carries a sticky running head — ordinal and title, aria-hidden apparatus — so the band spans the page and the margin is composed rather than empty; the wide-table section deliberately has none, because there the column IS the table. The honesty panel gives up its hot left rule for ink: that token means a contradiction on the repo sheet, and one token may not mean two things one page apart. Also: the last synthetic-bold Anton heading pinned to 400 (3,639 pixels differ from the real face; forcing 900 is byte-identical to the shipped 700); nine attack-group boxes in three columns so the ninth stops orphaning; the evidence class column widened in rem, not ch — a ch on an 11px mono th made the first attempt a measured no-op; EVIDENCE(n) brought to the row's first baseline (-2px, was ~15px); phase bands and phase-bar labels on one line at 390; the glossary from four columns to two with its terms ruled; the chips single-column and the two waiting states welded into one dashed box under one heading (530px to 348px); P1-P6 given a legend on the only surface that showed them without one; and two screen-reader nits in the KEY. Measured after: 0 sub-44px interactive targets on all four pages at both widths (94 interactive elements on the repo sheet alone), documentElement.scrollWidth equals the viewport at 1440, 390 and 360 on every page, and the only horizontal scroller left is the section index, which says so with an edge fade and now scrolls itself. tsc clean; 597 pass / 1 skip / 0 fail (baseline 576); build green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
Round 3 scored 4/4/4 across three judges and left 36 findings. Everything here answers one of them, measured in real Chrome at 1440 and at a TRUE 390px layout viewport (device-metrics override, lifecycle asserted live). The changes that move the score: - THE STICKY CHROME IS ONE OBJECT. Floating, the header bar goes transparent and only its pill paints, so a 13px slit opened between the pill and the chapter rail and live body text scrolled through it at every scroll position on the longest page. A fixed, full-bleed paper ground now sits behind the whole stack, painted only while the rail is stuck. Measured: 11 text hits in that band, now 0, at both widths. - ONE CHROME AXIS. The rail took whatever the page's content column gave it (904px on methodology, 1264px on the repo page) against a 1100px pill — three left edges for one chrome system. Both pills share the 1100 axis; measured pill x=169.7 w=1100.7 against rail x=170 w=1100. - THE PROSE FLOOR ON A PHONE. Two of three prose classes got SMALLER at 390 than at 1440 because the shared clamps bottom out below their own desktop value. The per-control honesty copy is 15px, exposure prose 16px, detail lines 14px; the 13px JetBrains Mono evidence lines are deliberately untouched, because that is the register for machine truth. - THE CONTROL ROW SPENDS ITS WIDTH. The identifier and its verdict share one line at every width: at 1440 the reason moves into the ~950px that sat empty between them (44 rows, 4890px -> 3762px), and at 390 the chip comes off its own row and drops under the identifier only when it does not fit. - THE FAMILY CHIP CARRIES ITS DENOMINATOR. It read a bare "100%" for a family that had left a check unanswered — the one element a skimmer reads, collapsing the two numbers this site spends paragraphs keeping apart. It now reads "8/8 passed" with the unanswered count inside the same pill. - THE KEY NAMES EVERY STATE. The legend named four states while the control list rendered a fifth (info, 26 marks on one listing). Five entries now, and the info glyph is a solid ring with a dot against unverified's dashed ring, so ring STYLE carries the distinction rather than fine ray detail. - ONE NAV SET AT EVERY WIDTH. "Action" was hidden below 560px, which three judges read as two products. It leaves the pill at every width and moves to the footer, so the four the brief calls persistent are the four that render, everywhere. - TWO EMPTY STATES ARE A LEDGER, AND THEY COME AFTER THE DATA. Ordering by emptiness is the honest layout, and it reverts by itself the day a panel has data because the selector asks whether it is waiting. Also: both meters on one 72px track (a 100% bar was drawing SHORTER than a 90.9% one, because the track length came from the column header's character count); the methodology's five tables break out of the reading column and gain 196px; the local-lane contract block no longer clips its last token; a two-sided, scroll-aware rail fade that never washes the active pill; the nine identical attack-group disclaimers removed from the MARKUP and stated once above the list; an unanswered row links to the remedy section this listing actually renders; the repo meta is a fact list on a phone. FOUND WHILE MEASURING, on no judge's list because every round is measured at 1440 and 390: between the 768px card breakpoint and ~1000px the directory table was CLIPPING its last two columns unreachably (185px on the round-3 build). In that band only, the wrap scrolls with the same right-edge shadow the method tables use and the thead stands down from sticky. Verified in a real browser, not in the source: 0 document overflow at 320, 360, 390, 414, 768, 1024, 1200 and 1440 on all four pages; 0 non-inline sub-44px targets on all four pages at both widths; the whole listing card is the tap target at 390 (hit-tested at three points). tsc clean, 593 tests / 0 fail, build green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
All six re-measured in real Chrome at 1440 and at a true 390px layout
viewport (VerifyViewport device-metrics override), on all four pages plus
all three repo listings. Evidence in MEMORY/WORK/sscsb-site-redesign/
signal/final/.
D1 The trial switcher covered body text at 4-7 of 11 evenly-spaced scroll
positions per page at 390 (worst: 100% of the word PASS in a status chip
on the repo page at scrollY 3817) and 1 of 11 on the directory at 1440.
A fixed control cannot be proven clear of text at every scroll position,
so Signal stops floating it: it is in normal flow under the footer, where
the page reserves real space for it. Now 0 of 11 on every page at both
widths. --switcher-clearance is zeroed because an in-flow strip occupies
the space that padding was approximating; the 44px tap floor is untouched.
layout.ts's tap-to-open affordance stands itself down by asking the live
element for its own position.
D2 span.tx-reported rendered as a 637px amber bar holding 62px of text with
prose flowing under it: .tx-incident is a grid, a grid item blockifies, so
the shared inline-block computed to block. It also spent --degraded, the
"looked for, not found" hue, on an editorial provenance tag. Now hugs its
word (82.4px box / 62.4px text), paper fill + hairline + muted italic, and
sits on the incident date's line.
D3 "resolved by a signed local scan" was rendered 7x on p4gs/sscsb-action in
two competing treatments: hoisted once, 4 compact chips, and 2 verbatim
223-character paragraphs. Repeats were counted per FAMILY, so one sentence
could clear the threshold in one phase and miss it in another. The scope is
the page now: one note at the top of the grid, every row a chip. Measured:
the full sentence appears exactly once in the document (was 3), renderings
are {family-note: 1, ctl-src: 6}. The truncated chip names its own note
rather than reading "see the note above", which named neither of the two.
D4 The status chip sat at three different y-offsets across 44 control rows at
390 (level, 28px below, 52px below) because it was the last item of a
wrapping flex line. It is a pinned grid cell at <=767px. Measured: one
distinct dy (0) and one distinct x over all 44 rows, on all three listings.
D5 .meter-score .meter-fill painted the PASSED bar in #2B5FD9 - the one brand
accent, two columns from the green pass glyph, beside a neutral coverage
meter. The accent is blue precisely so it can never read as a verdict. The
score bar is var(--pass) now and coverage stays neutral ink. Every other
accent use is audited in a comment beside the rule: all are interactive,
brand/identifier, or provenance/scope - none encodes control state.
D6 overflow-wrap: anywhere sheared 22 identifier chips mid-token on the
methodology page at 1440 with the chip background split across lines.
:not(pre) > code goes nowrap at >=768px (the :not(pre) scope is
load-bearing: white-space is inherited and pre.code's text lives in its own
<code>). Measured 0 broken at 1440 on every page. The comma half: chip runs
were joined with a literal ", " that landed outside the chip's own edge -
148 on methodology, 14 on the repo page. Signal's own renderers use a
separator-free chip list; runs in threats-shared markup are rewritten on
the way out. Now 0 on the repo page and 6 on methodology, all six confirmed
to be commas doing grammatical work in prose.
Only site/src/designs/signal/ and its own tests are touched. Gates: tsc
clean, 626 pass / 1 skip / 0 fail (baseline 613 + 13 new), build green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
Three of these are about whether the page tells the truth well; four are
craft. Every one was re-measured in a real browser at 1440 and at a true
390px layout viewport, and two of the seven turned out to have a different
cause than the defect report named.
1. THE HONESTY PANEL NOW SETS TO A MEASURE. `.honesty-body` filled 834px of
a 7fr track and ran 107 characters per line — the first thing on the
methodology page, and the statement the whole site rests on. Every other
prose block here is capped at 62-72ch; this one had been missed. Capped
at 62ch: 107 -> 69 cpl at 1440, the 7fr track untouched.
2. THE GHOST NUMERAL IS NO LONGER STRUCK THROUGH — for two reasons, and the
reported one was the smaller. `.sec-rail-in` is a <span> carrying block
children and nothing blockified it (`position: sticky` does not), so it
was an inline box where a block-start border and vertical padding
contribute nothing to layout: its 12px of padding had never existed and
its 2px rule painted at the numeral's own box top. Raising the padding,
as suggested, changed nothing — measured. `display: block` makes the
padding real. Underneath that sat the actual strike: `.sec-head` spans
the full page by design, and the rail was inset from the section's
CONTENT top, which is the head's own top, so the band ran through the
glyph at 59% of its height. The rail now starts below the band. Verified
by scanning every border that spans each numeral horizontally: 5 rails x
5 widths (1100-2560), 0 crossings, rail-rule clearance +9.6px.
3. A PHONE GETS ONE WAITING PANEL, AND IT IS A REAL ONE. The round-4 fix
clip-pathed the second panel's eyebrow and heading away and welded the
dashed boxes, which made the two widths disagree about what the page
contained: a sighted phone reader saw one heading followed by two
unrelated paragraphs, the second under a heading where it is a
non-sequitur, while a screen reader heard the correct two-section
structure. `home.ts` now renders a single honest panel when BOTH
exemplar panels are waiting — one eyebrow, one heading, both sentences
verbatim from exemplars.ts, one way out — and the pair is swapped out
with `display`, not clipped. At 390 the heading list is now NOT ENOUGH
LISTINGS YET / STILL UNCHECKED for eye and screen reader alike.
4. THE CONTROL IDENTIFIER OUTRANKS THE GROUP CODE. `code` is 0.9em, which
is right in running prose and wrong inside a box already at the smallest
step: the ids in the restacked question cards rendered at 9.9px, 54 of
them, under group codes at 11px. `code { font-size: max(0.9em, 0.75rem) }`
states the floor once as an effect rather than as a list of selectors
that goes stale, and the id is set a step above the groups in ink.
Measured at 390: id 13px #141210, groups 12px #5C564C.
5. THE SMALLEST PHONE TEXT IS ALSO THE BEST-CONTRASTED. 11px was carrying a
large volume of load-bearing state text on a phone, most of it in the
muted token at 4.70:1. Two token overrides below 760px do the whole job —
`--t--3` 11px -> 12px (the ramp stays monotonic) and `--ink-3` #6E675C ->
#5C564C (4.70:1 -> 6.11:1). Tokens rather than a selector list, so a chip
added tomorrow inherits the floor. Measured at 390 across all four pages:
157/86/74/30 elements under 12px -> 0, 0, 0, 0. Desktop is unchanged.
6. THE COVERAGE FILTER SAYS WHICH CONTROL EMPTIED THE TABLE. The reported
finding — no empty-state node in the DOM — did not reproduce: the strip
ships and works, and a probe that ticks the box and reads the DOM in the
same synchronous block sees it still hidden, because a MutationObserver
callback is a microtask. What was real is that the message was generic.
The coverage checkbox is the one control that exists for "find the
listings under the floor", so when it matches nothing the honest answer
is a finding, not a miss; it now gets its own sentence, chosen from the
state of the controls, and rendered only on a board where it is true.
The orphan `<thead>` — 43px of ruled headings on blank paper inside a 2px
box — leaves with the rows.
7. THE COVERAGE NUMBER CARRIES ITS VERDICT. The repo hero set 87.1% in 84px
Anton and never said whether it cleared the floor; the page's innerText
contained no "75", no "floor", no "provisional". A mono verdict rides
under the numeral in both directions, solid ink when the floor is
cleared and dashed accent when it is not — never colour alone — and the
below-floor form links down to the panel that already names the
unverified controls and the one-line fix. The directory row says it too,
but ONLY where it was silent: a below-floor row already says PROVISIONAL
three times, and a fourth accent chip there spends the one hot colour on
nothing.
The honesty contract is unchanged and re-checked live: unverified is a
visible third state and uncounted, provisional tags stay, lane marks come
from trust.ts with the local badge reading weaker than the action lane,
below-floor listings still name their unverified controls and the fix, and
contradictions are still named on the row and on the sheet.
Gates: tsc clean, 641 pass / 1 skip / 0 fail (baseline 613; 28 added, none
weakened or removed), build 30 pages.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the four-up design trial with a five-up one and re-roots the site on its own domain.
What changes
Two new designs.
signal— the reference-inspired register (paperclip.ing / cocoindex.io / xirp.spotify.com / openai.com-the-defense-factory): warm paper, one blue accent, Inter Tight / Inter / JetBrains Mono, a pill nav that condenses atscrollY > 24and never leaves the screen, numbered chapter rail, hairline zebra tables.bulletin— a Swiss/brutalist poster grid: ink on warm paper, one hot accent spent only on state, hard 2px rules, oversized Anton numerals, broadsheet tables.manualis retired — directory deleted, registry entry andDesignIdmember gone. Ledger stays the default; registry order is ledger, console, chain, signal, bulletin.The site serves at the root of
sscsb.dev.BASE_PATH/sscsb/→/,SITE_ORIGIN→https://sscsb.dev, rootCNAMEupdated, the umbrella "Sensible Security Tools" landing retired. Share/copy URLs now derive from config via a newshare-urls.tsinstead of being hand-built per design. The scan-intake relay's CORS allow-list gainshttps://sscsb.dev(the old origin stays for the transition, with a comment saying when to drop it) — note the relay has no Git integration and needs a manualvercel deploy --prodfromrelay/.Two build defects this exposed
build.tswrote every page under a hardcoded"sscsb"literal, soBASE_PATHand the output tree could disagree silently. Now derived.BASE_PATH="/"it would have overwritten the home page.Verification
mainwas 450).tsc --noEmitclean. 30 pages build.mainacross six pages each, once the intended path/domain/switcher changes are normalized out.Old
tools.sensiblesecurity.xyzURLs are deliberately not redirected — that host was never shared. Its CNAME comes out of the zone after cutover so no unverified hostname is left pointing at GitHub Pages.🤖 Generated with Claude Code
https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm