Skip to the guide
Texas Find 'Em The map How we know

Home Style guide

The design system, measured

Texas Find 'Em
Style Guide

Every specimen on this page is painted by the site's own stylesheet, copied out of index.html by scripts/build.mjs on every build. Nothing here is a mock-up of a control — it is the control. If a button changes on the site it changes here at the next build, and if it stops matching, the build is what has gone wrong.

The contrast figures below are measured in your browser as the page loads, not typed in, so they can never be stale. Switch schemes and they recompute.

Stamped from index.html on August 24, 2026 data — 1,895 lines of the site's own CSS.

01 — Color

Eight schemes, and not one color is typed into a component. Each scheme sets eight values; every rule on the site reads one of those, or one of the three grays derived from --key-rgb underneath them. That is what lets a control be written once and work in all eight. Adding a scheme is a :root[data-theme=…] block, a row in THEMES, and the literal list in the small <head> script that paints before the first frame — three places, and check-browser.mjs fails if the last two disagree.

Pick a scheme — everything below repaints

The colors this scheme sets

The three grays derived from them

There is an eighth value that has no swatch: --key-rgb, which is --key again as three bare numbers. It exists only so the grays below can be built with rgba(), and it has to be kept in step by hand — a scheme whose --key-rgb disagrees with its --key gets body copy in one color and headings in another, with nothing to say why.

Don't write a hex into a component rule, and don't reach for a gray that isn't one of these three. A fourth gray is a fourth thing to check in eight schemes.

--accent-text is for small text, never for a display heading

--accent-text is the accent lightened until it clears 4.5:1 as small text on a panel — on Lonestar, #FD878A. That is what it is for: a footer link, an error line, Reset, a count in the bar. Lightening a red that far produces a washed-out pink, and a display word set in it reads faded rather than emphatic — the opposite of what an <em> in a headline is asking for. Display headings are --key; an accent inside one is the brand --accent.

The line is a measurement, not a judgement: a rule declaring a font-size under 16px has said it is a label, which is the small-text case the token was solved for — a label at 10.5px uppercase mono that happens to be marked up as a heading keeps the pink, because that is the small-text case the token was solved for. No size of its own, 16px or above, or a clamp(), is display type and is bound. verify-docs.mjs check 12 fails the build on any heading that takes it.

Don't reach for --accent-text because a heading needs emphasis. It got in three times that way — once in an h1, twice as an option letter — every time by copying the block next door.

02 — Contrast, measured live

Computed from the rendered page, so these numbers describe what is actually on screen right now. Small text needs 4.5:1; a control's edge or a shape needs 3:1. Anything failing is a real readability problem, not a style preference — this site is read in cars, in Texas, in daylight.

PairWhat it isRatioNeeds 

The worst result across all eight schemes

PairWorst schemeRatioNeeds 

A row failing here fails for somebody, in some scheme, today. Fixing it usually means changing a token rather than a component — which is the point of having tokens.

03 — Type

Three faces, one job each. Bungee is the voice and appears only where the site is speaking as itself. Overpass is everything you read. Overpass Mono is for anything a machine produced — a count, an ID, a date, a label that names a field rather than describing it. A number the reader is meant to compare goes in the mono face so the digits line up.

Bungee · the wordmark, Find 'Em, section headings
Texas Find 'Em
The lockup · .imk inside the name, at headline size
Texas Find 'Em
The same lockup at bar size · three paths, flat 6px a side
Texas Find 'Em
Overpass 700 · a store name, a lead-in
Beechnut H-E-B
Overpass 400 · body copy, at --dim
Stops land under Here ya' go: closest first in Near me, in drive order on a roadtrip, with how far into the drive each one is.
Overpass 700 uppercase, .17em tracking · every field label
Search mode
Overpass Mono · counts, IDs, dates, anything tabular
345 · #768 · heb-541 · August 24, 2026

The mark lives inside the name, not beside it. .imk is one declaration and it serves every place the name is printed — the headline, the sticky bar, the phone nav panel and a document page's bar — because everything in it sizes off 1em. The span reserves cap height and the mark overflows it evenly, so the line box never grows; set a height on the span instead and the whole masthead moves down. Only the gap is set per place: .0603em at the headline, a flat 6px in a bar. .wm-space is a real space at no size, so the heading's own text is still Texas Find 'Em to a screen reader and to a copy-paste.

Find 'Em in the name is --accent, never --accent-text, at every size. It is the brand name, and WCAG 1.4.3 exempts a logotype from the contrast floor by name — text that is part of a logo or brand name has no minimum contrast requirement. That exception is easy to miss from the arithmetic: the bar's lockup is 21px and Bungee's computed weight is 400, so it reads as small text to a checker and was migrated to the lighter tint once. The result was the same two words in two different reds depending on how big they were. A wordmark does not change color with its size.

Werks and Yer are spelled that way on purpose. Both have been "corrected" and reverted once. The one place the joke stops is inside Yer Stuff, where the settings block is headed Settings and not Yer Settings — a second Yer inside the first one is a tic rather than a voice.

Don't set Bungee on anything that is merely important — it is the site's own voice, not an emphasis level. And don't put a comparable number in the proportional face; digits that don't line up can't be scanned.

04 — Controls

.act is the only button shape on this page, Yer Stuff included: 2px border, uppercase, wide tracking, hover lighting the border to the accent, 34px tall and 44 on a phone. Yer Stuff used to have its own smaller, dimmer variant, which made the same kind of thing look like two kinds of thing depending which half of the page you were on. Both of those classes are gone and they don't come back.

.act — resting, and pressed

.seg — a group where exactly one is on

.cbx — the chain picker

.feat — an amenity toggle

.seg as a tab bar — Yer Stuff's three panels

Not a new control — .seg is how this page already says "pick exactly one of these", five times over. What differs is that this one switches a panel, so underneath it is a real tablist: arrow keys, aria-selected rather than aria-pressed, and a roving tabindex so the group is one Tab stop.

.stuff-row — a saved bookmark. Every row is this height.

Lake Austin H-E-B
#768 · Austin · 8 amenities
Open Directions
No longer on the map
Saved as heb-99999 — closed, or renumbered by its chain.

A store number, not a street: sixty-three H-E-Bs in Houston means the town alone cannot tell two apart. An amenity count, not the list. Both lines truncate rather than wrap, because uniform height is the whole point and a narrow screen is the one thing that breaks it.

.locate, .showbtn, .findem — the ones with a job of their own

.sb-btn — the bar's buttons, and the one link that leaves

One place now. This was one declaration for two controls: the masthead carried the same links and .jumplink shared the rule so the two copies could not drift — they had drifted once already, underlined accent text with a down arrow at the top of the page against bordered pills on the bar. One Stop Shop took the masthead's links away: three of the four sections they pointed at no longer exist, the fourth is a page, and the way into it from the box is .boxinfo below. So the bar is the only place this shape appears, and the class it shared its rule with is gone. aria-current marks where you are rather than only where you can go. Below 700px it loses its border and becomes a tab, because a 390px phone has 354px of bar and the mark has to fit in it too.

.stop-hours — the four things a card can say about hours

Open until 11 PM·regular hours
Open 24 hours
Closed·opens tomorrow 6 AM·regular hours
Hours not published

Open and shut are the same weight. A closed store is not a warning, it is the other half of the answer, so neither gets a color the other doesn't and neither gets an icon. “Regular hours” is not decoration. Not one of these chains publishes a holiday schedule anywhere this map can read, so every one of these is a claim about an ordinary week and has to say so — a bare “Open now” on Thanksgiving morning is the failure this caveat exists to prevent. “Hours not published” is a finding, not a blank. A chain that publishes nothing is saying something, and the card says it back rather than leaving a gap that reads as shut.

.hchip — the bookmark row's three states, and the week behind it

Open Closed —
As published by H-E-B on 2026-08-24
Monday6 AM – 11 PM
Tuesday6 AM – 11 PM
Wednesday6 AM – 11 PM
Thursday6 AM – 11 PM
Friday6 AM – 11 PM
Saturday6 AM – 11 PM
Sunday—

The chip is a fixed width in all three states, and that is the whole point of it. A bookmark row is one height whatever the chain publishes — rows used to swing between 78 and 138px, and fixing that is why the closed row carries a store number and an amenity count rather than a street and a list. A chip that grew with its word would put that back, so the times are behind the disclosure with the address and the amenities, where the rest of the detail already lives. It is a caption and not a control, so it wears a pill's clothes rather than an action's. Seven rows, always, so a day nobody published is visibly missing rather than quietly absent.

.trip-when — what an itinerary asks, and .trip-open — what each stop answers

min each 165.7 mi · about 3h 07m driving
Arrive about 8:15 AM · open until 11 PM
Arrive about 9:40 AM · open 24 hours
Arrive about 6:20 AM · closed · opens 8 AM
Arrive about 10:05 AM · no Sunday hours published

The fourth state is the one that matters. Two Shipleys publish Monday-to-Friday hours and nothing at all for the weekend, and the first build of this rendered that as “Closed” on a Sunday. A chain that published nothing has not told you it is shut, and the page must not put that word in its mouth — so an unpublished day says so, in the same italic as “Hours not published” on a card. The control stores nothing. Not the date, not the time, not the minutes — it is something you run over an itinerary, and it starts at now on every load. The date is read off the itinerary and never written back, because an itinerary's id is from_to_date: writing a date would move the drive rather than edit it. Two of the three numbers behind an arrival are measured and one is not. The route's duration is OSRM's own, costed segment by segment; the stop's distance along it is measured; the pace between them is assumed even, which is why every line says “about”. The minutes at each stop are yours, not the site's — six stops at thirty compounds into three hours, and that is far too big a term for the page to invent.

#openNow — a control that must never become a link

Same clothes as Coming soon, because it does the same job. It is deliberately not in the query string, not in a bookmark and not in a shared link: results that change by the hour make a link that lies to whoever opens it tomorrow. The shareable version of this idea is “open 24 hours”, which is a stable fact about twenty specific stores and lives in the amenities. It also hides itself once the hours are past ninety days old, because by then the rest of the page has stopped claiming anything about right now and this would be promising what nothing else will say.

Don't introduce a second button style for one section, and don't reach for a smaller or dimmer variant to signal that something is secondary — the words already do that. If a control genuinely isn't a control, it's a pill; see below.

The box's tabs

Three rooms across the top of the search box — Places, Roadtrips, Settings — and a two-way .seg inside the first two for the mode. They are not a .seg: as a third segmented control stacked on two more the box read as a pile of switches. A .box-tabs is a recipe box — rounded at the top only, right-aligned on the panel's edge, and the selected one takes --panel and overlaps the edge so the tab and the box are one surface rather than two stacked things. Buttons take the 44px floor; a plain .seg button is 26px, right for 5 / 10 / 20 beside a label and wrong for the page's main navigation.

Don't add a fourth. Three fit a 390px row with room to spare; four measured 69–73px each with none, and a fifth room or a word longer than Roadtrips breaks it. See roadmap/one-stop-shop/ for the measurements.

.boxinfo — the way out, in the corner of the box

One link to /how-it-werks/, absolutely placed on the panel rather than written into each pane, so it cannot say three different things on three tabs. It has been in the masthead twice and under the lockup once; all three put the explanation of the box somewhere other than the box. 44px of target around a 23px drawing — the smallest thing on the page still takes the floor everything else here takes — and it is drawn as an inline svg for the same reason the longhorn is: a glyph arrives at a weight nobody chose and will not take a scheme's colors. --faint at rest, because it is the one control in the box nobody came for; --key and a fill on hover, so it is plainly a control the moment you look at it.

Whatever pane you are in

.tp-item / .tp-pick — the schemes, on show or picked

Nine swatches is the right control for half a desktop panel and the wrong one for a phone, where it is nine rows deep and pushes everything under it off the screen. So the same nine are a radiogroup of .tp-item buttons above 760px and a <select> below it — the platform's own picker, one press, no menu to open inside a scrolling panel — with the current scheme's two colors beside it so the choice keeps its colors. One state, two controls: both are written by the same render and repainted by the same paint, and the one that isn't showing is display:none, which takes it out of the accessibility tree as well as off the screen. The check stays on the selected swatch because a border alone cannot say this one to somebody who can't see the color it is drawn in.

Don't show both at once. They are the same setting, and two live copies of one setting is two things to keep in step — the failure the masthead's links and the bar's spent a release drifting into.

05 — Pills are captions, actions are controls

The one thing on a card you cannot press used to wear almost exactly the clothes of the only things you can. A pill is flat: a tint, no border, no hover. An action has a border and lights up. The CSS has to keep saying which is which, because a reader decides what to touch from shape alone.

.pill — a caption. Nothing happens.

PharmacySushi barCurbside

.act — a control. Something happens.

A card never says a store is shut when nobody said so. Hours not published and Closed are different sentences and the page must not collapse them. A stop with no hours has told you nothing, which is not the same as telling you it is closed — and the first is far more common than people expect.

06 — The result card

Three columns, and all three are the height of the card: the chain stripe, the content column holding the store block and the actions row, and the distance. That is what makes the stripe on the left and the rule on the right start and stop at the same two points. The distance column is a sibling of the content column, not a child of it — it used to sit two levels deeper and centered itself against the top two thirds of a card it was only part of.

The card is a <div>, not a button. The part you press to light the dot is button.stop-hit inside it; .stop-actions sits beside that button and never inside it, because a link inside a button is invalid HTML and because tapping Directions must not also toggle the dot.

Directions
3MI

Don't put the distance column back inside the press target, and don't let anything but a coordinate reach a map link — three Buc-ee's in Angleton and sixty-three H-E-Bs in Houston mean a mapping app handed a store's name picks one of them confidently and without saying which. verify-links.mjs asserts that rather than trusting it.

The mark on a card is a bookmark, not a star. Starring reads as rating, and nothing here rates anything; what the button does is put a stop somewhere you can find it again. It is an inline SVG path filled currentColor, because an emoji arrives at a weight nobody chose and won't take the accent when the button is pressed.

A saved row shows a store number, not a street — and an amenity count, not the amenity list. Both exist to buy one thing: every row the same height, whatever a chain happens to publish. The full address and the full list are on the card itself.

07 — Disclosures

This page has one disclosure idea: a <details>. The browser already has the keyboard behavior, the state a screen reader announces, and a find-in-page that can open a closed row. Every one opens with a + and closes with a −, one size and one color, drawn from CSS rather than swapped in markup so the two places that render a disclosure cannot drift apart. Set in the mono face, so the two glyphs are the same width and the row doesn't shift when you open it. The two are a saved bookmark (.stuff-mark) and a saved itinerary (.stuff-trip). Nothing else on the page opens: Yer Stuff, The Places and Roadtrips were folds until One Stop Shop made them panels behind a tab bar, and a panel is not a disclosure.

The amenities panel keeps its caret — a panel toggle is a different idiom. The scheme picker used to be the other exception and has no caret left to keep: it is nine swatches on the Settings panel, with nothing to open. And there are no rules in or under a section. The only rule left on the page is the sign-off's, and it is the width of the content column. A rule is structure, not prose.

A tab is a view, not a preference. Which panel you were last on is never stored. The saved shape is { theme, map } and nothing else, so two devices merge cleanly — a tab in there would mean one device's browsing quietly changing another device's screen.

.advbar — three groups, and they wrap as units

Five controls in one wrapping row put whatever the words happened to measure on each line: Amenities and Coming soon on top, Open now and Find 'Em underneath — two controls that do the same kind of thing, split across two rows by arithmetic. Three groups instead. .advtoggle alone; .advtoggles holding Open now and Coming soon, which never split; .advgo holding Reset everything and Find 'Em, which stays hard right at every width — a primary button that moves to the other side of the panel the moment a row wraps is changing place for a reason nobody reading it can see. One row above 1000px, stacked in that order below it.

Don't let an empty group draw a gap. Both toggles are hidden until a chain has something announced or the hours are fresh enough to answer, and an empty flex box still eats 12px either side of itself — hence :empty{display:none} on both.

Everything is a result

The box is where you ask. The results column is what you get. Retrieving is another way of asking, so Yer Places and Yer Trips render in that column — the same heading, the same map, the same width as a result card — and not inside the box. They were inside it for two builds, which put the thing you asked for in the asking half of the page while the getting half showed an invitation to search. Exactly one of the three things that column can show is ever showing, and #results says which: Here ya’ go for a search, Yer Places or Yer Trips for an answer out of storage. The SHOW segment goes with the search — it caps a search, and a saved list has a cap of its own that means something else. The box shrinks to what is left in it, measured at 115px against 419.

Don't toggle .hidden on something without checking what its class does to display. The UA's [hidden] is display:none at the lowest specificity there is, so .list and .seg both ignored it and the search results stayed on screen under the saved list.

.stuff-cap — a long saved list, without a page number

Measured at 390px against the panel's own height: five bookmarks is 621px, ten is 1,137, twenty is 2,168 — two and a half screens — forty is five and eighty is ten. The box stops being a box somewhere between ten and twenty, so ten is the cap. Not pagination. Page 2 of your own bookmarks is a place you can be lost in: it holds state nobody asked for, it has to be reset when the list changes under it, and it answers show me my stuff with which tenth? The list is already sorted newest-first, which is a real ranking for something you chose to save — so the cap is on recency and the way out is one press. It sits at the bottom, where you arrive when you run out of rows; the top of that panel already carries the mode switch, and a second control under it is the pile of switches the tabs exist to undo. The button names the number it is hiding, because Show all 43 is a decision you can make before you press it. The tab's own count never changes — Yer Places 43 is the truth about what you saved, whatever the list is showing.

Showing the 10 you saved most recently, of 43.

Don't write it when the list fits. Under eleven rows there is nothing to cap, and a control that says showing 8 of 8 is furniture explaining itself.

08 — The numbered path

One place in the app, and it is a sequence you follow rather than a list of facts: the stops inside a saved itinerary. (The How It Werks steps used the same path until One Stop Shop moved them to /how-it-werks/, where they wear .tfe-steps out of css/page.css — a document-page class, and not this guide's subject.) A 28px disc filled --accent and numbered --accent-ink — a pressed button made round — joined by a dotted line that stops at the last one. A path running on past its end would promise a step that isn't there.

The numbers are a CSS counter, not typed, so a step inserted in the middle renumbers itself. list-style:none takes list semantics with it in Safari, so the <ol> carries role="list" to put them back.

  1. Lake Austin H-E-B · 2652 Lake Austin Blvd
  2. North Lamar P. Terry's · 6001 N Lamar Blvd
  3. Luling Buc-ee's · 10070 I-10

The disc colors come out of the theme rather than being typed, and that is load-bearing. Filling with --key breaks three schemes whose accent is their key color; filling with --accent-ink renders as an outline in the two whose ink is their background. check-browser.mjs measures the worst of all eight rather than trusting any of it.

09 — Brands and map dots

One color per chain, and it does two jobs: the stripe down a card and the dot on the map. These are the chains' colors, not the scheme's, so they are the one set of values that doesn't change when you switch schemes.

Those two jobs are not one contrast test, and this table used to treat them as one. A dot is a shape and its visible edge is the --key rim every dot carries — so the rim is what has to clear the map ground, not the fill inside it. The same fill, read as text instead, answers to 4.5:1, not 3:1 — which is what the table below still checks, for the one place a chain color is still printed that way. Judging the fill against the ground failed a dozen rows that were never the thing keeping a dot visible, on a page that had already written the rim down two paragraphs below.

The rim, which is what makes a dot visible

PairWhat it isRatioNeeds 

The fills, and where each one is read rather than seen

ChainColorDot on landAs text on panelReads against

Dot on land is information, not a test — the rim above is the test.

As text on panel is a problem this page has mostly retired, and the one place it hasn't is a decision rather than an oversight. .stop-brand on a result card used to print the chain color here too; it reads --key now, and no longer belongs in this column at all. .tfe-sid on a document page kept its chain tint but not this raw value — each chain lightens along its own hue into a -text sibling token, solved to clear 4.5:1 against the panel it lands on, so it still reads as that chain, just not the color this table measures. .tfe-wr, the world-record badge, printed the raw value as text until the document tier began carrying the scheme you pick on the app: that took it from one panel to nine, thirty-six of the sixty-three chain-and-scheme pairs came in under 4.5:1, and one of them was live rather than theoretical — Buc-ee's is the only chain wearing a badge today and it measured 3.02:1 on Paper Boy. Its letters read --chain-text now and its ring still reads --chain, which is the honest split: 4.5:1 for text, 3:1 for the shape around it.

So nothing on a document page prints a raw chain color as text any more, and the column above is the app's problem alone — .stop-brand is out of it, and the document tier's two are solved per scheme. Against 4.5:1 the raw values clear in all eight dark schemes for Buc-ee's, Rudy's and P. Terry's, in none of them for H-E-B or Shipley, and in exactly one — Gig 'Em, the lightest — for Torchy's and Whataburger.

The colors are not moving, and the reason is that they are not chosen to be read against a background. A chain color is chosen to be told apart from the other six, on eight different grounds, which is a different problem with a different answer — H-E-B's red is theirs, Rudy's white is the only value that holds on all eight lands, and Shipley's brown exists because Rudy's gave up the caramel. Lightening them for legibility would undo that.

What carries the chain where .tfe-wr doesn't is the document tier's own version of the app's stripe and dot: the heading rule, the fact-card stripe and the map legend dot, all still on var(--chain) itself, all backgrounds or borders rather than text, and all clearing 3:1. That is the trade, it is taken knowingly, and the table above names it row by row rather than printing a verdict this page has decided not to act on.

Nothing on the map is labeled by hand. Town labels are placed by measuring, not estimating, and a label that would collide is removed rather than nudged. Don't replace either with a hand-kept list of the majors — the point is that zooming in can always undo a hiding.

A dot carries a thin --key rim so it stays visible where its own color doesn't clear the map ground. Don't remove it to tidy the SVG, and don't reach for the fill to make text legible — a chain color is chosen to be told apart from the other chains, which is a different problem from being read against a background.

10 — The document pages

The chain pages are the second thing this site is, and they are built on a second stylesheet: css/page.css, written by scripts/build-pages.mjs out of the same tokens the app uses. Every class there is prefixed tfe-, and every specimen below is rendered by that file — each one is real markup in a <template> beside an iframe that links css/page.css. Nothing is copied here, so nothing here can go stale. The frames link that file relatively, so they draw the same off the disk as they do over HTTP — a root-absolute /css/page.css resolves to file:///css/page.css and every specimen below comes up bare.

A document page runs no JavaScript at all, and that is the whole design constraint rather than a nicety. The crawlers these pages exist for — GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot — do not execute it, so anything a script would have drawn is simply not there when they read. The map is inline SVG, the hours are text, the amenities are text, and the interaction budget is spent on links.

The sticky bar, and the one button that leaves

Same shape as the app's: the mark on the left, the buttons hard right. Four of them move you down the page and are outlined; .tfe-btn-go is filled.

If it says Find ’Em it is filled, and if it is filled it says Find ’Em. One label, one colour, everywhere on the site. More, Look and every jump link are outlined; a Find ’Em with nothing behind it is <button disabled> and wears the dead look, which is the only exception.

This reverses the rule that stood here before, and the reversal is Dave’s on 2026-09-11: Find ’Em is the core feature. The old rule was “filled means primary, and there is exactly one per page”, written after filling the rows in Find More in Texas produced a stack of red slabs that shouted down the one action the page was for. What that rule got wrong is whose the accent is: it belongs to the verb, not to a position on the page. Every one of those rows does the same thing the bar’s button does, and dressing them as secondary made the site’s one action look like a footnote on every page below the fold. The stack of red reads as a stack of the same offer, which is what it is.

It sticks with CSS alone. .tfe-bar is a sibling of <header> rather than a row inside it, because position:sticky only holds while the element's containing block is on screen — a sticky child of a 90px header unsticks the moment the header scrolls past, which reads as the bar breaking rather than as markup. Its containing block is <body>, which is as tall as the page. The app gets the same effect with a scroll listener; a document page has no script and needs none.

The labels are one word each. These sit in a bar of fixed width, and The Facts and Every Location were the headings wearing their full names somewhere that only has to say where the button goes. The headings keep theirs.

.tfe-bar / .tfe-barin / .tfe-jump / .tfe-btn / .tfe-btn-go

The two lines the app and the documents share

Under the H1, above the answer sentence: Data Freshness and Truly Texas, and both are printed verbatim from index.html — the date from the UPDATED block, the Truly Texas line from v on the chain's row in CHAINS, which is the same string the card in The Places shows. The date is set in mono because the app sets it in mono; one fact should not change typeface between two pages of one site. verify-pages.mjs check 11 fails if either drifts.

.tfe-fresh / .tfe-truly

The answer sentence

The first thing in <main>, always, and the one paragraph an assistant is most likely to lift whole. It carries the count, the spread and the date it was checked, so a sentence quoted somewhere else still says how old it is. Nav lives in <header> rather than <main> for exactly this reason — a row of buttons above it would make the first paragraph of the document a list of links.

.tfe-answer

The facts, one card each

A bullet list was underselling these. Every one of them is something the build worked out across the whole chain — the town with the most, the share that have a thing, the earliest opening in the state — which is the part nothing else on the internet has. The left border is --chain, set once on <body>, so a page is tinted by the chain it is about.

.tfe-facts

The map, in the app's own box

The same .sign treatment the app's map sits in, double border and all, and the dots are the chain colors off BRANDS. Under it a hairline, the legend on the left and the way out on the right — the app's .maprow, one for one. The whole card is a link, so the call to action is prose rather than a button: this is a document, and a button that does nothing without JavaScript is a lie about what the page can do.

.tfe-card / .tfe-maprow / .tfe-legend / .tfe-cta

The table, which is the page's reason for existing

Every store the chain publishes, carrying what a result card carries: the number that tells two stores in one town apart, the address with the town under it, the hours, the amenities as the same flat pills the app uses, and a way to drive there. The columns are given percentage widths because auto layout handed the leftovers to the pills and squeezed the name column until a chain's own name broke at its hyphen. The box scrolls sideways; the page never does. No <caption>: the section's H2 says “Every H-E-B in Texas” eight pixels above it, and the caption was that heading again in smaller type.

.tfe-table / .tfe-sid / .tfe-town / .tfe-hrs / .tfe-pill / .tfe-map-link

The rollup, and the row that does not apply

A count, and a way in. Every row names a thing, says how many of it there are, and carries a .tfe-btn-go that opens the app with this page's search already applied — the drive and the chain, or the town and the amenity. It is the .tfe-links row the chain pages already use, pointed the other way.

A row counting zero gets a dead button, not a live one that runs a search coming back empty. It is a real <button disabled>, so the keyboard and a screen reader are told what the eye is told, and the line beside it states the negative instead — which is also a fact the build computed, and those are what earn a generated page its URL.

.tfe-links / .tfe-link-name / .tfe-btn / .tfe-btn-go / .tfe-btn[disabled]

The chosen stops, and why each one is there

A drive page is asked “where do I stop”, and a table of every stop on the road hands over the dataset instead of answering. Four to six, each with how far into the drive it is, how long that is, how far off the route it sits, and one line saying why it is on the list — the last stop before the longest empty stretch, the closest thing to halfway, the only one of its chain on the whole road. A stop with no sentence does not make the list.

The mile marker is the column being scanned down, so it is fixed width, set in the mono face with tabular figures: three digits under two must not shift the names beside them.

.tfe-stops / .tfe-mile / .tfe-stopbody / .tfe-stopname / .tfe-stopwhere / .tfe-stopwhy

Questions, and the rest of the map

The questions sit above the table rather than under it: they are the part a search engine quotes, and a thousand rows between the heading and the answers buries them. Every page ends by naming the others, which is how six documents that nobody links to become six documents that link to each other — and each row carries both ways in, the name to that chain's page and the button to its search. A sixth row reading “search every chain at once” used to sit under them and is gone: five buttons that each land somewhere specific say it better than one that doesn't.

.tfe-said / .tfe-qa / .tfe-conf / .tfe-subhead / .tfe-links

.tfe-ribbon / .tfe-route / .tfe-gapband / .tfe-milelab / .tfe-osm

A corridor page’s subject is the line, not the state, so that is what it draws. The stops sit at their real distance along the drive, the labels alternate above and below so two stops a few miles apart do not print on top of each other, and the empty stretch is shaded — but only when it is worth naming, at twenty miles or more. Shading the longest gap on a dense metro pair points at four miles of nothing in particular.

This replaced a crop of the whole state with the route drawn on it. That picture answered “where in Texas is this drive”, which is not a question somebody who just typed both town names has, and it spent most of a screen before the first fact. The line is a fifth of the height. The attribution is not optional: every position on it is a distance measured along OpenStreetMap geometry under the ODbL, so it keeps .tfe-route, and verify-pages.mjs fails any page carrying that class without the app’s own attribution sentence word for word. A picture is not less of a picture for being one-dimensional.

.tfe-foot / .tfe-sig / .tfe-copy

The last two lines are the app's own, read out of index.html at build time and printed on every document page — and the year inside the copyright is stamped from the system clock by build.mjs, so it rolls over on its own. verify-pages.mjs fails a page whose copy of either has drifted. Set the two lines the way the app sets them: the credit at 14.5px/600 in --dim, 7px, the copyright at 12px in --faint, and a link that rests with a 2px transparent bottom border and grows --accent under the pointer. The document footer had inherited its own smaller, fainter, unbolded version of all three.

Don't add a script to a document page, and don't move the nav into <main> to get it above the heading. verify-pages.mjs fails on either — the first because a page that needs JavaScript to say what it says is invisible to the readers it was built for, the second because it would put a row of buttons where the answer belongs.

11 — Breadcrumbs

Every page on this site that isn't the map opens with one, and until now the pattern was on nine pages and in no document — which is how a tenth page ends up with a lookalike. It is .tfe-crumb in css/page.css, and this page is wearing it above.

The rules, and every one of them has a reason

  • It starts at Home and ends on the page you are on. The last crumb is plain text, never a link to itself — a link that goes nowhere is a link that has to be tried before it can be ruled out.
  • The separator is &rsaquo; in its own span, marked aria-hidden. A screen reader announcing "Home right angle quotation mark H-E-B" is reading punctuation aloud. Hiding it leaves the words and the order, which is the whole of what a breadcrumb says.
  • It sits above the <h1>, inside the header, never in the bar. The bar is where you can go; the crumb is where you are.
  • Mono face, 12px, --faint. It is machine-produced structure rather than prose, which is what the mono face means everywhere else on this site.

.tfe-crumb

Home H-E-B Austin

The half a crawler reads

The visible trail is only one of the two, and shipping one without the other is the mistake worth naming. Every page carrying a crumb also carries a BreadcrumbList in JSON-LD saying the same thing in the same order, because these pages exist to be read by assistants that never see the markup. They have to agree — a visible trail that says one thing and structured data that says another is worse than either alone.

The same trail, as structured data

{"@type":"BreadcrumbList","itemListElement":[
  {"@type":"ListItem","position":1,"name":"Texas Find 'Em","item":"https://www.texasfindem.com/"},
  {"@type":"ListItem","position":2,"name":"H-E-B in Texas","item":"https://www.texasfindem.com/heb/"},
  {"@type":"ListItem","position":3,"name":"H-E-B in Austin","item":"https://www.texasfindem.com/heb/austin/"}]}

Don't add a crumb without its JSON-LD, and don't let the two drift. And don't put one on the map itself — the map is Home, and a crumb pointing at the page you are already on is furniture.

The mockups, which are free inside and identical at the edges

Each proposal under /roadmap/ ships a mockup, and a mockup is meant to look unlike the others: it is trying something, it is built for one argument, and holding its content to a house style would defeat the point of building it. Its chrome is a different question. These are public pages, and somebody moving between two of them is reading one site — so the breadcrumb and the footer are the same in every mockup, byte for byte, and the only part that differs is the project's own name at the end of the trail.

The mockups are self-contained on purpose — none of them loads css/page.css — so the shared block is genuinely copied into each one, and a copy that nothing checks is a copy that drifts. Three of them had, and every difference came from the same habit: the block was pasted, then quietly edited in place. One kept the app's own footer rule and squeezed its two lines to 9px where the others gave 18. One carried a second <footer> of leftover prose above the real one. One wrote the separator as a literal › where the others used the entity. None of that is visible in the file you are editing. It is only visible with two of them open side by side, which is how a reader meets them and never how they are written — so verify-docs.mjs compares them instead.

The shared block, in every mockup

<p class="tfe-crumb"><a href="/">Home</a> <span aria-hidden="true">&rsaquo;</span>
  <a href="/roadmap/">Roadmap</a> <span aria-hidden="true">&rsaquo;</span> Project name</p>

<footer>
<p class="tfe-crumb">Texas Find 'Em is independent and is not affiliated with,
  endorsed by, or sponsored by any of these companies.</p>
<p class="tfe-crumb"><a href="/roadmap/">Back to the roadmap</a> &middot;
  <a href="/how-we-know/">Where this comes from</a> &middot; <a href="/">The map</a></p>
</footer>

The footer reuses .tfe-crumb rather than inventing a class, because faint small underlined links is exactly what those are. It sits inside the page's content column, not as a child of <body> — two of them escaped it once and ran the full width of the window with their text against the edge while everything above sat centered.

Don't restyle a mockup's breadcrumb or footer to suit the mockup. If the shared block genuinely needs to change, change it in every mockup in the same commit — the check compares them against each other, so one file moving on its own is what fails.