Skip to content

Best-practice baseline: keyboard access, metadata, working carousel, repo hygiene - #1

Merged
tatangharyadi merged 14 commits into
mainfrom
fix/tatangharyadi/best-practice-baseline
Aug 1, 2026
Merged

Best-practice baseline: keyboard access, metadata, working carousel, repo hygiene#1
tatangharyadi merged 14 commits into
mainfrom
fix/tatangharyadi/best-practice-baseline

Conversation

@tatangharyadi

@tatangharyadi tatangharyadi commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Best-practice baseline for the site, done before adding games so the foundation
is clean first.

Measured result

Lighthouse, mobile, navigation mode:

Category Before After
Accessibility 82 100
Best Practices 100 100
SEO 100 100
Agentic Browsing 50 100
Failed audits 4 0

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, which
removes 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.next animations were dead — the JS never added the
class, 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/3 keyframes covering both directions and wired them up. The
controller also no longer relies on next/prev being undeclared globals that
only resolved because <button id="next"> becomes window.next.

Colour: Catppuccin Latte and Mocha

The site is now themed with CatppuccinLatte in
light mode, Mocha in dark, selected by prefers-color-scheme. Every rule
goes 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 :root blocks and nothing else. Hex values come
from upstream catppuccin/palette, not transcribed by hand. DESIGN.md
documents 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: sky 2.47:1, sapphire 2.78:1, teal 3.31:1, blue 4.34:1 —
only mauve clears 4.5:1, and only on that one surface. sky fails even the
3:1 non-text floor, so there is no font size at which it becomes legal, and a
sky focus ring would be illegible. --accent-text is therefore #026389,
Latte sky deepened 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 (true sky) is restricted to aria-hidden decoration.

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/base triad is built for gently stacked surfaces,
not separate zones, and sits close together either way: crust vs base is
1.17:1 in Latte and 1.14:1 in Mocha; mantle vs base 1.09 and 1.07. Dark
mode does not rescue it — Mocha is marginally flatter. Tone alone cannot
separate the nav from the content, so nav, .techstack and the mobile
slide-out each carry a 1px --border hairline on the content-facing edge. For
the slide-out that is not optional: at mantle over base its links otherwise
appear 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-hidden on 322 decorative glyphs;
<main> landmark; WCAG AA contrast on the active tab (plus bold, so selection
is not colour-only); prefers-reduced-motion guard. Open Graph / Twitter card /
canonical / favicon / JSON-LD — the link previewed as a blank card before.
Poppins via preconnect instead of an @import chain. Deleted dead CSS
(.portfolio*, .more-btn, the .casestudy-item single-dash typo rule) and
fixed nth-child(3) reading --casestudy-item1-filter. 100vh to 100dvh.
.gitignore plus untracking three committed .DS_Store files, LICENSE,
404.html, robots.txt, sitemap.xml, and real documentation (below).

.nojekyll is the one that unblocks the game work — Pages runs a legacy
Jekyll build here, which silently drops directories starting with _, exactly
what 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-step
    rationale, 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 / Pages
    limits.

  • DESIGN.md (new) — the visual system: the two flavours, the semantic token
    table, the accent rule and why #026389 exists, every contrast figure and the
    tightest 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 canonical
    section order. npx @google/design.md lint DESIGN.md reports 0 errors, 0
    warnings
    , 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 the
    dark half) with a -dark component variant each. That is deliberate rather
    than 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-dark and 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 it
    ships
    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 no
    to block, and void carousel.offsetWidth being a reflow rather than dead
    code.

  • CLAUDE.md (new) — one line, @AGENTS.md, so there is a single source
    rather 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

  • MIT LICENSE — I picked a common default. Change or drop it if you'd rather
    not license the code.
  • The band contrast change described above. It is the most visible
    consequence of adopting Catppuccin and it applies in both light and dark.
  • --accent-text is not a Catppuccin colour. #026389 is a derived value.
    It is defensible (no Latte cyan works as text) but it is a deviation from the
    palette, and mauve was the only in-palette alternative — rejected because it
    still fails in the nav at 4.09:1 and shifts the brand hue to purple.
  • Body text colour is now Latte subtext1 / Mocha subtext0. This affects
    the hero subtitle and all three tab panels, so it is a visible change, not just
    a token tweak. (The earlier #5c6370 body colour and #006d94 accent from
    this branch are both superseded by the palette.)
  • Slide duration is now uniformly 700ms (was 500ms for one of the two
    animations) so JS has a single value to read.
  • og-card.jpg is 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.
  • No in-page theme toggleprefers-color-scheme only. A toggle needs JS,
    localStorage and an inline blocking script to avoid a flash of the wrong
    theme; deliberate omission, easy to add later.

CI

.github/workflows/ci.yml runs on every PR. It is not a build — there is still no
build step, no package.json and no lockfile, and the checkers are stdlib Python
plus one pinned npx one-off. It checks only what duplication or prose makes
silent:

Check Catches
scripts/check_palette.py drift between the four copies of the palette
scripts/check_repo.py a deleted .nojekyll; a page missing from sitemap.xml
npx @google/design.md lint DESIGN.md falling off 0 errors / 0 warnings
node --check js/script.js a syntax error shipping a dead carousel

The 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 the theme-color meta pair in
each 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 0 on warnings, so the
workflow 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…"). That
prose 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.md remains the authority and now marks which of
its eight steps are automated, with an explicit note that green CI does not mean a
change is verified.

- .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.
@tatangharyadi
tatangharyadi marked this pull request as ready for review August 1, 2026 01:12
@tatangharyadi
tatangharyadi merged commit 820b76e into main Aug 1, 2026
1 check passed
@tatangharyadi
tatangharyadi deleted the fix/tatangharyadi/best-practice-baseline branch August 1, 2026 01:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant