Design system
Every shared component in the shapes it actually ships in. Two color scales, one type scale, Tailwind utilities only — no custom CSS beyond the tokens in app.css. Build new screens out of what is on this page, and add anything new here first.
Colors
Two scales carry the whole product: background for every surface, border and piece of text, accent for anything interactive. The accent is green, so nothing else is: a state that went well is an accent state. Beyond those, amber and rose for feedback, and two hues the application statuses spend on themselves because a status list is only worth coloring if the colors tell it apart. A section that wants weight goes dark on the background scale rather than solid accent, which would leave the buttons on it a shade of their own color.
Background scale
bg-background-50 … bg-background-95050
100
200
300
400
500
600
700
800
900
950
Accent scale
bg-accent-50 … bg-accent-95050
100
200
300
400
500
600
700
800
900
950
Status colors
Feedback only — never for branding or navigation. Success is the accent.Roles
bg-background-100bg-whitetext-background-600border-background-300bg-accent-600bg-accent-50bg-background-900Typography
Open Sauce One at every weight the app needs. Headings are bold and sit on background-900, supporting copy steps down to background-600.
Type scale
Hero
Applications that look the part
text-4xl font-bold sm:text-5xlSection
Applications that look the part
text-3xl font-bold sm:text-4xlPage
Applications that look the part
text-2xl font-boldCard
Applications that look the part
text-lg font-semiboldLead
Applications that look the part
text-lg text-background-600Body
Applications that look the part
text-base text-background-700Small
Applications that look the part
text-sm text-background-600Caption
Applications that look the part
text-xs text-background-500Eyebrow
Applications that look the part
text-sm font-semibold tracking-wide uppercase text-accent-600Brand
md in every bar, lg above the account cardsProse
Motivation letter
Body copy sits on text-background-700 and stays below max-w-2xl so lines never run too long to scan comfortably.
Secondary copy drops a step in both size and contrast, which keeps a card readable without a second color.
Forms
Every control is a plain presentational component with a Formik-connected wrapper next to it. The wrapper owns the label, the error and the accessible wiring, so a route only names the field.
PasswordRequirements
app/components/PasswordRequirements.tsx — the rules, ticking off as they are met- At least 12 charactersstill missing
- A lowercase lettermet
- An uppercase lettermet
- A digitstill missing
Formik fields
FormikInput · FormikSelect · FormikTextarea · FormikCheckboxControl states
Pass any Lucide icon as the icon prop.
Please enter a valid email address.
With an optional description.
Editing
Long forms are built out of these: a Disclosure per group of fields, an ItemCard per entry in a list, a Slider for the values that are easier to judge than to type, and RichText for the fields that may carry marks — inline only for a document field, blocks as well for the free-text fields a document is generated from. Every label the editors render is a translated string — the labels here are literal only because this page is not translated. The chrome around them — ApplicationSection, DocumentToolbar, EditorFrame, DocumentPreview and AutoSave — is shared by both document editors but cannot be shown here: it needs a live Formik context and a working preview endpoint.
Disclosure
app/components/Disclosure.tsxOpen by default. The panel is hidden, never unmounted.
Collapsed, with an action beside the toggle.
ItemCard
app/components/ItemCard.tsxAmberg Engineering GmbH
Since 2025
First in the list, so there is no move-up button.
Untitled entry
Nested one level down, on the tinted surface.
Slider
app/components/Slider.tsx · FormikSlider.tsxStandalone
83%
RichText
app/components/RichText.tsx · RichTextToolbar.tsx · RichTextGuards.tsx · FormikRichText.tsxPrints as a single line, so Enter is ignored here.
Enter starts another paragraph. Each one is stored on its own line.
Blocks as well as marks: three heading levels, two kinds of list and a line break, stored as line prefixes and a trailing backslash.
Standalone, invalid, with a placeholder
What gets stored
{
"intro": "Frontend engineer with **ten years** in *product teams*, mostly around CO~2~ reporting.",
"body": "Dear Ms Moreno,\nI read your posting for a **senior frontend engineer** with real interest.\nKind regards",
"profile": "# Frederik Hummel\n## Experience\n### Amberg Engineering\n- **Senior frontend engineer**, since 2025\n- *Freelance*, 2019 to 2025\n## Education\n1. MSc Computer Science\n2. BSc Software Engineering\n## Contact\nBahnhofstrasse 1\\\n8001 Zurich"
}LanguageField
app/components/LanguageField.tsxThe language the document is written in, not the language of this app.
FileInputButton
app/components/FileInputButton.tsxNothing picked yet.
Hidden label
labelHiddenFor rows inside an already-labelled group, where a visible label per row would bury the content. The label still reaches screen readers.
Surfaces and feedback
Cards are the one container. Badges label a state in a list, alerts explain something that just happened, an empty state replaces a list with nothing in it yet, and a modal is a native dialog — so Escape, the focus trap and the inert page behind it come from the browser rather than from us.
Card
Plain card
Border, white surface, rounded-xl, p-6.
Hoverable card
Add a hover border through className when a card is clickable.
Frontend Engineer
InterviewCards compose with everything else — this is a tracked application.
Badge
variant="neutral | muted | info | active | accent | accentSolid | warning | danger" — a state that went well is an accent state, there is no second greenAlert
variant="info | success | warning | error" — info is neutral, success is the accentEverything is stored locally
CV exported
No backup yet
Import failed
Inverted band
bg-background-900 — for a section that wants weight, never a solid accent panelBoth editors are ready
The accent has the band to itself, so the primary button is the brightest thing in it.
Empty state
No motivation letters yet
Letters you write show up here, next to the CV they belong to.
Spinner
Sized through className, inherits the text colorModal
app/components/Modal.tsx — a native dialog, opened with showModal()ConfirmDialog
app/components/ConfirmDialog.tsx — for deleting an application and for replacing a documentStatusLine
app/components/StatusLine.tsx — one per page, text-xs like a field hint; it takes a message id, so these are the only translated lines on this pageSaved to your account
Saving…
All changes saved
Not saved. Check your connection and try again.
Writing it now. This takes a moment…
AiNotice
app/components/AiNotice.tsx — beside every button that sends data to GeminiWriting documents sends your profile and this advertisement to Google Gemini, which processes them in the US.
Icons
Lucide, imported one file at a time through SVGR. They inherit the text color, so size them with size-* and color them with text-*.
In use
import FileUserIcon from 'lucide-static/icons/file-user.svg?react'file-userfile-textbriefcasegraduation-capsparklespenciltrash-2downloaduploadlanguagesSizes and colors
Application statuses
The one place the app spends color outside its two scales, on the grounds that here a color is the information rather than the styling. Read as a temperature: cold while it is nobody's move, warm while it is in flight, green once the news is good, grey once it is over by your own choice.
The ramp
statusVariants in app/utils/status.tsStatusPicker
the status field — an ARIA listbox, not a selectThe only control here that is not a native one, and the coloring is the whole reason: an option cannot be styled where it counts — Chrome and Safari on macOS draw the platform menu and drop it — so the options are the badges themselves. Focus stays on the trigger and aria-activedescendant tracks the highlight; arrows move, Enter picks, Escape closes, and a press anywhere else dismisses it.