diff --git a/data/repo-awareness-snapshot.json b/data/repo-awareness-snapshot.json index 7ac378077a..1222e1c0d2 100644 --- a/data/repo-awareness-snapshot.json +++ b/data/repo-awareness-snapshot.json @@ -1,8 +1,8 @@ { "version": "repo-awareness-snapshot-v1", "captured_revision": { - "sha": "310c0fed0f1df3456e6b7c6a7ab61e251944cef8", - "committed_at": "2026-08-28T03:53:31+08:00" + "sha": "3b6fa2a83fde1284cac7bdd7a016584a81b4d337", + "committed_at": "2026-08-28T20:44:01+00:00" }, "routes": { "modes": [ @@ -1697,6 +1697,11 @@ "section": "root", "catalogued": true }, + { + "path": "docs/brand/psychsift-logo.md", + "section": "brand", + "catalogued": true + }, { "path": "docs/care-plan-context.md", "section": "root", @@ -3801,6 +3806,9 @@ { "name": "audit" }, + { + "name": "brand" + }, { "name": "care-plan" }, @@ -3848,10 +3856,10 @@ } ], "counts": { - "documents": 482, - "catalogued": 108, + "documents": 483, + "catalogued": 109, "uncatalogued": 374, - "sections": 19 + "sections": 20 } }, "test_health": { diff --git a/docs/README.md b/docs/README.md index 92d9b55602..422f976ed0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,6 +35,7 @@ npm run docs:check-links - [design-system/SPEC.md](design-system/SPEC.md) — the complete v2 design system: roles, rules, rationale (never values) - [design-system/TOKENS.md](design-system/TOKENS.md) — reconciled token inventory: every role, winning name, owner, and what it replaces - [design-system/COMPONENTS.md](design-system/COMPONENTS.md) — the eight safety-component specifications plus the maturity matrix +- [brand/psychsift-logo.md](brand/psychsift-logo.md) — the PsychSift mark: arc-by-arc construction, colours, file set, and usage rules - [design-system/DECISIONS.md](design-system/DECISIONS.md) — conflicts C1–C5 resolved, clinical Q&A record, assumptions, blocked items - [design-system/GATES.md](design-system/GATES.md) — every design-system rule paired with its enforcement status - [design-system/FIX-GUIDE.md](design-system/FIX-GUIDE.md) — Hazard 1–2 sweep dispositions (Fixed / Documented / Deferred / Out-of-scope) diff --git a/docs/brand/preview.html b/docs/brand/preview.html new file mode 100644 index 0000000000..1717759545 --- /dev/null +++ b/docs/brand/preview.html @@ -0,0 +1,1286 @@ +PsychSift Mark + + + + + +
+
+ The PsychSift mark +
+

PsychSift

+

The mark, rebuilt as true vector geometry — every curve a circle with a stated centre and radius.

+
+
+
+
Drawn 28 Aug 2026
+
Primary file 863 bytes
+
Curves 4 arcs, 1 line, 1 circle
+
Source public/brand/
+
+ +
+
+ 01 +

The set

+ 13 SVG · 2 PNG +
+

+ One mark, cut for the grounds it actually has to sit on. The checkered stage shows where each file is transparent. +

+
+
+
+ +
+
Primary markpsychsift-mark.svg
+
+
+
+ +
+
On brand bluepsychsift-mark-blue.svg
+
+
+
+ +
+
On a light tilepsychsift-mark-light.svg
+
+
+
+ +
+
Maskablepsychsift-mark-maskable.svg
+
+
+
+ +
+
Small-size cutpsychsift-favicon.svg
+
+
+
+
+ +
+
+ +
+
+
Glyph, no tilepsychsift-glyph-*.svg
+
+
+
+ +
+
+ 02 +

How it is built

+ glyph units, S height = 100 +
+

+ The gap down the middle is drawn first, and it is one straight line. Each stroke's facing edge is that line, + offset by half the gap, so the two edges are exactly parallel — one width the whole way down, and neither can + curve against the other. Each stroke is then built out from its own edge: an outer sweep on a circle centred on + its own radius, tangent to the top and left of its box; a flatter arc two-thirds of the way down that hooks the + tail into a long fine point; a throat under the head; and a single blend that carries the throat onto the cut. + Both blends bend the same way, away from the other stroke, so nothing on either facing edge turns over. At both + ends of each stroke the two edges simply run into each other, and that crossing is the point — a sharp cusp, not a + rounded cap. +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SegmentCentrerRole
The cut44.6629118.806°one straight line; both facing edges are it, offset ±2.1
Outer sweep29.2009, 29.200929.2009upper outer edge, head to two-thirds down
Tail hook51.5937, 16.141855.1234draws the tail to a point
Throat44.8724, 20.028617.7232the scoop under the head
Blend17.4753, 33.932413.0carries the throat onto the cut
Head cusp41.3675, 2.655436°outer sweep meets throat, sharp
Tail cusp18.0434, 59.879381°the cut meets the tail hook, sharp
Lower stroke55.1029, 100.3813×−1.07the upper stroke, turned and 7% larger — exactly the same path
The point60.4716, 17.358110.458643.95 along the cut, 14.90 off it
+
+
+
+ +
+
+ 03 +

Down to 16 px

+ rendered at true size +
+

+ The 4.2-unit gap between the two strokes is the first thing to close up. The second row is a separate cut for + small sizes: each facing edge moves 1.5 units away from the other, widening the gap to 7.2. Only those two lines + move, so the outer silhouette is the one in the primary file. +

+
Primary markpsychsift-mark.svg
+
+
+ 16 +
+
+ 20 +
+
+ 24 +
+
+ 32 +
+
+ 48 +
+
+ 64 +
+
+ 96 +
+
+
Small-size cutpsychsift-favicon.svg — use at 32 px and below
+
+
+ 16 +
+
+ 20 +
+
+ 24 +
+
+ 32 +
+
+ 48 +
+
+ 64 +
+
+ 96 +
+
+
+ +
+
+ 04 +

Lockups

+ Inter Display SemiBold, outlined +
+

+ The wordmark is embedded as outlines, so the lockups render identically on a machine that has never seen Inter. +

+
+
+ psychsift-lockup-horizontal.svg +
+
+ psychsift-lockup-horizontal-tagline.svg +
+
+ psychsift-lockup-horizontal-reverse.svg +
+
+ psychsift-lockup-stacked.svg +
+
+ psychsift-wordmark.svg +
+
+
+ +
+
+ 05 +

Colours

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameHexWhere it is used
Deep Navy#0D1B2AThe tile; and the S on light grounds
White#FFFFFFThe S and the point, on navy
Brand Blue#2563EBThe blue tile; the lockup tagline
Cool Gray#F2F4F7The light tile
+
+
+ +
+
+ 06 +

What changed from the original

+
+

+ The mark on the brand sheet is the design. Measured from a high-resolution view of it, arc by arc, at the 50% + level of its anti-aliased edges — the one place on a blurred edge the blur does not bias. Three earlier drafts got + it wrong in instructive ways, and every error came from trusting a measurement the source could not support. +

+
+
+ 01 +

+ The terminals are sharp. At the size the original was first sampled, a sharp point blurs into a rounded + blob. The first draft fitted that blur and gave the mark rounded club-ends. Both tips are genuine cusps. +

+
+
+ 02 +

+ The tail leaves the main sweep. The second draft put the whole outer edge on one circle. It is not: + two-thirds of the way down, the outer edge peels away and hooks out — by three full units at the tip. That + hook is what draws the tail into a long fine point. Nearest-point fitting had hidden it; comparing coverage + against the original's grey levels showed it at once. +

+
+
+ 03 +

+ The middle line is one straight cut. Two drafts got this wrong the same way. The first gave each + stroke's facing edge its own circle, curving opposite ways, and the gap swelled from 5.4 units at its ends to + 8.6 in the middle. The second made both edges arcs of one circle, which fixed the width but left a 7.5-unit + blend reading as a knuckle on the spine. The strokes are now divided by a single straight line and each facing + edge is that line, offset — parallel by construction, so the gap cannot open and neither edge can curve + against the other. +

+
+
+ 04 +

+ The two blue points are gone, and the glyph is now centred in its tile rather than pressed against the + right edge. +

+
+
+ 05 +

+ The second stroke's outline stays 7% larger — kept, not corrected. Forcing the two to be identical + measurably worsens the match and opens the channel between them, which is the tension the mark depends on. +

+
+
+
+ +
+
+ 07 +

Clear space and misuse

+ the four rules that matter +
+

+ Keep a margin of at least one quarter of the mark's height clear on every side — of type, of image edges, of other + logos. The mark is already inset inside its own tile; this margin is measured from the tile. +

+
+
+ + + + + + + + clear space = 1/4 of the height + 1/4 + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RuleWhy
Never re-space the two strokes + the gap is a constructed 4.2 units; widening it breaks the one relationship the mark is built on +
Never stretch it + every curve is a true circle; a non-uniform scale turns them into ellipses and the cut stops being + parallel +
Never add a shadow or gradient + the mark is one flat colour on one flat ground, and it has to survive being printed in black +
Never place the navy mark on a mid-tone + use psychsift-mark-light.svg on pale grounds and + psychsift-lockup-horizontal-reverse.svg on dark ones +
Never re-trace a PNGstart from the SVG; the raster exports exist only for systems that refuse vectors
+
+
+
+
+
+ +
+
+ Do not stretch itthe circles become ellipses and the cut stops being parallel +
+
+
+
+ +
+
+ Do not add a shadow or gradientthe mark is one flat colour on one flat ground +
+
+
+
+ +
+
+ Do not re-space the strokesthe 4.2-unit gap is the relationship the mark is built on +
+
+
+
+ +
+
+ Do not put it on a mid-toneuse the light file on pale grounds, the reverse on dark +
+
+
+
+ +
+
+ 08 +

The files

+ 13 SVG · 2 PNG · public/brand/ +
+

+ Every file here is generated from the same geometry, so they cannot drift apart. If the mark ever changes, all + fifteen are rebuilt together. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileUse it for
psychsift-mark.svgthe primary mark, navy tile — the default everywhere
psychsift-mark-blue.svgon brand blue
psychsift-mark-light.svgon a light tile, for pale backgrounds
psychsift-mark-maskable.svgfull-bleed square for PWA install and Android adaptive icons
psychsift-favicon.svgbrowser tabs and anything under 32 px
psychsift-glyph-white.svgthe glyph alone, white, no tile
psychsift-glyph-navy.svgthe glyph alone, navy, no tile
psychsift-glyph-mono.svgthe glyph in currentColor, for inlining
psychsift-lockup-horizontal.svgmark plus wordmark, light backgrounds
psychsift-lockup-horizontal-reverse.svgthe same, dark backgrounds
psychsift-lockup-horizontal-tagline.svgwith CLARITY. EVIDENCE. BETTER CARE.
psychsift-lockup-stacked.svgmark above wordmark, for narrow spaces
psychsift-wordmark.svgthe wordmark alone
psychsift-mark-1024.pngraster export, where SVG is refused
psychsift-mark-maskable-1024.pngraster export of the maskable icon
+
+

+ The mark carries role="img" and a <title>, so a screen reader announces it as + "PsychSift" when it is inlined. Next to the word PsychSift, mark it aria-hidden instead, so the name + is not read twice. +

+
+ +
+
+ 09 +

Still open

+ two for the owner; one now settled +
+
+
+ 01 +

+ Two taglines. The brand sheet gives "CLARITY. EVIDENCE. BETTER CARE." in its header and "Clinical + clarity. Evidence. Better care." in its footer. The lockup uses the first. One of them should be retired. +

+
+
+ 02 +

+ Inter or Geist. The sheet specifies Inter throughout; the running application sets its interface in + Geist. The wordmark here is outlined artwork so it is unaffected either way, but a wordmark set live in Geist + would not match it. +

+
+
+ 03 +

+ The tail tips — settled. Both tails end in a clean angled cut at about 81°. A flatter tail hook would + draw them into finer points, closer to the blurry original; it was drawn, compared and rejected by the owner + on 2026-08-28. Fine points thin both strokes, they belong with a curved middle rather than a straight one, and + they are the first thing to disappear at 16 px. Do not revisit this without a reason the three of those do not + already answer. +

+
+
+
+ + +
diff --git a/docs/brand/psychsift-logo.md b/docs/brand/psychsift-logo.md new file mode 100644 index 0000000000..8f0726b834 --- /dev/null +++ b/docs/brand/psychsift-logo.md @@ -0,0 +1,265 @@ +# The PsychSift logo + +The files in `public/brand/` are the master artwork. They are true vectors — every curve is a +circular arc with an exact centre and radius, so the mark is identical at 16 px and at billboard +size, and the primary file is 863 bytes. + +This page records **how the mark is built**, so it can be rebuilt or extended without guessing. + +## What the mark is + +A rounded navy tile carrying an **S** made of two counter-turning strokes, with a single white +point settled to the upper right. + +The S is the initial of _Sift_ and reads as two halves of one judgement. The point is what has been +sifted out and come to rest. + +An earlier version of these files carried two small blue points falling below the white one. They +have been removed at the owner's direction, and the mark is now a single colour on its tile. + +## Provenance + +The mark was drawn from the original PsychSift brand sheet. The mark on that sheet is the +authoritative design; the large blue raster that circulated alongside it is a low-fidelity +enlargement and must not be used. + +The geometry here was measured from a high-resolution view of the original, arc by arc, at the +**50% level of its anti-aliased edges** — the one place on a blurred edge that is not biased by the +blur. Each arc was then fitted on its own, so its residual could be inspected rather than hidden in +an aggregate: per-arc RMS runs from 0.05 to 0.14 units in 100. Outside the middle the mark covers 94% of +the original by area (intersection over union); the middle departs from the original on purpose. + +Three earlier drafts got this wrong in instructive ways, and every error came from trusting a +measurement that the source could not actually support. + +**The terminals are sharp.** At the size the original was first sampled, a sharp point blurs into a +rounded blob. The first draft fitted that blur and gave the mark rounded club-ends. Both tips are +genuine cusps: the outer edge runs into the inner edge and the crossing is the point. + +**The tail leaves the main sweep.** The second draft modelled each stroke as four arcs, with the +whole outer edge on one circle. It is not: below about two-thirds of the way down, the outer edge +peels away from that circle and hooks further out — by three full units at the very tip. That hook +is what draws the tail into a long fine point, and without it the tail stops short and the stroke +reads heavy. Fitting by nearest-point distance had hidden this, because every point on a slightly +wrong outline still sits close to _some_ point of the right one. Comparing coverage against the +original's grey levels showed it immediately. + +**The middle line is one straight cut.** Two drafts got this wrong in the same way. The first drew +each stroke's facing edge from its own circle, curving opposite ways; two arcs bowing apart open a +lens, and the gap swelled from 5.4 units at its ends to 8.6 in the middle. The second made both +edges arcs of a single circle, which fixed the width but left each stroke reaching its edge through +a tight 7.5-unit blend that reads as a knuckle on the spine of the S. + +The mark now does the simplest thing instead: the two strokes are divided by **one straight line**, +and each stroke's facing edge is that line offset by half the gap. The two edges are therefore +exactly parallel — the gap is one width down its whole length, and neither edge can curve against +the other. Each stroke joins its edge with a single 13-unit blend under the head, and both blends +bend the same way, away from the other stroke. Nothing on either facing edge turns over. + +This is a deliberate improvement on the original rather than a copy of it. The original's channel +is 4.75 units wide with a standard deviation of 0.25 and bows by 1.25 units along its length; this +one is 4.35 with a standard deviation of 0.28 and does not bow at all. + +Two things were corrected, and one apparent oddity was kept. + +**Corrected.** The glyph is centred in its tile; the original sat hard against the right edge. And +every junction between arcs is tangent-continuous, so enlarging the mark reveals no kink. + +**Kept.** The second stroke's outer silhouette is drawn **7% larger** than the first's. Forcing the +two to be identical measurably worsens the match and opens up the channel between them, which is the +tension the mark depends on. A further 3.5° rotation improves the fit by another half a percent; it +was left out deliberately, because an arbitrary tilt is not something a logo specification should +carry. + +## Construction + +Work in **glyph units**: the S is exactly 100 units tall and 55.10 wide, with its top-left corner +at the origin. + +### The middle line + +The two strokes are divided by **one straight line**: + +| | | +| -------------------------- | ------------------------------ | +| A point on it | (26.2380, 48.6900) | +| Unit normal | (0.87626, 0.48184) | +| Direction | 118.806° | +| Upper stroke's facing edge | the line at normal·x = 44.6629 | +| Lower stroke's facing edge | the line at normal·x = 48.8629 | + +The two edges are the same line offset ±2.1 units, so they are exactly parallel: the gap is +**4.2 units wide down its whole length** and neither edge curves against the other. Everything else +in the mark is built to meet those two lines tangentially. + +The cut sits where it does for a reason: at normal·x = 44.6629 the point reflection that makes the +second stroke carries the **upper stroke's facing edge exactly onto the lower one**. The whole mark +is therefore invariant under that single transform, and the second stroke can be — and is — written +as a transform of the first rather than as its own path. If the gap changes, this position has to be +re-solved from `(normal·origin − gap) / 2.07`; it is not a free number. + +This is the part to protect. Give the two facing edges separate curves and the gap opens into a +lens; that is what the first two cuts of these files did. + +### The upper stroke + +Three arcs and one straight segment, meeting at two tangent points and two sharp cusps: + +| Segment | Centre | Radius | Role | +| ----------- | ------------------ | -------- | ------------------------------------------------ | +| Outer sweep | (29.2009, 29.2009) | 29.2009 | the outer edge, from the head to two-thirds down | +| Tail hook | (51.5937, 16.1418) | 55.1234 | the hook that draws the tail to a point | +| Throat | (44.8724, 20.0286) | 17.7232 | the scoop under the head | +| Blend | (17.3065, 33.5945) | 13.0 | carries the throat onto the cut | +| Facing edge | — | straight | the upper side of the middle line | + +- **Head cusp** at (41.3675, 2.6554) — outer sweep into throat, 36°. +- **Tail cusp** at (18.0434, 59.8793) — the cut into the tail hook, 81°. +- **Tangent points** at (3.9761, 43.9116) outer sweep into tail hook, (29.0679, 28.0493) throat into + blend, and (28.8667, 40.1963) blend onto the cut. + +The outer sweep is centred at (r, r) for its own radius, so it is tangent to both the top and the +left edge of the stroke's bounding box. That relationship fell out of the fit rather than being +imposed, and it is the quickest way to check a redraw. + +### The lower stroke + +The outer silhouette is the upper stroke's, point-reflected and enlarged 7%: + +``` +translate(55.1029 100.3813) scale(-1.07) +``` + +A negative uniform scale is a 180° rotation and a 7% enlargement in one step. That gives: + +| Segment | Centre | Radius | Role | +| ----------- | ------------------ | -------- | --------------------------------- | +| Outer sweep | (23.8579, 69.1363) | 31.2450 | the outer edge | +| Tail hook | (−0.1024, 83.1096) | 58.9820 | the hook into the tail | +| Throat | (7.0894, 78.9507) | 18.9638 | the scoop under the head | +| Blend | (36.4044, 64.0737) | 13.9100 | carries the throat onto the cut | +| Facing edge | — | straight | the lower side of the middle line | + +- **Head cusp** at (10.8397, 97.5400) — outer sweep into throat, 36°. +- **Tail cusp** at (35.7966, 36.3105) — the cut into the tail hook, 81°. +- **Tangent points** at (50.8485, 53.3959) outer sweep into tail hook, (24.0003, 70.3687) throat + into blend, and (24.2156, 57.3713) blend onto the cut. + +Every one of those numbers is the point reflection of the upper stroke's, including the facing edge +and the blend, so the second stroke is written in the files as + +``` + +``` + +and the table above is a convenience, not a second source of truth. Keeping that exact — rather than +approximately — true is what fixes the cut's position; see **The middle line** above. + +### The point + +A circle of radius **10.4586** centred at **(60.4716, 17.3581)**. + +Measured against the cut, its centre sits **43.95 units along** the cut from the cut's midpoint and +**14.90 units off** it, on the lower stroke's side. Its nearest approach to the S is 13.65 units — +a little over three times the width of the cut, which is what keeps it reading as a separate +particle rather than part of the letter. + +An earlier draft of these files had it at (58.9234, 16.5279) with radius 10.2165. That was a +measurement error, not a decision: 1.8 units in and 2.3% small, which crowded the S's shoulder and +made the point look like a stray rather than a counterweight. + +With the point included, the glyph spans 0 to 70.93 horizontally and 0 to 100.38 vertically. + +### The tile + +A square with a corner radius of **3/16 of its side** (18.75%). The glyph is scaled to **80% of the +tile height** and its ink bounding box — S and point together — is centred in the tile. At 512 px +that leaves 112 px clear on the left and right and 51 px top and bottom. + +An earlier draft weighted the placement 46% of the way from the S's own centre toward the glyph's +right edge, on the theory that the light point should not drag the heavy S leftward. It +over-corrected badly: the mark sat 65 px from the left edge and 164 px from the right. Centring the +bounding box is both simpler and visibly right. + +### Small sizes + +At 32 px and below the 4.2-unit cut closes up. `psychsift-favicon.svg` is a separate optical cut for +that range: the gap is widened to **7.2 units** by moving each facing edge 1.5 units away from the +other, and the blends are re-solved against the lines they now meet. Because only the facing edges +move, the outer silhouette is identical to the one in the primary file. Use it for favicons, browser +tabs, and anything rendered under 32 px. + +### The maskable icon + +`psychsift-mark-maskable.svg` is full-bleed, because Android crops adaptive icons to whatever shape +the launcher uses. Everything that must survive that crop has to sit inside a centred circle of +**66.7% of the icon's width**. The glyph is scaled to **62% of the height**, which puts its furthest +ink 168 px from the centre of a 512 px icon against the 171 px the safe circle allows. Do not raise +that 62%: an earlier draft used 58% with a different centring and still put ink 176 px out, outside +the safe circle. + +## Colours + +| Name | Hex | Use | +| ---------- | --------- | ------------------------------------ | +| Deep Navy | `#0D1B2A` | the tile; and the S on light grounds | +| White | `#FFFFFF` | the S and the point, on navy | +| Brand Blue | `#2563EB` | the blue tile; the lockup tagline | +| Cool Gray | `#F2F4F7` | the light tile | + +## The wordmark + +**Inter Display SemiBold**, tracked −0.02 em. The outlines are embedded as paths, so the lockups +render identically without Inter installed. Inter is licensed under the SIL Open Font License 1.1. + +In the lockups the wordmark cap height is 42% of the mark's height, set one fifth of the mark's +height to its right, with the cap-height centre aligned to the mark's centre. In the tagline +version the tagline is tracked out to finish flush with the wordmark. + +Note that the running application currently sets its interface in **Geist**, not Inter. The +wordmark is outlined artwork and is unaffected, but a wordmark set live in Geist would not match. + +## The files + +| File | Use | +| ----------------------------------------- | ---------------------------------------------------- | +| `psychsift-mark.svg` | primary mark, navy tile | +| `psychsift-mark-blue.svg` | on brand blue | +| `psychsift-mark-light.svg` | on a light tile, for pale backgrounds | +| `psychsift-mark-maskable.svg` | full-bleed square for PWA and Android adaptive icons | +| `psychsift-favicon.svg` | small-size optical cut, 32 px and below | +| `psychsift-glyph-white.svg` | glyph alone, white, no tile | +| `psychsift-glyph-navy.svg` | glyph alone, navy, no tile | +| `psychsift-glyph-mono.svg` | glyph alone in `currentColor` | +| `psychsift-lockup-horizontal.svg` | mark plus wordmark, for light backgrounds | +| `psychsift-lockup-horizontal-reverse.svg` | the same for dark backgrounds | +| `psychsift-lockup-horizontal-tagline.svg` | with "CLARITY. EVIDENCE. BETTER CARE." | +| `psychsift-lockup-stacked.svg` | mark above wordmark, for narrow spaces | +| `psychsift-wordmark.svg` | wordmark alone | +| `psychsift-mark-1024.png` | raster export, where SVG is not accepted | +| `psychsift-mark-maskable-1024.png` | raster export of the maskable icon | + +## Using it + +- **Clear space**: keep a margin of at least one quarter of the mark's height on every side. +- **Never** re-space the two strokes, stretch the mark non-uniformly, add a shadow or gradient, or + place the full-colour mark on a mid-tone background — use the light or reverse file instead. +- **Never** re-trace a PNG export. Start from the SVG. +- The mark carries `role="img"` and a ``, so it is announced as "PsychSift" when inlined. + When it sits next to the word PsychSift, mark it `aria-hidden` instead so screen readers do not + say the name twice. + +## Settled + +**The tail tips.** Both tails end in a clean angled cut at about 81°. A flatter tail hook draws them +into finer points, closer to the blurry original; that version was drawn, compared and rejected by +the owner on 2026-08-28. Fine points thin both strokes, they belong with a curved middle line rather +than a straight one, and they are the first thing to disappear at 16 px. Do not revisit this without +a reason those three do not already answer. + +## Two things still open + +- The brand sheet gives two different taglines: "CLARITY. EVIDENCE. BETTER CARE." in the header and + "Clinical clarity. Evidence. Better care." in the footer. The lockup uses the first. +- The sheet specifies Inter throughout; the application uses Geist. That difference should be + settled deliberately rather than left to drift. diff --git a/docs/design-system.md b/docs/design-system.md index e12da5922a..f9a9cea090 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -249,12 +249,21 @@ image"}` — never a possibly-empty variable alone. noindexed (robots.ts + layout metadata), and exempt from token/type-scale rules — but **promoting a mockup to production means bringing it onto the token system first** (see the legacy-hex table above). -- **Brand mark** is single-sourced in `src/lib/brand-mark.ts` (geometry + SVG builders). - `BrandMark` (`clinical-dashboard/brand.tsx`) renders it token-themed; `app/icon.svg`, +- **Brand mark** is the PsychSift S, single-sourced in `src/lib/brand-mark.ts` (geometry + SVG + builders). `BrandMark` (`clinical-dashboard/brand.tsx`) renders it token-themed; `app/icon.svg`, `app/apple-icon`, the PWA maskable icons, and `app/opengraph-image` all derive from it. To change the mark, edit `brand-mark.ts` then `npm run brand:update`; `brand:check` (in `verify:cheap`) guards `app/icon.svg` from drift. `app/favicon.ico` is a multi-resolution binary the toolchain can't emit — regenerate it offline from `icon.svg` when the mark changes. + Do not re-draw the paths by hand: they are the exact output of the construction recorded in + `docs/brand/psychsift-logo.md`, whose master artwork is in `public/brand/`, and the two strokes + are one path plus its point reflection — which is the only reason the cut between them stays + parallel. `app/icon.svg` deliberately uses the mark's small-size cut (a wider gap), because that + is the file browsers render at 16–32 px. +- **The tile colour is not the brand's.** `BRAND_LIGHT.tile` / `BRAND_DARK.tile` are pinned to + `--clinical-accent` per theme by `tests/design-token-contract.test.ts`, so the mark rides the + application's accent rather than the brand sheet's Deep Navy. Putting the mark on navy means + moving that accent, which is an application-wide decision — not a brand-asset one. ## 11. What NOT to do diff --git a/public/brand/psychsift-favicon.svg b/public/brand/psychsift-favicon.svg new file mode 100644 index 0000000000..4c98f6d1d7 --- /dev/null +++ b/public/brand/psychsift-favicon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512" role="img" aria-label="PsychSift"><title>PsychSiftSimplified PsychSift mark for favicon and small sizes. diff --git a/public/brand/psychsift-glyph-mono.svg b/public/brand/psychsift-glyph-mono.svg new file mode 100644 index 0000000000..cbe702b612 --- /dev/null +++ b/public/brand/psychsift-glyph-mono.svg @@ -0,0 +1 @@ +PsychSiftThe PsychSift glyph. diff --git a/public/brand/psychsift-glyph-navy.svg b/public/brand/psychsift-glyph-navy.svg new file mode 100644 index 0000000000..54a385c720 --- /dev/null +++ b/public/brand/psychsift-glyph-navy.svg @@ -0,0 +1 @@ +PsychSiftThe PsychSift glyph. diff --git a/public/brand/psychsift-glyph-white.svg b/public/brand/psychsift-glyph-white.svg new file mode 100644 index 0000000000..bfaaa2d0da --- /dev/null +++ b/public/brand/psychsift-glyph-white.svg @@ -0,0 +1 @@ +PsychSiftThe PsychSift glyph. diff --git a/public/brand/psychsift-lockup-horizontal-reverse.svg b/public/brand/psychsift-lockup-horizontal-reverse.svg new file mode 100644 index 0000000000..cff09a14c3 --- /dev/null +++ b/public/brand/psychsift-lockup-horizontal-reverse.svg @@ -0,0 +1 @@ +PsychSiftThe PsychSift horizontal lockup. diff --git a/public/brand/psychsift-lockup-horizontal-tagline.svg b/public/brand/psychsift-lockup-horizontal-tagline.svg new file mode 100644 index 0000000000..5eb53d6db7 --- /dev/null +++ b/public/brand/psychsift-lockup-horizontal-tagline.svg @@ -0,0 +1 @@ +PsychSiftThe PsychSift horizontal lockup. diff --git a/public/brand/psychsift-lockup-horizontal.svg b/public/brand/psychsift-lockup-horizontal.svg new file mode 100644 index 0000000000..df84c62c14 --- /dev/null +++ b/public/brand/psychsift-lockup-horizontal.svg @@ -0,0 +1 @@ +PsychSiftThe PsychSift horizontal lockup. diff --git a/public/brand/psychsift-lockup-stacked.svg b/public/brand/psychsift-lockup-stacked.svg new file mode 100644 index 0000000000..cc1faa59ba --- /dev/null +++ b/public/brand/psychsift-lockup-stacked.svg @@ -0,0 +1 @@ +PsychSiftThe PsychSift stacked lockup. diff --git a/public/brand/psychsift-mark-1024.png b/public/brand/psychsift-mark-1024.png new file mode 100644 index 0000000000..3b873a7705 Binary files /dev/null and b/public/brand/psychsift-mark-1024.png differ diff --git a/public/brand/psychsift-mark-blue.svg b/public/brand/psychsift-mark-blue.svg new file mode 100644 index 0000000000..807a0af89b --- /dev/null +++ b/public/brand/psychsift-mark-blue.svg @@ -0,0 +1 @@ +PsychSiftThe PsychSift mark on brand blue. diff --git a/public/brand/psychsift-mark-light.svg b/public/brand/psychsift-mark-light.svg new file mode 100644 index 0000000000..3e55c874ea --- /dev/null +++ b/public/brand/psychsift-mark-light.svg @@ -0,0 +1 @@ +PsychSiftThe PsychSift mark on a light tile. diff --git a/public/brand/psychsift-mark-maskable-1024.png b/public/brand/psychsift-mark-maskable-1024.png new file mode 100644 index 0000000000..69633acd3d Binary files /dev/null and b/public/brand/psychsift-mark-maskable-1024.png differ diff --git a/public/brand/psychsift-mark-maskable.svg b/public/brand/psychsift-mark-maskable.svg new file mode 100644 index 0000000000..a42d1a7b68 --- /dev/null +++ b/public/brand/psychsift-mark-maskable.svg @@ -0,0 +1 @@ +PsychSiftFull-bleed PsychSift mark with a maskable safe zone. diff --git a/public/brand/psychsift-mark.svg b/public/brand/psychsift-mark.svg new file mode 100644 index 0000000000..b36ac67f6f --- /dev/null +++ b/public/brand/psychsift-mark.svg @@ -0,0 +1 @@ +PsychSiftThe PsychSift mark: two counter-turning strokes forming an S, with a settled point. diff --git a/public/brand/psychsift-wordmark.svg b/public/brand/psychsift-wordmark.svg new file mode 100644 index 0000000000..7f6d5d2bd1 --- /dev/null +++ b/public/brand/psychsift-wordmark.svg @@ -0,0 +1 @@ +PsychSiftThe PsychSift wordmark, Inter Display SemiBold, outlined. diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 718d6fea48..ac23f32d15 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/app/icon.svg b/src/app/icon.svg index eb813f249c..07dcf1b4ff 100644 --- a/src/app/icon.svg +++ b/src/app/icon.svg @@ -1,13 +1,16 @@ - + - - - + + + + + + diff --git a/src/components/clinical-dashboard/brand.tsx b/src/components/clinical-dashboard/brand.tsx index ba5dac19b7..a3a58f37ab 100644 --- a/src/components/clinical-dashboard/brand.tsx +++ b/src/components/clinical-dashboard/brand.tsx @@ -1,12 +1,20 @@ import { cn } from "@/components/ui-primitives"; -import { BRAND_HIGHLIGHT_PATH, BRAND_PULSE_PATH, BRAND_PULSE_WIDTH, BRAND_TILE, BRAND_VIEWBOX } from "@/lib/brand-mark"; +import { + BRAND_COUNTER_TRANSFORM, + BRAND_GLYPH_TRANSFORM, + BRAND_POINT, + BRAND_STROKE_PATH, + BRAND_TILE, + BRAND_VIEWBOX, +} from "@/lib/brand-mark"; /** - * Site brand mark: a refined ECG pulse on a rounded clinical-teal tile. Colours - * come from the clinical accent tokens so the mark adapts to light/dark/forced - * colors. Geometry is the single source in `@/lib/brand-mark` (shared with - * app/icon.svg and the app-icon image routes). Size it via className - * (h-10 w-10 expanded sidebar, h-7 w-7 collapsed rail). + * Site brand mark: the PsychSift S — two counter-turning strokes divided by one + * straight cut, with a settled point — on a rounded tile. Colours come from the + * clinical accent tokens so the mark adapts to light/dark/forced colors. + * Geometry is the single source in `@/lib/brand-mark` (shared with app/icon.svg + * and the app-icon image routes). Size it via className (h-10 w-10 expanded + * sidebar, h-7 w-7 collapsed rail). */ export function BrandMark({ className }: { className?: string }) { return ( @@ -25,15 +33,11 @@ export function BrandMark({ className }: { className?: string }) { rx={BRAND_TILE.rx} fill="var(--clinical-accent)" /> - - + + + + + ); } diff --git a/src/lib/brand-image.tsx b/src/lib/brand-image.tsx index cc47d5f78b..431d9b173a 100644 --- a/src/lib/brand-image.tsx +++ b/src/lib/brand-image.tsx @@ -8,9 +8,12 @@ import { BRAND_LIGHT, brandMarkSvg, type BrandColors } from "@/lib/brand-mark"; export const BRAND_ICON_FIELD = BRAND_LIGHT.tile; /** Single-colour palette for manifest `monochrome` icons: platforms read only - * the alpha channel and recolour the silhouette themselves, so the tile and - * ink collapse into one white glyph on a transparent field. */ -export const BRAND_MONOCHROME: BrandColors = { tile: "#ffffff", ink: "#ffffff" }; + * the alpha channel and recolour the silhouette themselves, so the tile drops + * out entirely and the glyph alone carries the shape. Filling the tile white + * as well — as this did until 2026-08-28 — hands the platform a solid rounded + * square with no mark in it, because a white glyph on a white tile is opaque + * everywhere the alpha channel can see. */ +export const BRAND_MONOCHROME: BrandColors = { tile: "transparent", ink: "#ffffff" }; /** data: URI of the flat brand-mark SVG for inside ImageResponse. Satori * rasterises the SVG server-side, so no browser CSP or network is involved. diff --git a/src/lib/brand-mark.ts b/src/lib/brand-mark.ts index 7f58585df6..661e837764 100644 --- a/src/lib/brand-mark.ts +++ b/src/lib/brand-mark.ts @@ -1,31 +1,55 @@ -// Single source of truth for the Clinical KB brand mark: a refined ECG pulse on -// a rounded clinical-teal tile. Every surface that draws the mark derives from -// the geometry here so the favicon, the in-app , the browser-tab -// icon (app/icon.svg), and the generated app-icon / maskable / OG image routes -// can never drift apart. +// Single source of truth for the brand mark: the PsychSift S — two +// counter-turning strokes divided by one straight cut, with a settled point — +// on a rounded tile. Every surface that draws the mark derives from the +// geometry here so the favicon, the in-app , the browser-tab icon +// (app/icon.svg), and the generated app-icon / maskable / OG image routes can +// never drift apart. +// +// The construction is recorded in docs/brand/psychsift-logo.md and the master +// artwork lives in public/brand/. Do not re-draw the paths below by hand: they +// are the exact output of that construction, and the two strokes are one path +// and its point reflection, which is what keeps the cut between them parallel. // // Pure data + strings (no JSX, no imports) so it is consumable everywhere: the // React renders the exported geometry as JSX; scripts/generate- // brand-assets.ts writes app/icon.svg from brandIconSvg(); the next/og image // routes embed brandMarkSvg() as a data: URI. -/** 48×48 coordinate system shared by every rendering. */ -export const BRAND_VIEWBOX = "0 0 48 48"; +/** 512×512 coordinate system shared by every rendering — the master artwork's own. */ +export const BRAND_VIEWBOX = "0 0 512 512"; + +/** Rounded tile: a corner radius of 3/16 of the side. */ +export const BRAND_TILE = { x: 0, y: 0, size: 512, rx: 96 } as const; + +/** Places the 100-unit glyph inside the tile, centred on its ink bounding box. */ +export const BRAND_GLYPH_TRANSFORM = "translate(111.2867 51.2) scale(4.0804)"; + +/** The upper stroke. Four arcs and the straight cut, meeting at two cusps. */ +export const BRAND_STROKE_PATH = + "M41.3675 2.6554 A17.7232 17.7232 0 0 0 29.0679 28.0493 A13 13 0 0 1 28.8667 40.1963 " + + "L18.0434 59.8793 A55.1234 55.1234 0 0 1 3.9761 43.9116 A29.2009 29.2009 0 0 1 41.3675 2.6554 Z"; + +/** The small-size optical cut: the same silhouette with the 4.2-unit gap opened + * to 7.2 so it does not close up under 32 px. Used by the browser-tab icon. */ +export const BRAND_STROKE_PATH_SMALL = + "M41.3675 2.6554 A17.7232 17.7232 0 0 0 28.6276 27.115 A13 13 0 0 1 28.1033 38.5768 " + + "L16.8897 58.9696 A55.1234 55.1234 0 0 1 3.9761 43.9116 A29.2009 29.2009 0 0 1 41.3675 2.6554 Z"; + +/** The lower stroke IS the upper one under this transform — a 180° rotation and + * a 7% enlargement in one step. The cut is positioned so this carries the upper + * stroke's facing edge exactly onto the lower one; see the brand doc. */ +export const BRAND_COUNTER_TRANSFORM = "translate(55.1029 100.3813) scale(-1.07)"; -/** Rounded tile. */ -export const BRAND_TILE = { x: 2, y: 2, size: 44, rx: 14 } as const; -/** Subtle baked-in top highlight (theme-independent, very low opacity). */ -export const BRAND_HIGHLIGHT_PATH = "M2 16C2 8.3 8.3 2 16 2h16c7.7 0 14 6.3 14 14v1H2z"; -/** The refined ECG pulse: flat baseline, one clean QRS spike, flat baseline. */ -export const BRAND_PULSE_PATH = "M7 26h10.4l2.4-10 4 19 3-9H41"; -export const BRAND_PULSE_WIDTH = 3.3; +/** The settled point. */ +export const BRAND_POINT = { cx: 60.4716, cy: 17.3581, r: 10.4586 } as const; -/** Brand colours per theme. Light = Clinical Sky tile / white ink; dark inverts. - * These mirror --clinical-accent and --clinical-accent-contrast per theme in - * globals.css. They cannot read the tokens (the favicon and the generated PNG - * icon routes render outside any stylesheet), so they must be re-derived by - * hand whenever the accent moves — then `npm run brand:update` regenerates - * app/icon.svg, which `npm run brand:check` verifies in verify:cheap. */ +/** Brand colours per theme. These mirror --clinical-accent and + * --clinical-accent-contrast per theme in globals.css. They cannot read the + * tokens (the favicon and the generated PNG icon routes render outside any + * stylesheet), so they must be re-derived by hand whenever the accent moves — + * then `npm run brand:update` regenerates app/icon.svg, which + * `npm run brand:check` verifies in verify:cheap, and the design-token + * contract test fails if they ever disagree with the token. */ export const BRAND_LIGHT = { tile: "#1d6fb8", ink: "#ffffff" } as const; export const BRAND_DARK = { tile: "#74bdf0", ink: "#08203a" } as const; @@ -34,12 +58,17 @@ export type BrandColors = { tile: string; ink: string }; /** Inner SVG markup (no wrapper) with explicit colours — shared by the * flat SVG/PNG builders. The React renders the same geometry from * the exported constants above. */ -export function brandMarkInner({ tile, ink }: BrandColors): string { +export function brandMarkInner({ tile, ink }: BrandColors, small = false): string { const t = BRAND_TILE; + const stroke = small ? BRAND_STROKE_PATH_SMALL : BRAND_STROKE_PATH; + const p = BRAND_POINT; return ( `` + - `` + - `` + `` + + `` + + `` + + `` + + `` ); } @@ -50,22 +79,27 @@ export function brandMarkSvg(colors: BrandColors = BRAND_LIGHT): string { } /** Themed standalone SVG for the browser-tab icon (app/icon.svg): a - * prefers-color-scheme swap that a raster app-icon cannot do. Written to + * prefers-color-scheme swap that a raster app-icon cannot do. Uses the + * small-size cut, because this is the file that renders at 16–32 px. Written to * app/icon.svg by scripts/generate-brand-assets.ts (verified in verify:cheap). */ export function brandIconSvg(): string { const t = BRAND_TILE; + const p = BRAND_POINT; return ` - - + + + + + `; }