feat: build step 2 — the home page; close Q35, Q37, Q39; Q39's answer corrected the register
Pouya's rulings and the page are one commit, not two, because they are not separable: Q37 changed the credential label the page renders, Q39 scoped the arbitration copy, and Q35 gave Med-Arb the row that lets the footer link stand. Splitting them would produce a commit where the register says one thing and the page says another. RULINGS Q39 — ANSWERED, and my assertion was FALSE as a universal. Pouya checked rather than defended. Family arbitration in Ontario is gated: prescribed training, 14h on screening for domestic violence and power imbalances, 30h of Ontario family law for arbitrators outside the bar, 10h ongoing per two years. claims-auditor produced that counter-example from inside the repo on 2026-08-26 and it was right. The source is now IN the repo per R14 — docs/reference/ontario-family-arbitration-training.md, retrieved with its provenance — and §4 states which half it establishes: the family gate directly, the commercial half only by absence. So "commercial arbitration in Ontario requires no licence and no designation" is recorded as Pouya's stated position, unstamped. What disposes of the question is the scope exclusion: he does not do family arbitration, so it is NOT OFFERED and the gate never bears on the practice. The false universal survived in three more live places, found by grep, not recall: §4's NOT-NEGOTIABLE boundary bullet (the boundary paragraph breaching its own boundary), §9's Q33 closure row, and a comment in SiteHeader.astro. Q35 — ANSWERED, all four items, and the answer supplied a gate that was missing. Med-Arb gets an Offerings row and stays in the footer sitemap. The six subject-matter labels pass test 1. §4 gains "Subject-matter areas — the publication gate": competence to accept an appointment, AND a page that frames it as positioning without claiming history. Nothing in the nav or footer is unrowed any more. Q37 — ANSWERED. "Law and engineering" becomes "Legal training and engineering practice". A degree is not a practice; the parallel was doing the implying. Q38/R13 — the committed SVG does not close it; the walk-back went too far. It renders faithfully BECAUSE it is the raster. R13 stays open. R1 — surfaced and acknowledged; the interim licensure framing is now carried by a shipped page rather than a spec, which raises the stakes. BUILD STEP 2 Seven of docs/01's eight home sections, six new components, zero JavaScript. Section 7 (Latest insights) ships at step 7 with the collection it lists — recorded in docs/01 and in the page, not just here. Four credential slots, not three: §4's paired-disclosure condition requires the Q.Arb stage on any page that offers arbitration. No booking link (R6). The masthead tagline is suppressed on / (it duplicated the hero eyebrow). The step-1 proof sheet is deleted, and five live references to it were found by grep. WHAT THE REVIEWS FOUND — 24 findings across the two passes claims-auditor FAILED it on ten, every one implication or scope rather than fabrication, which is where D13 says the risk lives. The four that mattered: "I mediate and arbitrate" asserted a track record §4 does not hold for arbitration; the JSON-LD asserted arbitration twice and stated the stage nowhere (a crawler-only claim is still a claim); "at one published rate with preparation time included" misdescribed money against docs/07's two day rates and capped prep allowance; and "Law and engineering are not blended here" was Q37's struck parallel relocated into prose one day after Pouya struck it. It also found the Canadian Tax Foundation missing from §9 Q8 — a stale second copy of a fact that would have bitten at step 3. adversarial-reviewer found fourteen, three blocking. The first was class="section-head" on <SectionHeading> never matching — the parent-cannot- style-a-child defect for the FOURTH time, written into a diff where I had just added fresh warnings about it to two other components. Measured: 0px margin, 0px gap, headings over the card edges, with astro check and eslint both clean. I had looked at a screenshot of that section and passed over it. Fixed with a page-owned wrapper (48px, measured) and the prop is deleted from six components so it is now a build error. Also: the credential row was never "two up on a phone" and its comment said it was; PROCESS was hardcoded in the page against the reason written in site.ts; 83px of residual overflow at a 200% default font size, now 3px. Seven more I found myself first, including <Picture widths> declaring the untouched 1600px master as the <img src> fallback (254,626 B for a 476px slot, and the build log said "before: 349kB" either way), and a prop named `as` silently turning off type-checking for a whole component. VERIFICATION — run, not asserted. Full figures in AGENTS.md entry (w). npx tsc 6.0.3 · check 0/0/0 · lint clean · build clean · audit 0 1 <script> and it is JSON-LD; non-JSON-LD scripts 0; no JS bundle; identical page with script execution disabled (444 nodes, 6,578 chars) Phase 5 minifier check: no `animation` shorthand beside animation-timeline overflow 0 at 14 widths, AND every mark measured at 1.5654-1.5657 vs 1.5657 one h1, no heading skips, focus order == DOM order across 44 focusables 31 painted contrast pairs at 3 widths, 0 failures 72/72 hit-test points across 8 cards resolve to the card's link print 0 hidden, reduced-motion 0 hidden Lighthouse NOT RUN — tool unavailable until step 7 (R11) HTML validator NOT RUN, and 4 of 12 srcsets carry a duplicate 1x descriptor Opened for Pouya: Q40 (one OG image for nineteen pages), Q41 (does Q37 reach prose; may the comparative be restored; what LAT pre-hearing mediation means), Q42 (the four "also offered" processes have no row), Q43 (the process timings are published commitments with no row). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0148NztQskLKKApP5SzAA78e
This commit is contained in:
co-authored by
Claude Opus 5
parent
8a2f513577
commit
165d259f5c
@@ -0,0 +1,104 @@
|
||||
---
|
||||
/**
|
||||
* docs/02: "Three or four credential slots. NEVER matter counts — AGENTS.md §4."
|
||||
*
|
||||
* THE SUBSTITUTION PRINCIPLE IS THE POINT OF THIS COMPONENT (§4). Wherever a
|
||||
* design wants a "how much / how many", it takes a longer-arc credential
|
||||
* instead: something already substantial and true at launch that does not grow
|
||||
* by closing files. `Q.Med` / `JD + ML` / `EN · FA`, with `Q.Arb` fourth.
|
||||
*
|
||||
* So this component is not a stat row wearing a different name, and it must
|
||||
* never be handed one. §4 Forbidden bars every count of matters closed, hours
|
||||
* mediated or years in practice, plus settlement rates and dollar figures. The
|
||||
* slots come from src/data/site.ts, which mirrors §4; nothing is typed here.
|
||||
*
|
||||
* <dl> RATHER THAN A DIV GRID. Each pair is a term and its description, which
|
||||
* is what a description list is. It also fixes the reading order: a screen
|
||||
* reader gets "Q.Arb — Commenced August 2026" as one associated pair, which is
|
||||
* §4's paired-disclosure condition surviving into assistive technology rather
|
||||
* than being a visual arrangement only. Wrapping each dt/dd pair in a <div>
|
||||
* inside <dl> is valid HTML and is what makes the grid tractable.
|
||||
*/
|
||||
interface Props {
|
||||
slots: ReadonlyArray<{ value: string; label: string }>;
|
||||
}
|
||||
// No `class` prop: it was declared, never passed, and a parent cannot reach this
|
||||
// root anyway (SectionHeading carries the measurement; CLAUDE.md the rule).
|
||||
const { slots } = Astro.props;
|
||||
---
|
||||
|
||||
<dl class="credentials">
|
||||
{
|
||||
slots.map((slot) => (
|
||||
<div class="credential">
|
||||
<dt class="credential-value">{slot.value}</dt>
|
||||
<dd class="credential-label">{slot.label}</dd>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</dl>
|
||||
|
||||
<style>
|
||||
.credentials {
|
||||
display: grid;
|
||||
/* EXPLICIT COLUMNS, BECAUSE `auto-fit` NEVER DELIVERED THE ROW IT CLAIMED.
|
||||
This read `repeat(auto-fit, minmax(11rem, 1fr))` under a comment saying
|
||||
"two up on a phone, four up where there is room". `adversarial-reviewer`
|
||||
measured it: at 390px the resolved template was a SINGLE 342px track and
|
||||
all four items stacked, running the band ~430px tall — with
|
||||
`Q.Arb / Commenced August 2026`, which §4's paired-disclosure condition
|
||||
puts on this page, at the bottom of it. The arithmetic is not subtle: two
|
||||
tracks at an 11rem (176px) floor plus a 24px gap need 376px and the
|
||||
container is 342px, so `auto-fit` correctly dropped to one. A
|
||||
measured-sounding comment that was false is this project's own named
|
||||
failure mode.
|
||||
|
||||
`minmax(0, 1fr)` cannot overflow at any width or any root font size,
|
||||
which also retires the `min(11rem, 100%)` guard this line briefly
|
||||
carried — that guard was fixing the overflow symptom of a floor that
|
||||
should not have been there. */
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-6) var(--space-5);
|
||||
margin: 0;
|
||||
padding-block: var(--space-7);
|
||||
border-block: 1px solid var(--rule);
|
||||
}
|
||||
.credential {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.credential-value {
|
||||
font-family: var(--font-serif);
|
||||
font-size: var(--text-3xl);
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
color: var(--accent);
|
||||
}
|
||||
/* Four across only where four actually fit, measured rather than inferred.
|
||||
Two 11rem-equivalent tracks plus three 24px gaps need 776px of container;
|
||||
above 48rem the gutter is 48px each side, so that is a 872px viewport. 56rem
|
||||
(896px) is the clean token above it. Re-measure if --space-5 or the label
|
||||
type changes. */
|
||||
@media (min-width: 56rem) {
|
||||
.credentials {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.credential-label {
|
||||
margin: 0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
line-height: var(--leading-snug);
|
||||
text-transform: uppercase;
|
||||
color: var(--text-meta);
|
||||
/* "Legal training and engineering practice" is 39 characters and is
|
||||
deliberately long (AGENTS.md Q37) — it wraps to two lines at every width
|
||||
and must not be prevented from doing so. Do not add `white-space: nowrap`
|
||||
here, and do not shorten the label to make the row tidier: the asymmetry
|
||||
is the honest part. */
|
||||
text-wrap: pretty;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
/**
|
||||
* docs/02: "Mono label with optional maroon dot."
|
||||
*
|
||||
* NO SCOPED STYLES ON PURPOSE. `.eyebrow` and `.eyebrow .dot` are already
|
||||
* global (global.css) because `.section-inverse .eyebrow` has to recolour them
|
||||
* from an ancestor, and a scoped rule here would be a second source for the
|
||||
* same thing. This component exists to stop `<span class="eyebrow">` being
|
||||
* hand-typed on nineteen pages, not to own the look.
|
||||
*
|
||||
* AN EYEBROW IS A LABEL, NEVER A HEADING. It renders <p> or <span>, never
|
||||
* <h*>: docs/02's accessibility floor forbids skipped heading levels, and an
|
||||
* eyebrow above an <h2> is exactly where an <h3>-before-<h2> creeps in.
|
||||
*
|
||||
* ⚠️ NEVER NAME AN ASTRO PROP `as`. The prop deleted below was briefly called
|
||||
* `as`, and that name SILENTLY TURNED OFF PROP TYPE-CHECKING for every caller
|
||||
* of this component. Kept here because the next person to want a dynamic tag
|
||||
* will reach for `as` first.
|
||||
*
|
||||
* `astro check` reported it only as a hint — `ts(6196) 'Props' is declared but
|
||||
* never used` — which reads exactly like lint noise and is the reason it is
|
||||
* worth writing down. It is not noise: it is the compiler saying the `Props`
|
||||
* interface is not attached to anything.
|
||||
*
|
||||
* Measured rather than assumed, 2026-08-27. With the prop named `as`,
|
||||
* `<Eyebrow dot as="h9" bogusProp={1} />` compiled with **0 errors**. The same
|
||||
* probe against CredentialRow, SectionHeading, PracticeCard and ProcessStep
|
||||
* produced `ts(2322)` on all four, so the loss was specific to this file.
|
||||
* Renaming the single identifier `as` to `tag` — one variable changed, nothing
|
||||
* else — took the file from 1 hint / 0 errors to 0 hints, and the same probe
|
||||
* now fails correctly: `Type '"h9"' is not assignable to type '"p" | "span" |
|
||||
* undefined'`. `const { as = 'p' } = Astro.props` is read as a type assertion
|
||||
* somewhere in the generated TSX and detaches the binding.
|
||||
*
|
||||
* A DYNAMIC `<Tag>` FROM A VARIABLE was the first hypothesis for the lost
|
||||
* binding and it was WRONG — restructuring the template changed nothing, the
|
||||
* rename fixed it. Recorded so the wrong cause is not re-derived.
|
||||
*
|
||||
* DO NOT "FIX" A ts(6196) HINT WITH `Astro.props as Props`. It silences the
|
||||
* warning inside the component and leaves every call site unchecked, which is
|
||||
* strictly worse than the warning. If this hint appears on another component,
|
||||
* probe a bogus prop before believing the props are checked.
|
||||
*/
|
||||
interface Props {
|
||||
/** The maroon dot. Decorative — it is a CSS box, so it is invisible to AT. */
|
||||
dot?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* ONE PROP, AND THE OTHER TWO ARE DELETED. `tag?: 'p' | 'span'` had zero call
|
||||
* sites, so its `<span>` branch was unreachable code and the file carried two
|
||||
* near-identical templates for it. `class?: string` had zero call sites too, and
|
||||
* a parent cannot reach this root anyway (see SectionHeading, and CLAUDE.md).
|
||||
*
|
||||
* The rename that produced the finding below is kept; only the prop is gone.
|
||||
*/
|
||||
const { dot = false } = Astro.props;
|
||||
---
|
||||
|
||||
<p class="eyebrow">
|
||||
{dot && <span class="dot" aria-hidden="true" />}
|
||||
<slot />
|
||||
</p>
|
||||
@@ -49,24 +49,70 @@ import mark from '../assets/brand/sml-infinity-mark.png';
|
||||
interface Props {
|
||||
/** Rendered height. Width follows 1.5657:1. */
|
||||
size?: string;
|
||||
/** Accessible name. Omit for decorative use — the default. */
|
||||
/**
|
||||
* Intrinsic width of the 1x variant, in px. RAISE IT FOR A LARGE CALL SITE.
|
||||
*
|
||||
* The default 64 is sized for the header (50.1px wide) and footer (56.4px) —
|
||||
* with densities [1,2,3] that gives 64/128/192 and both are sharp to DPR 3.
|
||||
* The home page's approach section renders the mark at 225.5px, five times
|
||||
* larger, and inherited the same 64: measured 1.17x upscale at DPR 1, 2.35x
|
||||
* at DPR 2, 3.52x at DPR 3. A prop, because the component cannot infer this
|
||||
* from `size` — `size` may be a `clamp()`.
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* `eager` for the two above-the-fold marks; `lazy` for anything below it.
|
||||
* The component hardcoded `eager`, which is right for a masthead and wrong
|
||||
* for a 700px-wide decorative anchor two screens down.
|
||||
*/
|
||||
loading?: 'eager' | 'lazy';
|
||||
/**
|
||||
* Accessible name. Omit for decorative use — the default, and the case at all
|
||||
* three current call sites: the header and footer marks sit beside the name
|
||||
* they stand for, and the home page's mark sits beside a paragraph that says
|
||||
* what it is. Kept because the moment the mark appears without adjacent text
|
||||
* it needs one, and a component that cannot take an accessible name invites
|
||||
* `alt=""` on an informative image.
|
||||
*/
|
||||
label?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { size = '1.75rem', label, class: className } = Astro.props;
|
||||
/**
|
||||
* NO `class` PROP, AND ITS REMOVAL IS THE POINT. It existed, had no call site,
|
||||
* and `class:list` put it on the <img> — so a parent writing
|
||||
* `<InfinityMark class="foo" />` would get a rule compiled against the PARENT's
|
||||
* cid that never matches the child's root. That is the exact defect CLAUDE.md
|
||||
* records twice on this project. The prop was an invitation to reproduce it.
|
||||
* To position a mark, style a wrapper the parent owns.
|
||||
*/
|
||||
|
||||
// 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.
|
||||
const { size = '1.75rem', width, loading = 'eager', label } = Astro.props;
|
||||
|
||||
// THE DEFAULT IS SIZED TO THE LARGEST *DEFAULT* CALL SITE — the footer at
|
||||
// 2.25rem, i.e. 56.4 CSS px wide. With densities [1,2,3] that is 64/128/192 and
|
||||
// the footer is sharp to DPR 3.
|
||||
//
|
||||
// PASSING A WIDTH EXPLICITLY IS STILL THE POINT. Without it Astro emits the
|
||||
// It has now been wrong in both directions, which is why it is a prop:
|
||||
// - Too big, 2026-08-26: 320px, justified by a 4rem sample on the step-1
|
||||
// proof sheet (a page step 2 has since deleted), with densities stacked on
|
||||
// top — so the ladder double-counted its own headroom and every Retina
|
||||
// device pulled a 640px file into a 56px slot.
|
||||
// - Too small, 2026-08-27: the home page added a 225.5px call site and
|
||||
// inherited 64, upscaling 3.52x at DPR 3. Raise `width` at the call site.
|
||||
//
|
||||
// PASSING A WIDTH AT ALL 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;
|
||||
const INTRINSIC_WIDTH = width ?? 64;
|
||||
|
||||
// A NOTE ON MEASURING THIS, because the obvious probe lies. `img.naturalWidth`
|
||||
// on an image chosen from a `srcset` with an `x` descriptor is DENSITY-
|
||||
// CORRECTED: the 192px file selected at 3x reports 64. Reading it at DPR 1, 2
|
||||
// and 3 therefore returns 64 every time, which looks exactly like "the density
|
||||
// ladder is not being generated at all" — a far more alarming defect than the
|
||||
// real one. It is generated: 64x41, 128x82, 192x123 on disk, in all three
|
||||
// formats. Check the files, not naturalWidth. [verified 2026-08-27]
|
||||
---
|
||||
|
||||
<Picture
|
||||
@@ -76,9 +122,9 @@ const INTRINSIC_WIDTH = 64;
|
||||
formats={['avif', 'webp']}
|
||||
fallbackFormat="png"
|
||||
alt={label ?? ''}
|
||||
loading="eager"
|
||||
loading={loading}
|
||||
decoding="async"
|
||||
class:list={['mark', className]}
|
||||
class="mark"
|
||||
pictureAttributes={{ style: `block-size:${size}` }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
/**
|
||||
* docs/02: "Small bordered label for designations and sector chips."
|
||||
*
|
||||
* A PARENT CANNOT STYLE THIS ELEMENT. Astro does not pass a parent's scope
|
||||
* attribute to a child component's root, so `<Pill class="chip" />` compiles the
|
||||
* parent's `.chip` rule against the parent's cid and it never matches — the
|
||||
* defect CLAUDE.md records for <Button> in SiteHeader, which then recurred with
|
||||
* <Picture> in InfinityMark. CLAUDE.md names Pill as the next place it will
|
||||
* happen.
|
||||
*
|
||||
* THE HOOK IS A CUSTOM PROPERTY, and that is the one mechanism that legitimately
|
||||
* crosses the boundary: custom properties inherit. An ancestor sets
|
||||
* `--pill-border` / `--pill-fg` on ITSELF and this component reads it. No
|
||||
* :global(), no wrapper div, and no rule that silently does nothing.
|
||||
*/
|
||||
/**
|
||||
* No props. A `class?: string` was declared and never passed, and a parent
|
||||
* cannot reach this root regardless — see SectionHeading for the measurement
|
||||
* and CLAUDE.md for the rule. The custom-property hooks below are how an
|
||||
* ancestor influences it.
|
||||
*/
|
||||
---
|
||||
|
||||
<span class="pill"><slot /></span>
|
||||
|
||||
<style>
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-block: var(--space-1);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--pill-border, var(--border));
|
||||
border-radius: var(--radius-full);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--weight-medium);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
line-height: 1.4;
|
||||
text-transform: uppercase;
|
||||
color: var(--pill-fg, var(--text-meta));
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
/**
|
||||
* docs/02: "Sector chip, heading, one paragraph, arrow link."
|
||||
*
|
||||
* ONE LINK PER CARD, AND THE WHOLE CARD IS ITS HIT AREA. The link wraps only
|
||||
* the heading text, so its accessible name is "Construction & Infrastructure"
|
||||
* rather than the card's entire contents; a `::after` pseudo-element stretched
|
||||
* over the positioned card carries the click. Six of these on the home page
|
||||
* would otherwise be six links each announcing three sentences.
|
||||
*
|
||||
* The arrow is `aria-hidden` and outside the link text for the same reason.
|
||||
*
|
||||
* THE GRID MUST NOT TRY TO STYLE THIS ROOT. A parent's `.card { block-size:
|
||||
* 100% }` compiles against the parent's cid and never matches (CLAUDE.md;
|
||||
* it has now cost twice). The card sizes ITSELF to its grid cell below, so a
|
||||
* parent only ever needs `display: grid` and `gap` on its own element.
|
||||
*/
|
||||
import Pill from './Pill.astro';
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
chip?: string;
|
||||
title: string;
|
||||
/** Explicit: docs/02 forbids skipped heading levels. */
|
||||
level: 2 | 3;
|
||||
}
|
||||
// No `class` prop — declared, never passed, unreachable from a parent. The card
|
||||
// sizes itself to its grid cell instead; see the note above.
|
||||
const { href, chip, title, level } = Astro.props;
|
||||
const H = `h${level}` as 'h2' | 'h3';
|
||||
---
|
||||
|
||||
<article class="card">
|
||||
{chip && <Pill>{chip}</Pill>}
|
||||
<H class="card-title">
|
||||
<a class="card-link" href={href}>{title}</a>
|
||||
</H>
|
||||
<p class="card-body"><slot /></p>
|
||||
<span class="card-arrow" aria-hidden="true">→</span>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
position: relative;
|
||||
/* Sizes itself to its cell — see the note on why the grid cannot do this. */
|
||||
block-size: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-6);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-block-start: 2px solid var(--rule);
|
||||
border-radius: var(--radius-md);
|
||||
transition:
|
||||
border-color var(--dur-hover) var(--ease),
|
||||
box-shadow var(--dur-hover) var(--ease);
|
||||
}
|
||||
.card:hover {
|
||||
border-color: var(--accent);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.card-title {
|
||||
font-family: var(--font-serif);
|
||||
font-size: var(--text-xl);
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
}
|
||||
.card-link {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
.card-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.card:hover .card-link {
|
||||
color: var(--accent);
|
||||
}
|
||||
/* The focus ring belongs on the CARD, not on the heading text: the hit area
|
||||
is the card, so a ring around three words would point at the wrong box.
|
||||
`:focus-visible` on the descendant, ring on the ancestor. */
|
||||
.card:has(.card-link:focus-visible) {
|
||||
outline: 2px solid var(--focus-ring);
|
||||
outline-offset: var(--focus-offset);
|
||||
}
|
||||
.card-link:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
.card-body {
|
||||
margin: 0;
|
||||
/* Pushes the arrow to the bottom edge so a row of cards aligns on it
|
||||
whatever the body length. */
|
||||
flex: 1 1 auto;
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--leading-body);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.card-arrow {
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1;
|
||||
color: var(--accent);
|
||||
transition: transform var(--dur-hover) var(--ease);
|
||||
}
|
||||
.card:hover .card-arrow {
|
||||
transform: translateX(var(--space-2));
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.card:hover .card-arrow {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
/**
|
||||
* docs/02: "Numbered step, timing, body."
|
||||
*
|
||||
* THE NUMBER IS NOT CONTENT. It is `aria-hidden` and supplied by the caller
|
||||
* rather than by a CSS counter, because the compressed strip on `/` and the
|
||||
* full page at `/process/` must agree on it. The reading order for assistive
|
||||
* technology is heading then timing then body; the numeral adds nothing to it
|
||||
* and would be read as a bare digit before every step.
|
||||
*
|
||||
* <ol> IS THE CALLER'S JOB. These are ordered steps, so the parent wraps them
|
||||
* in an <ol> and this renders the <li>. That keeps "step 3 of 5" available from
|
||||
* the list semantics instead of from the decorative numeral.
|
||||
*/
|
||||
interface Props {
|
||||
n: number;
|
||||
title: string;
|
||||
/** "Day 0", "Days 1–7". docs/03: "Five steps with real timing." */
|
||||
timing: string;
|
||||
}
|
||||
// No `class` prop — declared, never passed, unreachable from a parent.
|
||||
const { n, title, timing } = Astro.props;
|
||||
---
|
||||
|
||||
<li class="step">
|
||||
<span class="step-n" aria-hidden="true">{String(n).padStart(2, '0')}</span>
|
||||
<h3 class="step-title">{title}</h3>
|
||||
<p class="step-timing">{timing}</p>
|
||||
<p class="step-body"><slot /></p>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
padding-block-start: var(--space-4);
|
||||
border-block-start: 1px solid var(--border);
|
||||
}
|
||||
.step-n {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
/* --gold-d, not --gold: on cream, gold measures 2.10:1 and gold-d 3.11:1.
|
||||
docs/02 permits gold-d for LARGE DECORATIVE text only, 24px+ — this is
|
||||
12px, so neither qualifies and the numeral is maroon. Kept as a comment
|
||||
because "make the step numbers gold" is the obvious next edit. */
|
||||
color: var(--accent);
|
||||
}
|
||||
.step-title {
|
||||
font-family: var(--font-serif);
|
||||
font-size: var(--text-lg);
|
||||
line-height: var(--leading-tight);
|
||||
}
|
||||
.step-timing {
|
||||
margin: 0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: uppercase;
|
||||
color: var(--text-meta);
|
||||
}
|
||||
.step-body {
|
||||
margin: 0;
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--leading-body);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
/**
|
||||
* docs/02: "Eyebrow + display heading + optional lede, one measure."
|
||||
*
|
||||
* THE LEVEL IS A REQUIRED DECISION, not a default. docs/02's accessibility
|
||||
* floor: "One <h1> per page; heading levels never skipped." A component that
|
||||
* defaulted to <h2> would silently produce an <h2> inside an <h3> section the
|
||||
* first time one is nested, and nothing would fail. `level` is explicit and
|
||||
* `astro check` enforces the union.
|
||||
*/
|
||||
import Eyebrow from './Eyebrow.astro';
|
||||
|
||||
interface Props {
|
||||
eyebrow?: string;
|
||||
/** 1 only on the page's single H1. */
|
||||
level: 1 | 2 | 3;
|
||||
lede?: string;
|
||||
dot?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* NO `class` PROP, AND NO `title` PROP. Both are deletions with reasons.
|
||||
*
|
||||
* `class` — a parent CANNOT reach this component's root. It was passed as
|
||||
* `class="section-head"` from `/` and the page's rule compiled to
|
||||
* `.section-head[data-astro-cid-<page>]` while the rendered div carried
|
||||
* `data-astro-cid-<SectionHeading>`. **Measured: `margin-block-end: 0px` and a
|
||||
* 0px gap on all three call sites** — 48px of intended separation missing, with
|
||||
* `.display`'s 0.98 line-height putting the glyphs over the top edge of the
|
||||
* cards below. `astro check` and `eslint` both passed. Fourth instance of the
|
||||
* defect `CLAUDE.md` records; a `class` prop here is an invitation to a fifth.
|
||||
* To space this block, wrap it in an element the page owns.
|
||||
*
|
||||
* `title` — two mechanisms for one job. The `heading` slot is the general one
|
||||
* (it takes an italic phrase, which `docs/02` allows once per headline); a
|
||||
* plain-text prop is the same thing minus a capability. One way in.
|
||||
*
|
||||
* `dot` defaults TRUE here and FALSE on `<Eyebrow>`, deliberately: a section
|
||||
* heading's eyebrow is the pattern the dot was designed for, and a bare
|
||||
* `<Eyebrow>` is used in places where it would be noise.
|
||||
*/
|
||||
const { eyebrow, level, lede, dot = true } = Astro.props;
|
||||
const H = `h${level}` as 'h1' | 'h2' | 'h3';
|
||||
const size = level === 1 ? 'size-display' : 'size-section';
|
||||
---
|
||||
|
||||
<div class="heading-block">
|
||||
{eyebrow && <Eyebrow dot={dot}>{eyebrow}</Eyebrow>}
|
||||
<H class:list={['display', size]}><slot name="heading" /></H>
|
||||
{lede && <p class="lede">{lede}</p>}
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.heading-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.size-display {
|
||||
font-size: var(--text-5xl);
|
||||
}
|
||||
.size-section {
|
||||
font-size: var(--text-4xl);
|
||||
}
|
||||
.lede {
|
||||
max-inline-size: var(--width-prose);
|
||||
font-size: var(--text-lg);
|
||||
line-height: var(--leading-relaxed);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
/* `color` INHERITS across the component boundary, which is how an inverse
|
||||
section reaches this — but `--text-secondary` resolves to a cream-only
|
||||
value, so it has to be overridden rather than inherited. Custom properties
|
||||
DO inherit, so an ancestor setting --lede-color would work too; a global
|
||||
ancestor selector is fewer moving parts for one rule. */
|
||||
:global(.section-inverse) .lede {
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
</style>
|
||||
@@ -152,6 +152,21 @@ const aboutLinks = [
|
||||
padding-block: var(--space-9) var(--space-6);
|
||||
margin-block-start: var(--space-9);
|
||||
}
|
||||
/* WHEN THE LAST SECTION IS ALREADY DARK, DROP THE MARGIN. The 96px above the
|
||||
footer is page rhythm against cream; where the page ends on the maroon
|
||||
conversion band or an ink section it becomes a 96px CREAM STRIPE sandwiched
|
||||
between two dark blocks, which reads as a layout bug rather than as air.
|
||||
Found by looking at a full-page screenshot of `/`, not by reading the CSS.
|
||||
|
||||
`:global()` on the ancestor half is the mechanism: Astro appends this
|
||||
component's cid to `.site-footer` and leaves the globalised part alone, so
|
||||
the selector can reach out of the component to <main> without a parent
|
||||
needing to style a child's root — the thing that cannot be done the other
|
||||
way round (CLAUDE.md). */
|
||||
:global(main:has(> :last-child.section-accent)) + .site-footer,
|
||||
:global(main:has(> :last-child.section-inverse)) + .site-footer {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
.footer-top {
|
||||
display: flex;
|
||||
|
||||
@@ -32,6 +32,26 @@ const items = PRIMARY_NAV.filter(
|
||||
const path = Astro.url.pathname;
|
||||
const isCurrent = (href: string) => path === href;
|
||||
const inSection = (href: string) => path === href || path.startsWith(href);
|
||||
|
||||
/**
|
||||
* THE MASTHEAD TAGLINE IS SUPPRESSED ON `/`, and this closes a step-1 review
|
||||
* finding rather than being a preference.
|
||||
*
|
||||
* SITE.tagline is `Mediation · Arbitration · Toronto`, and docs/01 specifies
|
||||
* that exact string as the home HERO EYEBROW. At >=76rem the header shows the
|
||||
* tagline too, so `/` opened with the same six words twice, 300px apart —
|
||||
* `adversarial-reviewer` #10, left open at step 1 to "decide at step 2".
|
||||
*
|
||||
* The hero keeps it, because the hero is where docs/01 puts it and where there
|
||||
* is copy underneath to qualify it. The masthead drops it, because the masthead
|
||||
* is the placement Q33-orig objected to in the first place: a line under his
|
||||
* name with nothing to qualify it, reading as a designation strip.
|
||||
*
|
||||
* This only ever REMOVES a claim from one page, so no §4 disclosure condition
|
||||
* is touched — the footer's designation strip carries `Q.Arb — commenced
|
||||
* August 2026` on every page including this one.
|
||||
*/
|
||||
const isHome = path === '/';
|
||||
---
|
||||
|
||||
<header class="site-header">
|
||||
@@ -40,7 +60,7 @@ const inSection = (href: string) => path === href || path.startsWith(href);
|
||||
<InfinityMark size="2rem" />
|
||||
<span class="brand-text">
|
||||
<span class="brand-name">{SITE.name}</span>
|
||||
<span class="eyebrow brand-tagline">{SITE.tagline}</span>
|
||||
{!isHome && <span class="eyebrow brand-tagline">{SITE.tagline}</span>}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -138,11 +158,19 @@ const inSection = (href: string) => path === href || path.startsWith(href);
|
||||
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. */
|
||||
ADR designations are voluntary credentials, not licences, and COMMERCIAL
|
||||
arbitral appointment in Ontario is not gated behind a designation — so the
|
||||
constraint was always positional, never legal, and Pouya accepts sole,
|
||||
party-appointed and co-arbitration work today. See §4 Offerings.
|
||||
|
||||
SCOPED 2026-08-27 (Q39). This comment said "Anyone may be appointed an
|
||||
arbitrator in Ontario", which Pouya checked and found FALSE as a universal:
|
||||
family arbitration carries prescribed training. It does not touch what this
|
||||
header renders — the masthead says Mediation · Arbitration · Toronto and
|
||||
family arbitration is not offered at all — but a false proposition of law
|
||||
sitting in a source comment is how one reaches a page. §4's Forbidden scope
|
||||
note: no file in this repo may assert as fact what the register has not
|
||||
verified, internal or not. */
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -159,7 +187,7 @@ const inSection = (href: string) => path === href || path.startsWith(href);
|
||||
.brand-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: var(--space-05);
|
||||
}
|
||||
/* 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
|
||||
@@ -169,7 +197,7 @@ const inSection = (href: string) => path === href || path.startsWith(href);
|
||||
same words as the hero eyebrow (docs/01). */
|
||||
.brand-tagline {
|
||||
display: none;
|
||||
font-size: 0.6875rem; /* 11px — the eyebrow floor in docs/02 */
|
||||
font-size: var(--text-2xs); /* 11px — the eyebrow floor in docs/02 */
|
||||
white-space: nowrap;
|
||||
}
|
||||
.brand-name {
|
||||
|
||||
Reference in New Issue
Block a user