Best-practice baseline: keyboard access, metadata, working carousel, repo hygiene - #1
Merged
Merged
Conversation
- .gitignore, and untrack the three committed .DS_Store files - .nojekyll, so Pages' legacy Jekyll build stops silently dropping directories that begin with an underscore (_framework/, _next/ and similar are what game engines and bundlers emit) - LICENSE (MIT, code only), 404.html, robots.txt, sitemap.xml - expand README from a single heading to stack, layout, local dev, deployment and the accessibility invariants worth not breaking
- convert the 358K profile PNG to WebP at 44K; it renders at 400px and the source was only 567px wide, so nothing is lost - delete three unreferenced JPEGs (572K) left behind when the card-based portfolio section was replaced by the carousel - add a 1200x630 social share card and an SVG favicon
The hero tab radios and the mobile menu checkbox used display:none, which removes an element from the tab order and the accessibility tree. The 'My Services' and 'For Recruiters' panels were therefore unreachable by keyboard and invisible to screen readers. Both now use a visually-hidden pattern with a visible focus ring on the associated label. Also: - accessible names on the three icon-only social links - aria-hidden on all decorative Boxicons glyphs (322 of them across the seven marquee rows) - wrap the sections in <main>; the page had no landmark - darken the active tab colour to meet WCAG AA on the light background, and mark it bold so selection is not signalled by colour alone - prefers-reduced-motion guard over the marquees and carousel - close the unclosed <span> in the h1, escape the raw < and > in the carousel buttons, rename the numeric tab ids - Open Graph, Twitter card, canonical, favicon, theme-color, JSON-LD Person; load Poppins via preconnect + link instead of an @import chain; drop the obsolete X-UA-Compatible meta - delete dead CSS: .more-btn, .portfolio*, .casestudy--item img, and the .casestudy-item single-dash typo rule - fix nth-child(3) reading --casestudy-item1-filter - 100vh to 100dvh; move scroll-behavior and font-family off the * selector; order the max-width media queries widest-first Lighthouse mobile: accessibility 82 to 100, agentic browsing 50 to 100.
The .casestudy--container.next animations were dead: the JS never added the class, and no rule covered the item moving into the centre slot, so cards jumped while their text animated. Adds fromItem1/2/3 keyframes covering both directions and wires them up. Also in the controller: - declare next/prev as string constants. They were undeclared globals that only resolved because <button id="next"> becomes window.next, which breaks under strict mode or an id rename. - read the slide duration from --casestudy-slide-duration so the button lock cannot drift out of sync with the CSS - disable the buttons during a slide rather than mutating inline pointer-events - skip the animation entirely under prefers-reduced-motion, so the buttons are not left waiting on an event that never fires - arrow-key support, and drop the unused carousel variable's dead weight by actually using it
Disabling the button the user just activated moved focus to <body>, so after pressing Next once a keyboard user was stranded outside the carousel and had to tab in from the top of the document again. Swap the disabled property for aria-disabled plus an opacity rule; re-entry was already guarded by isSliding, so nothing needed enforcing. Also add visually-hidden <h2> headings to the case study and tech stack sections. Both are named destinations in the nav but had no heading, and the tech stack rows are aria-hidden as a whole, which left the section an unnamed empty region under heading navigation.
README had grown into three audiences at once: a visitor wanting the live link, someone wanting to know how the carousel works, and someone about to change it without breaking accessibility. Split along those lines. ARCHITECTURE.md explains the build (no build step and why, file layout, design tokens, the CSS-only tab and sidebar patterns, how the carousel animates backwards out of the slot each item vacated, breakpoints, the .nojekyll and Pages constraints). AGENTS.md is the operational half: local dev, how to verify a change, and the four things that look like bugs but are load-bearing. Two of those were not documented anywhere: the fromItem keyframes deliberately having no `to` block, and `void carousel.offsetWidth` being a reflow rather than dead code. CLAUDE.md is a single line importing AGENTS.md, so there is one source. Also fixes a stale lead-in: the accessibility list said "Two patterns" while carrying three bullets, now four.
I had slots 1 and 3 backwards. Slot 1 is the slot a card has just left (opacity 0, off to the left, pointer-events: none); slot 3 is the on-deck card (visible, small, offset right). Verified against the :nth-child rules and the token values, and cross-checked the .next/.prev animation assignments against the node moves in showSlider. Also softens an overstatement: 404.html is self-contained in its styles but does reference the favicon.
Replace the ad-hoc palette with Catppuccin, driven by prefers-color-scheme: Latte in light mode, Mocha in dark. Colour now goes through nine semantic tokens defined once in :root with a single dark override block, so no rule references a palette name and a flavour swap touches only those two blocks. Add DESIGN.md as the reference for which token to reach for, with the measured contrast figures behind each choice. Two findings shaped the mapping, both documented: - Catppuccin Latte's entire cyan family is unusable as text. sky is 2.47:1 on base, sapphire 2.78, teal 3.31 — none reach 4.5:1, and sky fails even the 3:1 large-text and UI floor. --accent-text is therefore Latte sky deepened to 60% lightness (#026389, 5.04-5.90:1 on every surface), the one deliberately non-Catppuccin value in the system. It is also every focus ring, which is why reverting it to sky would be a real regression rather than a tidy-up. In-palette mauve was rejected: 4.09:1 on crust, and an off-brand hue shift. - crust/mantle/base sit within 1.07-1.17:1 of each other in both flavours, so the previous ~15:1 dark-band-on-light-page composition cannot be reproduced and dark mode does not rescue it. nav, .techstack and the mobile slide-out menu gain a 1px --border hairline on the edge facing the content; that line, not the fill, is what separates them now. The page reads flatter and more layered than before in both schemes — inherent to the palette. Also: - color-scheme: light dark, so native scrollbars and controls follow the theme. - theme-color metas become per-scheme pairs in index.html and 404.html. - 404.html gets its own inline Latte/Mocha block; its styles stay inline because it must render when the stylesheet is what failed. - Focus indicators added where colour alone was the only signal (social links, the 404 button) — WCAG 1.4.11. - Carousel arrows take their border from --text, not --border, because there the outline is the control's boundary and needs 3:1. Verified: 20 fg/bg pairs x 2 flavours against per-pair WCAG floors, 0 failures; computed values at runtime match in both schemes; Lighthouse mobile navigation 100/100/100/100, 55 passed 0 failed, audited once per scheme; console clean; carousel still keyboard-operable with focus retained on the arrow.
DESIGN.md was written from scratch and matched neither mandatory layer of the
format spec for the file it is: no YAML token front matter, and no canonical
section name. A best-practice baseline should not ignore the convention for its
own deliverable.
Restructured onto the canonical section order (Overview, Colors, Typography,
Layout, Elevation & Depth, Shapes, Components, Do's and Don'ts) with all the
existing rationale and every measured contrast figure preserved, and added
machine-readable tokens: 18 colours, 9 typography levels, 2 rounding levels, 7
spacing tokens, 20 components. Every hex is verified against css/style.css.
The format has no light/dark axis, so the two flavours are carried as two flat
token sets -- unprefixed for Latte, `mocha-*` for Mocha -- with a `-dark`
component variant for each. This is not only a workaround: because each Mocha
pair is reachable through a component, the linter's WCAG check runs over the dark
flavour too, which a light-only token set would leave unverified. Confirmed by
deliberately breaking hero-tab-dark and watching the rule fire.
The inline `{ light, dark }` shape the maintainer has endorsed upstream (issue
#13, PR #128) does not work in the released alpha: the nested object parses as a
token group, so every reference becomes a broken-ref error. Documented as the
shape to collapse to once it ships, with an explicit do-not-adopt-early note.
npx @google/design.md lint DESIGN.md: 0 errors, 0 warnings. Exports valid W3C
DTCG and a Tailwind theme carrying all 18 colours. Added the lint step to
AGENTS.md and noted the front matter as a third copy of the palette that must
stay in step with css/style.css and 404.html.
No CSS, HTML or JS changed.
The palette is written out in four places — the :root and dark blocks in css/style.css, 404.html's inline subset, DESIGN.md's front matter under semantic names, and the theme-color meta pair in each page. Every one of those duplications is justified and none was enforced by anything, so changing one and not the others produced no error, no visual break in the scheme you happened to be testing, and nothing for a reviewer to catch. That is the check this adds; the rest are cheap company for it. scripts/check_palette.py reads css/style.css as the source of truth and holds the other three against it, including rejecting a theme-color meta with no prefers-color-scheme attribute. scripts/check_repo.py covers .nojekyll and keeps sitemap.xml in step with the indexable pages, exempting noindex ones so 404.html stays correctly unlisted. The design.md linter exits 0 on warnings, but AGENTS.md holds DESIGN.md at 0 errors and 0 warnings — so the workflow gates on the JSON summary rather than the exit code, or CI would have permitted a state the docs forbid. The package is pinned; a floating alpha would turn CI red with no change here. No toolchain comes with this: stdlib Python plus a one-off npx, still no package.json and no lockfile. ARCHITECTURE.md said "there is no CI" and now says why there is, and what it deliberately does not cover — CI cannot tab through a page or run Lighthouse per scheme, so AGENTS.md stays the authority and marks which of its steps are automated. Each check was verified to fail on a deliberately broken tree, not only to pass on a clean one.
v4 targets Node.js 20, which the runners now force onto Node 24 and flag as deprecated on every run. Nothing in the workflow depends on the old major.
Drop an argument colour_vars never used, make the shebangs real rather than decorative so ./scripts/check_palette.py works, and realign the file tree row that .github/workflows/ci.yml pushed out of column.
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.
Best-practice baseline for the site, done before adding games so the foundation
is clean first.
Measured result
Lighthouse, mobile, navigation mode:
Images: 956K to 103K. The 100s hold in both colour schemes — audited
separately under light and dark.
The one that mattered
The hero tab radios and the mobile menu checkbox used
display: none, whichremoves an element from the tab order and the accessibility tree. I confirmed
in the live DOM that 0 of 3 tab inputs were focusable, so the My Services
and For Recruiters panels could not be opened by keyboard and their text was
never announced by a screen reader. On a site whose stated audience is
recruiters, that was the top issue. Both now use a visually-hidden pattern with
a visible focus ring on the label.
Carousel
The
.casestudy--container.nextanimations were dead — the JS never added theclass, and no rule covered the item moving into the centre slot. Cards jumped
while their text animated, which is why it read as half-broken. Added
fromItem1/2/3keyframes covering both directions and wired them up. Thecontroller also no longer relies on
next/prevbeing undeclared globals thatonly resolved because
<button id="next">becomeswindow.next.Colour: Catppuccin Latte and Mocha
The site is now themed with Catppuccin — Latte in
light mode, Mocha in dark, selected by
prefers-color-scheme. Every rulegoes through a semantic token (
--bg,--bg-alt,--bg-chrome,--text,--text-muted,--accent,--accent-text, …); no rule names a palette colour,so a flavour swap touches two
:rootblocks and nothing else. Hex values comefrom upstream
catppuccin/palette, not transcribed by hand.DESIGN.mddocuments the whole system.
Two findings shaped the result and are worth flagging:
Catppuccin Latte's entire cyan family is unusable as text. Against Latte's
page background:
sky2.47:1,sapphire2.78:1,teal3.31:1,blue4.34:1 —only
mauveclears 4.5:1, and only on that one surface.skyfails even the3:1 non-text floor, so there is no font size at which it becomes legal, and a
skyfocus ring would be illegible.--accent-textis therefore#026389,Latte
skydeepened to 60% lightness with hue and saturation preserved — 5.90 /5.48 / 5.04:1 on the three surfaces. It is the only non-palette value in the
system, and it is also every focus ring, so it must not be "corrected" back to
sky.--accent(truesky) is restricted toaria-hiddendecoration.The bands are visibly quieter than before, in both schemes. The old design
framed near-white sections with a near-black navy nav — about 15:1.
Catppuccin's
crust/mantle/basetriad is built for gently stacked surfaces,not separate zones, and sits close together either way:
crustvsbaseis1.17:1 in Latte and 1.14:1 in Mocha;
mantlevsbase1.09 and 1.07. Darkmode does not rescue it — Mocha is marginally flatter. Tone alone cannot
separate the nav from the content, so
nav,.techstackand the mobileslide-out each carry a 1px
--borderhairline on the content-facing edge. Forthe slide-out that is not optional: at
mantleoverbaseits links otherwiseappear to float on the page rather than sit in a menu. The net composition is
calmer and more layered than the original. That is inherent to the palette, not
a defect — but it is a real change in how the page reads, and the thing here
most likely to want a second opinion.
Everything else
Accessible names on the icon links;
aria-hiddenon 322 decorative glyphs;<main>landmark; WCAG AA contrast on the active tab (plus bold, so selectionis not colour-only);
prefers-reduced-motionguard. Open Graph / Twitter card /canonical / favicon / JSON-LD — the link previewed as a blank card before.
Poppins via preconnect instead of an
@importchain. Deleted dead CSS(
.portfolio*,.more-btn, the.casestudy-itemsingle-dash typo rule) andfixed
nth-child(3)reading--casestudy-item1-filter.100vhto100dvh..gitignoreplus untracking three committed.DS_Storefiles,LICENSE,404.html,robots.txt,sitemap.xml, and real documentation (below)..nojekyllis the one that unblocks the game work — Pages runs a legacyJekyll build here, which silently drops directories starting with
_, exactlywhat Godot/Unity/bundler exports emit.
Documentation
README had grown into three audiences at once, so it is split across four
documents, plus a one-line pointer:
ARCHITECTURE.md(new) — how the site is built: the no-build-steprationale, file layout, the design tokens and their non-colour constraints, the
CSS-only tab and sidebar patterns, how the carousel animates backwards out of
the slot each item vacated, the breakpoint ladder, and the
.nojekyll/ Pageslimits.
DESIGN.md(new) — the visual system: the two flavours, the semantic tokentable, the accent rule and why
#026389exists, every contrast figure and thetightest margins, and what was deliberately left out (no theme toggle, no
other flavours). Written to the DESIGN.md
format — machine-readable
tokens in front matter (18 colours, 9 typography levels, 20 components, every
hex verified against
css/style.css) plus rationale prose, on the canonicalsection order.
npx @google/design.md lint DESIGN.mdreports 0 errors, 0warnings, and it exports to valid W3C DTCG and a Tailwind theme. That lint is
a one-off command, not a dependency — there is still no
package.json.The format has no light/dark axis, which is the one real friction point.
Latte and Mocha are therefore carried as two flat token sets (
mocha-*for thedark half) with a
-darkcomponent variant each. That is deliberate ratherthan merely expedient: because every Mocha pair is reachable through a
component, the linter's WCAG check runs over the dark flavour too — verified by
breaking
hero-tab-darkand watching the rule fire. The inline{ light, dark }shape the maintainer has endorsed upstream(#13,
#128) errors in the
released
alpha, so the file documents it as the shape to collapse to once itships rather than adopting it early.
AGENTS.md(new) — the operational half: local dev, how to verify a change,and the things that look like bugs but are load-bearing. Two of those were not
written down anywhere before: the
fromItem*keyframes deliberately having notoblock, andvoid carousel.offsetWidthbeing a reflow rather than deadcode.
CLAUDE.md(new) — one line,@AGENTS.md, so there is a single sourcerather than two that drift.
README.md— trimmed to a visitor-facing overview plus pointers.Also fixed a stale lead-in that said "Two patterns" above a three-bullet list.
Verification
Served locally and checked in Chrome: 3/3 tab inputs focusable, mobile menu
opens from the keyboard, both slide directions animate the correct elements and
unlock afterwards, reduced-motion path cuts instantly without locking the
buttons, no console errors, HTML tag structure balanced, no duplicate ids.
For the colour work: 20 foreground/background pairs × 2 flavours checked against
per-pair floors (4.5:1 text, 3:1 large text and non-text indicators) — 0
failures; runtime computed values confirmed to match the documented table in
both schemes; no stale token or stray hex left anywhere in
css/or*.html;Lighthouse re-run under each scheme.
Worth a second look
not license the code.
consequence of adopting Catppuccin and it applies in both light and dark.
--accent-textis not a Catppuccin colour.#026389is a derived value.It is defensible (no Latte cyan works as text) but it is a deviation from the
palette, and
mauvewas the only in-palette alternative — rejected because itstill fails in the nav at 4.09:1 and shifts the brand hue to purple.
subtext1/ Mochasubtext0. This affectsthe hero subtitle and all three tab panels, so it is a visible change, not just
a token tweak. (The earlier
#5c6370body colour and#006d94accent fromthis branch are both superseded by the palette.)
animations) so JS has a single value to read.
og-card.jpgis the profile photo padded onto the old dark brand colour.It no longer matches the palette; a purpose-designed 1200x630 card in
Catppuccin would be better.
prefers-color-schemeonly. A toggle needs JS,localStorageand an inline blocking script to avoid a flash of the wrongtheme; deliberate omission, easy to add later.
CI
.github/workflows/ci.ymlruns on every PR. It is not a build — there is still nobuild step, no
package.jsonand no lockfile, and the checkers are stdlib Pythonplus one pinned
npxone-off. It checks only what duplication or prose makessilent:
scripts/check_palette.pyscripts/check_repo.py.nojekyll; a page missing fromsitemap.xmlnpx @google/design.md lintDESIGN.mdfalling off 0 errors / 0 warningsnode --check js/script.jsThe palette check is the one that earns its keep. The Catppuccin values are
written out in
css/style.css(source of truth),404.html's inline subset,DESIGN.md's front matter under semantic names, and thetheme-colormeta pair ineach page. Every duplication is justified; none was enforced. Editing one and not
the others produced no error and no visual break in whichever scheme you happened
to be testing — the single most likely silent regression in this repo, and the one
thing here a reviewer could not reasonably be expected to catch.
Two details worth flagging. The design.md linter exits
0on warnings, so theworkflow gates on its JSON summary rather than the exit code — an errors-only gate
would have let CI permit exactly the state AGENTS.md forbids. And each check was
verified to fail on a deliberately broken tree, not merely to pass on a clean
one: a check never observed failing is not yet a check.
This reverses a line I had written in
ARCHITECTURE.md("There is no CI…"). Thatprose is now replaced with why there is one and what it deliberately does not
cover — CI cannot tab through a page, operate the carousel from the keyboard, or
run Lighthouse per scheme.
AGENTS.mdremains the authority and now marks which ofits eight steps are automated, with an explicit note that green CI does not mean a
change is verified.