Insights
Grace Tarnasky's site, from launch-ready to public

On 9 August 2026 Grace Tarnasky's site was finished, deployed, and invisible. Production matched staging exactly, the only thing between it and the public was an under-construction block in an .htaccess file, and the site stayed that way for ten more days. It went live on 19 August, on her word. The build week is written up as it happened and the whole engagement is a published case study. This is about the thing itself: what the site is made of, and what those ten days were spent on.
Whose brand is on this site?
Hers, down to the hex codes, because she sent them to me.
On the first day of the build Grace handed over a palette document she had made herself: Linen #F6F2EC, Morning Fog #E8E2D9, Sage Mist #A8B89E, Harvest Gold #C4955A, Orchard Green #4A6741 and Bark #3A2F27, with rules attached. Gold is an accent and never a background. Gold and green never sit on the same element. Text and buttons are Bark, never pure black, and the page is never pure white. Four of those values went into the stylesheet as tokens, and everything else in the site is drawn from them:
:root{
--accent:#4A6741;--accent-deep:#3A5233;--accent-soft:#CFD9C5;
--ink:#3A2F27;--paper:#F6F2EC;--cream:#E8E2D9;
--clay:#8C6A3F;--slate:#6B5E54;--line:#DDD5C9;
}
The other two never became tokens. Harvest Gold is written out where it is used, in the favicon mark, and Sage Mist ended up in the header comment of the stylesheet rather than on the page.
Her document declared color and nothing else, so the type stayed a studio decision. What did not stay mine is the writing at the top of the page. The headline on the home page is her sentence, and so is the script line under it, and so is the tagline that feeds the footer, every automated email header and the organization slogan in the structured data. I do not overwrite those. An agent's site that sounds like the agent is worth more than one that sounds like me.
The type is Instrument Serif for the headline, Playfair Display for every other display line, Ephesis for the script, Inter for reading, and Archivo Black kept for places where the letters are not language: eyebrows, stat numbers, buttons. Nine woff2 files are served from her own domain. Load the home page and count the requests to Google: there are none, because a headline that waits on somebody else's CDN is a headline that can arrive late or, one day, differently.
Why does one site carry three themes?
Because the second one was wrong, and building the third was the fastest way to find out what right meant.
The fork arrived on 6 August with one look, grace: rounded corners, soft shadows, headlines in Besley. On the 7th I built a second, grace-alt, on the same palette but a different construction, with Archivo Black set large and uppercase, square corners and a 1000px column, and put a theme picker in her Settings so it could be switched without a deploy. Her note on the second theme was that it felt too forward, and that the first theme's femininity was what she wanted. That note is a design brief. It separates voice from structure: keep the softness of the first, keep the discipline of the second. On the 9th I built grace-mid, which is what the site runs today.
That last column is the part worth stealing. grace-mid is a layer, not a copy: its layout loads the base theme's stylesheet by path and adds its own on top, so there is no second copy of a 1,094-line stylesheet to keep in step. It works because of four lines in the engine:
$here = 'themes/' . Theme::active() . '/assets/' . $rel;
if (Theme::active() !== Theme::BASE && !is_file(App::$root . '/' . $here)) {
$base = 'themes/' . Theme::BASE . '/assets/' . $rel;
if (is_file(App::$root . '/' . $base)) return url($base) . asset_v($base);
}
Ask for an asset, get the active theme's copy, or the base theme's copy if the active theme has none. The cost of that convenience showed up in the dashboard rather than on the site. One admin include still pointed at themes/grace/assets/ with the path written out by hand, the layer had no copy of the file, nothing fell back, the uploader script 404'd, and every photo field in her dashboard quietly turned into a bare text input. No error, no red box, just a text box where the picker used to be. The rule that came out of it is one line long: admin views load theme assets through asset(), never through a written-out theme path.
Two smaller things from the same week. Masters can preview any theme with ?theme=grace-mid, which sticks for the session, and the parameter is ignored for everyone else, which is correct and also invalidated two of my own "verified with ?theme=" checks before I noticed the site had been rendering the configured theme at me the whole time. And the choice between the three was not made by eye: a pass over every visible text node, measured against the background actually painted behind it, put grace-mid at zero contrast failures across the home and about pages, against five for grace-alt and eighteen for the base.
What is each page for?
Each one answers a question somebody arrives with, and nothing else is on it.
The home page answers "who is this". Her sentence, a video her videographer shot, then bands in a fixed order: current listings, recently sold, what people said, the areas she works, and the valuation offer. Every one of those bands has an on and off switch in Settings. The giving-back band shipped switched off and stayed off, because it had inherited a list of charities that were not hers and a section with nothing true in it should not be styled, it should be absent.
The about page answers "can I trust her", in her writing, with two chips at the top: get to know me, or work with me. The buyers and sellers pages answer "what happens if I call you", as a numbered walk through her process. Those numbers are positional, never typed, so reordering the steps renumbers the page and an empty list hides the section altogether. She edits them, and the questions and answers under them, in her dashboard, which is the whole point: anything a client can see on her page, she has to be able to change without me. Hardcoding copy in a theme makes me the bottleneck on her own words.
The journal answers whatever she wants it to that week. The market pages and neighborhood guides answer "what do homes cost here", and they are a different piece of machinery, the guides and the valuation form. Every one of these sections carries an edit pill when she is logged in, wired by a helper that prints nothing at all for a visitor:
function edit_link(string $adminPath, string $label = 'this section'): void
{
if (!can_edit()) return;
printf(' data-edit-link="%s" data-edit-label="%s"', e(url($adminPath)), e($label));
}
How does the listings surface actually read?
Like a filing cabinet, not a portal, and that is a deliberate choice about size.
The index has a search box and a status menu. The search runs three LIKE comparisons across the columns a person would actually type into, and the status menu submits on change:
if ($q !== '') {
$sql .= ' AND (l.title LIKE ? OR l.city LIKE ? OR l.zip LIKE ?)';
array_push($params, "%$q%", "%$q%", "%$q%");
}
$sql .= " ORDER BY FIELD(l.status,'active','coming-soon','pending','sold'),
(JSON_LENGTH(COALESCE(l.photos,'[]')) = 0),
l.created_at DESC";
The ordering clause is the interesting half. Active homes first, then coming soon, then pending. Inside each of those, listings that have photographs come before listings that do not, and only then does the newest win. On a one-agent site the useful sort answers "which of these can I actually show you", and a record with no pictures belongs at the end of the page rather than the top of it. Twelve to a page, and a page number that is not a number lands on page one rather than an error.
Her sold homes get a page of their own. For a while the track record was only reachable as /listings?status=sold, which meant the one URL anybody would guess or share returned a 404, and a filtered view cannot carry its own title, description or breadcrumb. /sold is a real path now and the old query string redirects to it permanently.
What did the fork bring with it that was not true?
A sentence about a career that was not hers.
Her site is a fork of another agent's, which is the fork-per-client model I run, and the code came across with copy baked into it. The subhead over the sold page was a hardcoded line describing more than a hundred closings and a price range topping out well over a million dollars. All of it accurate for the agent it was written about, none of it true on this domain. A brand sweep catches colors and names. It does not catch a claim.
The fix was to stop writing that sentence and start computing it. The page counts her own rows, takes the range from the prices actually on the page, and reads her published headline figure separately, because closings run ahead of the photographs it takes to show them. Today the line under "Recently sold" reads: "Ten of them here, with photos, from $114K to $680K. Twelve homes closed in all." Nobody types that. It corrects itself as she adds a home.
The same lesson arrived a second time in a different costume. Her Washington license number reached the site with one digit transposed and stood for a day before she caught it. Correcting it was not a settings edit: by then the wrong digits had been copied into a published post and its metadata, her email signature, five chunks of the assistant's knowledge base and two rows of chat log, so the repair had to sweep every text column of every table in every database, on three environments. A credential is never one field. It is a string that has already been copied somewhere you have forgotten.
What changed between launch-ready and public?
Sixty-nine commits on a site that was already finished.
The site was code-and-data identical to staging on 9 August, and the launch itself was a five-minute checklist waiting on her: she had new footage coming from her videographer and wanted to read the whole site herself before strangers did. Both are good reasons. What I did with the wait was work on the site, and the log between 10 and 19 August, merges excluded, is where that number comes from.
A sample, because the shape of the list is the point. On the 10th the hero moved to Instrument Serif with a single gold button, from a typography mockup I had been working through separately. On the 13th the reveal animation stopped being able to strand the page: all three themes had carried their own copy of an observer with no support check, against a rule that hid content unconditionally, so a browser without IntersectionObserver threw on the constructor and left a reader looking at a transparent page. The same day, static assets got a year of caching and a jpg twin for the Safari versions that cannot read WebP, which turned into its own post about old browsers. On the 17th an SEO pass. On the 19th, hours before the gate came down, the phone got its own video edit and its own poster frame rather than a crop of the desktop one, the reviews became a carousel she can swipe and whose wording she can edit, and the buyers and sellers pages handed their numbered steps over to her.
One of the thirteenth's changes has outlived the rest. Stylesheet cache-busting had been a hand-typed number in the layout, bumped by hand at deploy time, and it had reached ?v=78. Miss one bump against a year-long cache header and the edge serves last week's stylesheet, which is exactly how the color scheme of the site this one was forked from once ghosted back onto a page I had swept clean. Now the number is the file:
function asset_v(string $relPath): string
{
$m = @filemtime(App::$root . '/' . ltrim($relPath, '/'));
return $m ? '?v=' . $m : '';
}
A version a human has to remember is not a version. The stylesheet her site is serving right now answers to ?v=1787609145, which is the file's own modification time, and it moves whenever the file does whether I think about it or not.
What the ten days were actually for
They bought the parts of the site that only exist because nobody was waiting on them.
Three things generalize past this build. The first is that the client's own brand document, if one exists, outranks my taste, and asking for one is cheaper than presenting a palette and negotiating. The second is that a rejected design earns its day. Her note on the loud theme told me more than any question I knew how to ask her, and the quiet theme the site runs today came straight out of that note, one day's work later. The third is the rule I keep re-learning: anything visible has to be editable. Steps, questions, review wording, section switches, the whole of the copy. Every hardcoded sentence is a future text message asking me to change a word.
The phone deserves its own note. Her site is read on one, mostly, so the hero has a separate 9:16 edit and a separate poster frame rather than a desktop clip cropped down, which is the same instinct as designing for one thumb applied to video.
The theme is still an open question. The site has run grace-mid since 9 August because I chose it, and the picker is still in her Settings with all three in it. Since 24 August her site deploys straight from my machine to hers, no staging step in the middle, so the day she picks one it is a switch and a purge. I have not asked her again. So the next thing on this site is a conversation: all three are still in her picker, and the day she names one it costs her a click and me a cache purge.
Common questions
When did gracetarnasky.com go live?
On 19 August 2026. The site had been finished, deployed and identical to staging since 9 August, sitting behind an under-construction block while she gathered new video and read the whole thing herself. The launch itself was a checklist, not a build: drop the gate, purge the edge, submit the sitemap.
Why does the site have three themes?
Because the second one was a miss and the third one is what her note about it turned into. The base theme came with the fork, a second was built to try her palette on a louder frame, and her note on it, that it felt too forward and that the femininity of the first theme was what she wanted, produced the third, which is what runs today. All three stay in the theme picker in her dashboard, switchable without a deploy.
Does the search on the listings page search the MLS?
No. It searches her own listing records in her CMS, matching what you type against the street, the city and the ZIP, and it sorts active homes first and homes with photographs ahead of homes without. It is a way through her inventory, not a portal search of every home on the market.
Can Grace change the site herself?
Yes, and that was a design constraint rather than a feature. Her listings, journal posts, review wording, the numbered steps on the buyers and sellers pages, the questions and answers under them, and the on and off switch for every band on the home page all live in her dashboard. Logged in, every section on the public page carries a pill that opens the screen that edits it.
Related