fullstack.rocks skills · demos

I read the watch homepage (index.html, styles.css, main.js), clients/watch/profile.md, clients/watch/design.md, research/accessibility.md, and the watch SVGs and asset notes. I did not render the page and I did not change any files. Every local image, stylesheet, and script the page references is on disk. The only external request is Google Fonts. Every text colour in styles.css is one of the eight design tokens, and every text/background pair is in the design matrix (all at least 6:1).

ID tag severity where finding fix
A11Y-01 a11y medium examples/maison-lumen/sites/watch/index.html:256 The format error is tied to the fieldset with aria-describedby="req-format-error". On failure, main.js:142 focuses the first radio, and that radio has no aria-describedby. The polite status only says “Please check the marked fields.” A screen-reader user who missed the name and email checks does not hear “Choose an appointment format.” On both format radios (lines 259 and 260), add aria-describedby="req-format-error". Leave the fieldset’s aria-describedby as it is. main.js already sets aria-invalid="true" on both radios.
A11Y-02 a11y medium examples/maison-lumen/sites/watch/styles.css:307 Below 768px, .site-nav { display: contents; } removes the nav box. Safari does not expose an element with display: contents as a landmark, so the “Main” nav landmark is gone on small screens. Delete display: contents. Inside the existing @media (max-width: 767px) block, lay the header out as a grid: .header-inner { display: grid; grid-template-columns: 1fr auto; grid-template-rows: auto auto; }, .wordmark { grid-column: 1; grid-row: 1; position: relative; z-index: 1; }, .site-nav { display: grid; grid-template-columns: subgrid; grid-template-rows: subgrid; grid-column: 1 / -1; grid-row: 1 / span 2; pointer-events: none; }, .menu-toggle { grid-column: 2; grid-row: 1; justify-self: end; pointer-events: auto; }, .nav-list { grid-column: 1 / -1; grid-row: 2; flex-direction: column; padding-bottom: var(--space-16); pointer-events: auto; }. Keep the closed list as .js-ready .nav-list:not(.is-open) { display: none; } (see USE-01). The list stays in normal flow under the bar, which is what design.md specifies.
A11Y-03 a11y medium examples/maison-lumen/sites/watch/styles.css:506 The modal width is min(40rem, 100vw - 3rem). 100vw includes the scrollbar, so at a 320px width with a classic scrollbar the dialog is wider than the visible viewport and the page can scroll sideways. design.md requires 320px reflow with no horizontal scroll. Change that width to min(40rem, calc(100% - 3rem)). Leave max-height: calc(100dvh - 3rem) and overflow-y: auto.
USE-01 usability medium examples/maison-lumen/sites/watch/index.html:8 The head script adds js before main.js runs. .js hides the nav list until .is-open and hides the inline request dialog (styles.css:309 and :504). If main.js fails to load or throws, the Menu button does nothing and both “Request a bench appointment” buttons do nothing, because html:not(.js) no longer applies. That is the mobile nav and the page’s only action. Delete the head script on line 8. Rename the CSS gates .js .menu-toggle, .js .nav-list:not(.is-open), .js .nojs-only, .js .request:not(:modal), and html:not(.js) .request-trigger, html:not(.js) .close-request so they key off html.js-ready / html:not(.js-ready) instead of js. At the end of main.js, after dialog.close(), previewButton.disabled = false, and the listeners are bound, add document.documentElement.classList.add('js-ready'). If main.js never finishes, the link list stays visible, the triggers stay hidden, and the form stays inline in the appointment section.
USE-02 usability medium examples/maison-lumen/sites/watch/main.js:46 Nav, footer, and dossier clicks call preventDefault and history.pushState, and nothing listens for popstate. Back changes the URL and leaves the scroll position and aria-current where they were. aria-current is also set only on those clicks (markCurrent at lines 32–37 and 52), so scrolling never marks the section. Add window.addEventListener('popstate', () => { const hash = location.hash; const target = hash && document.querySelector(hash); markCurrent(hash); if (!target) return; const details = target.closest('details'); if (details) details.open = true; const heading = target.matches('h1, h2, h3, summary') ? target : target.querySelector('h1, h2'); (heading || target).scrollIntoView(); if (heading) heading.focus({ preventScroll: true }); });. Add an IntersectionObserver on #watch, #workshop, #ownership, and #appointment with threshold: 0.4 that calls markCurrent('#' + id) for the section crossing the middle of the viewport. Observe only those four ids; header links are the only ones markCurrent knows.
COPY-01 copy medium examples/maison-lumen/sites/watch/index.html:43 Profile §2 prices the T-01 at CHF 48,000 before tax, and §6 says to state cost plainly. This is the only page, and the price never appears. Profile §8 gives no delivery window. After the lead paragraph, before .action, add <p>CHF 48,000 before tax.</p>. Do not add a lead time, a countdown, or allocation language. Delivery is already named in the appointment body at line 230.
COPY-02 copy medium examples/maison-lumen/sites/watch/index.html:201 The visible row is “A written 25-year spares commitment”. design.md says this is spare parts, and that it is neither a warranty nor free maintenance. That boundary is only in the design notes. The disclosure at line 216 restates the reserve and never says workshop labour is charged. Change the row text to A written 25-year spares commitment. Parts only, not a warranty. In the disclosure’s first paragraph, after the reserve sentence, add The commitment covers those parts. Workshop labour is quoted before any work. It does not include free maintenance. Keep the existing revision sentence.
COPY-03 copy medium examples/maison-lumen/sites/watch/index.html:62 Profile §6 says to name the material and explain technical terms. “German-silver” appears only in alt text (lines 65 and 107). The visible calibre sentence says “bridges” and “Swiss lever escapement” with no gloss. A design patron cannot tell the metal or the escapement. Replace the calibre paragraph with Individually removable bridges in German silver (nickel, copper and zinc). A conventional Swiss lever escapement: lever, escape wheel and balance. Construction the next watchmaker can read.
COPY-04 copy medium examples/maison-lumen/sites/watch/index.html:112 Each finishing link says “See this operation in the dossier”, and the disclosure says the three operations “are identified separately”. All three links share #sample-dossier, and main.js:61 always writes that hash. The disclosure has one shared paragraph and no per-operation block. The sample is labelled illustrative, which is right; the “separately” claim is not true of the page. Keep each link’s href="#sample-dossier" so a no-JS visit still lands on the visible summary. Add data-target="dossier-bevels", data-target="dossier-corners", and data-target="dossier-black-polish" on the three links. Inside the disclosure, after the existing finishing-map paragraph, add three headings with tabindex="-1": <h3 id="dossier-bevels">Hand-rounded bevels</h3><p>The map marks this bridge edge as hand-rounded and polished, separate from machine operations.</p>, <h3 id="dossier-corners">Four inward corners</h3><p>The map marks these internal corners as finished by hand, separate from machine operations.</p>, <h3 id="dossier-black-polish">Black-polished steel</h3><p>The map marks these steel fittings as flat mirror polishing, separate from machine operations.</p>. In the dossier click handler, open the details, then const target = document.getElementById(link.dataset.target);, history.pushState(null, '', '#sample-dossier'), scroll the target into view, and target.focus({ preventScroll: true }). Do not invent serials, rates, dates, or signatories.
UX-01 ux medium examples/maison-lumen/sites/watch/styles.css:396 At 768px the three diptychs share one row. Content width is about 704px, so each half of a pair is about 110px wide. The before/after evidence, which is the point of the section, becomes too small to judge. design.md stacks one full figure below 768px and gives each figure four columns on the 12-column grid. Change the finishing media query from min-width: 768px to min-width: 1024px. Leave the single-column stack below that.
UX-02 ux medium examples/maison-lumen/sites/watch/styles.css:389 At 1024px the ledger sits on columns 11–12, about 135px wide. “Water resistance” and “Power reserve” cannot sit on one line with their values. design.md says to move the ledger to a lower row across columns 9–12 rather than compress it. In the min-width: 1024px calibre rules, set .ledger { grid-column: 9 / -1; grid-row: 3; } and keep .calibre-diagram { grid-column: 9 / 11; grid-row: 1 / span 2; } and .calibre-photo { grid-column: 5 / 9; grid-row: 1 / span 2; }.
A11Y-04 a11y low examples/maison-lumen/sites/watch/styles.css:166 main:focus { outline: none; } removes the dual ring from the skip-link target. main.js also focuses the section headings, and :focus-visible does not reliably match that programmatic focus, so the ring can be missing after a keyboard nav activation. scrollIntoView aligns the heading to the viewport edge, which clips a 2px offset ring. Delete the main:focus rule. Add #main:focus, h1:focus, h2:focus, #request-title:focus, #dossier-bevels:focus, #dossier-corners:focus, #dossier-black-polish:focus { outline: 2px solid var(--graphite); outline-offset: 2px; box-shadow: 0 0 0 2px var(--paper); } and .dark h2:focus { outline-color: var(--paper); box-shadow: 0 0 0 2px var(--movement); }. Add h1, h2, h3, #main { scroll-margin-top: 0.5rem; }.
A11Y-05 a11y low examples/maison-lumen/clients/watch/assets/calibre-t01-exploded.svg:5 The alignment-axis path is opacity="0.45". Sage #B9C2B9 at 45% over movement #171C19 blends to about #606761, contrast about 2.96:1, under the 3:1 non-text floor. Full-opacity strokes on that file pass at 9.44:1. design.md says functional strokes stay opaque. Remove the opacity="0.45" attribute from that path so it uses the SVG’s sage currentColor. Leave the other paths unchanged.
A11Y-06 a11y low examples/maison-lumen/sites/watch/styles.css:518 .field legend { display: block; } is in the same rule as the labels. Changing legend’s display has dropped the group label from the accessibility tree in Safari and Firefox, so “Appointment format” can be silent while the radios remain. Remove legend from the display: block rule. Add .field legend { width: 100%; float: left; padding: 0; margin-bottom: var(--space-8); }. Keep the legend as the first child of the fieldset.
COPY-05 copy low examples/maison-lumen/sites/watch/index.html:207 Below 768px the service stages stack with no arrows (styles.css:457). The items read “Contact”, “assessment”, “agreed work”, “returned record”. The lowercase lines look like a broken sentence once they are separate list items. Capitalise each item: Contact, Assessment, Agreed work, Returned record. Leave the desktop → CSS as it is.
COPY-06 copy low examples/maison-lumen/sites/watch/index.html:43 Profile §2 offers a matte silver dial and a charcoal dial. The page shows and names only silver, so the charcoal watch is absent from the only page. After the price sentence from COPY-01, add <p>Also made with a charcoal dial.</p>. Leave the hero caption as the silver portrait it describes.
COPY-07 copy low examples/maison-lumen/sites/watch/index.html:119 “Four inward corners” plus “Each internal corner is finished by hand.” never says what the four belong to. A collector will count them across the whole calibre and doubt the number. Profile §2 uses the same unqualified phrase. Change the body on line 120 to These four internal corners are finished by hand and named in the dossier. Keep the H3. Do not add a per-bridge count.
COPY-08 copy low examples/maison-lumen/sites/watch/index.html:194 “Qualified independent repairers have permission to use them” never says who qualifies or that the permission is in the pack. A buyer cannot tell whether an independent watchmaker may service the watch. After that sentence, add The repair pack states that a working independent watchmaker may use those documents. Do not name a certification body.
UX-03 ux low examples/maison-lumen/sites/watch/index.html:82 The ledger has winding, frequency, power reserve, case size, and water resistance. Profile §2 also specifies 44 mm lug-to-lug, sapphire front and back, and stop-seconds. Those are what the technically literate collector uses to judge the watch, and they are missing on the only page. After the water-resistance row, add <div><dt>Lug-to-lug</dt><dd>44&nbsp;mm</dd></div>, <div><dt>Crystal</dt><dd>Sapphire, front and back</dd></div>, and <div><dt>Setting</dt><dd>Stop-seconds</dd></div>. Land UX-02 in the same change so the longer labels have the wide ledger row.
UX-04 ux low examples/maison-lumen/sites/watch/styles.css:193 On the dark band, .dark .arrow filters the arrow to about #C2C2C2. The link text is sage #B9C2B9. design.md says the arrow uses the link colour. The footer wordmark filter adds sepia(0.2) (styles.css:601), which yellows the mark instead of rendering it as paper #F3F0E8. Replace each <img class="arrow" …> with <span class="arrow" aria-hidden="true"></span>. Style .arrow as width: 1rem; height: 1rem; flex: none; background-color: currentColor; mask: url("../../clients/watch/assets/arrow-right.svg") center / contain no-repeat; and the same -webkit-mask. Delete the .dark .arrow filter rule. Change the footer wordmark filter to brightness(0) invert(0.95).

The page follows the brief’s voice, section order, token contrast, one appointment action, and keyboard form. Nothing in the text pairs fails WCAG AA, and there is no high-severity break. What a buyer or an audit would actually hit is the format-radio error, the mobile nav landmark, Back after in-page links, the missing price, and the 25-year line reading as a warranty. Fix the medium rows first, and leave the dossier and black-polish alt text as written: they match the photographs, including blank inspection fields and screw heads.