The app screens get the 2026 design direction, with a dark mode that is a first citizen - #381
Merged
Conversation
Apolloccrypt
force-pushed
the
feat/droom-app
branch
2 times, most recently
from
September 3, 2026 01:48
95f2be1 to
4d10c5c
Compare
…is a first citizen The tekst is pinned and finished. This is the experience: dashboard, sign, parashare, signup, account and the four auth screens on the token system from docs/brand/design-direction.md, in light and in dark, with the loading, empty, error and confirmation states that were never drawn. frontend/app-2026.css is the app layer. It does two things. It defines the 2026 palette from section 2 of the direction, written three times so an explicit theme choice and the system preference both win and no colour exists only inside a media query. And it points the v4 token names at those values, so every rule already written against --bone or --ink-hair follows the theme without being rewritten. That alias layer is what made a dark mode on ten screens affordable at all. Marketing pages do not load the file and do not change. What is new on the screens, beyond colour: The dashboard gets a loading state with the shape of the dashboard instead of a spinner on an empty page: the hero, the three ways in and the first register rows are all reserved. Lighthouse measured CLS 0.236 before and 0.005 after, and the performance score went from 83 to 95. The register rows get a meter with the number next to it, never a bar alone, and a status chip in the four words dashboard.js actually renders. The filters become a segmented control with the count inside the button, because the count is the reason you click it. Under 760px the three columns stack in reading order. parashare gets a real 44px copy button next to the link instead of a click-anywhere box, and the confirmation lands on the button rather than overwriting the link you just asked to see. Upload progress carries its percentage in text and on the track, so a screen reader hears the number the eye reads. The burn-on-read warning moves to the wait colour: it is not a fault. signup loses the marketing rhythm that left holes between its five steps and becomes one column. When you are already signed in the form was dimmed to 0.4 opacity, which looked disabled but was not: the fields stayed in the tab order and every label underneath dropped to 1.78:1. It is now inert, and readable. The auth screens put the error above the form with role="alert", and passkey and code sit next to each other as two ways in instead of one with an "or" underneath it. On sign.html the document layer is pinned to fixed colours on purpose. A PDF page is white paper in every theme, and the seal is a printed artifact: under a dark theme it would have previewed a stamp that no printer will ever produce. tests/app-contrast.test.mjs is the new gate. It walks all ten app screens in Chromium at 390 and 1440, in light and dark, reads the computed colour of every visible text node, flattens it against the first opaque background behind it, and fails under AA. Source inspection cannot do this: the whole point of the alias layer is that an old rule now resolves differently per theme, and only the browser knows the answer. It also checks that a coarse pointer gets a 44px target, with the deliberate exception of a text link inside a running paragraph. 792 distinct colour pairs, none under AA. Measured, not claimed. Lighthouse against a local server, accessibility first: dashboard 100, sign 100, parashare 100, signup 100, account 100, login 100. On main those were 96 for the dashboard and 84 for parashare. Best practices 100 on all six. Performance 95, 86, 75, 97, 94, 98. The reduced-motion screenshot of the dashboard is byte-identical to the normal one, in both themes: everything stops and no state disappears. Repo gates green: static-sanity eleven checks, first-screen, pricing-fold, navigation-shell, user-dashboard-documents (25 checks), ui-truthfulness, site-claims, seo-contract, links, frontend-loading-contract, csp-inline, cache-bust.
The review on the first round was right on all four counts. 1. The page loaded the dark critical CSS, the two theme-color metas and app-2026.css, but its own <style> block still carried fifteen light hexes. In dark that gave h1 #0B3A6A on #0A0F16 at 1.67:1, .vlead #475569 at 2.53:1 and the FAQ h2 #64748B at 4.03:1. Every colour in that block is now a token: --ink-1 for headings, --ink-2 for running text, --ink-3 for meta. 2. .next-box stayed a light-blue panel in a dark page, so the link to /help/authenticator-apps sat at 2.19:1. It is --accent-soft on --line now, which is the same panel in both themes. 3. The gate had a hole exactly where the page broke: tests/app-contrast.test.mjs walked ten screens and the branch touched eleven. /signup/verified is in SCREENS now, and it is walked signed OUT, because you land there straight from the mail link and never signed in. That is what caught the last one: .nav-signin falls back to a hard #64748b in design-system.css, 4.03:1 on the dark paper, on every app page in the signed-out state. app-2026.css points it at --ink-2. 876 colour pairs over 11 screens x 2 widths x 2 themes, none under AA. axe-core 4.13.0 on /signup/verified and /parashare, both widths, both themes, both motion settings: 0 violations, down from 6 on this page. 4. The eight beacons in the parashare card heads were section markers, not measurements. design-system.css v4 puts status-pulse and a navy ring on every .dot; both are reset, and the one dot that really tracks something, #waiting-dot, keeps the pulse. Infinite animations on parashare go from 14 to 7, and the 7th in main is that one waiting beacon. The other six are the globe overlay outside main (hcPulse, globeSpin, blink), v4 chrome on a surface this branch does not touch. prefers-reduced-motion still zeroes all of them. One thing beyond the findings, on the same page: the FAQ rows drew Chromium's list-item triangle next to the page's own plus. The marker is off and the row is a flex line, so the summary is one 44px target with one indicator.
…switch on /account Measured on this branch with prefers-color-scheme: dark and nothing chosen: /index and /pricing stayed light, while /dashboard, /sign, /parashare, /account and /auth/login came up on rgb(10,15,22). Signing in threw a reader on a dark operating system from creme to black in one click, and there was no way to say no. The dark mode itself is good. How you arrived in it was not. So the system preference is honoured under exactly one condition now. The media block in app-2026.css and the critical <style> of all twelve app pages are scoped to [data-theme="auto"], and the explicit [data-theme="dark"] block stays as it was. Without a choice there is no attribute, nothing matches, and the app is light. That holds with JavaScript switched off too: the light default does not depend on any script running. frontend/js/theme.js applies a choice that was made. It is a plain blocking <script> at the end of the head of every app page, so it runs before the body exists and before the first paint and a reader who picked dark sees no flash. It cannot be inline under script-src 'self'. It also moves the theme-color meta, which was on its own media query and would have tinted the address bar dark above a light page. The switch is on /account: Follow my system, Light, Dark, stored in localStorage under paramant.theme.v1, no save button, and the page you are standing on repaints as its own preview. The radio is 0x0 and the label carries the 44px target. The text says what the choice covers, that it never leaves the browser, and that the public pages stay light. Gates: - tests/app-theme.test.mjs, new, 8 checks in Chromium over the six screens a reader meets after signing up. No choice on a dark system is light; "light" holds against a dark system; "dark" darkens on a light system; "auto" hands the decision back; the marketing pages stay light in every case; the switch works and carries to the next screen; every page loading app-2026.css loads theme.js. The last test serves the pages back with the [data-theme="auto"] gate removed and fails unless they go dark again, so the suite cannot pass against a site with no dark mode at all. - tests/app-contrast.test.mjs and tests/app-shots.mjs now store the choice instead of only setting colorScheme. Without that the dark half of both would have quietly measured and photographed the light theme twice. - /privacy gains the key in its local-storage list, which is where a reader can act on it, and site-claims row 28 already fails on a key that list does not name. docs/site-claims.md records what the key is for. There is no second inventory: tests/ui-truthfulness.test.mjs pins the name, the three values, that theme.js contains no fetch, XMLHttpRequest or sendBeacon, and the three sentences beside the switch. Measured: this suite against the parent commit fails exactly as reported, with all six screens on rgb(10,15,22) under a dark system and nothing chosen, and it is green here. app-contrast 876 distinct colour pairs over eleven screens, two widths, both themes, none under AA, up from 872 by the four the switch adds. The 36 screenshots are taken again under a stored choice, so the dark half is the dark theme and not sixteen more pictures of the light one, and the reduced-motion pair is still byte-identical to its normal pair in both themes.
Apolloccrypt
force-pushed
the
feat/droom-app
branch
from
September 3, 2026 01:52
4d10c5c to
8f3419d
Compare
Apolloccrypt
pushed a commit
that referenced
this pull request
Sep 3, 2026
Een contrastmeting over 4448 tekstparen op zestien pagina's, op 390 en 1440, wees een token aan. --ink-dim stond op rgba(11,58,106,.65) en landt daarmee op 4.09:1 tegen het papier, 3.96:1 tegen de voet en 4.18:1 op een witte kaart. Drie keer net onder de 4.5 die AA vraagt, en drie keer onder de tekst die de prijs, de limiet en de kleine letter draagt. Niet te zien met het blote oog, wel het verschil tussen voldoen en niet voldoen. --ink-dim, --lime-dim en --black-dim wijzen nu naar --ink-2 (#40505F, 7.95:1 op het papier). Dat is de enige wijziging aan het ontwerpsysteem; elke andere waarde staat zoals hij stond, en design-system.css groeit van 85374 naar 86270 bytes, waarvan het grootste deel commentaar met de gemeten getallen. Gemeten, licht, uniek onder AA, tegen main op 39179f5: hele site 82 -> 5 / 4 -> 1 (de rest zit in het style-blok van #382) /pricing 8 -> 0 /parasign 7 -> 0 /security 8 -> 1 (het lime vinkje, 1.16:1, bullet en geen tekst) /dashboard 0 -> 0 (#381 heeft die pagina al schoongemaakt) De vijf die overblijven staan met naam en reden in KNOWN_LIGHT in tests/theme-contrast.test.mjs. Geen van de vijf komt uit het ontwerpsysteem: alle vijf staan als letterlijke kleur in het style-blok van een pagina en alle vijf staan al op main. De test faalt op elke nieuwe. De statusregel van /parashare stond er ook op en is door #381 opgelost, dus hij is van de lijst af: korter mag, langer niet. Geen donkere modus. Een eerdere stand van deze tak droeg een volledige donkere tokenset achter [data-theme], zonder schakelaar in de nav, onleesbaar op /security, /pricing en het dashboard, en 31 KB zwaarder op alle 58 pagina's. Die laag is er in zijn geheel uit; wat blijft is de gemeten lichte winst. tests/theme-contrast.test.mjs meet elk tekstpaar op elke pagina op 390 en 1440, vlakt de tekstkleur af tegen de eerste ondoorzichtige achtergrond erboven en rekent WCAG 2.1. 4448 paren, vijf bekende gevallen, nul nieuwe. tests/motion-safety.test.mjs vergelijkt elke pagina met en zonder prefers-reduced-motion en faalt zodra een element verdwijnt, vervaagt of inklapt in plaats van alleen stil te staan. De review meldde dat /parashare een element verloor, 272 tegen 271. Het meetpunt deugde niet. Hij laadde elke pagina twee keer, een keer met en een keer zonder reduce, en legde de twee lijsten naast elkaar. Dat vergelijkt twee paginaladingen en niet twee instellingen. /parashare start 400ms na het laden een WebGL-globe waarvan het aantal knopen op een vast meetmoment in de ene lading wel en in de andere nog niet staat: acht keer op rij 272 tegen 267, en op de CI-runner kwam dat binnen twintig seconden nooit tot stilstand. Elke pagina wordt nu een keer geladen. Na de binnenkomst: opname A, dan page.emulateMedia reduce en opname B, dan terug en opname C. Dezelfde DOM, dezelfde scripts, een variabele. C is de controle: wat de pagina zelf tussen twee metingen weghaalt is in C ook weg en telt niet als schade van reduce. Vergeleken wordt op wat een layoutbox heeft, op een DOM-pad in plaats van op een volgnummer in een platte lijst. Dat is een scherpere claim en geen zwakkere. Gesaboteerd om te zien dat hij bijt: display:none, opacity 0 en visibility:hidden onder reduce geven alle drie rood, met pad en maat. tests/apply-nav-idempotent.test.mjs is nieuw. frontend/apply-nav.py stempelt de nav, de voet en de vier cache-bust-verwijzingen in 58 pagina's, en zijn DS_LINK liep achter op wat die pagina's dragen. Een onschuldige draai zou 58 pagina's hebben teruggezet naar een stylesheet-url die niet meer bestaat. De test draait de generator op een kopie van frontend/ en eist een lege diff. Alles in functiescope, zodat tests/*.mjs er geen naam bij krijgt. .gitignore vangt node_modules nu ook zonder slash. Een regel met slash matcht alleen een map, en een symlink is voor git een bestand: zo belandde er in een eerdere stand van deze tak een 120000-blob in de commit. Pixeldiff van index.html tegen main, dezelfde chromium, animaties en transities uit: op 390x844 en 1440x900 nul verschillende pixels. Over de hele pagina 3705 pixels op 46 regels, en die 46 regels zijn de twee voetregels die op --ink-dim staan. Dat is precies de wijziging en niets anders.
Apolloccrypt
added a commit
that referenced
this pull request
Sep 3, 2026
…very page (#380) One token: --ink-dim was rgba(11,58,106,.65) and is now var(--ink-2), #40505F, with the two legacy dim aliases pointing at the same. Measured on index, pricing, parasign, security and dashboard at 390 and 1440: from 82 unique text pairs under AA to 5 known cases that may only shrink, pricing and parasign at zero, the homepage pixel-identical outside its animated hero. The dark layer of the original design-system v5 is out: it sat behind a gate no switch reached and was unreadable on /security and /pricing; the app screens carry their own measured dark mode as a choice since #381. Two new gates: apply-nav.py must reproduce frontend/ byte for byte (the cache-bust versions in the generator and the pages had drifted, which would have pinned the site to a year-old stylesheet), and theme-contrast keeps the known list from growing. motion-safety measures one page load in three captures so the WebGL globe on /parashare no longer makes it flaky. The node_modules symlink is out of the history and .gitignore now catches a symlink as well as a directory.
Apolloccrypt
pushed a commit
that referenced
this pull request
Sep 3, 2026
…jurybesluit Het oude document beschreef een richting die op drie punten is teruggedraaid en op een vierde nooit is uitgevoerd. Het wees naar een tokenbestand dat door niets geladen wordt en naar elf screenshots van prototypepagina's die niet bestaan. Elke regel is nu getraceerd naar de PR die hem heeft besloten. De richting leeft in vier lagen en niet in een. design-system.css v4.1 op alle 58 paginas, licht only; nav.css; app-2026.css op elf app-schermen, met donker als keuze op /account onder paramant.theme.v1; en een eigen --hp-palet in de kritieke CSS van de homepage. Het document zei dat er een globale tokenlaag zou komen. Die is er niet en het bestand dat hem zou dragen is hier weg. Drie dingen zijn verworpen en staan nu met hun reden in het document. De STIL-balk in mono hoofdletters (#392: de stem van een terminal boven een pagina in papier, inkt en zinnen, en Help stond daardoor op 9px). Lime als primaire knop (#392: lime is de leestekenkleur van dit merk en las naast de kobalt-heroknop als een tweede aanbod). En donker op marketing (#380: onbereikbaar achter [data-theme] zonder schakelaar, onleesbaar wit op lime op 1.07:1, en 31 KB op elke pagina zonder gzip). frontend/design-tokens-2026.css is weg. Geen pagina, build, test of nginx-conf laadde hem, en hij was in tegenspraak met wat wel draait: hij schreef donker als :root:not([data-theme="light"]), precies de sprong van creme naar zwart die #381 heeft weggehaald en die docs/site-claims.md rij 28 en tests/app-theme.test.mjs bewaken. Zijn aliaslaag verschoof --space-8/9/10 en liet --space-10 en --space-11 ongedefinieerd, en --navy: var(--ink) zou elke navy op 58 paginas in een keer verzetten. De levende bron is app-2026.css. De elf prototypebeelden zijn vervangen door twee van de echte homepage, met scripts/brand-shots-direction.mjs. Dat script volgt de regel uit #393: een tempmap standaard, docs/brand/assets alleen met PARAMANT_WRITE_BRAND_SHOTS=1, en het hergebruikt de vlag en de docs-check uit scripts/brand-shots-dir.mjs in plaats van er een tweede kopie van te houden. Twee merkbesluiten staan open in sectie 8. Een gevuld accent per scherm, want /pricing draagt sinds de balkwissel zeven btn-primary plus de knop in de balk, en op het eerste scherm staan er twee naast elkaar. En de koele naad: de balk op rgb(248,250,252) tegen homepagepapier #FAF8F3, nu verborgen achter een gradient over de eerste 160px, met de balkkleur gepind door tests/navigation-shell.test.mjs. Poorten groen: tests/static-sanity.sh (twaalf checks), scripts/check-test-declarations.sh (134 suites), node --test tests/links.test.mjs, npx eslint@9 . (exit 0). Extra: tests/brand-shots-optin.test.mjs 10/10, scripts/check-cache-bust.sh 363 links, tests/frontend-loading-contract.test.mjs 7/7.
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.
Group app of the "Paramant moet een droom zijn" build. The claims are pinned and finished, so this is the experience: the ten screens of the product itself, on the token system from
docs/brand/design-direction.md(PR #373), in light and in dark, plus the states nobody drew yet.Not stacked.
feat/droom-systeemwas not on origin when this started, so everything needed is in this branch and nothing outside group app is touched.Repair round, after the review
Four findings, all four addressed on this branch.
1 and 2. /signup/verified was half dark. The page took the dark critical CSS, the two
theme-colormetas andapp-2026.css, but its own<style>block still carried fifteen light hexes. In dark that gaveh1#0B3A6Aon#0A0F16at 1.67:1,.vlead#475569at 2.53:1, the FAQh2#64748Bat 4.03:1, and a.next-boxthat stayed a light-blue panel with the help link on it at 2.19:1. Every colour in that block is a token now:--ink-1for headings,--ink-2for running text,--ink-3for meta,--accent-softon--linefor the panel,--surfacefor the subject box and the FAQ cards.3. The gate had a hole exactly where the page broke.
tests/app-contrast.test.mjswalked ten screens and this branch touches eleven./signup/verifiedis inSCREENSnow, and it is walked signed out, because you land there straight from the mail link and have never signed in. That is what caught a defect nobody had reported:.nav-signinfalls back to a hard#64748bindesign-system.cssv4, 4.03:1 on the dark paper, on every app page in the signed-out state.app-2026.csspoints it at--ink-2.4. The parashare beacons. Eight
.dotelements in the card heads were section markers, not measurements, and section 3 of the direction says the beacon only beats where something is really measured.design-system.cssv4 putsstatus-pulseand a navy ring on every.dot; both are reset in the page, and the one dot that really tracks something,#waiting-dot, keeps the pulse. Infinite animations on parashare go from 14 to 7, and the only one insidemainis that waiting beacon. The remaining six are named and left:hcPulseon the four frame corners,globeSpinon the loading ring andblinkon the session dot, all inside the globe overlay outsidemain, v4 chrome on a surface this branch does not redesign.prefers-reduced-motionstill zeroes all seven.One thing beyond the findings, on the same page. The FAQ rows drew Chromium's list-item triangle next to the page's own plus sign. The marker is off and the row is a flex line, so each summary is one 44px target with one indicator.
Measured after the repair.
tests/app-contrast.test.mjs: 876 colour pairs over 11 screens x 2 widths x 2 themes, none under AA, and the 44px tap check green on all eleven./signup/verifiedand/parashare, 390x844 and 1440x900, light and dark,no-preferenceandreduce: 0 violations in all 16 runs, down from 6 on this page.app-contraststill green at 880 pairs, and axe still 0 violations in all 16 runs. On/signup/verifiedin dark the body computes torgb(10,15,22), the h1 torgb(237,241,246)and the panel torgba(127,166,255,.12), so the clean run is a dark screen and not a light one measured twice./signup/verified, mobile profile, same local server: accessibility 89 to 100, performance 99 to 98, best practices 100, CLS 0.003 to 0.007. The single extra stylesheet is the whole difference on the two that moved.docs/brand/assets/app-2026/:verified-390x844-{light,dark}.pngandverified-1440x900-{light,dark}.png, shot signed out. The four parashare shots are regenerated.Rebased on
mainat 05bbd1b.Second repair round: dark is a choice, never a jump
One finding, and it was the right one to stop on.
What was measured. With
prefers-color-scheme: darkand nodata-themeanywhere,/indexand/pricingstayed light, while/dashboard,/sign,/parashare,/accountand/auth/logincame up onrgb(10,15,22). An advocate on a dark operating system went from creme to black by signing in, in one click, with no way to say no. The dark mode itself is good: dashboard dark had one AA fault at the time, light had none, and both are at zero now. What was wrong was how you arrived in it.Light is the default, everywhere. The dark token block in
app-2026.cssand the critical<style>of all twelve app pages are scoped to[data-theme="auto"], and the explicit:root[data-theme="dark"]block is unchanged. With no choice made there is no attribute, nothing below matches, and the app is light. That holds with JavaScript switched off as well: the light default does not depend on a script running. The dark palette itself did not move a single value.A three-way switch on /account. Follow my system, Light, Dark. Picking one is the change, no save button, and the page you are standing on repaints as its own preview. The radio input is 0x0 and the label carries the 44px target, so a 13px radio is never the thing you have to hit. The text next to it says what the choice covers, that it is kept in this browser only, and that the public pages stay light.
frontend/js/theme.js. A plain blocking<script>at the end of the<head>of every app page. It runs while the head is parsed, before the body exists and before the first paint, so a reader who chose dark sees no light flash. It cannot be inline: the site runs underscript-src 'self'. It writes nothing when there is no choice, so the light default and the no-JavaScript case are the same path. It also moves thetheme-colormeta, which was on its own media query and would have tinted the address bar dark above a light page; the two media-scoped metas are one plain meta now.Storage.
localStorage['paramant.theme.v1'], one ofauto,light,dark. Anything else, and a browser that refuses storage, reads as no choice. The key goes on /privacy, in the local-storage list, which is where a reader can act on it, and row 28 oftests/site-claims.test.mjs(from #372) already fails on a key that list does not name.docs/site-claims.mdrecords what the key is for. There is deliberately no second inventory:tests/ui-truthfulness.test.mjspins the name, the three values, thattheme.jscontains nofetch,XMLHttpRequestorsendBeacon, and the three sentences beside the switch.tests/app-theme.test.mjs, new. Eight checks in Chromium over the six screens a reader meets after signing up (/dashboard,/sign,/parashare,/account,/auth/login,/signup/verified):rgb(251, 250, 247), nodata-theme,theme-color#FBFAF7lightdarkrgb(10, 15, 22),theme-color#0A0F16auto/and/pricing, dark system, every choice/account/dashboardapp-2026.css/js/theme.jsThe sabotage. The last test serves the pages back with
[data-theme="auto"]rewritten to the:not([data-theme="light"])it replaced, in the stylesheet and in every page's critical<style>, and fails unless all six screens go dark again. Without it a suite like this one passes just as happily against a site that has no dark mode at all. It is green, so the seven tests above are measuring the gate and not the absence of one.Two suites were lying by omission until this landed.
tests/app-contrast.test.mjsandtests/app-shots.mjsreached dark by settingcolorSchemealone. From this commit on that no longer darkens anything, so both store the choice. Without that change the dark half of the contrast run would have measured the light screens twice and reported clean, and sixteen of the screenshots would have been more pictures of the light theme.Measured, before and after. This suite run against the parent commit fails exactly as reported: all six screens on
rgb(10, 15, 22)with a dark system and nothing chosen. It is 8/8 green here, sabotage included.app-contrast: 876 distinct colour pairs over 11 screens x 2 widths x 2 themes, none under AA, up from 872 on the parent by the four the switch adds. The 36 screenshots are taken again with the choice stored, so the dark half is the dark theme;dashboard-1440x900-light-reduced-motion.pngis still byte-identical todashboard-1440x900-light.png, and the same in dark.Rebased on
origin/mainat #385. The nine-line cache-bust collision with #380 is untouched and still nine files: this branch leavesdesign-system.css?v=23andnav.css?v=19exactly where they are, so whichever merges second takes #380'sv=24andv=20and nothing else has to be decided.What changed
frontend/app-2026.cssis the app layer. It defines the 2026 palette from section 2 of the direction, written three times so an explicit[data-theme]choice and the system preference both win and no colour exists only inside a media query. Then it points the v4 token names at those values, so every rule already written against--boneor--ink-hairfollows the theme without being rewritten. That alias layer is what made a dark mode on ten screens affordable at all. It is the last stylesheet in the head, so the hard-coded hexes the pages still carry can be corrected from one place. Marketing pages do not load it and do not change.Dashboard. The loading state now has the shape of the dashboard instead of a spinner on an empty page: hero, the three ways in, and the first register rows are all reserved. Register rows get a meter with the number next to it, never a bar alone, and a status chip in the four words
dashboard.jsactually renders (waiting,in_progress,completed,cancelled). Filters became a segmented control with the count inside the button, because the count is the reason you click it. Under 760px the three columns stack in reading order. The document dialog is excluded from the arrival animation: it isposition: fixed, and an animating ancestor was quietly dragging the whole overlay 6px down the screen.parashare. A real 44px copy button next to the link instead of a click-anywhere box, and the confirmation lands on the button rather than overwriting the link you just asked to see. Upload progress carries its percentage in text and on the track, so a screen reader hears the number the eye reads. The burn-on-read warning moved to
--sig-wait: it is not a fault. Three fields got real<label for>elements.signup. One column, no marketing rhythm leaving holes between the five steps. When you are already signed in, the form used to be dimmed to
opacity: 0.4, which looked disabled but was not: the fields stayed in the tab order and every label underneath dropped to 1.78:1. It is nowinert, and readable.account. One filled accent on the screen, not two. Dangerous work is a hairline that reddens. Two inputs that lived outside
.form-cardhad never been given a size and measured 22px on a phone.auth. The error moved above the form with
role="alert". Passkey and code sit next to each other as two ways in, instead of one with an "or" underneath making the other second-rate.sign. The document layer is pinned to fixed colours on purpose, and it is the one place in the app where a token is deliberately not used. A PDF page is white paper in every theme, and the seal is a printed artifact: under a dark theme
--cobaltresolves to a light blue, and a white-on-light-blue seal would have previewed a stamp no printer will ever produce.The new gate
tests/app-contrast.test.mjswalks all eleven app screens in Chromium at 390x844 and 1440x900, in light and dark, reads the computed colour of every visible text node, flattens it against the first opaque background behind it, and fails under AA. Source inspection cannot do this: the whole point of the alias layer is that a rule written years ago now resolves differently per theme, and only the browser knows the answer. It also checks that a coarse pointer gets a 44px target, with one deliberate exception, a text link inside a running paragraph, which lives under the reading rule with its underline.876 distinct colour pairs measured. None under AA. Eleven screens, two widths, and the dark half is the dark theme: both
app-contrastandapp-shotsstore the appearance choice now that a dark operating system alone no longer darkens anything.Measured, not claimed
Lighthouse 13.4.1 against a local server with the API stubbed, mobile profile.
Baseline on
main, same server, same run: dashboard perf 83, a11y 96, CLS 0.236. parashare perf 68, a11y 84. The dashboard skeleton is what took CLS from 0.236 to 0.005.SEO scores 66 on every app screen, before and after. That is the
robots: noindexthese pages carry in production, and it is correct.Reduced motion.
dashboard-1440x900-light-reduced-motion.pngis byte-identical todashboard-1440x900-light.png, and the same holds in dark. Everything that moves stops, and no state disappears.Screenshots
36 images in
docs/brand/assets/app-2026/, deviceScaleFactor 2, generated bytests/app-shots.mjs.Eight screens at 390x844 and 1440x900, light and dark:
dashboard,sign,parashare,signup,account,login,setup,verified(that last one shot signed out). Plus the dashboard loading silhouette and the reduced-motion pair.dashboard-1440x900-light.pngdashboard-1440x900-dark.pngdashboard-390x844-light.pngdashboard-390x844-dark.pngdashboard-1440x900-light-loading.pngdashboard-1440x900-dark-loading.pngdashboard-1440x900-light-reduced-motion.pngdashboard-1440x900-dark-reduced-motion.pngsign-*,parashare-*,signup-*,account-*,login-*,setup-*Gates
tests/static-sanity.sheleven checks,first-screen9,pricing-fold4,navigation-shell1,user-dashboard-documents25 checks,ui-truthfulness,site-claims22,seo-contract14,links,frontend-loading-contract7,frontend-module-scripts,product-heartbeat9,usage-purpose8,code-manifest,version-consistency,app-contrast2,app-theme8.scripts/check-csp-inline.shandscripts/check-cache-bust.shclean, 351 links on one consistent?v=.tests/heartbeat-lib.test.mjsfails on this machine for a missing@noble/post-quantum, identically on unmodifiedmain.Not in this PR
The three-way theme switcher.It is in this PR now, on/account, because leavingprefers-color-schemein charge until group systeem landed meant shipping the jump from creme to black. The app pages still answer to[data-theme="dark"]and[data-theme="light"]exactly as before, so a switcher in systeem that writes the same attribute keeps working; if that group lands its own control, one of the two goes and the CSS does not move.design-tokens-2026.cssfrom PR docs: de designrichting 2026 beschrijft de gemergede stand, niet het jurybesluit #373 is still not loaded by anything.app-2026.cssuses the same token names and the same values, so when that file goes global the two agree rather than fight. Folding them together belongs to step 6 of the rollout in the direction, once no page names the old tokens.