FAIL A blocker is open. Not production ready.

https://vocalcom.colibrity.com - graded against every rule this standard publishes. The rules this project does not meet are listed first.

80 pass 14 fail 70 unverified 11 n/a 0 waived

Unverified is not a pass. It is the verdict for a rule nobody has checked yet, and each one below carries what would settle it. On a build still in progress most of the list is expected to sit there; a rule moves out of it by being measured, not by being assumed.

Not a plain pass

ResultRuleTitleReason
fail a11y.axe-clean No automated accessibility violations 7 of 7 page(s) scanned by axe at WCAG 2.1 A and AA, at rest under reduced motion - / color-contrast x38 (.btn--small > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1); /blog/ color-contrast x26 (.btn--small > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1); /contact/ color-contrast x26 (.btn--small > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1); /deploying-without-drama/ color-contrast x26 (.btn--small > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1); /design-system/ color-contrast x90 (.mega-nav__demo > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1); /product-page/ color-contrast x44 (.btn--small > .btn__label[data-btn-label=""] #f7f5f2 on #ff4400 3.17:1)
fail a11y.contrast Text and interface contrast meets AA Measured, and open with Colibrity as this project's own recorded decision. The primary button ships Figma's own Button/Text Light #f7f5f2 on Corail at 3.17:1 against the 4.5:1 this rule asks for, and Figma's hover and pressed fills LIGHTEN, taking it to 2.91 and 2.64 - under even the 3:1 floor for UI. tests/contrast.test.mjs pins all three ratios, so the moment the palette moves the suite goes red. A fix exists (Marine text at 5.82:1, which also makes the ramp run the right way) and was deliberately reverted: changing how a client's primary button looks is Colibrity's call. Two more pairs joined it on 2026-08-28, both drawn on the services-1 homepage section and both reported to Colibrity on /design-system/ rather than corrected: the card's audience label is the text button's hover orange on white at 2.55:1, and the card's outline pill puts base Corail on white at 3.45:1. Same open decision, more instances. A third joined on 2026-09-25, measured by axe on every page with a footer: the footer's column links are Figma's muted-soft #6c667f on the panel's 80% white over Marine, which composites to #ceccd4 at 3.43:1. Figma's colour, so reported rather than corrected. Not waivable - this is a blocker - so it stands as failing until Colibrity answers. The decision is recorded in the theme repository, which is not a document anybody outside the build can open, so it is written out here rather than referred to by number.
fail delivery.real-content No placeholder content survives to launch 42 URL(s) from the sitemap index at /sitemap_index.xml and its 4 readable child sitemap(s): 42 carry placeholder content - /blog/ (ipsum, placeholder); /deploying-without-drama/ (ipsum, placeholder); /motion-that-respects-people/ (ipsum, placeholder); /forms-that-fail-well/ (ipsum, placeholder); /fluid-type-without-tears/ (ipsum, placeholder); /the-lazy-loading-trap/ (ipsum, placeholder), and 36 more - tried /sitemap_index.xml, /wp-sitemap.xml, /sitemap.xml, /sitemap_index.xml; /blog/: "- Hotleads New Learn more Hybrid / On-premise / Cloud Morem ipsum dolor sit amet, consectetur adipiscing elit. Learn mor"; /deploying-without-drama/: "- Hotleads New Learn more Hybrid / On-premise / Cloud Morem ipsum dolor sit amet, consectetur adipiscing elit. Learn mor"; /motion-that-respects-people/: "- Hotleads New Learn more Hybrid / On-premise / Cloud Morem ipsum dolor sit amet, consectetur adipiscing elit. Learn mor"; /forms-that-fail-well/: "- Hotleads New Learn more Hybrid / On-premise / Cloud Morem ipsum dolor sit amet, consectetur adipiscing elit. Learn mor"; scope: visible text, alt/title/aria-label/aria-description/aria-placeholder, description metas and application/json islands for the words; src/srcset/data-src/poster, rel=preload as=image and url() in style attributes and <style> blocks for the assets. Comments and <template> bodies were blanked first. Both lists are FIXED: /\b(lorem|ipsum|placeholder)\b/ case-insensitive, /\b(TODO|FIXME)\b/ as written, /(demo-image|dummy|placeholder|lorem|ipsum)/ on filenames. A stylesheet served from its own file is not read
fail design.contrast-in-mockup Contrast is proven in the mockup, not found in the browser The design-side pair of a11y.contrast, which is already an open blocker on this project: the primary button ships Figma's own light text on Corail at 3.17:1 against the 4.5:1 this rule asks for, and the hover and pressed fills lighten to 2.91 and 2.64. A fix at 5.82:1 exists and was reverted, because changing a client's primary button is Colibrity's call. The services-1 section adds two more of the same pairing on 2026-08-28 - a 2.55:1 card label and a 3.45:1 pill label, both orange on white - so the design-side finding is now a pattern in the file rather than one component. Grading this as unverified while its measured twin fails would understate a defect we have measured.
fail design.motion-spec Every animated surface has a defined motion spec The handoff file carries NO motion sheet and no motion data at all, measured 2026-08-28: get_motion_context on the whole Home section (767:26121, recursive) returns an empty node list, and the file's only motion expressions are two text annotations (the hero waveform's "animation with code"; the primary button hover naming osmo button pack 077, a reference rather than values). The project's own decision record carries the finding. Mitigated on our side, not fixed on theirs: DESIGN.md's motion key now holds the sheet - the standard's eight surfaces plus the waveform and marquee, each with trigger, duration in ms and named easing or a stated not-applicable - rendered at /design-system/#motion and pinned to the shipped code literals by tests/motion.test.mjs. The rule grades THEIR file, so it stays failing until Colibrity adds the sheet there; ours is the draft they are welcome to copy.
fail forms.anti-spam Forms carry three layers of spam defence Measured 2026-09-25: two of the three layers. The honeypot is on and stores nothing when filled. The per-IP rate limit is built (inc/forms-core.php, vocalcom_forms_rate_limited(), 5 accepted submissions per 10 minutes across every form, keyed on an HMAC of the address): six POSTs from one IP to the local /wp-json/vocalcom/v1/contact answered 200 five times and 429 the sixth, with 5 messages and 5 consent records stored, the refused POST writing none; the same on staging in Chrome. tests/forms-rate-limit.test.mjs pins it. What still fails is the last clause: the scored gate is off ('recaptcha' commented out in functions.php, no key), and inc/recaptcha.php passes on a transport error, so an unreachable verifier is not a refusal. Turning the gate on and failing it closed is on the production list.
fail html.buttons-vs-links A link goes somewhere, a button does something Measured 2026-09-25 on the seven sampled templates at https://vocalcom.colibrity.com . The markup half passes: 0 clickable div or span, 0 role=button on a non-button, and every control reached and operated with Tab and Enter in the keyboard pass (a11y.keyboard-operable). What fails is resolving hrefs: 278 anchors per page carry href="#" - the mega nav's dummy tabs (102 links, 21 cards, 6 banners and their 102 mobile twins), 43 footer column links, 2 footer legal links and the Request Demo button. They are the placeholder navigation STATUS 2e records, waiting on Colibrity's sitemap, and they are the reason this is a fail rather than a pass: a link that goes nowhere is exactly what the rule is about.
fail onpage.core-fields Core CMS fields carry the content, not only the builder 2 public type(s) read on colibrity-staging - post: 7 of 13 with no excerpt; page: 30 of 30 with no featured image (page does not support excerpt)
fail ops.fatal-alerts A fatal error reaches a mailbox somebody actually reads the fatal-error notice goes to an address on vocalcom.test (from admin_email), a reserved domain no mailbox is read on
fail perf.enqueue-hygiene Every asset loads once, has content, and only where used Read in Chrome on staging 2026-09-25 across the seven sampled templates: 8 to 23 resource hints in the head against the limit of 4 (most are Vite modulepreloads for the block chunks; 23 on the home page, 20 on the product page); one script under 100 bytes (block-text-anim, 90 bytes, on /contact/); no URL requested twice except reviews- demo.mp4 on the home page, which is two media range requests. The library clause was read in inc/vite.php: per-page block stylesheets and a registry chunk loaded only for the layouts a page renders.
fail perf.image-formats Images ship in a modern format at the size they display /blog/ the image audits estimate 150 ms off LCP or FCP (13 KB) - 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json, image audits read on every run, flagged images re-measured in Chrome at 412 x 823, 1.75x: / 15 KB of estimated savings that move no metric; /blog/ 13 KB of estimated savings that move no metric; /contact/ no savings; /privacy-policy/ no savings; /deploying-without-drama/ 31 KB of estimated savings that move no metric; articles-1-768x544.jpg.webp fits its 364x258 slot at 1.75x (768x544); Lighthouse's 31 KB is lighthouse#17080; /design-system/ no savings; /product-page/ 15 KB of estimated savings that move no metric
fail perf.lcp LCP at or under 2.5 seconds on mobile 4 template(s) over the line (/, /blog/, /deploying-without-drama/, /product-page/) - 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: / 2.84 s (2.75 s-3.12 s) LCP div.block-hero__media > img.block-hero__image; /blog/ 3.37 s (3.36 s-3.39 s) LCP div.post-card__media > img.post-card__image; /contact/ 1.65 s (1.65 s-2.73 s) LCP div.block-text__body > p; /privacy-policy/ 1.50 s (1.35 s-2.53 s) LCP div.legal-page__section-body > p; /deploying-without-drama/ 2.77 s (2.38 s-2.81 s) LCP figure.post-single__media > img.post-single__image; /design-system/ 2.48 s (2.10 s-2.50 s) LCP div.sg-intro > p; /product-page/ 2.87 s (2.38 s-2.91 s) LCP div.block-hero-b__media > img.block-hero-b__poster
fail perf.video-weight Video is deferred and never blocks the first paint /product-page/ reviews-demo.mp4 fetched 236 KB before LCP, on the critical path - 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json, video read off every run and off the served page: / 1 video file(s) under 2 MB; /blog/ no video on the page; /contact/ no video on the page; /privacy-policy/ no video on the page; /deploying-without-drama/ no video on the page; /design-system/ no video on the page; /product-page/ 1 video file(s) under 2 MB
fail security.csp-enforced-baseline A Content-Security-Policy is enforced, not only reported 7 sampled page(s) read with curl - / sends no report-only header beside the enforcing one; / enforcing header lacks object-src; / enforcing header lacks base-uri; / enforcing header lacks form-action; / enforcing header lacks frame-ancestors; /blog/ sends no report-only header beside the enforcing one; /blog/ enforcing header lacks object-src; /blog/ enforcing header lacks base-uri; /blog/ enforcing header lacks form-action; /blog/ enforcing header lacks frame-ancestors; /contact/ sends no report-only header beside the enforcing one; /contact/ enforcing header lacks object-src; /contact/ enforcing header lacks base-uri; /contact/ enforcing header lacks form-action; /contact/ enforcing header lacks frame-ancestors; /privacy-policy/ sends no report-only header beside the enforcing one; /privacy-policy/ enforcing header lacks object-src; /privacy-policy/ enforcing header lacks base-uri; /privacy-policy/ enforcing header lacks form-action; /privacy-policy/ enforcing header lacks frame-ancestors; /deploying-without-drama/ sends no report-only header beside the enforcing one; /deploying-without-drama/ enforcing header lacks object-src; /deploying-without-drama/ enforcing header lacks base-uri; /deploying-without-drama/ enforcing header lacks form-action; /deploying-without-drama/ enforcing header lacks frame-ancestors; /design-system/ sends no report-only header beside the enforcing one; /design-system/ enforcing header lacks object-src; /design-system/ enforcing header lacks base-uri; /design-system/ enforcing header lacks form-action; /design-system/ enforcing header lacks frame-ancestors; /product-page/ sends no report-only header beside the enforcing one; /product-page/ enforcing header lacks object-src; /product-page/ enforcing header lacks base-uri; /product-page/ enforcing header lacks form-action; /product-page/ enforcing header lacks frame-ancestors
unverified a11y.alt-text Images carry alt text that earns its place Half measured 2026-09-25: 0 images without an alt attribute on all seven sampled templates at https://vocalcom.colibrity.com (21 to 111 images each), and axe's image-alt rule passed on every one. The other half - that reading the alt text conveys what the picture does - cannot be judged while the images and their alt text are placeholders; it waits on Colibrity's copy and imagery.
unverified a11y.media-alternatives Video and audio carry captions and controls A caption mechanism, which does not exist: `<track>`, "captions" and "subtitles" appear nowhere in template-parts or src. The hero background video is muted and decorative, but the video block and webinar recordings can both carry speech, so this is a real gap rather than a technicality.
unverified a11y.screen-reader-pass One page and one form are driven with a screen reader Not checked against this build. Settled by: the report names the screen reader and browser used, lists the headings and landmarks in the order they were announced, and confirms the invalid submit was announced. Zero unlabelled controls were met on the two pages.
unverified a11y.statement The site publishes an accessibility statement A published accessibility statement naming the target, the known gaps and a contact route. None exists, and the open contrast blocker above means there is already a known gap to name.
unverified a11y.zoom-and-reflow The page survives 200 percent zoom and a 320px viewport A pass at 200 percent zoom and at a 320px viewport across the built templates. Not done.
unverified admin.editor-language The admin reads in the language the editor set Not checked, and it matters more here than anywhere: this project is four regions by four languages. Settled by: switching the wp-admin display language switches the labels of the site's own post types, columns and meta boxes. No slug, meta key or field name changes with it.
unverified admin.one-control-per-setting One control writes one setting Not checked. Settled by: for each setting the client can change, exactly one control in the admin writes it. Where a plugin supersedes a theme feature, the theme's control is not rendered while that plugin is active, checked with the plugin on and off. This theme has both a Theme Settings page and per-module tabs appended to it, which is one page, but the Yoast overlap is now the live case: Yoast has been the SEO engine since 2026-09-02 and functions.php:268-276 stands the theme's own engine down while it is active, but whether the Theme Settings SEO tab is also withheld while Yoast is on has not been looked at.
unverified admin.translation-declared Every field and every fixed string says how it translates Field half proved: inc/acf-fields.php gives every registered field a wpml_cf_preferences value and tests/wpml-acf-prefs.test.mjs (4 of 4) fails the suite if one is missing. String half: theme strings route through vocalcom_t() and String Translation, and node bin/lang-parity-check.mjs (2026-09-25, local) reads no default-language block copy on the translated pages beyond its three written exceptions. Open: a scan of each secondary-language template for fixed theme strings still in English.
unverified colibrity.js-hooks-are-data-attributes JavaScript targets data attributes, never CSS classes 254 DOM quer(ies) across 83 script(s), 17 state class(es) the scripts set themselves, none by a styling class among those read, but 12 selector(s) are built at runtime and were not read: src/js/blocks/block-features-anim.js:435, src/js/blocks/block-reviews-anim.js:319, src/js/core/entrance-store.js:110, src/js/modules/dialog-a11y.js:32, src/js/modules/form-validate.js:288, src/js/modules/head-sync.js:99, src/js/modules/head-sync.js:100, src/js/modules/header-metrics.js:18, src/js/modules/header-scroll.js:128, src/js/modules/video-tile.js:157, src/js/modules/video-tile.js:166, src/js/modules/waveform-lab.js:29
unverified comms.delivery-alerting A broken form or CRM connection raises an alert Not checked against this build. Settled by: break the CRM credential on purpose and submit once. An alert naming the site and the failure reaches the named recipient within 24 hours, and the handover names that recipient and the date they took it on.
unverified comms.email-authentication Automated mail arrives with SPF, DKIM and DMARC passing DNS read 2026-09-25: vocalcom.colibrity.com, staging's sending domain, publishes no SPF and no DMARC; colibrity.com publishes SPF (include:_spf.google.com) and DMARC p=none. The production domain's records are not set up yet. Open: a dated Authentication-Results header from a real send.
unverified comms.email-rendering Transactional emails render in the clients people use Not checked against this build. Settled by: the same message is opened in Gmail, Outlook and iPhone Mail, with a dated screenshot of each: no broken layout, no clipping notice, every link working. The raw source carries a text/plain part.
unverified comms.sender-identity Automated email carries a correct From, Reply-To and language Read 2026-09-25 on staging: notifications go From WordPress <wordpress@vocalcom.colibrity.com>, WordPress's defaults, with Reply-To set to the submitter (inc/rest-contact.php step 8). The production sending domain and name are not decided, and there is no autoresponder. Open: a received notification from each locale, read for its From, Reply-To and language.
unverified crawl.breadcrumb-trail Every page below the home page shows one breadcrumb trail Not checked against this build. Settled by: each sampled page below the home page shows exactly one trail, its root item in the page's language, its last item not a link, and its BreadcrumbList lists the same items in the same order under the same names. The Rich Results Test reports 0 BreadcrumbList errors.
unverified crawl.indexability-decided Every URL-minting template has a recorded index decision docs/EDITOR-HANDOVER.md section 11 carries the dated table (2026-09-25) of every URL- minting template and non-HTML route, measured on the local site: every noindex row answers noindex (date archives, search, the two dev pages; /wp-json/ and the sitemaps by X-Robots-Tag) and none of them is in a sitemap. Open: two rows have no decision yet - /elements/ is indexable today, and /blog/?category=<one> is indexable while duplicating /category/<one>/.
unverified crawl.internal-reachability Every indexable page is reachable by a static HTML link Not checked against this build. Settled by: the raw-HTML crawl reaches every URL in the sitemap: 0 orphans. No indexable page sits more than 3 clicks from the home page. A page found only in the sitemap counts as an orphan, not as reachable.
unverified crawl.search-console Search Console is verified and the build team has access Not checked against this build. Settled by: a dated screenshot taken from the build team's own Google account with the Coverage report open on the production property. Requested at kickoff, not at launch. After go-live the same account shows the sitemap submitted, with a read date.
unverified crawl.slug-change-redirect A renamed slug leaves a redirect from its old URL Not checked against this build. Settled by: every URL in the sitemap snapshot taken before the restructure, plus every old URL rebuilt from _wp_old_slug, returns 200 or exactly one 301 to a 200. 0 of them 404, and 0 chains of two hops or more.
unverified crawl.url-format Slugs are readable, hyphenated and in the page's language Not checked against this build. Settled by: every URL in the crawl is under 115 characters, separates words with hyphens rather than underscores, carries no numeric id or date segment, and is in the same language as the page it serves. 0 exceptions across the sampled templates.
unverified delivery.browser-matrix Every template is opened in all three browser engines Not checked against this build. Settled by: every template on the template list has been opened in Chrome, Firefox and desktop Safari - three engines, none checked in fewer - and each difference found is either fixed or written down.
unverified delivery.client-handover The client leaves handover able to run the site Not checked against this build. Settled by: the client has a dated recording of the session covering editing content, adding a page and what must not be touched, naming at least 2 attendees. The wider account table is not graded anywhere since ops.account-inventory was withdrawn - raise it, do not check it.
unverified delivery.editor-test The admin has been tested by someone who did not build it The kit exists and the test has not been run: docs/editor-test/task-script.md is the script and docs/editor-test/observation-sheet.md the per-tester sheet, and no filled sheet exists. Settled by: the handover carries a dated observation sheet naming the tester, the tasks attempted and each point of friction with what was done about it. The tester is not a developer on the build.
unverified delivery.monitoring-and-backups Uptime monitoring and a restorable backup are live at launch Not checked against this build. Settled by: the monitor's public status URL shows checks on the home page at five minutes or better, and the handover names the person it alerts and the date they took it on. If nobody will own it, the handover records that and it is not ticked.
unverified design.mockup-coverage Every unique section and page has its own mockup Not checked against this build. Settled by: on the handoff date, the frame count in the file matches the sections and page types in the agreed sitemap: 0 sections and 0 page types without a named frame. Record both counts and that date in the handoff message.
unverified design.naming-convention Layer and component names are agreed with the developer Not checked against this build. Settled by: the kickoff note lists the agreed name of every component and section frame and is countersigned by the developer. At handoff the names in the file are that same list: 0 renames between the agreed list and the delivered file.
unverified design.state-set The design specifies every state of every interactive element Not checked against this build. Settled by: every interactive component in the kit has all six states present as variants, with 0 components missing one. The focus variant is identical across components and matches the kit's single focus style.
unverified design.text-over-media Text over imagery carries a scrim the design specifies Not checked against this build. Settled by: the mockup names the overlay colour and opacity, or the gradient stops, for every text-over-media block. A dated export shows each block over the lightest and darkest plausible photo, body copy at 4.5:1 and display text at 3:1 in both.
unverified design.ui-kit The UI kit is built and published before the build starts Not checked against this build. Settled by: colibrity shares a published library URL at kickoff. Opening it, every colour and text value used in the mockups resolves to a published variable or style, and the file holds 0 detached instances of a kit component.
unverified env.deploy-provenance What production is running is a commit you can name Not checked against this build. Settled by: the live site exposes an identifier that resolves to one commit on the default branch of the repository handed over. A deployment naming no commit anywhere in its response or in a stamped file does not pass.
unverified env.env-config-not-in-db Environment switches are enforced in code, not in the database Measured 2026-09-25 on colibrity-staging: wp eval prints VOCALCOM_ENV staging, which mu- plugins/vocalcom-env.php derives from a wp-config.php constant, never a database row; WP_DEBUG_DISPLAY is false in wp-config.php; the gate's perf.third-party read zero third- party requests on all seven templates. The 2026-08-30 local-to-staging database push left staging still reporting staging. Open: the clause is a production database pushed onto staging, and there is no production host yet.
unverified env.no-production-data-off-production Production personal data does not travel to staging or a laptop Waiting on a production host. There is none yet, so there is no production personal data that could travel; the rule becomes checkable the day production takes its first submission.
unverified env.staging-parity Staging matches production where the checks depend on it no production host was supplied, so staging cannot be compared with it
unverified env.staging-sends-nothing-live Staging never writes to a production system Page half measured 2026-09-25: zero analytics, tag or advertising requests on all seven sampled templates (the gate's perf.third-party). A marked gate probe submission on staging stored nothing and sent nothing (message and consent counts 1 and 1 before and after). Nothing reaches a CRM: there is no connector and the vocalcom_form_submitted seam has no listener. Open: a real staging submission arriving in a test inbox.
unverified forms.confirmation A successful submit produces a confirmation nobody can miss Not checked against this build. Settled by: after a successful submit, focus is on the confirmation, a screen reader announces it, and it is still on screen 30 seconds later. Where it is a thank-you page, the handover names its URL.
unverified forms.value-formats Email and phone are checked without rejecting valid values Not checked against this build. Settled by: test@example is refused, test@example.com is accepted, and no address in the reviewer's list of 10 valid addresses is refused. +33 6 12 34 56 78 is accepted and stored as +33612345678.
unverified html.lists-and-tables Lists are lists and tables are tables An axe run. The standard's own site failed this on all 25 of its table headers, so it is not a safe assumption.
unverified html.no-duplicate-ids Ids are unique on the rendered page none of the 7 template(s) could be validated: the validator answered 429
unverified html.valid-markup Every template produces valid HTML none of the 7 template(s) could be validated: the validator answered 429
unverified legal.consent-record-context Consent records carry the context of the consent Not checked against this build. Settled by: two consents captured by one submit produce two records with different purposes. Each record names the URL it was given on and a collection method matching how it was actually collected, not a default string.
unverified legal.consent-retention The consent log is exportable and has a retention period Not checked against this build. Settled by: the period is stated in writing by the client or their DPO and appears in the privacy policy. An export of the log produces a file carrying every stored key. A record aged past the period is gone after the scheduled job has run once.
unverified legal.documents-published The privacy policy is a real page, linked where data is taken Measured 2026-09-25 on https://vocalcom.colibrity.com : /privacy-policy/ returns 200 on its own permalink, the footer links it on every sampled page, and both forms that collect personal data link it from their consent label (/contact/ and the register form on /product-page/). The footer's subscribe inputs and the hero phone field submit nowhere yet (src/js/modules/inline-form-validate.js dispatches an event and stops), so they collect nothing; they need the link when they are wired. Two inputs outstanding: staging is noindex by environment (x-robots-tag: noindex, nofollow, mu-plugins/vocalcom-env.php), so the no-noindex half can only be read on production, and Colibrity has not confirmed in writing that the text is the version their counsel approved.
unverified migration.redirect-map Every inventoried URL reaches its replacement in one hop no old-site URL inventory was supplied, so redirects could not be mapped
unverified migration.url-inventory The old site's indexed URLs are counted before anything is built Waiting on the redirect map's scope. A crawl of vocalcom.com plus Colibrity's Search Console export, reconciled into one dated list by language and template, is the input; neither has been started or supplied.
unverified onpage.site-identity The site's identity is set on purpose and the share card is seen inc/seo-defaults.php:47 makes the tagline the homepage hero heading, Figma's own copy, and :84 adds it to the front-page title; inc/seo-yoast.php:145-148 merges the Theme Settings social links into the Organization node's sameAs. Whether Colibrity has set the site title, the tagline and the separator on staging, which official accounts belong in sameAs, and the rendered share card in three platforms are theirs and undone: the project's decision record says Colibrity writes the copy. Settled by: the home page prints a deliberate title and tagline, every page uses one separator, the Organization node lists each official account in sameAs, and a dated screenshot of the home and one article preview exists for each of the three platforms.
unverified ops.backup-coverage Backups cover the database and the uploads, on a stated schedule Not checked against this build. Settled by: a dated screenshot of the backup listing shows a database backup under 24 hours old and a file backup under 7 days old. The handover states retention in days and names the copy held outside the hosting account.
unverified ops.cron-runs Scheduled tasks actually fire on the production host 21 scheduled event(s) on colibrity-staging at 2026-09-25T15:29:29Z, DISABLE_WP_CRON not set, none overdue - but DISABLE_WP_CRON is not set, so WP-Cron runs on visits and this run's own requests drained the queue before it was read
unverified ops.db-write-backup No write to a live database without a fresh dump of what it replaces Not checked against this build. Settled by: every path in the repository that writes to a remote database dumps it first, stops when that dump fails a size and gzip check, and prints the restore command. The dump from the last production write is on disk, dated.
unverified ops.recovery-drill The site has been restored and rolled back, once, on purpose Not checked against this build. Settled by: the handover carries dated terminal output from two runs: a restore of a backup no more than 7 days old into a non-production target, ending with that target's home page returning 200, and a rollback to the previous release. A tick with no output does not settle it.
unverified perf.animation-cost Animation is cheap and stops when off screen A read of the GSAP effects and scroll modules for properties other than transform and opacity, and for animation that keeps running off screen.
unverified perf.optimiser-output-fresh Optimiser output is regenerated and state classes safelisted Not checked against this build. Settled by: every class toggled from JavaScript is present in the production stylesheet: open the menu, a modal and one filter on the live URL and confirm the styling applies. The critical CSS is no older than the build it shipped with.
unverified perf.split-points-asserted Every deliberate split point is asserted in the build bin/assert-chunks.mjs reads the build manifest at :27, positive assertion at :33, negative at :37, exits non-zero at :44; run by `deploy`, `deploy:prod`. Whether the build actually FAILS when either assertion is broken cannot be established by reading, and this gate may not break one: breaking an assertion is a write into the repository under review and watching a build fail is a build. To settle it a person widens the allow-list (or empties the required-chunk list) on a scratch branch, runs the project's own build, confirms it exits non-zero, and reverts. Everything a reader can settle is met: scripts calling assert-chunks.mjs: build:check (runs), chunks:check (runs), deploy (runs), deploy:prod (runs); scripts that build or deploy: build, deploy, deploy:prod, release, release:prod; shipping files carrying it: none; dist/.vite/manifest.json declares 45 dynamic import(s); 13 dynamic import(s) in 83 source file(s); the clause that no module is bundled into two chunks is not measured by this check
unverified practices.empty-states Every dynamic component handles empty, long and broken The four self-filling region blocks are queried through vocalcom_region_query() so they are never empty by construction, and tests/region-blocks.test.mjs fails if a block builds its own WP_Query. The authored blocks have not been exercised with zero items, one, a very long title or a missing image.
unverified practices.mobile-viewport-chrome The layout survives the phone's real viewport Not checked against this build. Settled by: on a real phone at 390x844: no section clipped by the browser toolbar in either orientation, no text or control under the notch or home indicator, and a sticky header at 15 percent of viewport height or less in landscape.
unverified practices.no-hardcoded-content Editable content is editable, not baked into templates A read of the templates for copy, images, links or order baked in rather than coming from fields. inc/block-defaults.php deliberately ships default copy through ACF default_value, which is the right mechanism, but the audit has not been done.
unverified practices.one-change-per-deploy Performance and behaviour changes ship one at a time Judgement over the release history rather than a check. Recommended, so it never blocks.
unverified practices.real-device-check Anything animated or video-driven is checked on a real device A pass on a real iPhone and a real mid-range Android. The theme has GSAP animation and autoplay video, so it is squarely in scope. Not done.
unverified practices.responsive-breakpoints The layout holds at every width, not just the design widths Partially measured 2026-08-27 during the scaling rework in the project's decision record: the front page and /design-system/ at 375, 768, 1440, 1710, 1920, 2560 and 3840, horizontal scroll asserted programmatically (scrollWidth vs clientWidth) at every stop and zero found, with the root ladder and the 75.5 percent column verified at each rung. Still owed before this reads as proof: 320 itself, the inner templates, and an overlap pass beyond the two pages checked.
unverified security.admin-accounts-at-handover The administrator list is named and owned before handover Not checked against this build. Settled by: wp user list --role=administrator names no account called admin and no build account above editor, and a dated written confirmation from the client - an email reply or a countersigned handover line - claims every remaining administrator.
unverified security.no-error-disclosure No response leaks a PHP error, a stack trace or a server path Measured 2026-09-25 on staging: none of the seven sampled pages carries Fatal error, Warning:, Notice:, Stack trace or the host's absolute path; wp-config.php defines WP_DEBUG_DISPLAY false (WP_DEBUG and WP_DEBUG_LOG are on), and /wp-content/debug.log answers 403. Open: a deliberate fatal returning 500 without those strings, which needs a write to staging.
unverified security.public-post-rate-limit Login and every public form endpoint are rate limited Not built in the theme: no login throttle exists in the theme, its mu-plugins or its plugins (git grep for a 429 or a per-IP counter finds only the analytics transport). Whether Hostinger's LiteSpeed limits wp-login.php was deliberately not measured on 2026-09-25: twenty wrong-password POSTs risk blocking the reviewer's IP. Building the limit is on the production list, with the form rate limit and the enforcing CSP.
unverified security.versions-current Core, plugins, themes and PHP are current, and updates have an owner Read 2026-09-25 on staging: wp core check-update 0; two plugin updates available (ACF Pro 6.8.9 to 6.8.10, Yoast SEO 28.4 to 28.5); web PHP is 8.3, under php.net security support to the end of 2027, while the host's CLI PHP is 8.0, past its end of life. Judged on handover day, so these counts are a reading, not a verdict. Open: the handover naming who applies updates for twelve months, which is the client's.
unverified seo.headings-describe-content The heading outline reads as an outline A read of the heading outline once the copy is real. Recommended, so it never blocks.
unverified seo.open-graph Open Graph and Twitter card tags are complete 7 of 7 sampled page(s), 2 distinct og:image, 0 fault(s), but og:url could not be compared: / has no canonical to compare og:url against; /blog/ has no canonical to compare og:url against; /contact/ has no canonical to compare og:url against; /privacy-policy/ has no canonical to compare og:url against; /deploying-without-drama/ has no canonical to compare og:url against; /design-system/ has no canonical to compare og:url against; /product-page/ has no canonical to compare og:url against
unverified seo.redirects-on-launch Replaced URLs redirect to their new home Waiting on the redirect map, which does not exist and is not budgeted. The old vocalcom.com carries roughly 2,362 indexed blog posts plus about 52 pages per language. Nothing can be checked until migration.url-inventory produces the list.
unverified seo.structured-data Structured data is present, valid and singular Measured 2026-09-25 on staging: each of the seven sampled templates emits exactly one Organization node, named Vocalcom. A post's author now credits that Organization when the WordPress account has no real name (inc/seo-yoast.php vocalcom_yoast_author_fallback). Open: the Rich Results Test clause, which has no API.
unverified states.current-page The current page is marked in the navigation the main navigation links 0 page(s) on this host, and the rule is judged on 5: 7 page(s) read from the served document; 0 page(s) the main navigation links
unverified states.error-page-design The 404 and 500 pages are designed, not server defaults Not checked against this build. Settled by: the 404 opens with the site header, footer and type styles and at least one working link back into the site. Forcing a 500 on staging returns a page in the same chrome, not the raw server or PHP default.
unverified states.interactive-set Every interactive element ships all of its states Not checked against this build. Settled by: sample one control of each kind on three templates: hover, active, disabled and open each render a visible change. An element inserted by a filter or a load-more shows the same four states as one present at first load. Nothing a hover reveals is the only route to it on touch.
unverified states.loading-feedback Anything that takes time says so Not checked against this build. Settled by: throttle to Slow 3G, then submit a form and change a filter. Each shows a busy state within 500 ms. Force one to fail and the busy state clears rather than leaving a spinner running forever.
unverified states.sticky-chrome Sticky header and menu scroll behaviour is decided up front Not checked against this build. Settled by: scrolling past the stick point shifts the page by 0 pixels. A link to #section leaves its heading fully below the header. With the menu open the page behind it does not scroll, and closing it returns to the same position.
n/a colibrity.paged-canonical Paged views are canonical to themselves page two /blog/page/2/ is noindex with no canonical, which is how a closed host serves it. Re-run against the production host
n/a comms.crm-field-map Every form field lands in the right CRM field Submissions are stored and emailed. Nothing reaches a CRM: there is no connector in `inc/`, and the `vocalcom_form_submitted` seam has no listener.
n/a crawl.signals-agree Canonical, robots, sitemap and meta tell one story every sampled URL is noindex and robots.txt is a blanket Disallow: /, so the whole host is closed deliberately rather than sending conflicting signals. Re-run against the production host
n/a forms.file-uploads File uploads declare what they accept and why they refuse No form accepts a file. The only `type="file"` in the theme is a styleguide demo at inc/dev-styleguide.php:744, wired to nothing.
n/a legal.newsletter-separate Newsletter consent is its own optional checkbox This project has no newsletter. `cpt-subscriber` is commented out in functions.php:195 with the reason written beside it - a subscriber screen on a site with no newsletter is clutter the client has to ignore forever.
n/a legal.terms-consent-checkbox The terms box starts unchecked and the server enforces it No form asks a visitor to accept terms or a contract. Every registered form (contact in inc/forms-core.php:186, register in inc/forms-register.php:55) carries only the registry's default privacy consent, inc/forms-core.php:56-62, and the rule's own do-text says a terms box proves acceptance of a contract and is not a consent. Checked 2026-09-25. The server-side half was measured anyway for the privacy box: a POST to the local /wp-json/vocalcom/v1/contact with the consent unticked returned HTTP 400 "You must accept the Privacy Policy." and vocalcom_message and vocalcom_consent_log both stayed at 0 rows.
n/a seo.canonical Every indexable page declares a self-referencing canonical every sampled page is noindex and carries no canonical (/, /blog/, /contact/, /privacy-policy/, /deploying-without-drama/, /design-system/, /product-page/), which is how a closed host serves them. Re-run against the production host
n/a seo.no-noindex-leak Production is indexable and staging is not staging half holds: 7 of 7 sampled URLs noindex and robots.txt Disallow: /. The production half needs the production host
n/a seo.robots-txt robots.txt exists and points at the sitemap 200, 1 Sitemap: line(s), 131 same-origin asset path(s) and 7 template(s) tested against the * group, sitemap lines sound. robots.txt is a blanket Disallow: / and all 7 sampled URLs are noindex, so this host is closed deliberately and whether a ranking route or an asset is blocked cannot be judged here. Re-run against the production host
n/a seo.sitemap The sitemap lists every indexable URL and nothing else https://vocalcom.colibrity.com/sitemap_index.xml (index of 4), 42 URL(s), 10 sampled, every structural clause holds, but all 10 sampled URLs are noindex under a blanket Disallow: /: a closed host. Re-run the noindex clause against production
n/a states.preloader-budget The preloader has a hard cap and a fallback No preloader. `site-transition.php` is a route-change wipe, not a first-load gate, and nothing defers the first paint.

Passing

Where a pass needs evidence rather than an assertion, the evidence is named. An unproved pass is what the gate itself calls unverified.

ResultRuleTitleHow it is proved
pass a11y.keyboard-operable Everything works with a keyboard alone Measured 2026-09-25 on https://vocalcom.colibrity.com , keyboard only. / at 1440: 109 Tab stops, skip link first, every control reached. /contact/ at 1440: 90 stops through the whole form. / at 390 with the burger open: focus stays inside the panel and the header's logo, Request Demo and burger, and Escape closes it and returns focus to the burger. Outcomes tabs move with ArrowRight and show their panel; the reviews Next button pages on Enter and Space and keeps focus; the video lightbox is aria-modal, puts focus on Close, keeps six further Tabs inside, and Escape closes it and returns focus to the play button that opened it; the footer accordion toggles on Enter and Space. One thing to know: 23 stops on / land on a card whose entrance has not run yet, at opacity 0 for an instant - measured, a focused one is at 0.26 after 50 ms and 0.97 after 400 ms, because focus scrolls it in and the scroll reveals it. That is the trade src/scss/blocks/_shared.scss:18-35 documents over visibility:hidden, which took whole sections out of the tab order.
pass a11y.focus-visible A visible focus indicator on every focusable element Measured 2026-09-25 in the same keyboard pass as a11y.keyboard-operable (/ and /contact/ at 1440, / at 390 with the burger open, https://vocalcom.colibrity.com). Every stop showed a ring: 2px #0a0029 at an offset over a 6px rgba(255,255,255,0.9) halo, from mix.focus-ring (the theme's focus-ring decision). The two inputs whose own outline is none carry it on their pill wrapper through :focus-within - the hero phone field (.btn-input__field) and the footer subscribe input (.subscribe). The ring is ink against its own white halo, 20.1:1 over white and 16.1:1 where the 90% halo sits on Marine, so it holds its contrast over images, gradients and the Marine footer, which is why the theme chose a halo over a per-surface colour.
pass a11y.skip-link A working skip link is the first focusable element wp-starter/skip-link present: header.php, src/scss/base/_a11y.scss. Project stamped wp-starter 1.23.0, verified against wp-starter 1.10.0. Presence and version only - the behaviour was not exercised - 2 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-11
pass a11y.form-labels Every form control has a real label Measured 2026-09-25 on https://vocalcom.colibrity.com / , /contact/ and /product-page/ after the deploy: 0 inputs, selects or textareas without a label element, counted through each control's labels list at 1440. The footer's subscribe prompt is now a real label, one per form, each shown with its own form and pointing for= at its own input (template-parts/global/site-footer.php:146-147), with no aria-label left on either input; tapping the one visible label focuses the visible input at 390 and at 1440, checked in a browser. Every registered form prints "Fields marked * are required." once, before its first field (template-parts/global/form-body.php:105), in all four languages on staging. tests/form-labels.test.mjs pins all of it. Required fields carry both the required attribute and a * in the label, as before.
pass a11y.form-errors Errors say what is wrong and how to fix it 1 of 1 form(s) submitted empty
pass a11y.reduced-motion prefers-reduced-motion is honoured everywhere src/js/core/motion-guard.js, the verified wp-starter/motion-guard component. tests/motion- guard.test.mjs asserts prefers-reduced-motion is read in exactly one file and fails if a block stub regenerates a second reader, which is the failure mode that made the component necessary.
pass a11y.dialog-behaviour Modals trap focus, return it, and close on Escape src/js/modules/dialog-a11y.js, the verified wp-starter/dialog-a11y component, wired in src/js/main.js and used by modal-factory.js, legal-modals.js and success-popup.js rather than each rolling its own trap.
pass a11y.link-text Link text makes sense out of context 7 of 7 template(s) read for link text
pass a11y.icon-semantics Icons are hidden when decorative and named when they act Measured 2026-09-25. All 27 partials in template-parts/icons/ carry aria-hidden="true" and focusable="false", and none carries a <title>. Rendered on https://vocalcom.colibrity.com / , /contact/ , /product-page/ and /blog/ : 0 SVGs exposed to the accessibility tree without a name out of 89 to 143 per page, 0 leftover titles, and 0 icon-only links or buttons without an accessible name (28 to 34 per page, named by aria-label). The two role=img SVGs are the logo lockups, aria-hidden inside links that carry aria-label. axe's button-name and link-name rules passed on all seven sampled templates the same day.
pass a11y.target-size Tap targets are big enough and far enough apart 7 of 7 page(s) read at 390px for target size, collapsed menus opened
pass colibrity.filter-url-state Filters write their state to the URL wp-starter/filter-url-state present: inc/listing.php, tests/listing.test.mjs, tests/listing-fetch.test.mjs. Project stamped wp-starter 1.23.0, verified against wp-starter 1.17.0. Presence and version only - the behaviour was not exercised - 3 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-13
pass colibrity.filter-history-noise Filter changes do not litter the back button wp-starter/filter-url-state present: inc/listing.php, tests/listing.test.mjs, tests/listing-fetch.test.mjs. Project stamped wp-starter 1.23.0, verified against wp-starter 1.17.0. Presence and version only - the behaviour was not exercised - 3 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-13
pass colibrity.pagination-in-url Every page of a listing has its page number in the URL wp-starter/archive-state present: template-parts/post/pagination.php, inc/blog-seo.php. Project stamped wp-starter 1.23.0, verified against wp-starter 1.10.0. Presence and version only - the behaviour was not exercised - 2 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-11
pass colibrity.pagination-crawlable Pagination controls are links, not buttons wp-starter/archive-state present: template-parts/post/pagination.php, inc/blog-seo.php. Project stamped wp-starter 1.23.0, verified against wp-starter 1.10.0. Presence and version only - the behaviour was not exercised - 2 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-11
pass colibrity.filter-indexability Filter combinations do not become a crawl trap Measured on the local site 2026-09-25: /blog/?tags=wordpress and /blog/?category=notes&tags=wordpress canonicalise to /blog/. The one indexable filter, /blog/?category=notes, now canonicalises to /category/notes/ (inc/listing.php, vocalcom_listing_seo_canonical()), the archive that renders the same listing and that Yoast's category sitemap lists, so no filtered URL is its own canonical. Any other combination collapses to /blog/, so the set of indexable filtered URLs is bounded by the categories. Staging answers noindex everywhere and prints no canonical, so this is proven locally; tests/listing.test.mjs pins it.
pass colibrity.block-file-triple A block is three files sharing one slug 24 block(s) read from 123 tracked file(s), 24 registry entries: template, stylesheet, script and data-block agree for every block, and no stylesheet or script is orphaned
pass colibrity.dev-surfaces-hidden Developer surfaces stay, and their exposure is decided in code wp-starter/dev-pages present: inc/dev-pages-noindex.php, inc/admin-dev-pages.php, inc/dev-blocks.php, tests/dev-pages-noindex.test.mjs, tests/admin-dev-pages.test.mjs. Project stamped wp-starter 1.23.0, verified against wp-starter 1.23.0. Presence and version only - the behaviour was not exercised - 5 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-09-15
pass colibrity.adding-a-block Adding a block shows a picture first and never lands empty inc/admin-blocks.php:140-173 paints a screenshot of each block on ACF's picker card, keyed on its data-layout anchor, from the 25 images under src/assets/images/blocks that npm run blocks:thumbs regenerates from /sandbox/; inc/block-defaults.php:1-12 pre-fills every authored block with real copy and gives every new page a starter set. The thumbnails are inherited from wp-starter; the defaults are this build's own.
pass colibrity.tokens-from-the-design-file Design values reach the code as generated tokens, not typed numbers 0 colour literals in 70 stylesheet(s) outside src/scss/tokens/. wp-starter/design-tokens present: DESIGN.md, scripts/build-tokens.mjs, src/scss/tokens/_tokens.generated.scss, theme.json. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 4 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25
pass html.doctype-and-lang Doctype and a correct lang attribute on every page 7 of 7 template(s) carry the HTML5 doctype and a lang that agrees with the language root the URL sits under (en-US x7) - the sample sits under 1 of the 4 configured language root(s) (/), and the visible copy itself was not read - / -> doctype ok, lang="en-US", root / (en); /blog/ -> doctype ok, lang="en-US", root / (en); /contact/ -> doctype ok, lang="en-US", root / (en); /privacy-policy/ -> doctype ok, lang="en-US", root / (en); /deploying-without-drama/ -> doctype ok, lang="en-US", root / (en); /design-system/ -> doctype ok, lang="en-US", root / (en); /product-page/ -> doctype ok, lang="en-US", root / (en); the sample sits under 1 of the 4 configured language root(s) (/); the visible copy itself was not read, so the language of the words on the page is inferred from the config, not measured
pass html.single-h1 One h1 per page, and no skipped heading levels 7 of 7 template(s) have exactly one h1, none shared between pages, no heading above it and a heading outline with no skipped level (6 to 46 headings per page) - / -> 1 h1, 23 headings, outline 12233332223332233323333; /blog/ -> 1 h1, 18 headings, outline 122222222222223333; /contact/ -> 1 h1, 9 headings, outline 122323333; /privacy-policy/ -> 1 h1, 10 headings, outline 1222223333; /deploying-without-drama/ -> 1 h1, 6 headings, outline 123333; /design-system/ -> 1 h1, 46 headings, outline 123332333233232322332333333333332333233...; /product-page/ -> 1 h1, 29 headings, outline 12333222333322344423332323333; read from the server's HTML, so a heading or an id that a script writes into the page afterwards is not counted
pass html.landmarks Page regions use landmark elements 7 of 7 page(s) checked for landmarks
pass html.bem-naming Class names follow BEM and match their block slug 24 block root(s) and every class attribute in 184 template file(s) read, 217 design-system class(es): every root class is its slug, and no class outside the design system names a colour or a size
pass html.no-inline-handlers No inline event handlers or javascript: URLs 7 of 7 template(s) read, 2826 KB of rendered HTML: 0 of the 120 known on* event attributes and 0 javascript: hrefs, in the document the browser would build. A template sample, not every rendered page - /: 471 KB, clean; /blog/: 350 KB, clean; /contact/: 271 KB, clean; /privacy-policy/: 340 KB, clean; /deploying-without-drama/: 328 KB, clean; /design-system/: 615 KB, clean; /product-page/: 451 KB, clean; scope: 7 of 7 template(s), which is a SAMPLE and not every rendered page - which URLs a site serves is not readable from a template list. Handler names are a closed list of 120; comments and script bodies were blanked before scanning, and a soft 404 that answers 200 with real HTML is indistinguishable from the template it replaced
pass perf.cls CLS at or under 0.1 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: / 0.001 (0.000-0.001); /blog/ 0.000 (0.000-0.000); /contact/ 0.000 (0.000-0.000); /privacy-policy/ 0.000 (0.000-0.000); /deploying-without-drama/ 0.015 (0.000-0.015); /design-system/ 0.000 (0.000-0.000); /product-page/ 0.000 (0.000-0.054). The slow-network reload watched by eye was not exercised
pass perf.inp-tbt TBT at or under 200ms and INP at or under 200ms TBT 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: / 125 ms (51 ms-140 ms); /blog/ 18 ms (7 ms-37 ms); /contact/ 0 ms (0 ms-0 ms); /privacy-policy/ 2 ms (0 ms-51 ms); /deploying-without-drama/ 19 ms (0 ms-43 ms); /design-system/ 78 ms (76 ms-318 ms); /product-page/ 25 ms (21 ms-189 ms); no field data for INP
pass perf.score-floor Mobile performance score at or above 90 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: / 91 (90-93); /blog/ 90 (90-90); /contact/ 99 (94-99); /privacy-policy/ 100 (95-100); /deploying-without-drama/ 94 (94-95); /design-system/ 94 (89-97); /product-page/ 93 (93-94)
pass perf.bundle-budgets JavaScript and CSS stay inside their budgets 10 of 10 assertions pass in tests/bundle-size.test.mjs, 0 skipped - the run printed no byte figure; the ceilings tests/bundle-size.test.mjs asserts are 16450, 1800, 12350, 55000, 14000, 8000 bytes - node --test --test-reporter=tap tests/bundle-size.test.mjs (cwd /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom) -> exit 0; 10 tests, 10 pass, 0 fail, 0 skipped; newest built file 2026-09-25 (dist/.vite/manifest.json), newest source 2026-09-25 (src/scss/layout/_content-grid.scss). The accept's other branch is not settled here: a RAISED budget is allowed when the same commit says what the extra bytes bought, and whether a sentence explains a number is a reviewer's judgement, not a measurement. A raise made with no note leaves a green test behind it and reads as a pass above. This check also measures the budget test's BEHAVIOUR rather than its pin - it never reads `.wp-starter-version`, so a vendored copy that has drifted from `wp-starter/bundle-size-test` is graded as whatever it now contains.
pass perf.no-render-blocking Nothing render-blocking that does not have to be 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json, render-blocking audit read on every run: / nothing listed; /blog/ nothing listed; /contact/ nothing listed; /privacy-policy/ nothing listed; /deploying-without-drama/ nothing listed; /design-system/ nothing listed; /product-page/ nothing listed
pass perf.lazy-and-priority Below-the-fold images lazy load, the LCP image does not 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json, LCP element read off every run, the fold read in Chrome at 412 x 823: / LCP img.block-hero__image; /blog/ LCP img.post-card__image (first of 10 in the page); /contact/ LCP is p, not an image, so only the fold was judged; /privacy-policy/ LCP is p, not an image, so only the fold was judged; /deploying-without-drama/ LCP img.post-single__image; /design-system/ LCP is p, not an image, so only the fold was judged; /product-page/ LCP img.block-hero-b__poster
pass perf.fonts Fonts are subset woff2, self-hosted, with a swap policy 7 of 7 template(s) read for fonts
pass perf.caching-and-compression Static assets are compressed and cached far-future html gzip, css inlined in the HTML, js br (block-registry-rleWQ2PH.js); 3 hashed asset(s) checked
pass perf.third-party Third-party scripts are counted and justified 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json, third-party summary read on every run: / no third party; /blog/ no third party; /contact/ no third party; /privacy-policy/ no third party; /deploying-without-drama/ no third party; /design-system/ no third party; /product-page/ no third party
pass perf.page-weight Each template has a transfer and DOM budget 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: / 519 kB, 2646 elements (over 1400, passed by measurement: TBT 125 ms on the only page this template renders); /blog/ 469 kB, 1483 elements (over 1400, passed by measurement: TBT 18 ms, and this is the largest page its template renders); /contact/ 333 kB, 1406 elements (over 1400, judged on /product-page/, the largest page its template renders: TBT 25 ms); /privacy-policy/ 339 kB, 1388 elements; /deploying-without-drama/ 380 kB, 1372 elements; /design-system/ 423 kB, 4865 elements (over 1400, passed by measurement: TBT 78 ms, and this is the largest page its template renders); /product-page/ 418 kB, 2127 elements (over 1400, passed by measurement: TBT 25 ms, and this is the largest page its template renders)
pass perf.ttfb Server response starts inside 800 ms 7 template(s), median of 3+ mobile runs, psi/2026-09-25.json: slowest /blog/ 19 ms (10-19); /contact/ 14 ms, /deploying-without-drama/ 14 ms, /design-system/ 14 ms, / 10 ms, /product-page/ 10 ms, /privacy-policy/ 8 ms
pass practices.no-secrets No credential ever reaches the repository or the web root 4 environment path(s) probed (/.env -> 403, /.env.local -> 403, /.env.production -> 403, /.env.bak -> 403), all refused or redirected away; 0 credential-shaped file(s) among the 813 ever added across 57 ref(s) of a full checkout of /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom, .env ignored by .gitignore:8:.env. Filenames only, and a fixed list of them: .env, .env.local, .env.production, .env.bak over HTTP, and .env* / .envrc / *.env / auth.json / wp-config.php in the history. A credential hard-coded in an ordinary tracked file, or served from a path not on that list, is not covered - /.env -> 403; /.env.local -> 403; /.env.production -> 403; /.env.bak -> 403; git -C /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom log --all --no-renames --diff-filter=A --name-only, toplevel /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; Filenames only, and a fixed list of them: .env, .env.local, .env.production, .env.bak over HTTP, and .env* / .envrc / *.env / auth.json / wp-config.php in the history. A credential hard-coded in an ordinary tracked file, or served from a path not on that list, is not covered
pass practices.output-escaping All dynamic output is escaped at the point of output Both halves measured 2026-09-25. Code: every echo, print and <?= of a $ expression in the theme's PHP that carries no esc_*, wp_kses*, absint or json_encode on the same line - 206 lines, from git grep - was traced to its source: 206 safe, 0 unsafe. They are escaped where the variable is built, clamped to an enum, integers, core-escaped image helpers, or pass through helpers read and confirmed to escape every dynamic part (inc/helpers.php vocalcom_btn, vocalcom_render_image, vocalcom_video, vocalcom_icon). Live: a published local page carried <script>alert(1)</script> in the hero, text, CTA and features-3 fields, the button title and a javascript: button URL; https://vocalcom.test/xss-probe-tmp/ served 0 raw script tags and 13 escaped as &lt;script&gt;, the wysiwyg field stripped the tag, and the javascript: URL did not render. The probe page was deleted. Re-read after the same day's hook refactor: git grep -E '(echo|print|<?=)[[:space:]]*$' without an escaper gives the same 100 lines as before it, differing only in static data-* attributes, and the new outputs escape or validate (the waveform and video hook argument is matched against ^data-[a-z0-9-]+$, the Styleguide srcset goes through esc_url and esc_attr, the pagination is core's own escaped markup).
pass practices.console-clean Production loads with a clean console 7 of 7 page(s) loaded, scrolled and navigated from
pass practices.self-cleaning-modules Scripts clean up after themselves on page transition / to /design-system/ and back five times: 51 listeners on window and document and 31 ScrollTriggers, the same as the first load
pass practices.handover-notes The project ships with the notes needed to run it docs/EDITOR-HANDOVER.md plus the runnable kit in docs/editor-test/, in the repo rather than in a chat thread. CLAUDE.md and .claude/DECISIONS.md carry the developer half, with DECISIONS.md stated as the single source so the same decision cannot be recorded in six places again.
pass practices.soft-navigation-parity A soft navigation leaves the document as a hard load would / to /design-system/ by transition: title, canonical, og:url, og:type, body classes and stylesheets match a hard load, and one pushState fired, which is what a history-change page-view trigger fires on
pass practices.tests-before-deploy The theme's contracts are tests, and a deploy runs them first Measured 2026-09-15: package.json:26 deploy and :27 deploy:prod run npm run build:check && npm test && node deploy.mjs, so the build, the chunk assertion and the suite (87 test files, 1391 tests in about 8 s) all run before the upload and a red exit stops it. Proven the same day: the first deploy that ran the assertion refused to upload on a real defect, the preload helper split out of the entry since 2026-08-31, and uploaded only after the fix landed. The suite reads the built output rather than skipping it, because the deploy builds first: tests/bundle-size.test.mjs measures dist/ as just built.
pass seo.title-description Titles and descriptions are present, unique and the right length 7 of 7 sampled page(s) read, 0 fault(s) - title/description length: /: 43/109, /blog/: 50/110, /contact/: 53/144, /privacy-policy/: 60/160, /deploying-without-drama/: 34/154, /design-system/: 59/129, /product-page/: 58/152
pass seo.https-and-mixed-content HTTPS is enforced and nothing loads over http http://vocalcom.colibrity.com/ -> 301 -> 200; 7 page(s) scanned, 0 http:// asset(s) - element attributes only; url() inside inline style blocks is not scanned
pass seo.internal-links-resolve No internal link is broken 88 internal link(s) walked from 7 page(s), 0 broken or chained
pass seo.hreflang Multilingual sites declare reciprocal hreflang 4 language root(s), 60 alternate pair(s) walked, 16 distinct URL(s) read, 0 fault(s) - walked from the home page and each language root; inner templates are not walked
pass crawl.sibling-hosts Only the production host serves an indexable copy of the site 1 sibling host(s), 2 probes: 0 indexable, 0 gated, 2 noindex - https://vocalcom.tastysites.pl/ -> noindex; https://vocalcom.tastysites.pl/blog/ -> noindex
pass crawl.language-urls Each language version has its own crawlable URL 4 of 4 language root(s) answered when requested with Accept-Language: ja; 4 answer 200 with no redirect, 4 are linked as anchors from the home page; 3 of 3 region root(s) answered when requested with Accept-Language: ja, 3 answer 200 with no redirect, 3 findable by anchor, hreflang or sitemap - GET / (Accept-Language ja) -> 200; GET /fr/ (Accept-Language ja) -> 200; GET /es/ (Accept-Language ja) -> 200; GET /pt/ (Accept-Language ja) -> 200; region GET /af/ (Accept-Language ja) -> 200 [anchor no, hreflang yes, sitemap not read]; region GET /la/ (Accept-Language ja) -> 200 [anchor no, hreflang yes, sitemap not read]; region GET /me/ (Accept-Language ja) -> 200 [anchor no, hreflang yes, sitemap not read]
pass crawl.content-in-server-html The content that must rank is in the server HTML 7 of 7 template(s) carry an h1, body copy and nav with JavaScript off - /: h1 "AI-first contact center platform", 4 nav links in nav and header; /blog/: h1 "Blog", 6 nav links in nav and header; /contact/: h1 "Contact", 5 nav links in nav and header; /privacy-policy/: h1 "Privacy Policy", 5 nav links in nav and header; /deploying-without-drama/: h1 "Deploying without drama", 6 nav links in nav and header; /design-system/: h1 "Design system", 7 nav links in nav and header; /product-page/: h1 "The AI contact center built for banks th", 5 nav links in nav and header
pass crawl.published-urls-resolve Every published URL answers at its own address 49 URL(s): 42 from the sitemap at /sitemap_index.xml, 29 navigation link(s) from 10 page(s), every one 200 within one redirect
pass crawl.language-parity Every language publishes the same set, or says what it leaves out node bin/lang-parity-check.mjs against the local site on 2026-09-25 after the fix: 25 things checked across en, fr, pt-pt and es, 0 failed, 9 expected, each expected gap with a written reason, block stacks and copy included. The English primary menu is back to 4 items: bin/seed-starter.php no longer seeds that menu, which is what had re-added Elements, Design System and Kitchen Sink after the 2026-09-10 run. /elements/ is an internal page, noindexed and out of the sitemap, declared English-only. Staging after the db:push carries 4 primary items in all four languages.
pass onpage.schema-fidelity Structured data says what the page actually says 7 sampled page(s) with JSON-LD read against their own copy (/, /blog/, /contact/, /privacy-policy/, /deploying-without-drama/, /design-system/, /product-page/): every headline, author, date, rating, price and count agrees with the page
pass onpage.favicon The site ships a favicon Google can crawl 96x96 png at /wp-content/themes/vocalcom/favicon-96x96.png (robots.txt is a blanket Disallow: /, so the indexability half cannot be judged on this host) - GET https://vocalcom.colibrity.com/wp-content/themes/vocalcom/favicon-96x96.png -> 200; robots.txt Disallow: /
pass admin.client-mode The client's admin is trimmed, and the trim can always be undone Done as per-user self-service rather than as a site-wide switch, and the rule allows both. inc/admin-cleanup.php hides menus per user from that user's own Profile screen; empty meta means every menu is visible, so nothing is hidden by default and an administrator can un-hide anything they hid. That satisfies the reversal clause by construction: there is no state a user can put themselves in that they cannot get out of. It also satisfies the clause this project is the reason for. The theme replaced its old single site-wide toggle with the per-user one precisely because the toggle exempted administrators, and administrator is the role Colibrity's own people hold on this site.
pass admin.capability-boundaries What the client must not do is refused, not hidden The three inboxes are browse-only by capability, not by CSS: cpt-message.php:105-107, cpt-subscriber.php:89-91 and cpt-consent-log.php:115-117 each register with map_meta_cap => true and create_posts => do_not_allow. region-content.php:69-76 does the same for the region taxonomy with manage_terms. The URL half of the acceptance has not been run against a live account, so this is the mechanism verified rather than the refusal observed.
pass admin.fields-in-code Field definitions live in the repository, and a field name is the data Every field group is registered in PHP under version control - inc/acf-fields.php, inc/acf-options.php and inc/legal-docs.php - and none is authored in the ACF UI, so a field change arrives as a reviewable diff. The rename half of the rule is a discipline going forward rather than something a first grading can evidence: there is no previous release to diff field names against yet.
pass admin.duplicate-content An editor can duplicate any content item, translations included wp-starter/duplicator present: inc/duplicate-core.php, inc/duplicate-post.php, bin/duplicate-check.php, tests/duplicate-post.test.mjs. Project stamped wp-starter 1.23.0, verified against wp-starter 1.23.0. Presence and version only - the behaviour was not exercised - 4 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-09-15
pass comms.submission-delivery A submission is stored before anything is emailed Measured locally 2026-09-25: a temporary mu-plugin made every wp_mail() fail (pre_wp_mail returning false) and one contact form was POSTed to https://vocalcom.test/wp-json/vocalcom/v1/contact with the consent ticked. It answered 200, and in the same request a vocalcom_message row was written with vocalcom_msg_mail_sent = no, shown as the Email sent column of the Messages list (inc/cpt-message.php:198), beside a vocalcom_consent_log row: purpose contact, type privacy, status accepted, version 1.0, text "I accept the Privacy Policy.". Nothing was lost when mail failed. The mu-plugin was trashed and both probe rows deleted. Worth knowing: the visitor is still told the message was sent, which is true of the storage and not of the email.
pass comms.conversion-events The events that count are agreed, fired once, and observable wp-starter/conversion-events present: inc/analytics-events.php, inc/analytics-capi.php. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 2 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25
pass design.not-colour-alone No information is carried by colour alone Measured 2026-09-25 on https://vocalcom.colibrity.com / , /contact/ and /product-page/ with filter: grayscale(1) on <html> and reduced motion so every section had entered. Exports: docs/evidence/2026-09-25/greyscale-home.jpg, greyscale-contact.jpg, greyscale-product-page.jpg, greyscale-contact-error.jpg and greyscale-reviews-dots.png. Links inside running text are underlined; the Learn more text links differ by weight (600 against 400) and a trailing mark; required fields carry a * in the label; an error shows its message text and sets aria-invalid; the active tab is a filled dark pill with light 600 text against light pills; the active reviews dot is opacity 1 against 0.3. There are no charts, map keys or status chips on these pages. 0 distinctions left to hue alone.
pass forms.server-side-validation Every validation rule runs again on the server wp-starter/form-registry present: inc/forms-core.php, inc/rest-contact.php, src/js/modules/form-validate.js. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 3 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25
pass forms.error-timing An error appears on blur, not while someone is typing 1 of 1 form(s) typed into, left and corrected
pass forms.input-font-size Form inputs are 16 pixels so iOS does not zoom on focus 7 of 7 page(s) read at 390px
pass forms.input-type-and-autocomplete Every field declares its type and its autocomplete token 7 of 7 page(s) with every identity field read
pass forms.submit-lifecycle A submit happens once and loses nothing Measured 2026-09-25 on https://vocalcom.colibrity.com/contact/ in Chrome after the deploy: a double click on Send sent one POST, and replaying that same payload from the page answered 200 with the same success message and stored nothing, so the pair left exactly 1 message and 1 consent record (5 and 5 across the whole run of five distinct submissions). The server-side guard is a fingerprint of the form, its sanitised data and the consents given, written only after a successful store (inc/rest-contact.php, step 4b); locally two identical curl POSTs stored 1 and 1, where the same test stored 2 and 2 before the fix. A refused submit (the 429 above) left every typed value and the ticked consent in the form. All test rows were deleted afterwards. tests/forms-rate-limit.test.mjs pins the guard.
pass forms.cache-safe-submit A form still submits from a cached page wp-starter/form-registry present: inc/forms-core.php, inc/rest-contact.php, src/js/modules/form-validate.js. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 3 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25
pass legal.consent-record Every consent is recorded with what it takes to prove it wp-starter/consent-log present: inc/cpt-consent-log.php, inc/privacy-exporters.php, inc/rest-contact.php. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 3 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25
pass legal.cookie-banner Refusing cookies is as easy as accepting them Fixed 2026-09-25 and checked in a browser locally, because the banner renders only when analytics is on and staging has no tag. Reject all is now the same primary, small button as Accept all (template-parts/global/cookie-bar.php:108-128), both one click from the first screen. With tracking forced on for one page load through a throwaway mu-plugin that wrote no option row, the two computed identically: background #ff4400, text #f7f5f2, weight 600, 16px, 36px tall, differing only in width by label length. Settings and Save choices stay ghost. tests/analytics.test.mjs:550 fails if Reject ever stops matching Accept's variant and size.
pass legal.no-tags-before-consent No analytics or marketing tag fires before a choice wp-starter/analytics-registry present: inc/analytics-vendors.php, inc/analytics-head.php, src/js/modules/tracking.js, inc/consent-cookie.php. Project stamped wp-starter 1.23.0, verified against wp-starter 1.22.2. Presence and version only - the behaviour was not exercised - 4 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-08-25
pass legal.no-pii-in-urls No personal data travels in a URL 7 of 7 page(s) with every form submitted (3 submit(s), 11 valid form(s) that send nothing, 2 POST(s) sent to the real server as acknowledged gate probes and their responses followed); no analytics request fired on this host, so the page-path report can hold none
pass states.no-js-fallback Content is visible when JavaScript does not run Measured failing and fixed the same day, 2026-09-25, on https://vocalcom.colibrity.com with JavaScript disabled. Before: / and /product-page/ each left 38 text elements at opacity 0 - every section below the first - because .anim-hidden is lifted only by a block module, by reduced motion or by the first-screen rule; /blog/ was clean. The fix is @media (scripting: none) beside the reduced-motion override in src/scss/blocks/_shared.scss, pinned by tests/blocks-split.test.mjs. After the deploy: 0 of 237 text elements invisible on /, 0 of 256 on /product-page/ and 0 of 104 on /blog/, and with JS on the entrance still runs (measured locally: the articles header at opacity 0 before scroll, 1 after). Inactive tab panels and review slides stay hidden without JS by design, so each such section shows its first state.
pass states.error-pages A missing URL returns a real 404 in the right language 4 language root(s): / 404 lang=en-US, /fr/ 404 lang=fr-FR, /es/ 404 lang=es-ES, /pt/ 404 lang=pt-PT - GET https://vocalcom.colibrity.com/colibrity-gate-no-such-page/ -> 404 lang="en-US"; GET https://vocalcom.colibrity.com/fr/colibrity-gate-no-such-page/ -> 404 lang="fr-FR"; GET https://vocalcom.colibrity.com/es/colibrity-gate-no-such-page/ -> 404 lang="es-ES"; GET https://vocalcom.colibrity.com/pt/colibrity-gate-no-such-page/ -> 404 lang="pt-PT"
pass states.scroll-reveal-recovery Scroll reveals fire on deep links and on back navigation nothing hidden above the scroll position after loading /product-page/#wps-form-1-hp, or after leaving and returning by a bfcache restore
pass ops.deploy-target-pinned A destructive deploy refuses to run against the wrong target deploy.mjs:83-92 reads PROD_EXPECTED_URL_HOST from the environment file with no hardcoded fallback, and :135-142 refuses the production deploy unless the resolved site host matches it. An empty pin refuses too, which is the right default before go-live, and db-sync.mjs carries the same rails. The deliberate-misfire half of the acceptance has not been run, so this is the refusal read in the code rather than observed.
pass security.rest-route-authorisation No REST route writes without an authorisation check or a stated bound 3 register_rest_route call(s), 3 live route(s) under vocalcom/v1, 3 row(s) in docs/EDITOR-HANDOVER.md; registered in code, not live: /csp-report (inc/csp-report.php:124): every live route is capability-gated or listed with its bound
pass security.submission-store-private Stored form submissions are unreachable without logging in 2 submission type(s): vocalcom_message, vocalcom_consent_log; vocalcom_message #1510 answers 200 at its permalink and 404 at its REST route
pass security.upload-types-and-execution Uploads cannot execute, and no script-capable type is allowed read on colibrity-staging as an administrator: no type added to core's list, an uploads .htaccess: no script-capable type is added, and .php and .phtml are refused in /home/u835041983/domains/vocalcom.colibrity.com/public_html/wp-content/uploads
pass security.response-headers Every HTML response carries the baseline security headers 3 of 3 sampled URL(s) carry all five headers with the rule's values (/ max-age=31536000; /blog/ max-age=31536000; /contact/ max-age=31536000) - GET / -> 200 [nosniff=nosniff, x-frame-options=sameorigin, referrer-policy=strict-origin-when-cross-origin, permissions-policy=geolocation=(), camera=(), microphone=(), hsts max-age=31536000]; GET /blog/ -> 200 [nosniff=nosniff, x-frame-options=sameorigin, referrer-policy=strict-origin-when-cross-origin, permissions-policy=geolocation=(), camera=(), microphone=(), hsts max-age=31536000]; GET /contact/ -> 200 [nosniff=nosniff, x-frame-options=sameorigin, referrer-policy=strict-origin-when-cross-origin, permissions-policy=geolocation=(), camera=(), microphone=(), hsts max-age=31536000]; no response was marked a cache HIT, so the cached-response half of the acceptance test is not settled here
pass security.wp-attack-surface WordPress's default attack surface is closed at setup xmlrpc.php POST 403, ?author=1 301 -> /, /wp-json/wp/v2/users 401, DISALLOW_FILE_EDIT true at runtime
pass security.comments-closed Comments are closed at every layer unless the brief asks for them wp-starter/comments-off present: inc/comments-off.php, tests/comments-off.test.mjs. Project stamped wp-starter 1.23.0, verified against wp-starter 1.23.0. Presence and version only - the behaviour was not exercised - 2 file(s) under /Users/tasty/Sites/vocalcom.test/wp-content/themes/vocalcom; .wp-starter-version names 1.23.0; shipped.yml verified 2026-09-15