landonorris.com · Hero · 02/10/2026 · Seed subject (chosen by the steward)
landonorris.com, hero section
A celebrity hero drawn in WebGL, with no visible headline. Its transferable lessons are in the colour system and in what it gets wrong: neutrals built at the accent's hue, a bright accent kept to fills, and a type scale that falls to 5.7px.
Findings at a glance
20 of 20 findings
| Ref | ||||
|---|---|---|---|---|
| T11 | A topographic line field as the groundReplicated as a specimen: contour lines that bend around the cursor | Technique | WebGL | Recorded |
| T10 | Two-face kinetic marquee with a drawn signatureReplicated as a specimen: the hero that becomes a card | Technique | Motion | Recorded |
| T9 | A cursor lens that reveals a second render of the subjectReplicated as a specimen: the X-ray lens | Technique | WebGL | Recorded |
| T8 | Hover reveal becomes a labelled touch control | Technique | Interaction | Promoted |
| T7 | Live peripheral data on a shaped tagWaiting for a second exemplar | Technique | Composition | Recorded |
| T6 | Brand-glyph loader with an aperture exitAbout 2 to 2.5s on every load, and plays under reduced motion | Technique | Motion | Rejected |
| T5 | Light hero sheet contracting over a dark page | Technique | Motion | Recorded |
| T4 | The logotype's two voices become the display grammar | Technique | Type | Promoted |
| T3 | The subject is the statement: no visible headlineAs a limit on rule 1: works only when visitors already recognise the subject | Technique | Composition | Promoted |
| T2 | A bright accent is a fill on light, a line only on dark | Technique | Colour | Promoted |
| T1 | Neutrals built at the accent's hue | Technique | Colour | Promoted |
| I2 | Read accessible names from the AX tree, never rebuild them | Instrument trap | Instrument | In the probe |
| I1 | Headless shell renders WebGL in software; a scene can vanishTo the estate browser rule | Instrument trap | Instrument | Promoted |
| F6 | 5.3 MB desktop, 7.1 MB mobile | Failure mode | Performance | Already covered |
| F5 | Accent-coloured focus ring at 1.13:1 | Failure mode | Accessibility | Promoted |
| F4 | Reduced motion ignored, loader included | Failure mode | Motion | Promoted |
| F3 | LCP cannot see a canvas hero | Failure mode | Performance | Promoted |
| F2 | Hero subject drawn only in a canvas, no image fallback | Failure mode | Performance | Promoted |
| F1 | Viewport-proportional type with no floor (5.7px at 992)Led to the 12px house floor | Failure mode | Type | Promoted |
| A1 | Split-letter animation hidden from assistive tech | Technique | Accessibility | Promoted |
Analysed: 02/10/2026 · By: Design Lab · Evidence: captures/landonorris.com/2026-10-02/
(frames + measurements-pass2-chromium.json). Third-party site, held as internal evidence only
(ADR 0001). It carries a real person's likeness and a sponsor's identity. Nothing here is
republished, and nothing of theirs is carried into any build.
Instruments: Playwright on full Chromium in new headless mode (channel: 'chromium', renderer
ANGLE Metal), 1440×900 and 390×844 @3x, plus widths 1920/1024/992/900/768 for the type scale.
Canvas context types were logged by an init script before any page script ran. Transfer size came
from CDP encodedDataLength. Contrast and OKLCH values were computed from the declared colour tokens
and checked against pixels sampled from the screenshots.
1. Corrections to the seed notes (01/10)
Four of the five seed observations were wrong or incomplete. Each is resolved below with the measurement that settles it, because a seed note that stood unchallenged would have been promoted as fact.
| Seed note | What the measurement shows |
|---|---|
| Display face Mona Sans Variable 700, 38/44px, uppercase | That was the <h1>, and it is visually hidden (clip: inset(50%), a 1×1 box). The hero has no visible display type at all. The stacked masthead is a 131×60 SVG (2 paths, fill #282c20, no <text>). It is a drawing, not typesetting |
| A single variable family across the whole page | Two families load: Mona Sans Variable (UI, labels, body) and Brier 700, a high-contrast display serif (frames/d-type-specimen.png). Brier sets 18 elements (17 visible), all below the hero, almost all at display sizes of 32 to 110px; two small 20px labels are the exception. A scan of the hero alone sees one family because the hero has no live display type |
| 21 canvases, zero video: the motion is drawn | True, and it needs refining. 1 WebGL2 canvas (three.js r174, full viewport) renders the portrait and the helmet. 19 are 2D canvases: a full-viewport ambient layer, an 800×571 layer around the subject, and small animated icons, at least four of which sit inside Rive wrappers (the Rive runtime is present). 1 never received a context |
body background rgb(40,44,32) is unexplained |
Resolved. It is the site's ink token (#282c20): the logo fill, the icon strokes, the STORE label, and the ground of every section after the hero. The hero's light ground is not CSS. A canvas paints it, sampled at rgb(252,252,250) at three points, which matches none of the declared tokens. On scroll the light hero contracts into a framed card and greys out, uncovering the dark page beneath (frames/sheet-scroll.png) |
| Technique list: two-face masthead, cut-out subject, acid accent, line-art overlay, shaped tag | Confirmed visually. Each one is measured and written up in §3 |
⚠️ The instrument trap that nearly produced a false analysis
The first pass used Playwright's default chromium.launch(), which is chromium-headless-shell.
Its WebGL is software (SwiftShader). The portrait never rendered, with no error and an empty
console: the hero showed a bare ground with drifting grey blobs (frames/shell-swiftshader-no-portrait.png).
Full Chromium on Metal renders it correctly (frames/chromium-metal-portrait.png).
Rule for this project: every capture uses channel: 'chromium' and records the WebGL renderer
string. A screenshot taken without that check may be a picture of a different page.
(Recorded via learn.sh, 02/10/2026.)
⚠️ Scope, measured the same day: the shell renders WebGL; it was THIS site's scene that did not
draw. A known WebGL2 test (one triangle over a cleared buffer, read back with readPixels) gives
identical pixels under SwiftShader and Metal. SwiftShader is slower and has lower limits (max texture
8192 against 16384). The 2D Rive canvases on this site also drew under the shell. Why the portrait
did not draw is unknown: the site may skip its scene on a software renderer, or the scene may exceed
a software limit. So the claim is "a WebGL scene can silently fail or be skipped on a software
renderer", never "WebGL does not render".
2. What was measured
Composition (1440×900)
| Element | Box | Notes |
|---|---|---|
| Logotype, top-left | 131×60 at (17,17) | SVG; the link is named "Lando Home" through its title attribute only |
| Monogram, top-centre | 37×37 | Rive canvas; the link is named "Go to home" |
| Primary action | 118×60 at (1235,17) | the only filled control: #d2ff00 fill, #282c20 label, 7.2px radius |
| Menu | 60×60 at (1363,17) | outlined; a Rive icon |
| Peripheral tag | 99×203 at (17,680) | SVG outline (1px, #535450), live "next race" data |
| Subject | centred, eye line at ~47% of the height | bleeds off the bottom edge with no containing box |
Every piece of chrome sits exactly 17px from its edges: logo 17/17, menu right edge 1423
(1440−17), tag bottom 883 (900−17). That is one inset token (1.25rem at a 13.33px root) on all
four corners. Chrome in the corners, the subject alone in the centre.
Colour (declared tokens, OKLCH computed)
| Role | Hex | L | C | H |
|---|---|---|---|---|
| Accent (lime) | #d2ff00 |
0.935 | 0.226 | 121.7 |
| Ink (dark green) | #282c20 |
0.285 | 0.022 | 122.8 |
| Ink tints | #3b3c38, #535450 |
0.35, 0.44 | 0.007 | 118 |
| Paper family | #f4f4ed, #efefe5, #ebeee0 |
0.94 to 0.97 | 0.009 to 0.019 | 107 to 117 |
| Greys | #dde1d2, #c8cbbd, #b9bbad, #b4b8a5 |
0.77 to 0.90 | 0.019 to 0.027 | 113 to 119 |
| Rendered hero ground | #fcfcfa (sampled) |
0.991 | 0.003 | 106 |
| Off-ramp exceptions | black #111112, orange #ff6b00 |
286, 45 |
All eleven neutral tokens sit at hue 106 to 123°. The accent is 121.7°. Chroma on the neutrals is 0.006 to 0.027, so they read as off-white, grey and near-black, yet every one of them leans toward the accent's hue.
| Pair | Contrast |
|---|---|
| Ink on hero ground | 13.89:1 |
| Ink label on lime fill | 12.26:1 |
| Lime on the dark ground (sections below) | 12.26:1 |
Micro labels #535450 on ground |
7.43:1 |
| Lime focus ring on the light ground | 1.13:1 |
| Lime focus ring on the lime button | 1.0:1 |
Type scale: proportional to the viewport
The root font size is clamp(992px, 100vw, 1920px) / 1728 × 16, set in px. The page is a
1728-wide artboard, scaled as one picture.
| Width | Root | Smallest visible text | Largest in the first viewport |
|---|---|---|---|
| 1920 | 17.78px | 11.1px | 22.2px |
| 1440 | 13.33px | 8.33px | 16.7px |
| 1024 | 9.48px | 5.93px | 11.9px |
| 992 | 9.19px | 5.74px | 11.5px |
| 900 (mobile scale takes over) | 10.42px | 8.33px | 13.0px |
| 390 | 10.83px | 8.67px | 13.5px |
Motion and loading
- Loader: an accent flood with a one-line label, then the monogram assembles, the race-number
glyph fills with the portrait texture, and the glyph scales open as a window onto the page
(
frames/d-intro-*,frames/shell-intro-1200-aperture.png). The page appeared between 2.0s (still the loader) and 2.5s (page shown) after DOMContentLoaded, on a fast connection. It plays again on reload (frames/d-reload-600.png). - No CSS animations at rest (
getAnimations()returned 0). Everything moves through JS, three.js and Rive. A shared easing token exists:cubic-bezier(0.65, 0.05, 0, 1)over 0.75s. Because nothing runs through CSS, its use cannot be confirmed fromgetAnimations(). - Idle motion: 9.3% of pixels change in 1.5s with no input.
- Cursor response: moving the cursor from the left to the right of the face changes 20.6% of the frame. The wireframe helmet draws on and turns with the pointer.
prefers-reduced-motion: reduce: the media query matched, and nothing changed. The loader played identically (frames/r-intro-*), and idle motion was 12.2% over the same window.- Touch: the cursor reveal becomes an explicit control on mobile, a lime icon button in the thumb
zone labelled "tap to lock" (
frames/m-settled.png,m-tap.png).
Cost
| Desktop | Mobile | |
|---|---|---|
| Transfer (CDP, to network idle + 3s) | 5.3 MB, 60 requests | 7.1 MB, 68 requests |
| Largest type | images 2.4 MB | fetched assets 5.1 MB |
| Reported LCP | 164 ms, a 1,001 px² DIV |
216 ms |
The LCP number is meaningless for this hero. Canvas content is not an LCP candidate, so the metric recorded the loader's label (its size matches) and never registered the portrait.
Accessibility, as found
- ✅ Split-letter kinetic labels are done properly: the per-letter spans are
aria-hidden, with a whole-word visually hidden twin. All 11 visible split<h2>s arearia-hidden, and the heading order shows each paired with a visually hidden<h2> - ✅
<html lang="en">; there is an<h1>, though visually hidden - ✅ The logotype link is named ("Lando Home"), though only through its
titleattribute, the lowest-priority source in the accessible-name computation. Chromium's AX tree resolves it (source: attribute:title). ⚠️ A first pass reported this link as unnamed: it read onlyaria-labelandinnerText. Names are now read from the AX tree, never reconstructed. - ❌ The focus ring is lime at 1.13:1 on the light ground and invisible on the lime button
(
frames/focus-strip.png). The menu button has no focus indicator (outline: none) - ❌ Visible text falls to 5.7px at 992 wide
- ❌ Reduced motion is ignored, the loader included
3. Techniques, named generically
Each technique has a verdict. Most earn no line, and that is the expected outcome.
T1 · Accent-hue neutral ramp: ✅ PROMOTE
What: every neutral (ink, paper, the greys) is built at the accent's own hue with near-zero chroma. So the loud accent reads as belonging to the palette rather than sitting on top of grey.
Measured: eleven neutrals at H 106 to 123° against an accent at 121.7°, C 0.006 to 0.027. The only exceptions are a near-pure black (C 0.002) and a secondary signal colour.
Suits: any client with one dominant brand hue taken from the logo, which is most of them. It is the step the skill's colour section already gestures at ("use oklch for tints/shades") without ever saying what to do with the neutrals.
Does not suit: a monochrome identity (tinting would invent a hue the brand does not have), two competing brand hues (pick one, or keep the neutrals true), and colour-critical products like paint, fashion or print, where the page must stay neutral so the product's colour reads true.
Failure mode: chroma creeping above about 0.03 makes the "white" look stained. Tinted paper also shifts how photographed white or grey products look, so check it next to the client's own photos.
T2 · A bright accent is a fill on light, and a foreground only on dark: ✅ PROMOTE
What: a high-lightness accent (here L 0.935) appears on the light ground only as a filled block carrying ink text (12.26:1). It becomes text, line or stroke only on the dark ground (12.26:1).
Measured, including the one place it breaks: the focus ring is a 2px lime outline. On the light
ground it is 1.13:1, and on the lime button 1.0:1, so it effectively vanishes
(frames/focus-strip.png). In every frame captured the site keeps the rule where it designed, and
breaks it in exactly the place nobody designs.
Suits: every build with a light or bright accent (lime, yellow, cyan, light orange). It is a constraint, not a style. A dark accent such as navy or oxblood is the mirror case.
Failure mode: the focus ring. :focus-visible { outline-color: var(--accent) } is the obvious
thing to write and it fails WCAG 1.4.11 (3:1) on every light surface.
T3 · The subject is the statement: ⚠️ RECORD, promote only as a counter-indication
What: the hero has no visible headline. The H1 exists but is visually hidden; the only visible words are the logotype (a drawing), one action, and 8px labels on a corner tag. A cut-out portrait, centred, eye line at 47% of the height and bleeding off the bottom with no box, makes the whole argument. The chrome is pushed to the four corners on one 17px inset.
Suits: only when the visitor already recognises the subject: a public figure, an iconic product, a brand the visitor came looking for by name.
Does not suit: any business the visitor does not already know, which is every local and SME client in this estate. A stranger landing on a headline-less hero cannot tell what is offered. Our existing rule ("a hero demonstrates, it does not announce") is right, and this is its extreme form. The extreme only works on top of recognition the client does not have.
Verdict: no new technique line. One clause on rule 1 is worth adding so a builder inspired by an exemplar like this does not drop the headline (proposal 6 below). The corner-anchored chrome on a single inset is worth knowing as part of the same composition, but not on its own.
T4 · The logotype's two voices become the display grammar: ⚠️ OPTIONAL, one clause
What: the mark stacks a high-contrast serif word over a heavy grotesque word. Below the hero, every display statement repeats that split: grotesque caps for the sentence, with the operative words switched to the serif in the accent colour. The serif in the type system (Brier) matches the serif in the mark in form. Because the mark is a drawing, the face inside it cannot be read from the DOM.
Mobile: the brand supplies a second lockup, both words on one line (an SVG <img>, 173×19,
with alt text). The stacked mark is never squashed.
Suits: a client whose own mark already pairs two faces. It extends the EDITORIAL entry's "real second voice", which currently offers only an italic or the brand colour.
Does not suit: inventing a pairing the brand does not have. That is a new identity, and per "you cannot change a company's logo" it is not ours to make. Read it out of their mark, never into it. The mobile point lands on the same ruling: use the client's horizontal lockup if they have one, and ask for it if they do not. Never rearrange the stacked one.
T5 · Light hero sheet over a dark page: ❌ RECORD ONLY
What: the page's true ground is the dark ink. The hero is a light layer above it, and on scroll the hero contracts into a framed card and desaturates toward the ground, so the mood changes in one continuous move rather than at a section edge.
Why no line: expensive (here a canvas and three.js), with one exemplar, and it needs a strong single opening image to work. Revisit if a second exemplar does the same with plain CSS (a sticky section with a scale transform).
T6 · Brand-glyph loader with an aperture exit: ❌ DO NOT PROMOTE
What: an accent-colour flood and label, then the brand glyph assembles and opens as a window onto the page.
Why not: it costs about 2 to 2.5s on every load, reload included, and it plays under reduced motion. For a client whose visitors want a phone number or a price, a loader is pure delay. It is a memorable signature for a fan site and a toll everywhere else.
T7 · Live peripheral data on a shaped tag: ❌ RECORD ONLY (watch for a second exemplar)
What: a genuinely live fact pinned to a corner on a tag whose outline has a cut tab, rather than a rectangle: an SVG 1px outline, 8px uppercase labels, and a small animated drawing.
Could suit: a client with a real live fact, like "open now", "next available slot" or "today's special". It does not suit a static fact dressed up as live.
Failure mode, measured: 8.3px labels at 1440 that fall to 5.7px at 992.
T8 · Hover reveal becomes a labelled touch control: ⚠️ OPTIONAL
What: the cursor-driven helmet reveal is replaced on mobile by an explicit, labelled button in the thumb zone, not left as an invisible tap target. It is a sound pattern, but the label is 8.7px. The principle is standard, so a line would be cheap; the evidence is one site.
T9 · A cursor lens that reveals a second render of the subject
What: the subject is rendered in WebGL (one webgl2 canvas, three.js r174, full viewport), and
the cursor drives it. Moving the pointer from the left to the right of the face changed 20.6% of the
frame: a wireframe helmet draws on and turns with the pointer, and at one scroll position a circular
lens over the eye showed a second, fully coloured render of the helmet (frames/d-scroll-300.png).
This is the site's signature technique, and the first pass of this analysis described it as
"cursor response" without naming it.
Replicated as the X-ray lens specimen: two render passes of our own object composited in a
shader, with the image-underneath fix from F2. Replicated a second time on a renovation (02/10/2026), at
Pete's direction: the house as found is the subject, the new design is drawn in line over it around the
pointer (the wireframe), and a glass lens shows the finished house in full colour (the second render).
Same shader idea, two decoded photographs instead of two render passes: /specimens/house-lens. And a third time (03/10/2026), grown into several views of one place:
/specimens/woodland-lens shows an empty log, and the glass chooses who lives there, the same spot
after dark, in winter or in spring, with a field-guide label naming what is under it. Five generated
pictures from one camera position, registration measured within 1.5 px. Suits a product whose making is the story.
Does not suit a client with nothing physical to show.
T10 · Two-face kinetic marquee with a drawn signature
What: after the hero contracts (T5), a statement runs as a scroll-linked marquee in two faces
(the display serif in the accent and a heavy grotesque in paper), and an accent signature is drawn
over a card (frames/d-scroll-600.png, d-scroll-900.png). Recorded, and replicated in the
"hero that becomes a card" specimen with our own signature and typefaces.
T11 · A topographic line field as the ground
What: contour lines run behind the hero and the dark sections, drawn on canvas (which canvas was not resolved, see §6). Replicated as a shader specimen in which the lines bend around a hill raised under the cursor. Suits outdoor, survey and geography subjects; does not suit text-heavy pages, where moving lines behind copy cost legibility.
4. Failure modes measured here that our builds can avoid
These are not techniques. They are what this site gets wrong, measured, and each one maps to a rule our skill either lacks or states too loosely.
| # | Failure | Measurement | Skill gap |
|---|---|---|---|
| F1 | A viewport-proportional type scale with no floor | smallest text 5.74px at 992 wide; the root is set in px, so the visitor's browser font-size preference is overridden | Typography says "fluid scales with clamp()" and nothing about a minimum |
| F2 | The hero subject exists only in a canvas | on a software renderer the subject never draws, and with the headline visually hidden nothing on screen says what the page is | Performance says nothing about canvas or WebGL heroes |
| F3 | LCP is blind to a canvas hero | reported LCP of 164ms from a loader label; the subject arrives seconds later | Performance targets LCP < 2.5s with no warning that the metric can be fooled |
| F4 | Reduced motion ignored, loader included | the query matched; intro and idle motion unchanged | the skill says "every animation guarded" but never names the loader or ambient motion |
| F5 | Accent-coloured focus ring | 1.13:1 and 1.0:1 | covered by T2 |
| F6 | Weight | 5.3 MB desktop and 7.1 MB mobile, against our 500 KB initial bundle and 3s on 3G | already covered; noted to show this class of hero is outside our targets by an order of magnitude |
5. Verdict: proposed lines for web-frontend
Outcome, 02/10/2026: all nine are in
web-frontend. The steward promoted 1, 2, 4, 5, 6, 7, 8 and 9, rewording several on the way in (1 as an illustration with its n=1 provenance; 6 as "demonstrates does not mean wordless"; 8 on the stronger ground that the skill recommends hover effects and never said what becomes of them on touch). Proposal 3 was split, then completed when Pete set the floor at 12px from 320 to 2560 wide. The WebGL finding went to the estate's browser rule instead, after its wording was corrected (§1).
For the steward to judge (the shared design skill is not ours to edit). These are ranked. 1 to 4 are measured, generic and transferable; 5 is a small amendment; 6 to 9 are optional and weaker. None of them names the site.
1 · Colour Selection, new: neutrals from the brand hue
Build the neutrals from the brand hue. Take the accent's OKLCH hue and make the ink, the paper and every grey at that hue, with chroma between about 0.005 and 0.03. The accent then reads as part of the palette instead of pasted onto grey. Skip it for a monochrome identity, two competing brand hues, or a colour-critical product (paint, fashion, print). Above about 0.03 the "white" looks stained, so check the paper next to the client's own photos.
2 · Colour Selection, new: where a bright accent may go
A light, bright accent is a fill on light grounds, and a line only on dark ones. Lime, yellow and cyan sit barely above 1:1 against off-white (an acid lime measured 1.13:1). Use them as a filled block carrying dark text, and as text or strokes only on dark grounds. The place this breaks unnoticed is the focus ring: an accent-coloured
:focus-visibleoutline disappears on the light page and vanishes on an accent button. Draw focus in the ink colour, or as a two-tone ring, at 3:1 or better against what it sits on.
3 · Typography, new: floor the fluid scale
If type scales with the viewport, floor it. A root size computed from
vwshrinks every label with the window: one measured site's 8px labels at 1440 wide fell to 5.7px at 992. Useclamp(<min in rem>, <vw expression>, <max>)on the sizes themselves, never a px root derived fromvw, which overrides the visitor's own browser font size. House floor: no visible text below 12px at any width.
(12px is a house standard, not a WCAG number; WCAG sets no minimum size. Ruled by Pete on
02/10/2026: 12px at any width from 320 to 2560, now in web-frontend with a "check the narrow
end" line.)
4 · Performance, new: canvas heroes
A hero drawn in a canvas needs a real image underneath, and its LCP is not evidence. A WebGL scene can fail, or be skipped, without a sound: on a software renderer one measured site's subject never drew, though WebGL itself worked there. Put the subject in as an
<img>(eager,fetchpriority="high") that the canvas covers once its first frame has drawn, and keep a visible line of text that says what the page is. Canvas is not an LCP candidate, so the metric reports whatever small element painted first. Measure the subject's arrival yourself.
5 · Accessibility, amend the reduced-motion line
…every animation guarded by
prefers-reduced-motion, and the loader and any idle ambient motion count. A brand intro is the one most often left unguarded: skip it under reduced motion, and play it once per session at most.
6 · Direction rule 1, optional clause
The extreme form, a hero with no visible headline where the subject is the whole statement, only works when the visitor already recognises the subject. A local business never has that, so its hero always keeps a visible line saying what it does.
7 · EDITORIAL entry, optional clause
If the client's own logotype already pairs two faces, that pairing is the second voice: the operative words in one face, the rest in the other. Read it out of their mark; never invent a pairing their brand does not have. On mobile, use their horizontal lockup, or ask for one. Never rearrange the stacked mark.
8 · Interaction, optional
Anything revealed on hover needs a visible, labelled control on touch, in the thumb zone. An invisible tap target is not a replacement for hover.
9 · Kinetic type, optional (only matters if we animate letters)
Split-letter animation spells words out to screen readers. Put the per-letter spans inside
aria-hidden="true"and keep one whole-word copy visually hidden.
6. Open questions
- Does the loader replay on in-site navigation, or only on a full load? Only reload was tested. This does not change T6's verdict.
- What draws the contour lines: the full-viewport 2D layer, or the WebGL canvas? Unresolved, and irrelevant to any verdict above.
Proposal 3's 12px floor is a house-standard decision, not a measurement.Settled by Pete, 02/10/2026: 12px, 320 to 2560.