What the site actually does, so I stop guessing. Last checked against the CSS on Aug 9, 2026.

Widths — there is only one

64rem / 1024px. The header, the footer, main, post titles, wide images, captions, videos and the prev/next bar all end on the same line. The number is not a taste call: it is the reading measure plus one sidenote gutter — 665 + 56 + 256 = 977, rounded up.

Prose sits at the measure (66rch, ~665px) inside that band, left-aligned rather than centered. The left edge of the prose is the site’s alignment line: the header rule, the footer rule and the logo all start from it.

Anything wide grows to the RIGHT. Never into the left margin. There is exactly one exception, below.

Articles

The cover picture

<photo-cover> — it was never called cover-picture.

image: /static/2004/hero.jpg
image-desc: The desert sands of Dan Gin Shan.   # optional caption

The one element allowed past the band: full-bleed to the viewport, capped at 1600px, and flush under the header’s rule with no seam. Its caption is the exception to the exception — that stays on the body width, because the image is a flourish and the caption is text that belongs with the prose.

No rounded corners on it. It runs to the window edge, and a curve there looks like a bug.

Images

The thumbnail template — long edge 800px

Crop and resize everything that goes into /static/* to one of three boxes. One number, three shapes:

shape box used by
Portrait 3:4 600 × 800 books, film, devices — covers, posters
Landscape 4:3 800 × 600 album photos
Square 1:1 800 × 800 anything that is neither

Where 800 comes from. Measured against the built site on Aug 1, 2026, the widest a thumbnail ever renders is 245px — that is /album/, whose masonry columns grow to fill the band. Everything else is smaller: the grids on /books/, /film/ and /devices/ top out at 193px, and the home-page strips are fixed at 192px.

surface widest render
/album/ masonry 245px
/books/ · /film/ · /devices/ grid 193px
home strips (books, album) 192px

A retina screen wants twice that, so 490px is the floor today. A 600px-wide source clears it with about 20% to spare, which is the headroom: the rendered size can grow from 245 to 300px before anything needs re-cutting. Landscape gets more room again, because there the long edge is the width.

Do not go bigger “just in case.” 800 on the long edge is already 3× the largest thing on the page. Past that you are paying bytes on every page view for pixels no screen will resolve, and the album strip alone loads eight of them.

Format and budget

⚠️ When a thumbnail busts the budget, cut the long edge — not the quality. This section used to say that over 100 KB “means the quality slider is too high, not that the image is complicated.” That is wrong, and the first real album photo disproved it: a bookshop interior, which is several hundred legible book spines, i.e. detail everywhere and nothing for the encoder to flatten. Measured 2026-08-01, same source:

long edge q82 q75 q70 q65
800 122.9 KB 97.1 KB 91.7 KB 86.5 KB
700 90.1 KB 70.6 KB 66.8 KB 62.6 KB
600 69.9 KB 54.6 KB 52.0 KB 49.2 KB

Dropping from q82 to q65 at 800px saves 36 KB and visibly softens the faces. Dropping to 600px at q75 saves 68 KB and costs nothing you can see, because 600 still clears the 490px retina floor by 22% — the headroom this page already documents. So 800 is the template, not a minimum. For a detail-dense frame, take the long edge to 600 and leave quality at 75.

⚠️ And sometimes you need both levers. The rule above was written from one photograph; a second, a San Francisco street thick with foliage, needed 600 and q68 to reach 55.1 KB — 600/q75 landed just over the line at 60.0 KB. Reach for the long edge first, then give up a little quality, and check the rendered width rather than the long edge when you do: /album/ is masonry, so a portrait photo’s long edge is its height and its width is what has to clear 490px. The San Francisco cut is 554×600 — 600 on the long edge, but only 13% of headroom on the edge that actually matters.

Keep the master

Keep the untouched source before cropping — it is what makes a re-cut possible when this template changes. There are two conventions, and they are not interchangeable.

Collection thumbnails — masters go in _src/ at the repo root, one folder per collection, under the same slug as the published file with no suffix:

_src/books/  _src/film/  _src/album/  _src/devices/  _src/own/

static/books/the-lord-of-the-rings.webp   ← published, 800px long edge, 3:4
_src/books/the-lord-of-the-rings.jpg      ← master, whatever the source gave

Same stem, different tree, so the pair is obvious at a glance and a re-cut is a loop over _src/ rather than a filename dance. Two properties come free with that leading underscore and are the whole reason for it:

⚠️ Add masters with plain mv + git add, never git mv. git mv writes the rename straight into the index without running LFS’s clean filter, so the real binary goes in and git status looks perfectly normal. The only check that actually proves it:

git ls-files -s _src/books/1984.jpg          # → blob sha
git cat-file -p <sha> | head -1              # → version https://git-lfs.github.com/spec/v1

_src/film is singular while the published folder is static/films/ — deliberate, matching the /film/ page; don’t “fix” either one. Adopted 2026-08-01, replacing a <name>-original.<ext> suffix that sat inline next to the published file (and, for a few hours, a static/*/src/ layout that shipped with the deploy).

Per-post imagesstatic/<year>/ — keep the old <name>-original.<ext> suffix, in place. ⚠️ Do not migrate these. 41 of them are live and three are linked directly from posts, so their URLs are pinned by guardrail 2. The suffix stays the convention for anything under a year folder; _src/ is only for the five collections above. Meanwhile, do not add a 700 KB master casually.

Adding a book or an album item

Append to the end of _data/books.yaml or _data/album.yaml. The last entry is the newest — that was the whole ordering protocol for the album, and it still is. Books no longer work that way (2026-08-07): an A–Z split scatters chronology across 27 files, so nothing records when a book arrived and the home strip shows the favourites rather than the newest. There is still deliberately no date field: a re-read has no single date worth recording, and a second field is a second thing to keep true.

There is no highlight flag any more (2026-08-08). Favourites live in _data/books-favorites.yaml and everything else in _data/books/ — being in the favourites file is the flag. The trade: the two grids read different files, so nothing guarantees they are disjoint the way the old where_exp did, and a book listed in both renders twice. media: video or media: audio on an album item overlays the matching icon.

An album entry is three fields — title, img, url — plus optional media. There is no alt: the includes emit item.alt | default: item.title, so the title is the alt text. ⚠️ That means an album title is doing two jobs at once — the caption on the home strip and the description a screen reader reads with the picture unseen. Write them to survive being read aloud.

⚠️ Album entries carry no w:/h: either, and that is deliberate. Ragged heights are the point of a masonry page, and per-photo dimensions are two hand-maintained numbers that no build step can check. They were tried and removed on 2026-08-01. Anyone reaching for them again should know the measurement first: page.scss sets aspect-ratio: auto on masonry images, which cancels the rule that derives a ratio from those attributes, so they reserve nothing — 16px with them, 16px without. The CSS has to change before the data change means anything.

Album thumbnails are cut at their native aspect ratio, never pre-cropped to 4:3. /album/ shows the real shape and the home strip crops to 4:3 itself with object-fit: cover; pre-cropping would throw away the picture the masonry page is there to show.

Books are already split, into _data/books/01.yaml for numbers and symbols, then a.yaml through z.yaml, one file per initial letter. Only _data/books-favorites.yaml carries comments (2026-08-07: “leave the comment in just one of them… to read and understand and for the AI to remember”); the letter files are data and nothing else. Jekyll turns a data directory into a hash keyed by filename and iterates it in filename order, not filesystem order (verified 2026-08-01), which is what puts 01 first and z last with no sort filter. /books/ derives its section headings from those filenames.

⚠️ A book is filed by the first letter that is not an article. “The Children” belongs in c.yaml, and the rule covers The, A and An — so A Room of One’s Own is in r.yaml (Brajeshwar, 2026-08-07). The filename is the only place that knows the right letter; deriving headings from titles instead would put every The … under T, which is the thing this fixes.

⚠️ This paragraph used to say “by year, never by letter.” That advice was written on 2026-08-01 and was right for what the data then encoded: position in the file was the record of recency, so an alphabetical split would “scatter chronology across files and break the one rule the whole thing rests on.” It did exactly that, deliberately, on 2026-08-07 — the shelf reached 167 entries and an A–Z index became worth more than the ordering. The cost landed on the home page, whose Books strip could no longer show the newest seven and now shows the favourites instead; see the comment in index.html. Do not buy recency back with a read: or added: field — that was proposed and rejected twice, on the ground that a re-read has no single date worth recording.

Favourites are their own file, _data/books-favorites.yaml, since 2026-08-07 (“you can remove the highlight as my favorites into a separate YAML”). There is no highlight key any more: being in that file is the flag. It sat inside _data/books/ as _books.yaml for a day; Jekyll reads underscore-prefixed data files without complaint, so it became a key in site.data.books that every loop had to skip by name. Moved back out on 2026-08-08 — site.data.books is letters only. The trade is that nothing now guarantees the two grids are disjoint the way where_exp did — a book listed in both renders twice.

The cost of a directory is a flatten step wherever the whole list is wanted at once, since a directory is a hash of arrays rather than one array:

{% assign books = "" | split: "" %}
{% for group in site.data.books %}{% assign books = books | concat: group[1] %}{% endfor %}

⚠️ That snippet is wrapped in a Liquid raw block in this page’s source, and has to be. Liquid runs before Markdown, so a fenced code block is no protection: the example executed on first write and took the build down. Naming the tag in prose does it too — a bare mention outside a raw block reads as an unclosed opening tag and fails with “‘raw’ tag was never closed”. Both mistakes were made writing this paragraph.

⚠️ slice, where and where_exp all stop working on a directory, and slice does it silentlyindex.html kept building green while its Books strip rendered one item instead of seven. Count the items; do not trust the exit code. Inside an include tag, group[0] and group[1] are a hard error instead (“Invalid syntax for include tag”), so assign them to plain variables first.

Where they go

/static/books/, /static/album/, /static/films/, /static/devices/ — bare filenames in the matching _data/*.yaml. A img: value beginning with / is used verbatim instead, which is the escape hatch for borrowing a file from another folder.

What is inconsistent today

Worth a pass when convenient, not urgent:

  today against the template
books 360 × 480 (9 files) 1.5× on /album/ — soft on a retina screen
film 225 × 300 (97 files) 1.2× — visibly soft wherever it renders at 193px

129 of 136 files are already 3:4, so the shape is right and only the resolution is short.

The grid itself

Fluid — repeat(auto-fill, minmax(11rem, 1fr)), 8rem inside .album — so cards flex with the band and there is no fixed “styled as” size. /album/ is different again: multi-column masonry, so each image keeps its own height and nothing is cropped.

Gallery: a container, then a plain markdown list of images, optionally linked.

<div class="gallery" markdown="1">
- ![Alt](/static/one.webp)
- ![Alt](/static/two.webp)
</div>

Size and float classes: .small (40%) · .medium (60%) · .left · .right.

.large and .full both take the whole band and are now identical. Both are kept because both are in the archive, but there is one behavior to maintain, not two.

Videos and embeds

Ornamental. The writing has to work without them.

They take the full band, keep a 16:9 box, and get the same rounded corners as images.

<video width="100%" height="auto" poster="" controls muted loop preload="metadata">
  <source src="movie.mp4" type="video/mp4">
  <source src="movie.webm" type="video/webm">
  [Video Format - Non-Supported Browser]
</video>

⚠️ Images in posts carry no width/height attributes. That causes layout shift as they load, and it is why sidenotes need a ResizeObserver to re-place themselves once an image finally has a height. Worth fixing one day.

Rounded corners

Images, videos and embeds inside main get --border-radius (7px) — the same curve as the appearance panel, the search palette and the prev/next bar, so media matches the chrome instead of inventing a second one. It is a default at the lowest possible specificity, so anything with its own opinion simply overrides it.

Styles

Avoid gradients. Still true, and still zero of them in the CSS.

Avoid box-shadows. No longer true, and deliberately so. The appearance panel, the search palette and the archives year strip each use one. They are floating surfaces, and a shadow is how a floating surface says so. Everything flat stays flat.

Custom, strange, and why I did that ones — a shorter list than it used to be:

Post navigation

Under every article: PREV · (shuffle) · NEXT. One bar, prev hard left, next hard right, and whichever exists fills the width it is given — the first and last post have only one neighbor and their single link takes the whole bar rather than leaving half of it empty.

The circle in the middle goes to /random/, which sends you to a random post. It is 38px inside a 46px bar, centered absolutely rather than laid out in flow, so it sits in the same place whether the bar has two links or one. Its fill is --color-primary eased toward the bar — the same “on” color the selected pill wears — so it inverts correctly in every theme without a per-theme rule.

/random/ carries the list of every post URL inline and picks one in JavaScript. With JavaScript off it shows a real link to a post chosen at build time, which the daily rebuild rotates on its own. It never sends you back to the post you came from.

Timeline

Four pages wear it: /about/, /cv/, /about/brajeshwar.com/ and /now/. A vertical hairline with a marker per entry, and it is built from ordinary Markdown — no classes in the content.

<div class="timeline" markdown="1">

## Razorfish

2014 Sep — 2016 Mar · Creative Director

Prose, with [links](/) and *emphasis* that work normally.

</div>

## is an entry. The line directly under it is the sub-title — a date, a role, a place — and it is styled by position (h2 + p), not by a class, so there is nothing to remember. /now/ is the exception: its entries are bullets under a year heading, so it keeps a ul/li shape.

Order is document order. Nothing sorts. /about/, /cv/ and /now/ read newest-first; /about/brajeshwar.com/ reads oldest-first, and needed no setting to do it — the blocks are simply in that order.

The numbers, all measured rather than chosen:

Open in

On /cv/ and on every post, level with the title and flush right: OPEN IN · OpenAI · Claude | Markdown. /cv/ adds a PDF button; posts do not.

The AI links do not upload anything. They open a new chat pre-filled with “Read https://brajeshwar.com/<page>.md — I have questions about this post”, pointing at the plain-text twin that exists for every page and post. Append .md to any URL on this site and you get one; /llms.txt indexes them all.

PDF is the browser’s own print dialog. There is no PDF generator here, and assets/print.5a26c5ac.css already makes the printed page a clean document.

Heading anchors

Hover any heading with an id and a small # appears in the left margin. Clicking it links to that heading.

It is --step--1 regardless of the heading’s size, sits one --space-3xs clear of the text, and its click target is the full height of the heading rather than the glyph — so it is easy to hit without being loud. Invisible until hover or keyboard focus, and hidden entirely below 480px where there is no margin to sit in.

⚠️ ids come from kramdown, and this site runs plain kramdown, not GFM — so a heading that starts with a letter gets a real anchor (## Razorfish#razorfish) and one that starts with a digit gets section, section-1, and so on. /about/’s date labels are in the second group, which is accepted.

The reader’s controls

The gear in the header. Five axes, all remembered between visits and all applied before first paint, so nothing flashes:

Color is opt-in. The resting theme is monotone gray and links are underlined rather than colored. Prose follows the reader’s font choice; the interface never does.


Typography

Headings H1

Headings H2

Headings H3

Headings H4

Headings H5
Headings H6

Plotting ear goes let far she not star bit rat bad men. Low pox lemon rap gob whale pal bee apple vet boy air lot hog? Hum box said nag fish cop laugh dot yet zap hoe bad zoo bug image run fix hit hum cow. What have I done?

This is a blockquote text.

Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.