<!-- https://getkonjo.com/design/foundations/content-format · source: docs/design/foundations/content-format.md -->

# Content format

Dates, numbers, names, counts, and what happens when something does not fit.

> Part of the [Konjo design language](../konjo-design-language.md). The laws in the spine
> apply here unless this chapter contradicts them.

Formatting decisions get made per-screen by whoever is building it, and that is how one app
ends up showing "2 days ago", "Aug 20", "20/08/2026" and "2026-08-20T14:00:00Z" on four
adjacent surfaces. These are settled here so they stop being decisions.

## Dates and times

**Choose by how the person will use the fact.**

| Situation | Format | Example |
|---|---|---|
| Something upcoming, within a week | Weekday and time | `Thu 6:30pm` |
| Something upcoming, further out | Date and time | `Sep 4, 6:30pm` |
| Something upcoming with **no time of day** | Date alone, no filler | `Sep 4` |
| Something that just happened, under an hour | Relative | `12 min ago` |
| Something that happened today or yesterday | Named day and time | `Yesterday 7:15pm` |
| Something older | Absolute date | `Aug 20` |
| Older than the current year | Date with year | `Aug 20, 2025` |

**A date with no time is written as a date, never padded to look like one.** A birthday, a
test date, a due date, a trial expiry — none of these has a clock, and every "upcoming" row
above carried one until round 8 needed four date-only fields on one screen. Never write
`Sep 4, 12:00am`, `Sep 4 (all day)`, or `Sep 4 —`.

- **Relative time stops at "yesterday".** "23 days ago" makes a person do arithmetic to
  recover a date they could have just been shown.
- **Never both.** "Aug 20 (2 days ago)" is two answers to one question.
- **Times are local to the reader** and lowercase — `6:30pm`, not `6:30 PM` or `18:30`.
- **A range collapses what repeats**: `Thu 6:30–7:30pm`, `Sep 4–6`.
- **Absolute, always, for anything with consequence.** A waiver signature, a promotion date, a
  payment — those show the **full date including the year**, `Aug 22, 2026`, even in the current
  year. The table above drops the year for ordinary dates because the reader supplies it from
  context; a record is read later, out of context, sometimes years later, and a date without a
  year stops being a record.

## Numbers

- **Counts are plain integers** up to 999, then abbreviated: `1.2k`, `14k`.
- **A badge count caps at 99+.** The exact number stopped mattering long before that, and the
  dot has a fixed size.
- **Money always shows currency and two decimals**: `$45.00`, never `$45`. A price that
  changes shape between rows is a price people distrust.
- **Percentages are whole numbers** unless the fraction is the point. `73%`, not `73.2%`.
- **A percent delta needs a base of at least 10** (ten dollars, for money). Off a smaller base
  one unit of movement swings the figure by double digits — “+3100% vs 1” reads as a bug, not a
  trend. Show the absolute change instead: “+31 vs 1”. In code: `formatYoyDelta` in Studio's
  `homeDashboard.ts`.
- **Zero is written, not hidden.** "0 students" beats an empty cell, which reads as a bug.
- **Never a decimal on something uncountable.** "4.5 classes" is not a thing.

## Durations

Time *at* something, as opposed to time *since* something.

- **Months up to two years, then years and months**: `14 months`, `2 years 3 months`,
  `5 years`. Never `1.2 years`, never `427 days`.
- **Under a month, use weeks**: `3 weeks`. Under a week, the app almost never needs a duration —
  use a date. If what you actually want is *how long has this been sitting*, that is
  [dwell](#dwell--how-long-this-has-been-sitting), not a duration, and it has its own rules.
- **Round down, never up.** Someone at a rank for 13 months and 29 days has been there 13
  months; rounding up overstates a fact people care about.
- **Unknown is a legitimate value** and is rendered as such, not as zero. A student who trained
  elsewhere before joining has an unknown time at rank, and `Unknown` is the honest answer —
  this is the case [content missing](#empty-and-unknown) exists for.

## Dwell — how long this has been sitting

A **third** kind of time, and the one every queue in Studio needs: leads, dunning, sub requests,
the at-risk list, the waiver chase. Not time *at* something (a duration — an achievement) and
not time *since* an event (relative time — which stops at "yesterday"). Dwell is a **debt**:
how long a thing has been waiting for a person to act on it.

- **Always days, plainly.** `Today` · `1 day` · `13 days` · `47 days` · `90+ days`.
- **Never weeks or months.** Applying the duration rules above to a dwell renders a 13-day
  stall as "1 week" — because durations round down — which understates exactly the fact the
  screen exists to surface. A duration rounds down because overstating time-at-rank flatters
  someone; a dwell must never round down, because understating a stall hides work.
- **Never relative phrasing.** "13 days ago" makes a person do arithmetic (see above); "13 days"
  is the answer they wanted.
- **One format at every age**, so a column of them sorts by eye. A format that changes shape at
  day 7 makes the column unscannable, which is the one thing it is for.
- **Cap the display, not the value.** Past 90 days write `90+ days` and keep sorting on the real
  number.
- **Dwell is never a colour on its own.** Whether a number of days is a warning is an SLA
  question, which is a [product fact](../product-facts.md), not a palette one.

## Pluralisation

Write both forms; never `student(s)`, never `1 students`. For zero, prefer words over the
digit where the sentence allows: "No classes this week" beats "0 classes this week" in prose,
while a stat tile keeps the numeral.

## Names and people

- **A person is their full name** the first time and in any list. Not "A. Torres", not a
  username.
- **Initials are two characters**, first and last. One name gives one initial.
- **Never truncate a name mid-word.** If it does not fit, the layout is wrong — a name is the
  one string a person will always notice being mangled.
- **Honorifics come from the art**: sensei, sifu, and whatever the dojo's style uses. Konjo is
  multi-style; do not hardcode one tradition's title into a shared component. See
  [rank](rank.md).

## Truncation

**Truncation is not a layout strategy.** It is what happens when a layout already failed.

- **Never truncate**: a person's name, a belt rank, an error message, a price, a date, a
  primary action's label.
- **May truncate at two lines**: a post body, a description, a note — with the full text one
  tap away.
- **May truncate at one line**: a secondary metadata line where the leading words carry the
  meaning.
- **If it does not fit, it reflows.** A title wraps to three lines and then steps down a type
  size; it does not get an ellipsis.
- **Ellipsis at the end, never the middle**, except for a filename where the extension is the
  useful part.

Combined with text scaling: whatever you test at 100%, retest at 200%, where every one of
these gets harder. See [accessibility](accessibility.md).

## Empty and unknown

- **Missing optional data shows nothing** — no "—", no "N/A", no empty label. The row is
  shorter.
- **Missing required data is an error**, not a dash. If a class has no time, that is a bug the
  screen should surface, not smooth over.
- **"Unknown" is only correct when unknown-ness is the fact** — an unrecognised belt from a
  visiting student, for instance.

## Lists of things

- **Two items join with "and"**: `Kata and Footwork`. Three or more use commas with a final
  "and".
- **Long lists cut at three and count the rest**: `Ana, Marcus, Priya and 4 others`.
- **Order by what the reader is looking for** — soonest first for events, most recent first
  for a feed, alphabetical only when there is no better answer.
