This site’s homepage used to be a plain, system-font layout. Over the last few weeks I rebuilt it around an editorial reference, then spent a good chunk of that time undoing and redoing decisions once I actually saw them live. Some of that back and forth is worth writing down, because the parts that took two tries are more instructive than the parts that worked the first time.

Starting from a reference, not a blank page

Rather than design the new homepage from scratch, I started from Rachel Chen’s portfolio as a visual reference: a light-mode, whitespace-heavy layout with a serif accent word in the hero, a mono label style for dates and metadata, and flat, sharp-cornered cards instead of drop shadows. The brief called for Tiempos Text as the body serif, which is a paid Klim Type Foundry face with no license in this project, so I swapped in Crimson Pro, an open source serif in the same high-contrast editorial family.

The rest of the brief shipped close to as specified: Geist Sans for headings and interface text, Geist Mono for labels and metadata, flat elevation, 0px radius on cards, pill-shaped buttons and the cursor’s label chip. The one deliberate swap I kept revisiting was the accent color. An early pass shifted it from the brief’s orange to an antique gold, to avoid matching Rachel Chen’s site byte for byte, but it read as a worse color on this content, so it went back to the original #E65F2E.

Two rounds of picking fonts

The three-font system worked, but three families is more than this site needs, so I went looking for a way to collapse it to two: one primary sans for everything structural, one secondary serif for body copy and a single hero accent word.

First attempt was Raleway paired with Alice, both self-hosted through Fontsource with no Google Fonts CDN request at runtime. It rendered correctly, passed every check, and still looked wrong the moment it was live. Alice ships a single static weight with no italic face, so the hero’s accent word and the “Trusted by” partner names, which both leaned on an italic treatment, had to lose it. That’s the kind of loss you don’t feel from a screenshot of the font on its own, only once it’s sitting in the actual layout it has to carry.

The pairing that actually stuck was Geist Sans, kept from the original three-font system, paired with Lora as the new secondary serif. Lora ships as a variable font with a full weight range and a real italic face, so nothing had to be stripped out to make the two-font system work. That’s a fairly narrow, specific reason a font pairing lives or dies, and it only shows up once you check for it directly instead of judging a specimen block.

A Google Fonts tool that actually has all the fonts

Somewhere in the middle of the font back and forth, a Font Pairing Preview tool existed on the site with a curated list of about 120 fonts across five categories. Once I’d been through two real font swaps myself, a list that size felt like it was missing the fonts people would actually reach for.

Google runs a metadata endpoint at fonts.google.com/metadata/fonts that returns every family it serves, no API key required. I fetched it once, kept the family name and category for each of the 1,942 results, and checked the file into the repo as a static snapshot rather than calling the endpoint from the browser on every page load. The tool went from two <select> dropdowns to a pair of text inputs backed by a shared <datalist>, so typing a few letters of any of those 1,942 names filters it down live.

Typing into a search field and having it fetch two web fonts on every keystroke is a bad idea, so I added a Generate button: type a heading font and a body font, hit Generate, and only then does it validate both names against the snapshot and load them from Google’s CSS2 endpoint. An invalid name shows an inline error instead of silently failing. The chosen pair also gets written into the URL’s query string, so the address bar itself is the share link. Land on that URL cold, and the exact pairing you were looking at loads and renders immediately, no re-selecting anything.

The favicon and the mobile menu learned the same trick

The favicon became a white, rounded-square badge with the black AG monogram centered inside it, the same treatment most app icons use so the mark stays legible against any browser chrome color.

The mobile hamburger icon inherited that idea directly. It’s now the same AG monogram, but on a badge that matches the site’s text color instead of a fixed black, so it inverts correctly between light and dark mode, and it morphs into an X when the menu opens. Both states, the monogram and the cross, sit in the same spot and cross-fade between each other rather than being swapped out in JavaScript.

That menu also went through a layout bug worth naming. The hamburger button sat noticeably short of the actual right edge of the screen, and the cause wasn’t padding, it was that the collapsed <nav> element was still a flex sibling of the button even at zero width, so justify-content: space-between was still reserving a gap after it. Swapping to margin-left: auto on the button fixed it, since that pushes the button to the edge regardless of what an invisible sibling does afterward.

A cursor that forgot where it was

This site runs a custom cursor: a small dot that tracks the pointer, plus a separate label chip that names the link you’re over (“Read this,” “Send an email,” and so on). Two bugs showed up in it once it was actually being used instead of just tested.

The first: scrolling the page with the mouse held still left the label chip stuck showing whatever it last pointed at, because the chip’s visibility is driven by mouseover and mouseout events, and neither of those fires from a scroll wheel alone. The content moves under the cursor, but nothing tells the script that happened. The fix listens for scroll directly and clears the chip immediately, so the next real mouse movement re-evaluates it against whatever actually ended up under the pointer.

The second was worse to watch: clicking a nav link to Blog or Tools made the cursor visibly fly in from the navbar on the new page, every time. This site is a plain multi-page app, so the cursor script restarts fresh on every navigation, and the dot’s default CSS position is the page’s top-left corner, right under the brand mark, until the first real mousemove event. That first move was using the same 90ms transition as every other move, so it animated smoothly from the corner to the real pointer position instead of snapping there. The fix disables the transition for exactly that first position update, forces the browser to apply it, then turns the transition back on for every move after.

Cards borrowed from a sister project, not its colors

I also run GattyWorks, and its blog cards use a frosted glass treatment I wanted here: a translucent surface over backdrop-filter: blur(18px) saturate(150%), 16px corner radius, and a hover state that lifts the card 4px and tints both the border and the shadow toward the accent color. I read the actual CSS in that project to get the technique right, then rebuilt it with this site’s own tokens, not GattyWorks’ teal. Its own written design principle is explicit that “teal lives in the text, not the surface,” and the equivalent here is the same idea with orange: the accent shows up in a border and a shadow tint, never as a card’s fill color.

The rest of the details

A handful of smaller changes landed alongside all of that:

  • The homepage now reads “Abhishek P | Human in Tech” as one line, instead of the name sitting in a separate eyebrow above a floating “Human in Tech” tagline that had no visual connection to it.
  • The Experience timeline gained a fifth, earlier entry: freelance cryptography, blockchain, and adversarial machine learning work with startups, NGOs, and PhD professors, starting in my second year of engineering, before UniCourt.
  • The closing “hello@abhiigatty.com” link at the bottom of the page is now a large pill button instead of plain text, dark by default and tinted to the accent color on hover.
  • The site no longer checks the visitor’s OS-level dark mode preference on first load. It defaults to light mode until someone explicitly picks dark from the toggle, since the previous behavior meant a lot of first-time visitors saw dark mode without ever having chosen it.
  • The homepage headshot changed to a new photo, and the profile portrait now carries a soft accent-tinted shadow on hover that fades in and out slowly instead of snapping.

None of these are big changes on their own. What they have in common is that every one of them only became obvious once the previous version was live and I was actually using the site the way a visitor would, not reading a diff of what changed.