This site uses one functional cookie to keep feature rollouts consistent for you. Nothing is set until you choose. See the privacy notice.
Dev notes
Adding WCAG 2.1 AA compliance to the app. Not a weekend checkbox exercise — a systematic audit of every primitive component, backed by automated axe scans at both the unit and E2E layers. The interesting part is where the tooling helps and where it doesn't.
Lighthouse gives you a score. Scores are comforting and mostly useless. A page can score 100 and still be unusable with a keyboard because Lighthouse doesn't test interaction patterns — it checks static HTML snapshots. The real gaps are in focus management, keyboard navigation, and dynamic content announcements. You need tests that render your components and poke at them.
The app already had @axe-core/playwright running in E2E tests against public routes. That catches page-level issues — missing landmarks, broken heading hierarchy, color contrast on the rendered page. But E2E tests are slow and coarse. You can't easily test every variant of a button or every error state of a form.
The new layer is vitest-axe — axe-core running inside unit tests with Testing Library. Render a component, pass the container to axe, assert zero violations. It runs in milliseconds, catches the same WCAG rules, and you can test every prop combination. The two layers complement each other: unit tests catch component-level violations early, E2E tests catch composition issues where individually accessible components break when assembled.
// src/test/a11y.ts
const axe = configureAxe({
runOnly: {
type: "tag",
values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"],
},
});
// in any component test
const { container } = render(<Button>Save</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();The approach is to start at the bottom of the component tree and work up. The audit covered the eight primitive components that existed in src/components/ui/ at the time: Button, IconButton, Input, Textarea, Modal, Tooltip, InfoTip, and Chip. Every feature page is built from these, so fixing them fixes a large surface area. FilterBar and Select arrived afterwards, built on the conventions this set.
Each component gets its own audit pass. The pattern is the same every time: write a failing axe test, fix the violation, then add behavioral tests for keyboard interaction and screen reader announcements. Some highlights:
aria-label. Making it a required prop means TypeScript catches the missing label at compile time, not in a browser audit.htmlFor/ id pair, screen readers announce the input with no context. Error messages need aria-describedby and aria-invalid so they're announced when the field gets focus.onFocus/onBlur and dismissing on Escape (WCAG 1.4.13).aria-modal="true" and wasn't marking background content as inert.Axe is great at structural checks — missing alt text, broken ARIA references, contrast ratios, heading hierarchy. It can't test interaction patterns. It won't tell you that your focus trap has a hole, or that your Tooltip doesn't show on keyboard focus, or that your Chip removal doesn't announce anything to screen readers. Those are behavioral tests you have to write yourself with Testing Library and user-event.
The 3D pages (particle lab, motion lab, the v1 landing hero) are inherently inaccessible as visual experiences. The approach there is descriptive ARIA labels on the canvas container and making sure the page is usable without the 3D content. You can't make a WebGL scene screen-reader-friendly, but you can make sure it doesn't trap focus or block navigation.
Unit-level axe tests run in the existing npm test step — no CI changes needed. The E2E axe scans already ran against public routes. The gap was authenticated routes: calendar, vitals, settings, and the operator dashboard. Those need real Auth0 credentials that CI doesn't have for forks, so they run as a separate local-only test script until the secrets are configured in the repo.
The primitives now come from the design system, and its Storybook already runs Chromatic on every PR for visual regression. It turns out Chromatic has accessibility tests built right in — the same axe-core engine as the unit and E2E scans — and they were sitting there switched off. There was no library to add and no new pipeline to wire up. It was a toggle in the Chromatic project settings.
Because the Chromatic step is already part of CI, flipping that toggle means the a11y results land in the same build as the visual snapshots — no separate job, no extra minutes to reason about. And the coverage is exactly the shape that's awkward to get elsewhere: Chromatic scans every story, so every button variant, every input error state, every open modal gets an axe pass. E2E scans see whole pages; unit tests see one render at a time. This sits in between and sweeps the entire component catalog automatically.
So the defense now has three axe surfaces from one engine: fast unit checks while writing a component, story-level checks across every variant in Chromatic, and page-level checks in E2E where accessible pieces get composed into real screens.
Every page needs a skip-to-content link (visually hidden, visible on focus) and semantic landmarks: <header> for the nav bar, <main> for the content, <nav> for navigation. Screen reader users navigate by landmarks — if your page is all <div>s, they're scrolling through the entire DOM linearly.
The app uses Framer Motion for page transitions, card reveals, and scroll animations. Users with vestibular disorders can set prefers-reduced-motion in their OS. The v2 landing sections already respected this via useReducedMotion(). The audit extends it to everywhere Framer Motion is used: essential animations collapse to simple opacity fades, decorative ones (3D scenes, particle effects) disable entirely.
WCAG SC 1.4.3 requires a 4.5:1 contrast ratio for normal text and 3:1 for large text (18px+ or 14px+ bold). SC 1.4.11 extends the 3:1 minimum to UI components and graphical objects. Axe catches most contrast issues at render time, but dynamic states (hover, focus, active) need manual verification.
The audit found several muted text colors sitting below the 4.5:1 threshold against both light and dark backgrounds. Adjusting these was straightforward, the tricky part was making sure the fixes held across both themes. The design token system helps here since contrast only needs to be verified at the token level, not per-component.
Every new component should follow the same three-layer test pattern that came out of the audit. First, axe scans for every visual variant (default, loading, error, disabled, empty). Each variant can produce different DOM structures that need separate evaluation. Second, label and ARIA assertions using getByLabelText, aria-describedby checks, and role verification. Third, keyboard behavior tests with user-event — tab order, Escape dismissal, Enter/Space activation.
The specifics to verify depend on what the component does:
htmlFor/id or aria-label. Use useId() for stable IDs.aria-disabled over native disabled so elements stay keyboard-focusable.tabIndex values greater than 0.prefers-reduced-motion. Essential animations become opacity fades, decorative ones disable.aria-live="polite", error messages use role="alert".WCAG (Web Content Accessibility Guidelines) sounds like a dense legal document, and honestly, parts of it are. But the core idea is simple: people interact with the web in different ways, and your UI shouldn't assume they're all using a mouse with perfect vision. WCAG organizes everything under four principles, remembered by the acronym POUR:
There are three conformance levels: A (bare minimum), AA (the standard everyone targets and what most legal requirements reference), and AAA (ideal but rarely required in full). This project targets AA across the board.
Each rule is called a "Success Criterion" and has a number like SC 1.4.3 (contrast) or SC 2.1.1 (keyboard). You don't need to memorize them, but knowing the numbering system helps when you see them referenced in axe violations or audit reports. The first digit maps to POUR: 1 = Perceivable, 2 = Operable, 3 = Understandable, 4 = Robust.
The fastest way to build real intuition is to use your own app without a mouse. Tab through the page. Can you reach every button? Can you tell where focus is? Can you dismiss a modal with Escape? Try a screen reader too — VoiceOver is built into macOS (Cmd+F5). You'll immediately feel the difference between a labeled input and an unlabeled one.
After that, the most practical learning path:
<div> and <span> for everything. Use <button>, <nav>, <main>, <label> — the browser gives you keyboard support, screen reader announcements, and focus management for free. ARIA is a patch for when HTML doesn't have the right element, not a replacement for using the right element.You don't need to read the full WCAG spec. Most day-to-day web development touches the same dozen or so criteria repeatedly: labels (1.3.1, 4.1.2), keyboard access (2.1.1), focus visible (2.4.7), contrast (1.4.3), error identification (3.3.1), and name/role/value (4.1.2). Get comfortable with those and you're ahead of most developers.
A quick reference for reviewing PRs that touch UI. Not every item applies to every PR, but scanning the list catches the common gaps.
aria-describedby and announced with role="alert"aria-labelprefers-reduced-motiontabIndex values greater than 0<main>, <nav>, <header>)The project now has three layers catching accessibility issues at different stages. First, eslint-plugin-jsx-a11y runs recommended WCAG rules at lint time, catching structural issues like missing alt text, invalid ARIA attributes, and non-interactive elements with click handlers before the code even runs. Second, vitest-axe runs axe-core inside unit tests, scanning every component variant against WCAG 2.1 AA criteria in milliseconds. Third, @axe-core/playwright runs full-page axe scans in E2E tests against rendered routes.
Each layer catches things the others miss: lint catches patterns (like a div with onClick but no role), unit tests catch rendered DOM issues (like a generated ID that doesn't match), and E2E tests catch composition problems (like two components that individually pass but together create duplicate landmarks).
These three all live in this repo. On top of them, the design system that backs the primitives runs its own story-level axe pass in Chromatic, so the shared components are also getting scanned in their own catalog before they ever land here.
The bottom-up approach (primitives first) left a gap: feature-level components in operator/, calendar/, fantasy/, and learn/ were assembled from accessible primitives but introduced their own violations. Operator pages had charts with no text alternatives (screen readers announced empty containers), skeleton loaders that cluttered the accessibility tree, and stock bars with no semantic meaning. The calendar had a combobox that couldn't be operated by keyboard, toggle buttons with no ARIA state, and time grid slots that were click-only.
The fix was systematic: run axe on every feature component, fix what it finds, then add behavioral tests for keyboard interaction. 29 new tests across two test files cover the feature layer now.
The route-level axe suite reported every public page as clean. Two of them had never been scanned at all.
The specs waited on networkidle before scanning — sensible-looking, and wrong, because it is a promise about whichever third party a page happens to call rather than about the page itself. Two fantasy routes fetch NBA data through the backend. When that upstream stopped answering, those requests hung, the wait never resolved, and the tests timed out. A timeout reads as a slow test. It is easy to miss that it also means the assertion never ran.
Replacing the wait with things the page actually controls — load, the main landmark, and document.fonts.ready — made axe run on them for the first time. It found real serious-impact colour-contrast failures that had been shipping to users: white text at 40% opacity on the particles lab, twice, and a translucent amber notice on the court-vision page. Both are fixed, the amber one by adopting the light/dark warning pair the rest of the app already uses instead of a single translucent colour that cannot satisfy both themes.
Two things worth keeping from that. document.fonts.ready matters more than it sounds: fonts change computed colours, and axe's contrast rule reads computed colours, so scanning before they settle reports violations that do not exist. And a green accessibility suite is a claim about coverage, not proof of it. The failure mode here was never a red build. It was a report saying “clean” about pages it had quietly stopped looking at.
Making the scan actually run turned up three violations, and the most instructive one was my own. A loading placeholder carried aria-label on a bare span. A span has no role, so there is nothing for the label to name, and ARIA prohibits it — serious impact. The repo already had the right pattern one file away: hide the shape with aria-hidden and put the words in an sr-only element, which is what the skeleton component does. I had invented a worse version of a solved problem, and the scan that would have caught it was the one not running.
A second lives in the shared component library rather than here: the marquee duplicates its content and marks the clone aria-hidden, but the clone still contains focusable buttons. Hiding something from assistive technology while leaving it in the tab order is worse than not hiding it, because a keyboard user lands on a control a screen reader insists does not exist.
That one is fixed upstream now, in both framework packages. The clone is marked inert declaratively rather than having its focusables stripped by an effect after render, which is what left the gap: an effect runs after paint, so between mount and that effect the duplicate held tabbable controls, and every re-render reopened the window. inert takes the tab order and the accessibility tree out together, which is exactly the pair that had come apart.
The Angular port of the same component carried the identical bug, comment and all, and was still in review — so that one got fixed before it ever shipped rather than after. Worth noting how it was found: not by auditing the design system, but by a scan on a page that happened to use it. A shared component library means one mistake reaches every consumer, and it also means the first consumer to look properly finds it for everyone.
Once the scan ran, it went intermittently red on whichever route happened to lose a race. The tell was the shape of the failure: every muted and foreground element failing contrast at once. If the foreground token genuinely failed, the app would be unreadable rather than slightly off, so the colours being measured were not the colours anyone sees.
Every colour here comes from a custom property, and which set is live depends on a preference read at runtime. An unpinned scan races that, and sometimes measures muted text against the other theme's surface. Two smaller versions of the same problem sat underneath it: fonts change computed colours, so scanning before document.fonts.ready reports violations that do not exist, and an element mid-transition is measured against a background it is only passing through.
So the scan now pins the theme before anything renders, waits for the tokens to resolve, and lets animations settle — the same mechanism the screenshot workflow already used. All three waits are facts about this page, which is the property that matters. The failure this replaced was the worst kind of red: real-looking, unreproducible, and not a bug. That is how a suite earns the reputation that gets it ignored.
Adding eslint-plugin-jsx-a11y to the ESLint config catches a category of issues that neither axe nor manual testing reliably finds: patterns that are always wrong regardless of runtime state. Things like a div with an onClick handler but no role attribute, an img without alt, an anchor without href, or an interactive element without a keyboard handler. The plugin runs the recommended WCAG rules from the jsx-a11y package.
One gotcha when using it alongside eslint-config-next: Next.js already registers the jsx-a11y plugin internally, so you only add the recommended rules object, not the full plugin config. Trying to register the plugin twice causes a "Cannot redefine plugin" error.
In the browser, the Firefox Accessibility Inspector and the axe DevTools extension complement this by letting you inspect the live accessibility tree, run on-demand axe scans, and simulate how screen readers interpret your page. Between lint, unit tests, and browser dev tools, most violations get caught before they reach a PR.
CalendarGrid has a pattern that doesn't fit neatly into accessibility rules: a day cell that's clickable (to create an event) but also contains clickable EventChip buttons. You can't put role="button" on the outer div because that creates nested interactive elements, which is an axe violation. You can't remove the click handler because clicking empty space in the cell should open the event creator.
The solution is event delegation: the outer div handles onClick but checks if the click target is inside a button or anchor (via closest("button, a")). If it is, the click belongs to the child and the parent ignores it. The div keeps its aria-label for screen readers and handles Enter/Space for keyboard users, but the eslint rule for no-static-element-interactions gets a targeted disable comment since the pattern is intentional. This is the right tradeoff — the alternative (wrapping all empty space in invisible buttons) creates worse keyboard navigation.
Automated tools catch roughly 30-40% of accessibility issues. The rest requires human judgment. Some things to build habits around: Test with sound off to make sure nothing depends solely on audio cues. Test at 200% zoom to verify layouts don't break. Respect user preferences via media queries: prefers-reduced-motion for animations, prefers-color-scheme for themes, prefers-contrast for high-contrast modes.
Make click targets large enough for users with motor impairments and avoid time-limited interactions like auto-dismissing popups. Use skip links so keyboard users can bypass repetitive navigation. Provide captions for video and audio content. Consider offering dyslexia-friendly font options for text-heavy pages. The goal isn't perfection, it's building habits that make accessibility a natural part of development rather than an afterthought audit.
Accessibility tests run at three different points in the development cycle, and each one activates automatically — no extra commands or config needed.
Lint time (eslint-plugin-jsx-a11y) — fires on every save if your editor has ESLint integration, and runs in CI as part of the standard lint step. Catches structural issues before the code even executes: missing alt text, click handlers without keyboard support, invalid ARIA attributes. Zero developer effort to activate — if your ESLint config inherits from the project's eslint.config.mjs, you get the rules.
Unit tests (vitest-axe) — run as part of the normal npm test command alongside all other Vitest tests. The axe matchers are globally registered in src/test/setup.ts, so any test file can import axe from @/test/a11y and call expect(results).toHaveNoViolations(). These run in CI on every push.
E2E tests (@axe-core/playwright) — run via npm run test:e2e for public routes and npm run test:e2e:auth for authenticated routes. Public route scans run in CI. Authenticated scans require real Auth0 credentials and run locally.
The process is the same every time. Write your component, then add an accessibility test alongside your other tests:
axe from @/test/a11y, render the component, pass the container, assert zero violations.getByRole, getByLabelText, and toHaveAttribute to verify roles, labels, aria-expanded, aria-selected, and other ARIA state. These document the component's accessible API.userEvent.keyboard to simulate keypresses and assert the right callbacks fire.<h1>. If it's visually redundant with PageHeader, use sr-only to hide it visually while keeping it in the accessibility tree.text-muted/30). The base token already meets contrast requirements; reducing its opacity drops it below the 4.5:1 threshold.// typical a11y test for a new component
import { axe } from "@/test/a11y";
it("has no axe violations", async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("is keyboard accessible", async () => {
render(<MyComponent onAction={onAction} />);
const trigger = screen.getByRole("button", { name: "Do thing" });
trigger.focus();
await userEvent.keyboard("{Enter}");
expect(onAction).toHaveBeenCalled();
});That's it. The lint rules catch the obvious structural issues as you type, the axe scans catch WCAG violations when tests run, and the E2E layer catches composition issues on the full page. If all three pass, the component is in good shape. The tests run automatically in CI — no manual steps needed beyond writing them.
The aria-live attribute controls how screen readers announce dynamic content updates. aria-live="off" is the default, no announcements. aria-live="polite" waits until the screen reader is idle to announce updates, which is the right default for status messages, search result counts, and saved confirmations. aria-live="assertive" interrupts immediately and should be reserved for critical alerts and errors.
The project uses polite regions for toast notifications (RefreshBar), search result counts (CardSearch), and character counts (Textarea). The key insight is that the live region container must exist in the DOM before the content changes — if you conditionally render the container and the content at the same time, screen readers miss the announcement because they weren't watching the region when it appeared.
The recommended ruleset from eslint-plugin-jsx-a11y is a good starting point, but two rules needed project-level tuning after the first CI run caught 14 issues.
no-noninteractive-tabindex — flags tabIndex={0} on non-interactive elements, which is usually correct. But scrollable containers are the exception: keyboard users need to tab into them to scroll with arrow keys. The fix is to give the container role="region" with an aria-label, then configure the rule to allow tabIndex on region and tabpanel roles. This way the rule still catches genuinely wrong uses while allowing the legitimate accessibility pattern.
// eslint.config.mjs
"jsx-a11y/no-noninteractive-tabindex": [
"error",
{ tags: [], roles: ["tabpanel", "region"] },
],
// in the component
<div
className="overflow-x-auto"
role="region"
aria-label="Stats leaderboard"
tabIndex={0}
>no-unused-vars — the default @typescript-eslint/no-unused-vars config from eslint-config-next doesn't recognize the standard _-prefix convention for intentionally unused variables, and it flags the throwaway variable in rest destructuring patterns like const { name: _, ...rest } = obj. Adding argsIgnorePattern, varsIgnorePattern, and ignoreRestSiblings fixes seven warnings in one config change. Not an accessibility rule, but it came out of the same lint audit.
Update — August 15, 2026
Swapping the whole site onto the Verdigris & Ember palette was the best stress test this audit has had, because a recolour attacks contrast everywhere at once. Three bugs got through the diff, and each one was caught by a different layer — which is the whole argument for having more than one.
Chromatic caught the one I shipped upstream. In the design-system repo, the Textarea's character count sat in neutral-500 — fine for years at 4.7:1 on the old cool gray, quietly nudged to about 4.2:1 when the ramp warmed. I found it as a flagged story on the Chromatic build during review: the a11y check runs against every story, so a token change that regresses a component it never mentions still gets caught. The InfoTip glyph had the identical bug, found by then grepping for the same pattern. Both moved a step darker on light with a dark-theme override, and the other neutral-500 consumers stayed put deliberately: disabled controls are exempt, and borders and icons carry the 3:1 rule, which they clear.
The Playwright sweep caught the theme mismatch. Retuning the NBA matchup pages put band accents — values tuned for light surfaces — into dark-theme table text, and the both-theme axe run flagged 36 nodes on one page. The fix was not a better hex, it was the right kind of value: text now uses the theme-aware feature accent variables that flip with the theme, so there is no single colour trying to survive both backgrounds.
And one bug no gate could see. The landing's contact card put a cursor-following glow under muted copy at full token strength, unreadable at exactly the moment someone hovered. jsdom's axe computes no contrast at all, and the E2E sweep scans the resting state, where the glow is transparent — pointer-driven decoration under text is structurally invisible to both. The fix went into the primitive: the card now caps any accent it is handed to 22% alpha before the glow ever renders, so no future caller can reintroduce the bug. When the tooling cannot watch a state, make the state impossible instead.
Update — August 15, 2026
The playoff bracket had been failing the local accessibility sweep on colour contrast, and the obvious reading was that the warm neutrals had pushed muted text under AA. That reading was wrong, and acting on it would have meant retoning a token to fix nothing. Muted measures 5.02 to 7.00 against every surface in both themes, and the specific elements axe flagged measure 7.0:1 and 14.5:1 once the page has settled. Running axe by hand against the same page, with the same waits the spec uses, produced no violations at all.
There was a real defect, just not that one. A matchup with no teams yet was dimmed with forty percent opacity, which is a reasonable way to recede a border and a bad way to recede text. Measured on the running page, twenty-one elements sat at an effective opacity of 0.4, which puts the seeds and the score select well under AA for anyone reading them. The card keeps full opacity now and recedes with a lighter surface instead. The buttons and the select were already disabled, which is what tells a screen reader the state and what exempts a control from the contrast rule to begin with; the blanket opacity was doing neither.
The rest was the scan racing the stylesheet. It only appeared under the full thirty-test parallel run against a dev server; with one worker, one theme passed and the other failed, and in isolation it passed outright. The tokens are aliases of the shared package now, and an alias whose source stylesheet has not arrived resolves to nothing rather than to an error — so a scan can catch the page with its text in the browser default over a surface that is already correct. The spec waits for background and muted alongside foreground now, and for the main landmark to be painted in a real colour.
Worth recording that this never reached CI, because CI scans a production build rather than a dev server compiling routes on demand. A check that is red locally and green in CI is its own kind of problem: it trains you to ignore the local one.
Update — August 16, 2026
A consumer upgrade reported that the shared secondary button rendered an ember label on an ember tint at 3.2:1, under AA. I set out to fix it and asked for the measurement first, which is the only reason the next part came out right: that button does not use the ember ramp at all. It tracks verdigris and measures 6.66:1 in light and 11.68:1 in dark, and in the old blue palette it measured 6.16:1, so it never failed and nothing regressed. Sweeping every foreground and background pair in both ramps across both palettes turned up nothing at 3.2:1 to fix.
Two of my own assumptions went with it: I had reasoned about the tint as a color-mix over a surface, and the package has no alpha or mixing anywhere — every fill is a flat opaque ramp step. Retoning the identity ramp, which was the obvious move, would have changed the brand in every consuming app to fix a component that was already passing.
The measuring did find a real breach, one component over. The error starburst badge fills with the ramp’s 400 and labels with its 900, which clears comfortably for primary, success and warning — but the error ramp’s 400 is darker than its siblings, landing that label at 3.62:1 at ten pixels bold, under the large-text exemption. The label moved to the ramp’s darkest step for 5.84:1, rather than lightening the fill, because the saturated red is what makes the seal read as an error in the first place.
The root cause is the part worth keeping. A contrast helper existed in that package and had only ever been pointed at the chart palette, so not one component pair had ever been measured — which is exactly how a 3.62:1 label shipped green through a system that advertises contrast checking. There are eighteen component pairs under test now, read out of the real stylesheets and folded through the cascade to the actual label and fill. A guard is only worth what it is aimed at, and this one had been aimed at the half of the system nobody was shipping.
The original 3.2:1 report is not disproven, only relocated: it was measured inside the consuming app, which carries its own bridge over these ramps. Two measurements taken in different contexts disagreeing is a thing to re-measure, not to declare settled.
Update — August 16, 2026
The design system had the gel button on the books as a 3.45:1 AA failure, recorded in 0.2.36 and deferred as a decorative variant that was close enough to fix later. It reproduces exactly, which is most of why it survived a year of me looking at it. It is also the wrong measurement: the real floor was 1.69:1 at rest and 1.52:1 on hover.
The recorded figure reads white against the bare primary-500 gradient stop, and that stop sits underneath a 55% white gloss. So it describes a surface nobody renders. Understating a contrast defect by more than half is bad in a specific direction — it turns a serious failure into a tolerable one on the page where I decide what to fix next, which is exactly how it stayed deferred.
The correction also produced the fix, which the original number could not have. Darkening the ramp was the obvious move and was never going to work: a 55% white gloss caps whatever is under it at 3.35:1 even over pure black, so no ramp step reaches 4.5 while that gloss stands. Both had to move. The button ships primary-900 under a 14% gloss now, measuring 5.12:1 at rest and 4.73:1 on hover at the worst point along the fill.
The audit’s own sampler is the thing that failed here, and it failed the way the rest of that week’s findings did. It read the discrete ramp steps a background names, so an interpolated gradient midpoint and a translucent layer were both invisible to it, and both of those blind spots err toward passing. It composites the fill properly now, and it asserts against itself: the composited reading must stay below the bare-stop one, because if compositing ever silently drops out the ratios rise while the button gets worse. The fuller account of that week is in green checks.