Repository navigation
Conversation
The header and footer carried a placeholder: a teal rounded square with a green square inside, next to the name in live text. Both are replaced by the real wordmark — "Execil" in Satoshi Black with the tittle of the "i" swapped for a cross badge, drawn from a client reference image. The letterforms are outlines, not <text>. The badge has to land exactly where the dot was at every size, which live text cannot promise across fallback fonts. Wordmark carries the paths inline rather than pointing at the file in public/brand/, so the letters can take currentColor and the badge can take theme tokens; an <img> could do neither. Two token changes fall out of that: textOnBrand, because the cross sits on the brand fill and needs a colour that survives the Dark theme. Button currently uses surfaceRaised for this, which the Dark theme turns translucent — a latent bug, left alone here. surfaceBrand overridden to teal600 on the inverse surface, because teal700 on ink900 leaves the badge reading as an unstyled dot. Nothing else on a dark subtree uses surfaceBrand today. public/brand/ also holds the second direction the client asked to keep. Nothing references it; see the README there.
The badge was surfaceBrand, teal700. Against the ink50 page that measures 5.97:1, which looks fine in isolation — but the badge does not sit on the page, it sits against the ink900 letterforms, and there it separates by only 2.74:1. At header size the mark stopped reading as a badge and became part of the "i": one dark mass with a speck in it. teal600 lifts that adjacency to 4.13:1 and still holds the white cross inside the badge at 4.18:1. Brighter trades one for the other — teal500 would reach 5.72:1 against the letters but drop the cross to 3.02:1, mush at 16px. teal600 is the balance point, and it is already the accent the hero uses on this ground. That makes the badge one colour on both grounds, so surfaceMark carries it rather than surfaceBrand, and the Dark theme no longer needs the surfaceBrand override this branch added. The tagline keeps teal700. It is real text at a small size and needs the 6.31:1.
This reverts commit 558ba50.
Colour was the wrong lever. The badge replaces the tittle of the "i", so it does not sit against the page — it sits against the letterforms, and teal700 separates from ink900 by only 2.74:1. Reaching for a brighter teal fixed that number and cost the white cross inside the badge, which is the trade the previous attempt lost. Space works where value did not. A ring 20% of the badge radius is masked out of the letterforms, so the ground shows through around the badge. A mask rather than a filled circle, because the ring then shows whatever the mark is placed on — the same drawing works on the light page and on the inverse footer with no second colour. Narrower than ~15% stops registering at 28px; wider than ~25% starts eating the stem of the "i". Every colour stays where it was. The component needs a unique mask id per instance, since the header and footer both render one, so this is the first hook in Wordmark. Verified in the prerendered HTML: two distinct ids, each matching its reference, and the same values after hydration.
Below the nav breakpoint the square source is cover-cropped into a 4:3 band. Centred, the crop took the top of her head; anchored to the top, it showed the empty band above her hair instead. "right 18%" sits between: the head clears with a small margin, and the same anchor still serves desktop because a box taller than the image only crops sideways. The band itself is now capped at 480px through layout.heroBandMax. Just under the breakpoint it reached 700px, which is most of a tablet's first screen spent on the picture.
The Worker's own workers.dev subdomain serves the whole site and stays enabled by choice (#34), and the preview aliases from non-production branch builds live on the same domain. Both are public, and robots.txt says Allow: /. The prerendered canonical already points at execil.net, which Google usually honours; X-Robots-Tag is the signal that does not depend on it being honoured. _headers matches host-first when the rule is an absolute URL, and `:name` is a placeholder for one label, so a single rule covers execil.<subdomain>.workers.dev and every <alias>-execil.<subdomain> preview. No Worker change and no run_worker_first: these are asset responses, which is what every prerendered page is. nosniff and Referrer-Policy come along while the file is open. Not HSTS — that one binds the apex and every subdomain for as long as browsers remember it, so it is a decision, not a commit.
logo.png is new. Organization.logo pointed at og.png, which is a 1200x630 social card with a headline across it — Google reads that field for the knowledge panel and wants the logo itself, on white, 112px minimum in both axes. The new file is the wordmark on white at 1024x406. The OG card's lockup was still the placeholder mark, a teal square with a green square inside it. Both now draw the real wordmark from the same geometry as public/brand/cross-dot/. Two things about drawing it in Pillow. Glyph pen positions are listed rather than laid out, because this Pillow has no Raqm: its own layout drops the kerning, which would set the word 25 units wide and land the badge 78 units late. And the knockout ring samples the canvas before a letter is drawn — after the text, that point is the tittle, and the ring came out black.
The meta description was 215 characters against Google's ~155, so it was being cut mid-sentence in results. Now 155. The 404 inherited the home page's title and its `index, follow`. Its 404 status is what actually keeps it out of the index, but the tab said "Execil | Medical Billing & RCM" and a crawler reading markup before status saw an invitation. It cannot set a route head — nothing matched, so there is no route to hang one on. React 19 hoists a rendered <title> into <head>, but the root head's title is emitted first and wins, which left the document with two. So the title moves to the home route, and the root keeps none. The robots meta is deleted outright rather than moved: `index, follow` is the default, so it only existed to collide. Sitemap gains lastmod, the one field there Google reads.
The picture is the LCP element on narrow screens, where it sits above the copy. Markup alone is not enough there: the preload scanner finds the <img> only once the CSS that sizes it has arrived, and fetchpriority cannot start a request nobody has discovered yet. Verified on the built site at 375px — one request for the hero, initiator "link", the 900w candidate, and no "preloaded but not used". That needs the same src, srcset and sizes in two files, so they move to hero-image.ts. A mismatch there is not a no-op: the browser resolves a different candidate and downloads the picture twice. FAQPage covers the eight questions. Expectations first: since 2023 Google shows FAQ rich results only for government and health *authority* sites, and Execil is neither. It is valid, cheap, and read by other consumers of structured data. It sits on the home route rather than beside organizationLd in the root, because the questions render on one page.
Two separate things, both visible as "too much space at the sides". The header put its gutter inside the container rather than outside it, which is the reverse of Section and SiteFooter. That inset the brand by a gutter relative to every section below, so the wordmark sat 24px right of the hero copy it sits directly above — close enough to read as a mistake rather than a margin. And containerWide was the canvas's 1160. At 1440 and up that left the header's three groups closer to each other than to the edges of the screen: 244px of nothing at each side against 110px between the groups, which reads as a narrow strip floating in a wide page. 1280 puts the outer margins and the inner gaps in the same range. The container is shared, so sections widen too. Prose is unaffected — it has its own containerText — and the grids simply get their columns back.
The badge sits on surfaceAccent over the hero's wash, which ends at surfaceAccentSoft — near enough that the 1px border was doing all the separating and not much of it. shadow.sm lifts it. The dot gets a glow: a tight ring plus a soft bloom, alpha doing the work, because a solid ring at 8px just looks like a bigger dot. It is a Dot prop rather than a Badge style, so it stays keyed to tone — a brand dot glows brand, and `subtle` does not glow at all, having nothing to announce. Badge is the only caller today. Eyebrow moves from base to md. At 13px, uppercase and letterspaced, it was reading as a caption under the heading rather than a label over it.
The provider range moves from 1-20 to 35-40 in the three places it is
user-facing — the meta description, the hero lead and the Fit list — plus
the OG card, which is regenerated, and the glossary entry for Practice
that defines the term.
The last trust point was "US-facing team · 24-hour processing": one list
item carrying its own separator, so it rendered a text middle dot where
the other four have a teal Dot. Now two items, six in the row.
"13 wks" was the only abbreviation among four stat cards ("60%", "13.5%",
"25 min"), and the one whose unit a reader has to expand. Now "13 weeks".
Six changes from a review of the first pass. The desktop header was appearing at 940px, but the nav links, the phone number and the CTA need 1000px before they all stop wrapping — so between 940 and 1000 the "desktop" header rendered three lines deep. The breakpoint moves to 1040, a gutter of slack past where it fits. Hero shares that token for its side-by-side layout, and HERO_SIZES repeats the number because a `sizes` attribute cannot read a token; both move together, and the token comment now says so. The comparison table used auto layout, so each column sized to its longest cell and the surplus went to whichever asked for most. That left the Execil column half again as wide as the vendor column it is compared against, with its text stopping well short of filling it. Fixed layout, 22% for the labels and the value columns even, so the comparison does not look weighted before it is read. The hero's top padding drops to the bottom's value. The canvas drew it taller, which made sense when the hero was centred text; the picture now supplies that height on the right. The badge dot only had a static bloom, which reads as a slightly fuzzy dot rather than a live one. It gains a travelling ring on ::after, animating opacity and transform only — no colour named, composited not repainted, and the global prefers-reduced-motion rule in app.css already covers ::after. Stat sources go from xs to base. At 11px they were the smallest text on the page by two steps; one step would not have been visible. Provider bands gain 21-34, 35-40 and 40+ in place of 20+, so the range the copy now names is selectable rather than folded into a catch-all.
The observer only ever called setCurrent when it found a section in the band, so with nothing there the last match stayed highlighted. Scrolling back over the hero left Services lit.
Sources were xs in Results and base in Problem; both are sm now. The assessment findings line was sm, which is caption size for a sentence that carries the offer — md. Pulse 2400ms to 1300ms.
Long block comments explaining what was tried and why, across the files this session touched. Replaced with one line where a constraint is worth recording and nothing where the code already says it. The Wordmark emitter in the scratchpad produces the shorter docstring too, so a regenerate does not undo this.
A link is current from its section's top to the next linked section's top, so Systems reads as Specialties and Fit and Results as Why Us. The last chapter ends at its own bottom, leaving the form unlit. Replaces the 10% intersection band, which went blank over every unlinked section.
Two crisp 2px rings on ::before and ::after, half a period apart, easing out to three times the dot's size, and the dot breathes to 1.18x on the same beat. Replaces the single blurred ring. New motion.easeOut token.
og.png and logo.png are drawn at 3x and downsampled: PIL draws shapes without antialiasing, so the badge circle was a staircase and the cross a blob. The favicon cross is heavier so it holds at 16px. The header wordmark goes from 28px to 32px, which takes the badge from 9px to 10px and the cross past a pixel on 1x screens.
Chrome rasterises masked SVG content through an intermediate surface, so the header wordmark rendered soft while the nav text beside it was crisp. The ring is now subtracted from the glyph outlines with skia-pathops in the generator, leaving plain filled paths: no <mask>, no useId, and the same drawing in public/brand/cross-dot/.
Six of nine suggested changes. The breach clause deferred entirely to the business associate agreement; 45 CFR 164.410(b) sets a ceiling the agreement cannot exceed, so the statutory 60 calendar days is now stated alongside it. The rest are additions to existing wording, each checked against what the site does: transport is TLS 1.2 or higher and older versions are refused (measured against execil.net); no advertising and no profiling by health condition; email is named as an insecure channel for patient data; the under-18 clause now says what happens if we find such data. Turnstile keeps the existing hedge about browser storage rather than taking the suggested "no persistent cookies" — Cloudflare's own docs say Turnstile can issue a cf_clearance cookie, so that claim would be false. What Cloudflare does state, and what is now quoted, is that the signals are not used to identify, profile or target, and that the cookie is partitioned to one site. Both changed pages get today's date, as the Changes section promises. Retention periods, at-rest encryption and a 30-day response deadline are left out: each asserts an operational fact no one has confirmed.
Squashed from seven commits on feat/header-utility-bar. Prototype G, chosen from eight header variants explored on prototype/header-layouts. Header. The old row carried three groups, and space-between could never centre the nav between a 104px wordmark and a 380px cluster of phone plus CTA. The phone moves to a contact strip, 32px on the Inverse surface, right-justified in line with the CTA below it; below the nav breakpoint the email address drops and the phone stays. The main row is full-bleed, so the wordmark and the CTA each sit a gutter from the viewport edge rather than 160px in at 1600. Nav links are 32 apart, 40 above screen.navWide, and the CTA is held 32 off the last link, 64 above it, so the button reads as an action and not a sixth nav item. Also fixed on the way: the header applied its gutter inside the container where Section and SiteFooter apply theirs outside, which inset the wordmark by a gutter against every section below it. Behaviour is unchanged. The scroll-spy highlight, the pin through smooth scroll, and nav_clicked / phone_clicked / cta_clicked all carry over; the prototype had none of them. The email link is untracked, since the typed event union has no event for it. Hero. The strip made the header 101px while the hero kept its 80px top padding, so its first line fell from 149px down the page to 181px. heroTop is now 32px lighter than sectionMd, exactly the strip, and the badge sits back at 149. The mobile bleed derives from the same token, so the picture still meets the header with no seam. The trust row is a grid rather than a wrapped flex row, which broke wherever content ran out and left the last item unaligned. Three equal columns on desktop, two on mobile. Copy. The provider range is 20+ in all four user-facing places, the glossary entry defining Practice, and the OG card, which is regenerated. Form bands lose 21-34 and 35-40. "Specialty-assigned" is rephrased rather than shortened so the claim survives: coders assigned by specialty is the thing being said, and "specialty billing" would read as billing for a specialty clinic. The hero lead is rewritten to drop a subjectless opening fragment and three parallel promise-clauses. New tokens: space.s64, screen.navWide. Link gains a `utility` variant for the strip, so its small size lives in the variant rather than being pushed through `style`.
The badge sat wrong on the "i" in link previews: its knockout ring bit a chunk out of the "l", which the real wordmark's does not. Reimplementing the wordmark in Pillow had already drifted twice before this. Pillow has no Raqm here, so its layout dropped the kerning and the glyph pen positions had to be listed by hand. The ring sampled its colour after the letters were drawn, so it came out black. Now the ring lands in the wrong place. Each fix was local to a symptom. Both files now rasterise public/brand/cross-dot/execil-wordmark-notag.svg through headless Chrome and composite it, so the lockup is the same artwork the site renders rather than a second drawing that has to be kept in step. That drops the pen-position table, the badge and cross geometry, and the ground sampling. It does add headless Chrome as a dependency of the script, which is a real cost for a run-once generator. Worth it: correctness here is only verifiable by eye, and this is the third time by eye caught it late.
scripts/social-images.py writes public/brand/social/. Sizes are from LinkedIn's own spec page: logo 400x400 with a 268x268 floor, cover 1512x256. The 300x300 and 1128x191 numbers in most guides are stale. The logo is the badge rather than the wordmark, because LinkedIn renders it at roughly 60px in the feed, where "Execil" across 400px would be about 50px wide. Opaque white behind it, since LinkedIn puts the logo on both light and dark surfaces. The cover is a strip: wordmark and two lines of copy on the left, the hero photograph fading into the wash on the right. The copy is inset 112px rather than hugging the padding, so a modest horizontal crop does not eat the wordmark. The SVG rasteriser moves to scripts/_brand.py, shared with og-image.py instead of copied. Verified by hash that og.png and logo.png are unchanged by that refactor.
The stacked layout repeated both column names above every value, so "Typical billing vendor" and "Execil" each appeared eight times in a table of eight rows. The names now sit once at the top, on the same two-column grid as the pairs below so they line up, with a rule under them. What ties a value back to its heading after the heading has scrolled away is the teal on the last column, which the cells already had; the heading takes the same teal so the pairing is stated rather than inferred.
scripts/__pycache__ was committed alongside _brand.py.
This was referenced Sep 13, 2026
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.
Four commits of content since #41. GitHub will list 29 commits and a large
diff on this PR — see the note at the bottom, that is an artefact of #41 being
squash-merged, not new work. The real change against
mainis ten files:The link preview badge
The badge sat wrong on the "i" in WhatsApp previews: its knockout ring bit a
chunk out of the "l", which the real wordmark's does not.
The cause was that
og.pngcontained a second implementation of the logo,redrawn in Pillow, which had drifted three times. Pillow has no Raqm here, so
its layout dropped the kerning and the glyph pen positions were listed by hand.
The knockout ring sampled its colour after the letters were drawn and came out
black. Then the ring landed in the wrong place. Each fix addressed a symptom.
og.pngandlogo.pngnow rasterisepublic/brand/cross-dot/execil-wordmark-notag.svgthrough headless Chrome andcomposite it, so the lockup is the artwork the site renders rather than a copy
kept in step by hand. That deleted the pen table, the badge geometry and the
colour sampling from the script. It costs a headless Chrome dependency in a
run-once generator, which is a real trade: correctness here is only checkable by
eye, and by eye caught it late three times.
LinkedIn Page assets
scripts/social-images.pywritespublic/brand/social/. Sizes come fromLinkedIn's spec page:
logo 400x400 with a 268x268 floor, cover 1512x256. The 300x300 and 1128x191
figures in most guides are stale.
The logo is the badge rather than the wordmark, because LinkedIn renders it at
roughly 60px in the feed where "Execil" across 400px would be about 50px wide.
Opaque white behind it, since LinkedIn puts the logo on both light and dark
surfaces.
I could not verify whether LinkedIn crops covers horizontally, so the copy is
inset 112px rather than sitting on the padding. If it clips after upload, that
is one constant.
Mobile comparison table
The stacked layout printed both column names above every value, so "Typical
billing vendor" and "Execil" each appeared eight times in eight rows. They now
appear once, on the same two-column grid as the pairs below. What ties a value
to its heading after the heading scrolls away is the teal on the last column,
which the cells already had; the heading takes the same teal.
Verified at 375px: one occurrence of each heading, and no horizontal overflow —
the card spans 24 to 351 in a 375 viewport and the widest cell's
scrollWidthequals its width.
Also
scripts/__pycache__had been committed alongside_brand.py. Removed, and__pycache__/is now ignored.Note on the commit count
#41 was squash-merged: its merge commit
ad98d88has a single parent, sonone of
dev's commits are ancestors ofmaineven though their content is.The merge base is still
0b1bac1, which is why this PR lists 29 commits.Merging this one with a merge commit rather than a squash fixes it —
devbecomes an ancestor of
mainand the next PR will show only its own work.Squashing again keeps the two histories diverging and every future release PR
will look like it is re-landing everything.
Checked
pnpm checkandpnpm buildpass.