fix: replace the traced infinity mark with the real artwork; add §4 Offerings

Q32 — the traced mark was a WRONG SHAPE and had shipped. Pouya compared
it against the master and rejected it. Two grounds reproduce from the
path and are verified here: all four cubic branches meet the origin at
exactly 90 degrees, so the loops are tangent rather than crossing and at
stroke-width 28 render as two kissing circles (signed crossing number 0;
the strokes fuse across 61% of the mark's height at 2rem); and the
master's ink bbox is 2668x1704 = 1.5657:1. The path is deleted, not kept
as a fallback.

Pouya's 1.23:1 figure is reconciled rather than left dangling: it is the
bounding box of the path's COORDINATES, not the curve. Control points sit
at y +/-160 where the curve reaches +/-120, so the hull is 400x320 and
with stroke 428x348 = 1.2299. A trap rather than a slip — x is monotone,
so the control points give the right width and a 33% inflated height, and
the "does the width look right" check passes.

The real artwork is now in the repo: master, tight crop (the render
source, so the file's aspect ratio IS the mark's), full lockup, and the
SVG. InfinityMark renders AVIF/WebP; a Retina device takes 3,063 B.
Favicons regenerated; favicon.svg deleted.

Q33/Q36 — Pouya accepts arbitration appointments now. §4 gains an
Offerings category: competence for an offering, permission for a
credential, with an explicit boundary so it cannot become a route around
D13. The masthead tagline is restored, and the footer designation strip
now carries "Q.Arb — commenced August 2026" so §4's paired-disclosure
condition is actually met on every page rather than only asserted.

Two conventions added to CLAUDE.md, both earned this session: anything a
spec makes a claim about must be reachable from the repo (R14 — the
traced mark survived two review passes because the artwork was not here
to compare against); and a command that did not run is not evidence of
absence (`timeout` is not installed on macOS, so four Drive reads never
executed and were reported as an empty directory).

Reviews: claims-auditor FAIL/13 and adversarial-reviewer 2 blocking, all
resolved. The severe one was self-inflicted — `flex: none` landed on the
<img> while <Picture>'s <picture> wrapper is the flex item, so the logo
compressed to 28.5x32 at 1024px with seven nav items. The page-level
overflow check passed throughout because the brand block absorbed the
deficit by crushing the mark. Harness now asserts rendered aspect ratio.

Opened: Q38, Q39. Closed: Q32, Q33, Q36. Narrowed: Q35. Added: R13, R14.
AGENTS.md entry (v) carries a RESUME HERE section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XquaEq4BgWMCwUqLEyNkF
This commit is contained in:
Pouya Lajevardi
2026-08-26 17:30:19 -04:00
co-authored by Claude Opus 5
parent 8134709548
commit 364b09632e
20 changed files with 764 additions and 117 deletions
+95 -43
View File
@@ -1,29 +1,53 @@
---
/**
* The SML infinity mark. AGENTS.md D7 keeps it unchanged; docs/02 requires it
* be inline SVG rather than a raster. (The "~470 KB PNG" this comment used to
* cite had no source anywhere in the repo — see AGENTS.md Q34. The mark is
* geometry, which is reason enough without a number.)
* The SML infinity mark.
*
* PROVENANCE — read before editing the path. This geometry is lifted verbatim
* from the deployed site's own loading-thumbnail SVG (the element it labels
* `smlMark`), fetched from https://adr.smlcompany.ca/ on 2026-08-26. The only
* change is arithmetic: the source drew the path offset by `translate(60 0)`
* inside a 1200×800 frame, and that offset is folded into the coordinates here
* so the curve is symmetric about the origin. Stroke widths (28 outer, 6 inner)
* and the 0.7 inner opacity are the source's.
* ⚠️ DELIBERATE, TEMPORARY EXCEPTION TO docs/02's "inline SVG, never a PNG".
* Tracked as AGENTS.md Q38, with a standing reminder (R13) so it cannot become
* permanent by neglect. Read both before changing this file.
*
* It is therefore SML's own artwork, not a redrawing — but it came from a
* loading placeholder, which is not necessarily the canonical file. See
* AGENTS.md Q32.
* WHY A RASTER — and the reason is PAYLOAD, not fidelity. The mark is not a
* stroked curve; it is a shaded ribbon of variable width that twists in three
* dimensions, maroon flowing into champagne, passing over itself at the
* crossing. That is gradient-mesh artwork, not the flat vector paths docs/02
* assumes.
*
* Two-tone by design: the outer stroke takes `currentColor` so the mark sits
* correctly on cream, ink, and maroon; the inner hairline is gold. Gold as an
* icon stroke is explicitly allowed — the rule it must never break is gold as
* TEXT on cream, which measures 2.10:1 (docs/02).
* We do hold an SVG — src/assets/brand/sml-logo-source.svg — and **it renders
* faithfully**: rasterised at 8333px it reproduces the master exactly, at the
* same 1.566:1 [verified 2026-08-26]. An earlier version of this comment implied
* it was inadequate artwork. It is not; that was unfair and is corrected here.
* What rules it out is weight and composition: 257,278 bytes against 3,063 for
* the AVIF a Retina browser takes — 84× — and SEVEN embedded base64 PNGs plus
* a 1,225-stop gradient mesh, so inlining it would breach CLAUDE.md's rule
* against base64-inlining images. The exception ends when a vector master lands
* that is both faithful AND light.
*
* WHAT THIS REPLACES, and why it had to go. Until 2026-08-26 this component
* drew a hand-traced cubic path lifted from the old site's loading thumbnail.
* Pouya compared it against the master and it was wrong in three ways; two are
* reproducible from the path itself:
*
* 1. TANGENT, NOT CROSSING. All four branches met the origin at exactly 90°,
* so the loops were mutually tangent on a vertical line rather than
* crossing. At stroke-width 28 that renders as two circles kissing — the
* one thing an infinity mark must not be. Verified by computing the
* tangent vector of every segment at the origin.
* 2. WRONG PROPORTION. The real mark's ink bounding box is 2668 × 1704 =
* 1.5657:1. The traced path measured 1.667:1 ink / 1.597:1 stroked.
* 3. FLAT. Two uniform strokes standing in for a shaded ribbon.
*
* It is deleted rather than kept as a fallback, on Pouya's instruction: a wrong
* mark that renders is worse than a missing one, because it stops looking wrong.
*
* SOURCE OF TRUTH. src/assets/brand/sml-infinity-mark.png is the master tight-
* cropped to its ink bounding box, so the file's aspect ratio IS the mark's and
* layout can be tuned to it directly. Provenance: docs/reference/brand-assets.md.
*/
import { Picture } from 'astro:assets';
import mark from '../assets/brand/sml-infinity-mark.png';
interface Props {
/** Rendered height. Width follows the 11:7 aspect ratio. */
/** Rendered height. Width follows 1.5657:1. */
size?: string;
/** Accessible name. Omit for decorative use — the default. */
label?: string;
@@ -31,36 +55,64 @@ interface Props {
}
const { size = '1.75rem', label, class: className } = Astro.props;
const decorative = label === undefined;
const PATH = `M -200 0
C -200 -160, 0 -160, 0 0
C 0 160, 200 160, 200 0
C 200 -160, 0 -160, 0 0
C 0 160, -200 160, -200 0 Z`;
// Sized to the largest render that SURVIVES, which is the footer at 2.25rem =
// 56.4 CSS px wide. The first attempt used 320px, justified by the 4rem sample
// on the proof sheet — a page step 2 deletes — and then stacked densities on
// top, so the ladder double-counted its own headroom and every Retina device
// pulled a 640px image into a 56px slot.
//
// PASSING A WIDTH EXPLICITLY IS STILL THE POINT. Without it Astro emits the
// untouched 2668px master as the <img src> fallback: 1,146,406 bytes, which any
// client without AVIF or WebP support would actually download, sitting in dist
// looking like an optimisation had happened.
const INTRINSIC_WIDTH = 64;
---
<svg
<Picture
src={mark}
width={INTRINSIC_WIDTH}
densities={[1, 2, 3]}
formats={['avif', 'webp']}
fallbackFormat="png"
alt={label ?? ''}
loading="eager"
decoding="async"
class:list={['mark', className]}
viewBox="-220 -140 440 280"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
role={decorative ? undefined : 'img'}
aria-hidden={decorative ? 'true' : undefined}
aria-label={label}
focusable="false"
style={`block-size:${size}`}
>
<path d={PATH} stroke="currentColor" stroke-width="28"></path>
<path d={PATH} stroke="var(--rule)" stroke-width="6" opacity="0.7"></path>
</svg>
pictureAttributes={{ style: `block-size:${size}` }}
/>
<style>
.mark {
/* 440 : 280 — the viewBox. Height is set inline; width follows. */
aspect-ratio: 11 / 7;
inline-size: auto;
/* THE FLEX ITEM IS THE <picture>, NOT THE <img>.
`class:list` lands on the <img>, which Astro's <Picture> wraps in a
<picture> — and that wrapper is what `.brand`'s flex layout actually sizes.
So `flex: none` on .mark reached the wrong box entirely and the mark was
free to be squeezed: measured 28.52 x 32 at 1024px with seven nav items,
against a correct 50.09 x 32 — an aspect of 0.891 where it should be 1.5657.
This is the SAME defect CLAUDE.md already records for <Button> in
SiteHeader — a parent cannot style a child component's root — reintroduced
inside the fix for it, one round later. The bare `picture` selector below is
scoped by Astro's cid, which the emitted markup does carry, and the height
now goes on the wrapper via `pictureAttributes` rather than on the image.
Worse than the bug: the page-level overflow check passed throughout, because
the brand block absorbed the deficit by crushing the logo. "0 overflow at
every width" was true and misleading — it measured the document, not the
elements inside it. The harness now asserts the rendered aspect ratio. */
picture {
display: block;
flex: none;
aspect-ratio: 667 / 426;
inline-size: auto;
}
.mark {
/* 2668 / 1704 reduced — the master's exact ink bounding box, so the box the
layout reserves is the shape that fills it. */
inline-size: 100%;
block-size: 100%;
/* Belt and braces: if anything ever squeezes the box again, the artwork
letterboxes instead of distorting. */
object-fit: contain;
}
</style>
+23 -1
View File
@@ -27,6 +27,17 @@ import InfinityMark from './InfinityMark.astro';
// deployed, which is the same cadence everything else here updates on.
const year = new Date().getFullYear();
// Med-Arb was filtered out of this column for a few hours on 2026-08-26 and is
// RESTORED, because removing it was wrong on three counts and `adversarial-
// reviewer` caught all three. (1) docs/01 specifies this footer as the FULL
// sitemap and lists /med-arb/ as deliberately out of the primary nav, "linked
// contextually" — so dropping it here left the page with no site-wide link at
// all, on a project whose entire premise is crawlability. (2) The label names a
// PAGE, and docs/01 frames that page around the C.Med-Arb arc rather than as a
// present offering, so listing it is not the §4 inference it looked like.
// (3) It settled half of Q35 unilaterally while the other half — Energy and
// Shareholder — stayed in both nav and footer, and the record claimed no
// unilateral action had been taken. Both halves of Q35 go to Pouya together.
const processLinks = [
{ href: '/mediation/', label: 'Mediation' },
{ href: '/arbitration/', label: 'Arbitration' },
@@ -48,8 +59,19 @@ const aboutLinks = [
<InfinityMark size="2.25rem" />
<span class="footer-brand-name">{SITE.name}</span>
</a>
{
/* The designation strip carries the credentialing STAGE, not just the
held designation, and that is a §4 Offerings condition rather than a
flourish. The masthead says "Mediation · Arbitration · Toronto" on
every page; §4 permits the arbitration half on the condition that the
site "makes the first while stating the second plainly", and
"neither half may be dropped". Before this line rendered
CREDENTIALS.inProgress, that condition was unmet on every page that
ships — the constant existed in site.ts and was rendered nowhere.
Q.Arb reads as commenced, never as held (§4). */
}
<p class="footer-designation">
{CREDENTIALS.designations.join(' · ')}
{[...CREDENTIALS.designations, ...CREDENTIALS.inProgress].join(' · ')}
</p>
</div>
+66 -24
View File
@@ -38,7 +38,10 @@ const inSection = (href: string) => path === href || path.startsWith(href);
<div class="wrap header-inner">
<a class="brand" href="/">
<InfinityMark size="2rem" />
<span class="brand-name">{SITE.name}</span>
<span class="brand-text">
<span class="brand-name">{SITE.name}</span>
<span class="eyebrow brand-tagline">{SITE.tagline}</span>
</span>
</a>
<nav class="nav" aria-label="Primary">
@@ -131,22 +134,44 @@ const inSection = (href: string) => path === href || path.startsWith(href);
/* --- Brand --------------------------------------------------------------- */
/* No tagline under the name. `SITE.tagline` reads "Mediation · Arbitration ·
Toronto", and docs/01 specifies that string as the HOME HERO EYEBROW —
nothing asks for a header tagline. Set under the name on every page it
stops being page copy and becomes a designation strip, with `Arbitration`
unqualified beside a name. AGENTS.md §4 records Q.Arb as commenced August
2026 and requires it never read as held. See AGENTS.md Q33. */
/* The tagline is BACK, and the reasoning is worth keeping rather than just
the outcome. It was removed on 2026-08-26 because `Arbitration` under
Pouya's name read as a held capability, and §4 records Q.Arb as merely
commenced. Q33 answered that the same day, and the premise was wrong:
ADR designations are voluntary credentials, not licences. Anyone may be
appointed an arbitrator in Ontario, so the constraint was always
positional, never legal — Pouya accepts sole, party-appointed and
co-arbitration work today. See §4 Offerings, which is where the register
now draws the line between a CREDENTIAL claim and an OFFERING. */
.brand {
display: flex;
align-items: center;
gap: var(--space-3);
/* 44px floor — this is the tap target that goes home. */
min-block-size: 44px;
/* 48px, not 44. Still clears the touch floor, and it reserves the height the
two-line brand takes at >=76rem so the sticky header is one constant 81px
across every width where it is sticky — which is what --header-h and
scroll-padding-top are keyed to. One number instead of two bands. */
min-block-size: 48px;
color: var(--accent);
text-decoration: none;
margin-inline-end: auto;
}
.brand-text {
display: flex;
flex-direction: column;
gap: 2px;
}
/* The tagline appears only where there is room for it — see the 76rem block.
Measured: at 11px with 0.18em tracking the string is ~285px wide, and
restoring it under the name pushed the one-row header past its content box
by 18px at 1024 with six items and 84px with seven. The brand name carries
the identity on its own; the tagline is a flourish, and `/` opens with the
same words as the hero eyebrow (docs/01). */
.brand-tagline {
display: none;
font-size: 0.6875rem; /* 11px — the eyebrow floor in docs/02 */
white-space: nowrap;
}
.brand-name {
font-family: var(--font-serif);
font-size: var(--text-xl);
@@ -255,7 +280,7 @@ const inSection = (href: string) => path === href || path.startsWith(href);
color: var(--accent);
}
/* Nav takes its own row below 64rem. No `order` anywhere: reordering flex
/* Nav takes its own row below 66rem. No `order` anywhere: reordering flex
items moved the CTA onto row one visually while it stayed last in the DOM,
so a keyboard user tabbed off the brand, down through all 13 nav links
including the practice panel, and back UP to the CTA. WCAG 2.4.3, and
@@ -267,17 +292,17 @@ const inSection = (href: string) => path === href || path.startsWith(href);
/* `white-space` INHERITS into the Button, which is how a parent reaches a
child component's root at all here — see the note on why this wrapper
exists. Without it the label wrapped to two lines, standing the header up
at 93px instead of 77px. `flex: none` stops the row squeezing it. */
taller than it needed to be. `flex: none` stops the row squeezing it. */
.header-cta {
flex: none;
white-space: nowrap;
}
/* Below 64rem the CTA is dropped, not shrunk — and that is the whole reason
/* Below 66rem the CTA is dropped, not shrunk — and that is the whole reason
no `order` is needed. Wherever the nav has its own row, `/contact/` is
already on it, so the button is a duplicate link paying for itself in
header height. It was previously dropped only below 40rem and reordered in
between, which is what put focus order out of step with visual order. */
@media (max-width: 63.999rem) {
@media (max-width: 65.999rem) {
.header-cta {
display: none;
}
@@ -285,12 +310,19 @@ const inSection = (href: string) => path === href || path.startsWith(href);
/* --- Desktop ------------------------------------------------------------- */
/* --- Desktop: 64rem (1024px) ---------------------------------------------
The threshold is measured, not conventional. The one-row header has to hold
the brand, seven nav items and the CTA side by side, and **at 1024px it
does, with 32px of clearance between the nav and the CTA — measured in
headless Chrome with a seventh nav item injected.** Below that it does not,
which is why the nav takes its own row there.
/* --- Desktop: 66rem (1024px) ---------------------------------------------
The threshold is measured, and the first measurement was WRONG — worth
recording, because of how it was wrong. It read "32px of clearance at
1024px". That 32px was `.header-inner`'s own `column-gap`, i.e. exactly
zero slack, mistaken for headroom. The true max-content sum at seven items
is 48 + 184.3 brand + 24 margin + 32 gap + 480.1 nav + 32 gap + 197.2 CTA
+ 48 = **1045.6px — 21.6px OVER a 1024px viewport.** Nothing overflowed
only because flexbox shrank the brand and crushed the mark inside it.
Binary search on the built page with a seventh item gives the real
thresholds: the row fits from **1047px**, and the tagline from **1207px**.
66rem (1056) and 76rem (1216) are the clean tokens above each. Below 66rem
the nav takes its own row.
Insights is the seventh item and arrives by itself at build step 7
(SiteHeader gates it on the collection), so a breakpoint verified only
@@ -305,7 +337,7 @@ const inSection = (href: string) => path === href || path.startsWith(href);
Sticky only from here up, too. Below this the nav takes a second row and
the header stands at 137px, which is more of a small viewport than a
sticky header is worth. Deviation from docs/02 "Sticky"; recorded there. */
@media (min-width: 64rem) {
@media (min-width: 66rem) {
.site-header {
position: sticky;
inset-block-start: 0;
@@ -321,11 +353,11 @@ const inSection = (href: string) => path === href || path.startsWith(href);
}
/* nowrap, and flex:none so the nav is never squeezed below its content
width. Measured before this: at 960-1250px the seven-item nav broke to
two rows and the header stood at 142px instead of 80px. */
two rows and the header stood at 141px instead of 81px. */
.nav-list {
flex-wrap: nowrap;
flex: none;
/* 16px from 64rem, widening to 24px at 75rem where there is room for it.
/* 16px from 66rem, widening to 24px at 76rem where there is room for it.
Measured with seven items at every width from 1024px up. */
column-gap: var(--space-4);
}
@@ -372,14 +404,24 @@ const inSection = (href: string) => path === href || path.startsWith(href);
you scroll off the top. That is a smaller effect than docs/02's "condenses
on scroll", and docs/02 has been amended to say so and why. */
@media (min-width: 75rem) {
/* The tagline arrives at 76rem. The nav gap does NOT widen here as well:
measured, doing both at one breakpoint put the row 4px past its content box
at exactly 1200px with seven items. Two things growing at the same width is
how a breakpoint gets over-subscribed. */
@media (min-width: 76rem) {
.brand-tagline {
display: block;
}
}
@media (min-width: 80rem) {
.nav-list {
column-gap: var(--space-5);
}
}
@supports (animation-timeline: scroll()) {
@media (min-width: 64rem) and (prefers-reduced-motion: no-preference) {
@media (min-width: 66rem) and (prefers-reduced-motion: no-preference) {
.site-header {
animation-name: header-lift;
animation-duration: 1ms;