Skip to content
kc@kumarChandrachooda.com:~$ cd /design-system/core && read --section="01 identity" 0%
KC Design System — Blueprint / Blueprint

Drawn to scale.
One system, every component.

The blueprint style drafts the system in design-system-blueprint.css: Newsreader headings with an italic aside, Barlow for reading, a 24px grid under everything and each card's meta line boxed as a title block. Same components, same markup — on the drawing board.

Reading in Blueprint — blueprint / blueprint Browse all 8 →

01

Identity details

The accent line and kicker reproduce the rotated section labels of the slideshow; the notched label chip comes from the “Hello” tab on the about page. .ds-kicker--mono (the /ai gallery, the owner's decision 27, 8 Nov 2026) is the kicker set in mono over a 4px line.
Section kicker
Small kicker
Hello
Featured
The AI app
02

Buttons & chips

The solid button wipes to grey with a pop, like the original btn-black; the accent is the primary/brand button and hovers the same way — light wipe, ink text; the ghost outline skew-fills ink, mirroring salv-down-cv-btn.
azure
dotnet
outline
The busy button — the full-screen loader's DNA at 7px. These three are the nine banter boxes that were the page loader, kept alive at the smallest scale the system has. is-busy drops the hover wipe and the press bounce (both of them read as “your click landed”, which is the wrong thing to say while the click is still being honoured) and turns pointer events off; the label stays put, because swapping it for “Loading…” makes the button change width mid-press. The dots take currentColor, so they are white on the solid button and ink on the ghost with no variant rule to keep in sync. This is the state loadMore() applies to the load-more link while a page is in flight.
The flat family — the identity screens' buttons, kept as they were built and added as variations of .ds-btn (identity-ui-cleanup-docket.html, IU20). --flat is the family: no wipe and no pop, the hover swaps the fill. Each role sets the family's measures as ratios of one type step: --firm is the interruption's press, --accent --sentence the invitation's, --action the account's and --mini a row's. A firm ghost takes the colour of the card it sits in.
Block and danger on the default button, and the text button. --block fills its column; --danger puts the default button on the danger-deep ground and keeps its wipe. .ds-link-btn is not a modifier: it is a button reset with the link treatment, for a control that sits inside a sentence or a label.

Address not verified yet?

03

Tabs

One strip, three modes (_Tabs + .ds-tabs). The geometry is shared — the 2px rule, the accent tint and rule marking the selected tab (the rail's own current-row grammar turned horizontal), square corners because a radius meeting the rule leaves a notch, a scrolling strip with edge fades when the tabs outgrow the column — and the mode decides the element and the ARIA. --nav is anchors over real routes with aria-current (the group strip above navigates this very showcase); --panel ships as anchors and is promoted to the full tablist pattern by tabs.js once its panels are confirmed present; --display is a closed segmented radiogroup for choices about the reader's eyes rather than the content — the blog's Cards/List switch. One tab stop per strip: focus roves with the arrow keys, and selection is manual where it is expensive (a navigation) and automatic where it is free (a class toggle).
--nav: anchors over real routes, aria-current="page" on the one you are on, a count in mono beside each label. The selected tab is framed in ink on its open sides with the accent taking over the rule beneath; when the strip overflows, edge fades carry ‹ › arrows that scroll it a screenful at a time. This specimen is scenery — the live strip is at the top of this page, and each of its tabs is a document of its own.
--panel: live. The panels are siblings in this document, so this is the one mode that earns the full role="tablist" pattern — tabs.js promotes the anchors once it has confirmed every panel is present. Tab into the strip, move focus with ← → Home End, select with Enter or Space: activation is manual, because selecting swaps a panel and writes history, and arrowing across the strip should not do either seven times. The grip leading the strip is the end of its rule — drag it right to stand the strip up, left or down to lay it flat, or just press it. Standing, the accent rule moves to the left edge, the panels render next to the strip as a column, the tablist announces aria-orientation="vertical", and the choice persists for the reader in localStorage. There is no symbol for the change: a mark that stands for a change of layout is a verb, and both drawings of one (⇄ and its rotation) turn out to be somebody else's noun. The grip's strokes stay perpendicular to the rule they cap, so the mark reports the orientation without moving, and it holds the same top-left corner whichever way the strip is lying. The announcement is one fixed label with the state on aria-pressed — "Stack tabs, toggle button, pressed" — and below 992px, where there is no room for a column beside a body, the grip is greyed and inert.

Navigate selects between different documents. The state is the route, the element is an anchor, and the back button works because the browser is doing the navigating.

--display: live. A closed segmented object — 2px ink all round, dividers between segments, ink fill on the checked one — because a rendering preference is a setting, not a set of places: it is a radiogroup, a tablist here would promise a panel that does not exist, and the address bar is never touched. Arrows select immediately (the switch is one class toggle, so the APG default is free). The real one drives the blog archive's Cards/List choice and keeps it in localStorage.
View
The segmented control — .ds-seg, --display's sibling for a choice that must work with no script (identity-ui-cleanup-docket.html, IU15). The same closed object, but every option is a submit button with its own name and value, so it needs no js gate; the current one is aria-pressed="true". The real one sets the reader's type size on the settings page; these are plain buttons and post nothing.
05

Page hero & statline

Every listing page opens the same way: kicker, an oversized black title, a light lead and the mono .ds-statline counters whose pulsing dot marks the live count. Pages compose the pieces per surface — a breadcrumb above, chips or search below — so the hero is a pattern, not a partial. --compact (the /ai gallery, the owner's decision 27, 8 Nov 2026) is the shorter opener with a smaller title, and __mark the hero's own highlighter under one word.
The listing-page opener: kicker, title, lead and the mono statline with its pulsing live-count dot. Each page assembles the pieces directly — the series index adds a breadcrumb above, the archive adds search below — which is why this is a pattern and not a partial.
The Build Log

Blog

Notes on cloud & data architecture, .NET engineering and the practice of shipping software, written from the field, with the code to back it up.

  • 4 topics
  • 3 series
  • 18 articles
The AI app

AI on this site runs on your AI.

Nothing here hosts a model or keeps your keys.

The section head is the smaller sibling: a title on the left, something quiet on the right, over a hairline. It opens a section inside a page rather than the page. The pair is the component — a heading with nothing on its right is just a heading — and what goes on the right is a fact about the list below it, never a metric about the person reading.

Everything you have touched

6 of 21

Series you are part of

yours is the one with the accent edge
06

Form fields

The layer this system did not have. Before it there was exactly one input in the 48 sections there were at the time — .ds-input, defined in a page layer for the archive's search box — and no label, select, textarea, fieldset, hint, error, disabled treatment or validation summary anywhere. The Workshop did not ignore the system; it fell through the hole and wrote nine classes of its own, which is what any app needing a form would have done. Deliberately small: a field, its label, its hint, its error, three control shapes, a grouped choice, a summary and an actions row. No date pickers, no autocompletes, no wizards — those are components with behaviour, and behaviour belongs to whichever app can name why it needs it.
The layer this system did not have. Before it there was exactly one input in the 48 sections there were at the time — .ds-input, defined in a page layer for the archive’s search box — and no label, select, textarea, fieldset, hint, error, disabled treatment or validation summary anywhere. The Workshop did not ignore the system; it fell through the hole and wrote nine classes of its own. The control room is the named second consumer.

It is the card, the meta description and the OG tag. 61/220

Chosen from what exists — a new one is a request, not a text box.

What the system owns, and the distinction that took two components rather than one. A disabled input says “you could edit this if you had the right”. A derived value has no right to grant — nobody edits a slug the system computes — so it is not an input at all. Disabled is greyed rather than hidden, because hiding a control teaches that the rule does not exist and greying it teaches that it does.

slug — derived ⚠ outbox-without-a-scheduler frozen at first submission

That category does not exist.

A grouped choice and a summary. The system had neither. A fieldset because that is what a set of related controls is — the legend names the question, and a screen reader announces it with every option rather than once at the top.
when would you like this out?

A suggestion — the owner still sets the date. Nothing here writes scheduled or published; those two words appear on this control and nowhere else in an author’s half of the app.

Not ready to submit:

  • It needs a category.
  • The body is under the floor of 200 words.

Greying the button out is a courtesy. The endpoint re-checks every one of these before it does anything.

The identity field (identity-ui-cleanup-docket.html, IU07: the owner's square, mono field, promoted whole). The frame is on .ds-input-group, so a sigil cap before the control and a show cap after it share it and the focus ring is drawn once around all three; --square is the identity look, and the plain group keeps the control tier's corners. .ds-field--square sets the label, control, hint and error as the sign-in cards draw them, a label can carry a trailing control, and an error leads with its mark. Nothing here submits; the show caps do not toggle.

That email and password do not match.

15 characters or more, checked against leaked passwords.

A typed confirmation (IU19). .ds-field__label--sentence is a label written as a sentence, the phrase in it shown exactly as it must be typed; --tight is the denser group under it; and .ds-form-actions--split lets the first action fill while the rest hug their labels.
The ways in (IU09). .ds-way is one row per way to sign in or to confirm: a glyph in a ruled square, the way's name, a mono tail on the trailing edge. A button inverts to the ink ground; a disabled one keeps its frame and loses its inversion; --static states a way that is not pressed here; and --confirm is the step-up's tighter row. .ds-rule-or stands between two kinds of way.

A passkey assertionneeds WebAuthn

The folio (IU10, IU11, IU18): .ds-folio, the surface a form like these sits on. Square and still, one of three shadows (the soft accent; --interrupt's ink pop; --flat's none; --danger for the danger zone), and the drawn card's text roles at the drawn rhythm: __kick, __title, __sub, __note, __status and __meta. A section head as its first child is the band across its top.
Account

Sign in.

Everything published stays readable without it.

The site receives an account id and an email — nothing else.

oidc · magic link · password, hashed · no enumeration

confirm it is you

Sign in again to remove a credential

This action needs a credential proved in the last five minutes.

Set a catch phrase on the account page

step-up valid for 5:00 after it completes · evaluated server-side, never from this page

Sessions

2 active

Signing out everywhere else keeps this device signed in.

08

Badges & pills

The status vocabulary in one place: notched sheet labels, the “coming soon” pills, the draft-preview chip, date pills, the colour-coded role pills from the project cards and the tech chips. Grey consistently means “not yet”; periwinkle means live, featured or in focus. .ds-state--outline and its --flag (the /ai gallery, the owner's decision 27, 8 Nov 2026) draw the lifecycle words as a line on no wash.
Sheet labels: .ds-label with its notched-tab fold. Base ink for identity, accent for a live series, grey for one still in drafts — grey consistently means “not yet”.
Hello
Series · 4 parts
Coming soon · 2 parts
Status pills: the roadmap vocabulary. Two different components say “not yet” in two places, and they are kept together here so the difference between them stays a decision rather than an accident. The third used to be .ds-draft-badge; it is now a member of the lifecycle scale below.
Coming soon
Coming soon
Lifecycle states: where a piece of content is in its life. .ds-state plus one tone, and the tones are the scale — --draft is the only one with no signal colour, the two that are waiting on a person are dashed as well as coloured, and --published is the only filled one because it is the only terminal one. Read as a column at the right-hand end of list rows, which is why the type is mono: a shape the eye matches down a list rather than a word it reads five times.
Draft
In review
Changes requested
Scheduled
Published
Live In your assistant Coming
cache · in 12 posts · idf 3.71post · in 401 posts · idf 0.33
Preview
Date pills: one on the employment rail, one on certification cards, and a muted variant for a certification still being prepared.
July 2021 – Aug 2022
July 2021
In preparation
Role pills: .ds-project__role--* colour-codes the project cards, so the filter row doubles as a legend and the reader never has to learn the mapping twice.
Architect
Lead
Developer
R&D
Tech chips: the key stack on a project card. Uncoloured on purpose — the role pill is the one carrying meaning in that row.
C#
Azure Functions
Terraform
The tag: .ds-tag, a square, solid mono word that states what a thing is — the delete card's “irreversible” (identity-ui-cleanup-docket.html, IU19). Ink by default; the tones sit on their -deep grounds, the pairing that reads as text.
ink
irreversible
held
verified
invited
09

Loading & empty states

Three answers to one question — what is on screen while there is nothing to show. The cell wave (.ds-cellwave) for a fetch whose shape is not known; the skeleton (.ds-skeleton) for one whose shape is known exactly, the card emptied and wearing its own class list so it cannot drift from it; and the quiet placeholder (_EmptyState) for when the answer turns out to be none. Seventeen skeleton shapes ship, each documented beside the card it empties.
A skeleton is the card, emptied. It wears the component's own class list plus .ds-skeleton, keeps every structural wrapper, and swaps only the leaves that held text or an image for .ds-skel blocks. Nothing about the box is restated — border, radius, padding, gaps and the media ratio all come from the one definition that already exists, which is why a skeleton cannot drift from its card. That guarantees the part that matters: the grid does not reflow when content lands, because the column tracks and the media ratios are identical before and after. Height is an estimate, not a promise — the line counts stand in for a typical card, and each bar plus its stack gap is sized to equal the line-height it replaces, so a title that runs long still grows the card by a line. That is the honest limit of the technique. .ds-skeleton also switches off every transition and animation the borrowed classes bring with them: a card that lifts on hover or draws its border in has nothing to respond to yet. Widths are the exception and are set inline — the width is the content, and content is the one thing a skeleton cannot inherit.
Roots are never <a>, <button> or <details>, even where the real component's is: pointer events are off but the tab order is not, and a focusable aria-hidden element is a WCAG 4.1.2 failure. Call one with <partial name="_Skeleton" model="card" />.
ScaleHookUse it when
Page.ds-loader--counterThe site is booting. Once per session, never under reduced motion — see Page entrance, in Chrome.
Page.ds-loader--stampSame, where the monogram says it better than a number.
Region.ds-skeletonContent is coming and you already know its shape. Each of the seventeen is documented beside the card it empties.
Inline.ds-cellwaveContent is coming and you do not.
Control.ds-btn.is-busyA press is being honoured.
Below the page scale, all of them are for waits the reader started, so they appear at once rather than after a beat — the beat belongs to the caller, which should hold off ~300ms before showing anything so a fast response never flashes a placeholder. Every looping animation resolves to a legible static frame under prefers-reduced-motion rather than freezing mid-cycle, and the page loader is skipped entirely.
The cell wave — the segmented meter again, this time with nowhere to get to: six bordered cells passing a periwinkle pulse down the line. It is the loader for a fetch whose shape is not known in advance, where a skeleton would be a promise you cannot keep — a carousel page, a drawer list, a filter re-query. Where the shape is known, use the skeleton shelf instead; it says more.
Loading more
Seventeen shapes ship. Every card in the system has one, and each is documented in the section for the component it empties — the claim "every card has one" is the only thing that needs saying in one place. Render any of them by name with <partial name="_Skeleton" model="card" />.
flagshipcardpost-rowtopicseries-cardseries-itemseries-mappagenavauthorrelatedcontinueprojectcertfeatureedunotecardskill-group
And when nothing arrives. The empty state is the same decision one step later, which is why it is documented here rather than in a section of its own: a placeholder that never resolves into content is a wait that lied, and the two have to be designed against each other. The real _EmptyState partial in both its forms, over mocked models.
Message only: how a topic page reads when everything under it is still in drafts.

Nothing published under this topic yet.

Message, hint & link: the archive's no-results form, with a way back out of the dead end.

Nothing matched “owl-carousel”.

Try a different search or filter.

Clear search and filters
10

Drawer

An edge-anchored overlay panel (.ds-drawer) that slides in over a scrim, right by default and --left on request, in three widths. Square corners — it is a surface, and the 2px edge frames it. The markup is one <aside> plus a trigger; the dialog role, focus trap, Escape, scrim and scroll lock come from the behaviour layer. Its rows are generic on purpose: the navigator behind the tab on the right of this page is the same component that will hold saved-for-later articles.
Anatomy. A head (title, optional sub, close), an optional tools strip, a scrolling body and an optional foot that stays put. The trigger points at the panel by id — data-ds-drawer-open="…" — and everything else (dialog role, scrim, focus trap, Escape, scroll lock) is applied by the behaviour layer.
Rows are deliberately generic. .ds-drawer-item is an index, a label and a meta column, grouped under .ds-drawer__group. That row reads as a section link in the navigator on this page, and as a saved article when the drawer becomes the read-later shelf — same component, no fork.
Five edges to pull from. Modifiers on the same panel, from the drawer docket — only geometry and skin change; the dialog role, the trap and Escape are the same for all five. Their rows are the docket’s: full-bleed and ruled, the active one marked by an accent edge. --push moves the page aside instead of covering it, and --split docks as a column that squeezes the page — neither has a scrim, and the page stays scrollable beside them, which is what makes them right for reference you keep open while reading. The navigator behind the tab on the right of this page is a push drawer. --ink is the phone menu’s dark skin behind the scrim, for cross-app shelves; --float is a card inset from three edges, for pages wearing the floating header; --sheet rises from the bottom edge with its rows in columns, for deck mode and touch.
12

Toast

The third overlay, and the only one nobody opens (.ds-toast): it arrives on its own and leaves on its own, so there is no panel on the page and no id to point at — the host is built on first use and a caller supplies only the message. The primary skin is the fuse: a bordered card at the bottom right whose accent underline drains left-to-right as the dwell runs out, the signature wipe run in reverse as a countdown, paused by hover or focus along with the timer behind it. The secondary is the dock chip (--dock) — the dock's position and skin, a mono kind-tag and an action word, springing up from the bottom centre with the one overshoot the system allows. Cards carry a payload; chips carry a verb. One at a time per host, always dismissible, the hosts made empty at start-up as the live regions (assertive for failures, polite for everything else), entrance and exit frozen under reduced motion — the fuse is not, because it is a readout rather than decoration. A server-rendered callout marked data-ds-toast-lift is lifted into a held toast, and stays in place as the answer without script.
The fuse — the primary. A white bordered card at the bottom right whose bottom edge carries the accent underline, draining left-to-right as the dwell runs out: the signature wipe, run in reverse as a countdown. You can see how long the message has left, and hovering or tabbing into it pauses the drain — the bar and the timer behind it are held together, so the underline never lies about the time left. It is the workhorse because it carries a payload: a title, and under it the filename, slug or size the message is about.
A dwell of zero changes what the card has to carry. There is no countdown to draw, so the fuse goes — and with it both the thing that was colouring the kind and the only thing on the card that moved. So the border takes the kind, and spin gives the tag a pulsing dot for a message that is waiting on something rather than stuck. Fire the last two above and watch the edge, not the underline.
The dock chip — the secondary. A dark chip springing up from the bottom centre with a mono kind-tag and an action word in violet: the dock's position and the dock's skin, and the one moment of overshoot-and-settle the system allows. It is for one-line state that comes with a verb rather than a payload — turn off, undo, view. It carries no fuse: a chip you are meant to act on should not visibly be running out, so it dwells longer and leans on the verb instead.
The two hosts are independent, so a chip stating a mode and a card confirming an action can sit on screen together — they are saying different kinds of thing. Within a host it is one at a time: a second message supersedes the first rather than stacking under it, so a burst never turns the corner into a feed. Fire both above and watch it happen.
The ticket — for a payload. A full system card — the 2px border, the hard ink pop shadow, a filled accent label — punched in from the right edge with a slight rotation that overshoots and settles square. The one toast drawn at the top right, so it has a corner of its own. For a confirmation that carries something: a title and the thing it is about. No fuse is drawn on it; the label is the tag.
The stamp — once a visit. A square seal stamps down into the corner — scale, land, the accent shadow arriving with it — holds, and drops away. The tick and one word, nothing else. It is for the rare confirmation worth celebrating, and used more than once a session it would wear out its welcome, which is the point. It lands in the card’s corner and takes the card’s turn there. No close mark is drawn and a seal has no room for one, so the whole seal is the dismissal.
Neither has a caller yet. The docket gives the ticket to payload confirmations, with a kept reading position as its example, and the stamp to one-per-visit moments such as a contact form landing: the first is the blog’s, which is not being changed now, and the site has no contact form. The docket’s first toast, the plate, has no job since the fuse was kept as the default, and is not built.
Anatomy, held still. The same markup the behaviour layer builds, lifted out of its fixed host with the fuse stopped three-quarters through.
download

CV download started

KUMAR_CHANDRACHOODA_Resume.pdf · 412 KB

draft

Preview mode is on — drafts are visible

saved

Reading position kept

An Event Bus in Seven Files — part 3, 40%

Sent

Raising one. There is no panel on the page and no id to point at: the host is built on first use, so a caller only supplies the message. Either window.dsToast(…) from script, or the declarative trigger every button on this page uses — which is what the CV download and the copy-link buttons will carry.
<button data-ds-toast="CV download started"
        data-ds-toast-msg="KUMAR_CHANDRACHOODA_Resume.pdf &middot; 412 KB"
        data-ds-toast-kind="success">Download</button>

dsToast({ title: 'Filter cleared', variant: 'dock', tag: 'archive',
          action: 'undo', onAction: restore });
OptionDefaultWhat it does
variantfusedock for the chip, ticket for a payload at the top right, stamp for the seal (title only).
kindinfosuccess, warning, danger — recolours the border, the fuse and the tag (the tag on the -deep tone, which reads as text), and switches the host's live region from polite to assertive.
dwell3500 / 6000ms on screen; 0 stays until dismissed, and drops the fuse with it.
spinfalsePulses the tag. For a sticky message that is waiting on something — a poll, a write in flight.
action—The verb. onAction runs, then it dismisses unless the handler returns false.
tagthe kind, on a chipThe mono kind-tag. Set uppercase by the skin, so pass it lowercase.
message—A string, or a list of parts for mixed type: ['Reading ', { code: '/deck/state' }, ' until…']. Built out of nodes, never parsed as markup.
Accessibility and motion are not the caller's problem. The three hosts are made empty when the page starts and are the live regions, so a reader is already listening before anything arrives: a failure switches its host to aria-live="assertive" and interrupts, everything else is polite and waits its turn, and a toast carries no role of its own (it was role="status" or "alert" until 8 Nov 2026, a live region born with its words in it). Every toast is dismissible, its close at the 24px floor, and under prefers-reduced-motion the slide and the spring are frozen to a fade. The fuse is not frozen with them — it is the dwell readout rather than decoration, and hiding how long a message has left serves a reader who asked for less motion worse, not better.
The lift — a sentence that must survive without script. A done line the server says (an expiry, an address, a count) is rendered in the page as a callout carrying data-ds-toast-lift, and that callout is its no-JS answer. With script, a tick after the page starts, the first one is lifted into a fuse toast held until dismissed, read through textContent only: the tone is its kind, the __tag its kind word, the __title its title, the other paragraphs its message. A refusal is never lifted, and nothing inside a dialog is. The callout below carries no lift attribute, so this page does not raise a toast as it opens; the button raises the toast it would become.
done

A link is on its way

To ada@example.test. Following it within 30 minutes adds it as a way to sign in.

Markup

As rendered on the page behind this — captured, not transcribed.


    

Which CV would you like?

Pick the one closest to the role. Each is two pages and carries the same facts.

Applied AI

Agentic AI & Cloud Engineer · multi-agent systems, RAG and MCP on AWS

Cloud & .NET

C#, .NET 10 and ASP.NET Core microservices on AWS and Azure · Terraform & AWS CDK

Data Engineering

AWS lakehouse · Kinesis, Glue (PySpark), Apache Iceberg, Lake Formation

Everything

All three domains, with the standout project from each