01
Backend 01

Where it is going

The product interface is dark throughout today. It becomes a light work area with a dark sidebar as its anchor: quiet, high in contrast, generous with white space, thin on ornament.

Two sources give it character, and neither is decoration. The low-poly logo supplies a vocabulary of shapes nobody else has: facets and cut corners. The data location is the argument that sells the product, so it works in the interface rather than sitting in the imprint.

This part covers the product interface only. Logo, colour, type, product names and voice are the same brand and live in the Frontend part — sections 02, 03, 04, 05 and 07. Nothing there is repeated here, because a rule written twice is a rule that will eventually disagree with itself.

  • Light work area, dark sidebar. Both on the same hue axis, so the seam reads as a fold rather than a change of material.
  • The butterfly appears once, on the login, in a panel. Not on every screen.
  • Gold means AI. Nothing else, anywhere. Section 05.
App shellDraft, not built

Overview

EU region
Posts
128
This month
14
AI-generated
36
Zuletzt
Pricing page rewrittenKI2 hrs ago
Assade case studyyesterday
Newsletter 09/26Draftyesterday
02
Backend 02

What is binding

Three colours and four rules. Everything else in this part is a proposal that a better idea may replace.

The three colours

All three come from the brand palette in Frontend section 03. The relaunch brief proposed #8151D6 and #EE9E27, a hair away from the brand values. Two violets that differ by a hair are the failure the guide names first, so the brand values win.

:root{
  /* product action: primary button, active nav item, filled states */
  --violet:      #8250E6;  /* white on it: 4.95:1 */
  --violet-deep: #432B78;  /* hover on filled violet, and violet TYPE on pale */
  --violet-lift: #B49BF0;  /* focus ring on the dark sidebar */

  /* AI, and only AI */
  --gold:       #EDA938;
  --gold-ink:   #4A3006;  /* text on gold: 6.02:1 */
}
  • Gold is never a text colour. #EDA938 on white is 2.03:1.
  • Violet is never a text colour on the light ground either. #8250E6 on #F2F0F8 is 4.39:1 and misses. Violet type on pale is #432B78 at 10.02:1 — which is exactly what Frontend section 03 already says it is for.
  • Gold is not a warning, not an accent, not a "looks good here". The moment gold also means something else, it means nothing.
  • Violet is not a text colour in the sidebar either. #8250E6 on --side-bg is 3.18:1 — enough for a fill or an indicator, not for a word. Sidebar text is --side-ink at 13.83:1 or --side-ink-2 at 6.51:1.

The four rules

  • Body and secondary text clear 4.5:1 against the palest ground it actually sits on, not against white. A grey that passes on a white card can miss on --bg. When it gets tight it gets darker, never smaller.
  • Focus is visible everywhere focus can land.
  • prefers-reduced-motion is respected.
  • Sidebar and work area share one hue axis. The values are free, the shared axis is not: neutral near-black against near-white grey reads as two materials rather than one surface folded.
The three colours in placeDraft, not built
KI Published Draft
NeverGold as type — 1.80:1 on this ground, 2.03:1 on white.
NeverViolet as type on --bg — 4.39:1.
InsteadViolet type is --violet-deep — 10.02:1.
03
Backend 03

Tokens

Starting values, not scripture. Where the project already has a scale for radii, spacing or shadows, that scale wins.

:root{
  /* violet — all four from the brand palette, no new values */
  --violet:#8250E6; --violet-deep:#432B78; --violet-lift:#B49BF0;
  --violet-wash:rgba(130,80,230,.10);

  /* AI */
  --gold:#EDA938; --gold-ink:#4A3006; --gold-wash:#FDF2DF; --gold-line:#E7A63F;

  /* status — convention, not brand */
  --ok:#1A7B4E;   --ok-wash:#E3F4EC;
  --warn:#B4500F; --warn-wash:#FCEEE6;

  /* light work area */
  --bg:#F2F0F8; --card:#FFFFFF; --card-line:rgba(21,19,30,.06);
  --sunken:#F6F5FA; --line:#EBEAF2;
  --ink:#141223; --ink-2:#5D5A72; --ink-3:#6C6A7C;

  /* sidebar — same violet axis as --bg, see rule 4 */
  --side-bg:#241E40; --side-line:rgba(255,255,255,.10);
  --side-ink:#F1EFF9; --side-ink-2:#A9A2C8; --side-hover:rgba(255,255,255,.08);

  /* login gradient, identical to the website */
  --hero-1:#2C1B52; --hero-2:#1E1836; --hero-3:#141020;

  /* form and space */
  --radius:14px; --radius-sm:9px; --pad:22px;
  --content-max:1180px; --read-max:720px;
  --sidebar-w:246px; --sidebar-rail:74px;
}

Three values were corrected

Measured against rule 1, three numbers in the brief missed the bar the same brief sets. They are changed here, hue kept, darkened until they clear it.

TokenBriefHereWhy
--ink-3#8B879E · 3.47:1#6C6A7C · 4.66:1Secondary text missed 4.5:1 on white and on every other ground it sits on.
--ok#1E8E5A · 4.14:1#1A7B4E · 4.62:1Same, for status text. Measured against --ok-wash, the tightest of its grounds.
--hero-*#3a2c63 #241546 #0e0a1e#2C1B52 #1E1836 #141020The brief called for the website gradient. These are the actual brand grounds.

Both figures above are the tightest of three grounds: --card, --bg and the colour's own wash. Against white alone they read 5.26:1 and 5.27:1. --warn was checked the same way and passes as given, at 4.52:1 on --warn-wash.

The brief also states 8.4:1 for --gold-ink on gold. Recomputed it is 5.57:1 against the brief's gold and 6.02:1 against #EDA938. Both pass; the stated figure did not match either.

Into shadcn, not beside it

shadcn/ui already ships a token system. A second one next to it forces every component to answer which applies. These values go into the existing tokens.

shadcnbecomes
--primary--violet
--background--bg
--card--card
--muted--sunken
--muted-foreground--ink-2. The shadcn default is usually too light for 4.5:1 in a light theme.
--border--line
--ring--violet
--radius--radius

Gold, the sidebar colours and the hero gradient have no shadcn counterpart and stay separate tokens. Depending on version shadcn expects bare HSL triples without the hsl() wrapper — the hex values above have to be converted, not pasted.

The greys, on the grounds they sit onDraft, not built
On --card

--ink-2, 6.62:1
--ink-3, 5.26:1
brief #8B879E, 3.47:1

On --bg

--ink-2, 5.86:1
--ink-3, 4.66:1
brief #8B879E, 3.07:1

On --ok-wash

--ok, 4.62:1
brief #1E8E5A, 3.63:1

04
Backend 04

Shell and layout

The frame every screen sits in: a dark sidebar, a light work area, and a limit on how wide a line of text may run.

MeasureValueNote
Sidebar246 pxCollapsed to a 74 px rail. The rail needs a logo variant without the wordmark.
Work areamax 1180 pxThe value is a proposal. That there is a limit at all is the point.
Reading widthmax 720 pxChat messages and body copy, roughly 70 characters a line.
Radius14 px / 9 pxCards and controls.
  • Type is Instrument Sans, self-hosted, as in Frontend section 04. The brief proposed Space Grotesk, Inter and JetBrains Mono; it also says the fonts are a suggestion and that a house typeface wins. The brand has one.
  • No fonts from Google. The brief is right about this and the reason is worth repeating: the visitor's IP travels to Google, which the Munich court treated as a GDPR breach in 2022 (Az. 3 O 17493/20). For a product advertising GDPR compliance that is the most expensive mistake available.
  • Load the logo locally. On external dependency for your own mark is unnecessary.
  • Checked at 1280, 1440 and 1920, with the keyboard path visible the whole way.

Open and collapsed

The sidebar has exactly two widths. There is no third, and no drag handle.

OpenCollapsed
Width246 px74 px
ItemIcon 18 plus labelIcon 18, centred in a 44 px target
LabelVisibleTooltip on hover and on focus, to the right
Logologo-small-dark.svg, mark and wordmarkicon-dark.svg, the butterfly alone
Coming soonGrouped at the bottom under a headingGroup heading drops, the entries stay in order
  • The active item keeps its violet fill and its glow in both widths. Collapsing may not cost the answer to "where am I".
  • The state is remembered per user, not per session and not per tab. Someone who collapsed it once did so on purpose.
  • Collapsing is a real button with a label, reachable by keyboard, not a hover strip at the edge.
  • The tooltip appears on focus, not only on hover — otherwise the collapsed sidebar is unusable from the keyboard, and a rail nobody can read is a rail nobody uses.
  • Under 1024 px the rail is the default, under 768 it becomes a drawer — section 20.
  • Never labels under the icons. That is a third width in disguise and it breaks at the first long word.
WidthsDraft, not built
--content-max · 1180 px
--read-max · 720 px

Assistant text stops here, at roughly 70 characters a line. At 1400 px without a limit the same paragraph runs past 140, which is the third observation in section 07.

Sidebar, both widthsDraft, not built
Open · 246 px
Collapsed · 74 px
Same items, same order

The active entry keeps its fill and its glow. The wordmark becomes the butterfly. The label returns as a tooltip — on focus as well as on hover, or the rail is dead to the keyboard.

05
Backend 05

Marking AI

Gold marks AI and nothing else. A second meaning would cost the first one.

Colour alone is not enough. AI surfaces also carry a cut corner, so the marking survives where colour does not arrive: colour blindness, greyscale print, inverted display. The size of the bevel is free, its presence is not.

.facet{ clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px,
                       100% 100%, 12px 100%, 0 calc(100% - 12px)); }

The shape is not invented for the purpose. It is the cut corner of the low-poly butterfly, which is why the interface and the mark look related without the logo being pasted onto anything.

  • Text on gold is #4A3006, 6.02:1. Gold itself never carries text.
  • Where processing happens belongs to the model, not to a grey line under the composer. It is a selling point and a compliance statement, and it belongs where the decision is made.
  • Gold as a warning colour. Warnings are #B4500F.
  • Gold on a button that merely happens to be important.
Marking AIDraft, not built
AI-generated

Drafted in the studio. Review before publishing.

Processed in the EU
The same card without colour
AI-generated

The colour is gone, the cut corner stays.

06
Backend 06

Butterfly in a panel

The login carries the butterfly. This is the one place where the implementation is fixed, because any deviation shows immediately.

No rebuild, no fork, and no re-configuration. Take butterfly-field.js from Frontend section 06 and embed it with the brand configuration, character for character:

<butterfly-field style="position:absolute;inset:0"
  gradient="#2C1B52,#1E1836,#141020"
  color-deep="#2C1B52"
  color-primary="#8250E6"
  color-lavender="#D8CBF2"></butterfly-field>
  • No attribute is added and none is left out. The mark is one thing everywhere it appears. A butterfly that beats differently in the product than on the website is two marks.
  • The panel behind it carries the same three colours in CSS. The element switches itself off under reduced motion, data saver, and on devices below 4 GB or four cores — it paints the gradient in WebGL, so without the CSS gradient the panel is then empty. This is not a change to the element; it is the surface it sits on.

What the relaunch brief asked for

The brief lists six differences from the website version. Measured against the shipped element, two need nothing and four are dropped.

Asked forVerdictWhy
Size from the container, not the windowalready trueThe element measures its own box in groesse(). Nothing to do.
No scroll effectalready trueThe shipped file has no scroll listener at all.
No mouse control (parallax="off")droppedIt would make the product butterfly behave unlike the website one. The drift is part of how the mark reads.
height="0.2"droppedSame reason. If the figure sits wrong, the panel is the wrong size — change the panel.
CAM_Z 16droppedA source change, and not exposed as an attribute. Forking the file to fit one panel is how two versions start.
Narrow mode at panel widthdroppedAlso a source change. The shipped file halves the point count below 900 px window width and that is the behaviour the brand has.

Nothing is free to tune. Strength, glow, scale, width, frame rate and resolution are part of the configuration above. The draft below runs the same file with the same attributes as Frontend section 06 — a flat violet panel there means the element switched itself off.

LoginDraft, not built
07
Backend 07

What to fix

Observations from the interface as it stands. How each is solved is open; that each is a problem is not.

WhatWhy it hurts
Two free-text fields stackedThe chat composer and the global AI bar sit on top of each other, both expecting natural language. Where the AI surface is the whole module, a second AI input above it has no job. Decide: the global bar disappears in the studio, or it merges with the composer.
The EU notice is the least important text on screenIt is a selling point and a compliance statement, set as a small grey line. It belongs on the model.
Line length is unboundedAt 1400 px assistant text runs past 140 characters a line. Readable is 60 to 80.
The thinking block is not readableLight grey on dark, roughly 3:1, and always open. It is a detail you ask for, not default content.
Eight chats named “Ohne Titel”The list stops working after a day of use. It needs a fallback that says something. The coloured dots beside each chat need a stated meaning or they can go.
Filters above zero postsFurniture in an empty room. An empty state is an invitation to act — and after a move from another system the common case is import, not "write your first post".
The login puts the brand lastGoogle and Microsoft are solid white blocks; the own sign-in button is the quietest on the page. The fields barely have an edge, and there are two language switchers.
Navigation differs between screensCampaigns and Members appear on one screenshot and not the other. Role, space, or a bug — to be established, not guessed.
Five of ten nav items say "Soon"A product decision, not a design one. As it stands, every screen tells a trial customer that half the product is missing.
Flag emoji do not render on WindowsLetters appear instead of the flag. Frontend section 06 has 271 flags as SVG for exactly this reason.
Two of the fixesDraft, not built
Nowtwo free-text fields
Ask the AI …
Write a message …
Draftone field, model on the composer
Write a message …
Studio Engine Processed in the EU
Reasoning

Collapsed by default. --ink-2 on --sunken is 6.11:1, against the roughly 3:1 it has today.

08
Backend 08

Density, spacing and type

One spacing scale, one type scale. Every value outside them is a special case that needs a reason.

Spacing

Base 4. Seven steps, nothing between them: 4 · 8 · 12 · 16 · 22 · 32 · 48. Card padding is 22, the gap between cards 14, the gap between groups 32.

Type in the interface

Instrument Sans throughout, as in Frontend section 04. The tracking follows the same rule: tighter as the size grows.

RoleSize / weightTrackingColour
Page title20 / 600−0.034em--ink
Section title16 / 600−0.012em--ink
Card title14 / 6000--ink
Body13.5 / 4000--ink
Secondary12.5 / 4000--ink-2
Eyebrow, table header10.5 / 600, uppercase0.14em--ink-3
Metric25 / 600, tabular−0.034em--ink
  • Figures use font-variant-numeric: tabular-nums in tables, metrics and anywhere numbers sit above one another. Without it the digits have different widths and a column of figures visibly jitters between rows.
  • Row height 44 px, 36 in a compact table, never under 32.
  • Icons: 16 in text and buttons, 18 in the sidebar, 20 in the topbar. One size per surface, stroke-width stays 2 — Frontend section 06.
  • Two elevation levels, not three. Cards sit on a 1 px border with no shadow. Overlays get 0 12px 40px rgba(20,18,35,.28). A shadow on a card that does not float is decoration.
  • Radius: 14 container, 9 control, 99 pill, 2 tag.
Type scale and spacing stepsDraft, not built
Page title20 / 600 / −0.034em
Section title16 / 600 / −0.012em
Card title14 / 600
Body — the working size13.5 / 400
Secondary12.5 / 400 · --ink-2
Eyebrow10.5 / 600 / 0.14em
1.28425 / 600 · tabular-nums
4
8
12
16
22
32
48
09
Backend 09

Controls and their states

Every control has six states. One that is missing a state is not finished, and the one usually missing is loading.

StateHow
RestAs specified.
HoverFilled violet darkens to --violet-deep. Outlined and ghost get --violet-wash.
ActiveNo movement, no scale. The colour is enough.
Focus2 px --violet, offset 2. On the dark sidebar --violet-lift, 6.66:1.
Disabled40 % opacity, not a grey of its own. Not focusable.
LoadingThe label stays, a spinner takes the leading icon's place, the width does not change.
  • One primary per view. Two violet buttons on one screen mean neither is the answer.
  • The destructive colour is #B3323A, and it does one job. White on it is 6.12:1; as type on light grounds it is 5.41:1 on --bg. Filled destructive only inside a confirm, outlined everywhere else.
  • Label is verb plus object. "Post löschen", never "Delete", never "OK". Section 19.
  • Hit target 24 px minimum, 44 where touch is possible. Padding counts, the icon alone does not.
  • A button that shrinks while loading moves everything after it. Reserve the width.
  • outline:none without a replacement ring. Every removal of the ring is a keyboard user losing their place.
  • Disabling a control without saying why. If the reason matters, say it next to the control, not in a tooltip the keyboard cannot reach.
Every control, every stateDraft, not built
Rest
Hover
Focus
Disabled
Loading
Primary
Secondary
Destructive
Filled destructive only inside a confirm:
On Off Checked Unchecked
10
Backend 10

Forms

One column, label above the field, error beneath it. Anything else has to earn itself.

What a field is made of

PartSpec
Label12 / 600, --ink-2, above the field, always present. A placeholder is not a label — it disappears the moment someone types.
Field1 px --ink-3, radius 9, padding 10/12. A border you can see, not a hint of one.
Hint12, --ink-3, under the field, before any error.
Error12, #B3323A, with an icon, and the field border goes to 2 px in the same colour.
OptionalMarked in the label. Mark the minority: if most fields are required, mark the optional ones, and the other way round. Never both.
  • Validate on blur, not on keystroke. Once a field has errored, re-check it on every change so the error clears as soon as it is fixed.
  • The error says what to do, not what happened. “Enter an email address with an @” beats “Invalid format”.
  • Never disable submit to express validation. A disabled button gives no reason. Let it be pressed, then move focus to the first field that failed and name the count.
  • Password managers have to work. A real <form>, autocomplete="username" on the mail field, current-password or new-password on the password. A login built from divs is a login people abandon.
  • A form with unsaved changes warns before it is left, and the save bar states what is unsaved.
  • Errors never rely on colour alone: icon plus text. Rule 1 of section 02 applies to state as well as to type.
  • Field order follows the task, not the database.
A field, with everything it can carryDraft, not built
Pricing page 2026
emma.teisenda.com
Enter an email address with an @.
Deutsch
Sets the form of address. German uses du — Frontend section 07.
Internal, never published.
Require review before publishing
2 unsaved changes
11
Backend 11

Tables and lists

The table is the main working surface. It needs sorting, selection, bulk actions and a page size before it needs anything decorative.

PartSpec
HeaderSticky, 10.5 uppercase --ink-3. The sorted column names its direction.
Row44 px, separated by a 1 px --line. No zebra striping — the lines do that job without tinting every second row.
Selection34 px column on the left, header checkbox selects the page, not the set. Say which.
AlignmentText left, numbers right and tabular, dates left in one fixed format. Never centred.
ActionsRight, in a menu. Never visible on hover only — a keyboard cannot hover and a touch screen has no hover at all.
Empty cellOn em dash. Never blank, never "N/A", never "null".
  • Long values truncate with an ellipsis and carry the full text in title. A data table does not wrap; wrapping makes every row a different height and the eye loses the column.
  • The bulk bar appears at one selected item and states the count: "3 selected". It says what will happen to those three.
  • Sort, filter, page and tab live in the URL. A view that cannot be linked cannot be handed to a colleague.
  • Sorting returns to page 1. Staying on page 4 of a re-sorted set shows a random slice.
  • 25 rows by default, and the total is stated. Infinite scroll only where nobody needs to reach the end — never in a list people work through.
  • Column widths do not change between pages. Fix the layout, or every page turn re-reads as a new table.
  • Row click opens the detail, the checkbox does not.
Table with selection, sorting and bulk actionsDraft, not built
3 selected
Title StatusLanguageViewsChanged
Pricing page 2026PublishedDE12,48028.08.2026
Assade case studyReviewDE3,90227.08.2026
Newsletter 09/26DraftEN—27.08.2026
Why EU hosting mattersPublishedDE84024.08.2026
1–25 of 128
12
Backend 12

Empty, loading and failure

Four empty states, and they are not interchangeable. Offering "write your first post" to someone who has just migrated 400 posts is the failure this section exists to prevent.

StateBecauseOffers
First useNothing exists yetCreate and import, side by side, equally weighted. Section 17.
No resultA filter excluded everythingNames the filter and clears it. Never the first-use screen — the data exists, it is hidden.
No accessThe role is not allowedSays who can grant it. Section 16.
FailureSomething brokeWhat failed, and a retry. Never a bare code.

Waiting

DurationShow
under 300 msNothing. A spinner that flashes reads as a fault.
300 ms – 1 sA skeleton in the shape of the result.
1 – 10 sSkeleton plus one line saying what is happening.
over 10 sA background task with progress. The user leaves the page and is told when it is done.
  • The skeleton has the layout of the result. Otherwise the page jumps the moment data arrives, and the jump costs more attention than the wait.
  • Never a full-page spinner over content that is already there. Load in place.
  • Inline, banner or toast. Inline where the failure is local to one field or row. A banner where the whole page is affected. A toast only for the result of something the user just did.
  • 403, 404 and 500 keep the shell. A bare error page loses the navigation and with it every way out.
  • Offline is a persistent banner, and unsent form content survives it.
The four empty states, and a skeletonDraft, not built
No posts yet

Create the first one, or bring your existing ones across from the old system.

No results for “campaign”

128 posts exist; the filter is hiding them.

No access

Only owners can see billing. Emma Woelk can grant you the role.

List failed to load

The connection dropped. Your filters are kept.

Skeleton in the shape of the result
No connection. What you type is kept and sent as soon as you are back online.
13
Backend 13

Overlays and notifications

Four kinds of overlay, one job each. Choosing the wrong one is the most common mistake in a product of this shape.

TypeForSpec
DialogA decision that has to be made now420 for a confirm, 640 for a form. Centred, scrim rgba(20,18,35,.42).
SheetA task beside the context it belongs toRight, 480 wide, full height. The list behind stays readable.
MenuA choice among actionsAnchored to its trigger, 210 wide, destructive item last and separated.
TooltipThe name of an iconNever information needed to act. Does not exist on touch.
  • Focus moves into the overlay and returns to the trigger. Escape closes the topmost layer, and only that one.
  • The confirm button says what happens. “Delete”, not “OK”, not “Yes”. The title names the object: “Delete “Pricing page 2026”?”
  • A destructive confirm does not hold focus on the destructive button. Enter is pressed by reflex.
  • Toast: bottom right, 5 s, three at most. It reports something that already happened and carries the undo. aria-live="polite", or a screen reader never hears it.
  • Errors the user must act on are never toasts. A toast leaves before it is read and cannot be recalled.
  • One layer. A dialog does not open a dialog. If it must, it is a form that was cut in the wrong place.
  • Under 768 px a dialog becomes a full-height sheet. Section 20.
Dialog, menu and toastDraft, not built
Delete “Pricing page 2026”?
The post and its 12,480 views go. This cannot be undone.
View
Duplicate
Approve

Delete
Post deletedUndo
14
Backend 14

Navigation and information architecture

The sidebar answers where I am. The topbar answers what I can do here. Neither answers both.

  • The same role sees the same sidebar on every screen. Entries appearing on one screenshot and not the next are a bug, not a feature — section 07.
  • Seven top-level entries at most. Beyond that, group; a list nobody can hold in their head is a list nobody reads.
  • The current entry stays marked while a child page is open. Losing the mark is losing the answer to "where am I".
  • Tabs live inside one object. They are not top-level navigation and they do not carry unrelated screens.
  • Breadcrumbs only below the third level. Two levels do not need a trail.
  • The space switcher sits at the top of the sidebar, not inside the account menu. Which workspace you are in changes everything on screen; which account you are is a detail.
  • Global search is ⌘K and finds objects, not settings pages.
  • Every view is a URL, filters and sort included. Section 11.

The "Soon" question

Five of ten entries currently say "Soon". Whichever way it is decided, it is a product decision — but the interface has only two honest options.

  • Hide them for roles that cannot use them, or
  • Collect them once, at the bottom, under a heading that says what they are.
  • Interleaved with working entries. As it stands, every screen tells a trial customer that half the product is missing, five times over.
The same sidebar, two waysDraft, not built
Draftcollected once
15
Backend 15

The AI surface

This is the screen the product is sold on. It carries more rules than any other, and the first of them is that a person signs off before anything leaves the building.

What the surface is made of

PartSpec
ComposerThe only free-text field on the screen. Section 07.
Model rowDirectly on the composer: which module, and where it is processed. Not a grey line in the footer.
MessageMax 720 px, roughly 70 characters a line. Section 04.
ThinkingCollapsed by default, --ink-2 on --sunken: 6.11:1, against the roughly 3:1 it has today.
SourcesUnder the answer, as chips, each one openable. On answer with no source says so.
ActionsCopy, regenerate, approve. Regenerate keeps the previous answer reachable.
  • Send and stop are the same button. While the answer streams it says stop; it never sits beside a send button.
  • Stopping keeps what has arrived. Discarding a half-written answer discards work the user was reading.
  • Everything generated carries the gold marking from section 05 until a person approves it. Approval is a named action, never a side effect of editing.
  • Nothing generated publishes without that step. This is a rule about liability, not about taste.
  • The module names are fixed: TEISENDA Studio Engine, TEISENDA Content Engine. The other five are roadmap and must not appear as available — Frontend section 05.
  • A refusal is not an error. It is not red, it does not offer a retry, and it says what the model will do instead.
  • A model error names the next step. “Answer stopped. Try again, or ask something shorter.” Never a bare code.
  • What a run costs is visible before it runs, if it costs anything.
  • German is du, per the table in Frontend section 07. Consistent across composer, error and mail.
The chat surfaceDraft, not built
Rewrite the pricing page so EU hosting is in the first paragraph.
Reasoning

Collapsed by default. --ink-2 on --sunken is 6.11:1, against the roughly 3:1 it has today.

TEISENDA hosts in Frankfurt, and that belongs in the first paragraph, because it answers the question procurement asks first
Pricing page 2026GDPR note
AI-generated. A person has to approve it before publishing.
Write a message …
Studio Engine Processed in Frankfurt
Running …
16
Backend 16

Roles, access and settings

What a member can see is a product decision, and it surfaces on every screen. The interface has to state it rather than discover it at the moment of failure.

  • The navigation shows what the role can do. On entry that leads to a permission wall does not belong in it.
  • A permission wall names who can grant access, by name, and offers to ask them. Section 12.
  • Roles are visible in the member list, along with status — eingeladen, aktiv, deaktiviert — and last activity.
  • An invite states the role before it is sent, not after it is accepted.
  • An API key is shown once, at creation, with a copy button and a plain warning. Never retrievable afterwards. A key that can be read back is a key that leaks from a screenshot.
  • The danger zone is separated and last, and its actions require typing the object's name. A confirm dialog alone is a reflex away from a deleted workspace.
  • Billing states the plan, the period, the next charge with date and amount, and what happens at the limit. "Soon" is not a billing state.

Settings, in this order

Account · Workspace · Members · Billing · Integrations · API · Danger zone. Personal first, shared second, dangerous last.

Settings, members, keys and the danger zoneDraft, not built
MemberRoleStatusLast active
Emma WoelkOwnerActive4 min ago
redaktion@teisenda.comEditorialInvited—
API key — shown once
tsd_live_8f2c…a41d

Copy it now. Once this closes it cannot be retrieved.

Danger zone
Delete workspace

128 posts and every member. Type teisenda to confirm.

teisenda
17
Backend 17

Onboarding and import

The first screen after sign-up decides whether the product gets used. For a customer arriving from another system, the common first act is import, not authorship.

  • Import sits beside create on every empty list, equally weighted. Not in a settings page, not behind a menu.
  • A checklist has at most five steps, is dismissible, and does not come back once dismissed.
  • Sample data is labelled as sample and removable in one action. Sample data that cannot be told apart from real data poisons the first week.
  • Where data is processed is stated once at first run, prominently, and then lives on the model row — section 15. Repeating it on every screen turns a selling point into furniture.
  • Import reports rows read, imported and skipped, with the reason per skipped row and a file of them to download. “412 of 428 imported” without the missing 16 is an unfinished job handed to the customer.
  • An import that takes longer than ten seconds is a background task. The user leaves the page and is told when it is done — section 12.
First run and an import in progressDraft, not built
Setup
2 of 4·Dismiss
Workspace named
Team invited
Import existing posts
Generate the first draft
Import from WordPressRunning in the background
428 read 412 imported 16 skipped
View skipped
16 posts have no title. They need one before they can be created.
18
Backend 18

Motion, keyboard and focus

Motion has one job: to say what moved where. Past 200 ms it stops informing and starts delaying.

WhatDurationEasing
Colour, hover120 mslinear
Menu, toast160 mscubic-bezier(.2,.7,.3,1)
Dialog, sheet200 mssame entering, cubic-bezier(.4,0,1,1) leaving
Anything else240 ms ceiling—
  • prefers-reduced-motion is respected — rule 3 of section 02. Transforms become opacity, durations go to 0.01ms, and nothing exists only as motion.
  • Never animate the height of a list. It reflows everything below it on every frame.
  • Tab reaches everything actionable, in the order it is read. A control reachable only by mouse is a control half the users do not have.
  • The focus ring is never removed, and a custom control brings its own. 2 px --violet offset 2 on light, --violet-lift on the sidebar.
  • Escape closes the topmost layer and returns focus to the trigger.
  • A skip link to the main content is the first tabbable element, visible on focus. Without it every keyboard user tabs through the whole sidebar on every page.
  • Shortcuts: ⌘K search, ⌘↵ send, Esc close. No single-letter shortcut fires while a text field has focus.
  • Colour is never the only carrier of a state — icon or text alongside. Applies to status pills, errors and the AI marking (section 05).
  • Hit target 24 px, 44 on touch. Section 09.
Focus on both grounds, and the skip linkDraft, not built
On the light work area
Skip to content
Field focused
⌘K Search ⌘↵ Send Esc Close
19
Backend 19

Writing in the interface

German, du, per the language table in Frontend section 07. Interface copy follows the brand rules minus the rhythm: nobody reads a button for its cadence.

The examples in this section are in German, and only here. Everything else in this guide, the mockups included, is English. These are not: number format, date format and the difference between du and Sie do not survive translation, and they are what this section is about.

  • Buttons are verb plus object. "Beitrag löschen", never "Löschen", never "OK", never "Ja".
  • Titles take no full stop, sentences do.
  • Sentence case, not Title Case. German capitalises nouns; adding English title casing on top makes both wrong.
  • The error formula: what happened, why if it helps, what to do now. One sentence each at most.
  • Never "Ups", never "Etwas ist schiefgelaufen". Both say nothing and cost trust at the exact moment it is thinnest.
  • Dates: 28.08.2026. Relative time only under 24 hours, and the absolute value always in title. State the timezone wherever it can differ.
  • Numbers in German format: 12.480, decimals with a comma. Tabular figures in tables — section 08.
  • An empty state names what would be here, then offers the action. Section 12.
  • Product names are not translated. Studio Engine and Content Engine stay as they are — Frontend section 05.
  • No abbreviation the reader has to decode. "Bald" is a label, not an explanation of when.
  • One term per thing. Beitrag or Artikel, not both; whichever is chosen is the one in the navigation, the button, the empty state and the error.
The same eleven strings, twiceDraft, not built
NoOK · Ja · SpeichernYesBeitrag speichern
NoUps, etwas ist schiefgelaufen.YesDer Beitrag wurde nicht gespeichert. Die Verbindung ist abgebrochen — versuch es erneut.
NoUngültiges FormatYesGib eine E-Mail mit @ ein.
NoKeine Daten vorhandenYesNoch keine Beiträge. Leg den ersten an oder importiere deine bestehenden.
NoNeuer Beitrag ErstellenYesNeuen Beitrag anlegen
Novor 3 WochenYes28.08.2026
No12480 · 12,480Yes12.480
NoStudio-MotorYesStudio Engine
NoBitte wählen Sie eine SpracheYesWähl eine Sprache
NoElement gelöschtYesBeitrag gelöscht · Rückgängig
NoFehler 500YesDie Liste konnte nicht geladen werden. Deine Filter bleiben erhalten.
20
Backend 20

Responsive

The product is a desktop tool. Mobile is for reading and approving, not for building — and that has to be said rather than discovered.

WidthSidebarTableDialog
≥ 1280246 px, openFullCentred
1024 – 127974 px railFull, fewer columnsCentred
768 – 1023DrawerFull, fewer columnsCentred
< 768DrawerCards, label above valueFull-height sheet
  • Checked at 1280, 1440 and 1920, with the keyboard path visible throughout — section 04.
  • A horizontally scrolling table is not the mobile answer. Below 768 a row becomes a card: label above value, the two or three fields that matter, and the action.
  • The rail needs a logo without the wordmark. icon-* is the butterfly alone — Frontend section 02.
  • The composer stays above the on-screen keyboard.
  • Touch targets 44 px below 768, hover states dropped: there is no hover on touch, so nothing may depend on it — section 11.
  • What cannot be done on mobile says so, in place, instead of offering a control that will not work.
A table row below 768 pxDraft, not built
Posts
128
Pricing page 2026
StatusPublished
Views12,480
Changed28.08.2026
Assade case study
StatusReview
Views3,902
Changed27.08.2026
The same two rows

Label above value, the fields that matter, one action at the bottom at 44 px. No sideways scrolling, and nothing that needs hover.

The studio needs a bigger screen. On a phone you can read and approve.
21
Backend 21

Reporting and charts

A figure in this product is evidence for a decision. It has to say what it counts, over which period, and against what.

Which chart

QuestionChartNot
How did it develop?Line, or area for a single seriesBars. Twelve bars over twelve months hide the trend they are meant to show.
How do these compare?Bars, sorted by sizeA line. A line between unrelated categories draws a trend that does not exist.
What is it made of?Donut, at most four slices, largest firstMore than four slices, or a pie for anything anyone has to read a value off.
Where does it stand right now?A figure with its change and periodA gauge. It costs the space of a chart and carries one number.
Two measures at once?Two chartsA second Y axis. Two scales in one frame can be made to show any relationship at all.

Series colours

One series is --violet. Two are --violet and --ink-3. Beyond that, in this order and no other: #8250E6 · #432B78 · #B49BF0 · #6A4394 · #D8CBF2. A sixth series means the chart is the wrong shape.

  • Gold is never a series. It marks AI and nothing else — section 05. A gold bar makes the reader ask whether that column was generated.
  • Red and green only where they mean bad and good. As neutral series colours they announce a judgement nobody made.
  • Label the series directly, at the end of the line or above the bar. A legend forces a match by colour, which is exactly what a colour-blind reader cannot do.
  • A bar chart starts at zero. A truncated axis misstates the proportion, and the proportion is the whole reason for bars.
  • Horizontal grid lines only, 1 px --line. No vertical grid, no shadow on the plot, no third dimension.
  • Every axis carries its unit, every time series its granularity and its timezone.

Figures

  • Show the number, not only the shape. A sparkline without a value beside it is decoration.
  • A change carries sign, size and period: “+12 % vs. previous month”. An arrow alone says neither how much nor against what.
  • Direction is never carried by colour alone — sign and word alongside.
  • A percentage states what it is a percentage of.
  • Tabular figures, German format, empty value an em dash — sections 08 and 19.
  • No data is “No data yet”, not a flat line at zero. A zero line is a measurement; an empty set is not.
  • Rounding is decided once and kept. 12,480 in the table and 12,5 Tsd. on the card invites the question which one is true.

The control bar

Every reporting screen carries the same four controls, in this order, above the first chart.

ControlSpec
PeriodPresets — Today, 7 days, 30 days, 12 months, Custom. The chosen one is marked, and the absolute dates are written out beside it. “Last 30 days” without the dates is unciteable.
ComparisonOff by default. One choice, never two: “vs. previous period” or “vs. previous year”. When it is on, every figure on the screen carries its delta — not only the chart.
SegmentChips, multiple allowed, the count stated when more than one is active. Filters live in the URL — section 11.
Freshness"As of 14:32". A report is a snapshot; a number without a time cannot be quoted.
ExportCSV of the data behind the view with the filters applied, PNG of the chart as shown. Both carry period and filters in the filename.
  • A chart that updates itself while it is being read. If the data moves, say “New data available” and let the reader decide.
  • A period that resets when the reader navigates back.
The control barDraft, not built
Heute7 T30 T12 Mon.Frei
01.09.2025 – 28.08.2026 vs. previous year
As of 14:32
Segment Deutsch Organic +2 Filter Clear
CSV — Daten dieser Ansicht
PNG — Diagramm wie gezeigt

aufrufe_2025-09-01_2026-08-28_de-organisch.csv

More marks

MarkUse it whenRule
Stacked barsThe parts and the total both matterOnly when the total means something. Otherwise group them side by side — a stack makes every segment except the bottom one impossible to compare.
Horizontal barsLabels are longer than about twelve charactersSorted by size. Rotated axis labels are not the answer to long labels; turning the chart is.
HeatmapA cycle — weekday by hourNever for a trend. One hue, five steps, and the scale is stated. Section 21's series list does not apply; a sequential scale is a different job.
FunnelSteps that are genuinely sequentialEach step is a subset of the one above. If someone can skip a step, it is not a funnel and the drop-off figures lie.
SparklineInside a table row or beside a figureNo axis, and never without its current value beside it.
Target lineThere is an agreed number to hitDashed, --ink-3, labelled at the right end. Never in a series colour — a target is not a measurement.
Stacked, horizontal, heatmap, funnel, target, sparklineDraft, not built
Stacked · posts per quarter by language
Q1
Q2
Q3
Q4
DeutschEnglishFrançais
Horizontal · views per post
Why EU hosting matters18,402
Pricing page 202612,480
Newsletter 09/268,812
Assade case study3,902

Titles on the left, sorted by size. Rotated axis labels would be unreadable here.

Heatmap · views by weekday and hour
Mo Di Mi Do Fr 06:0010:0014:0018:00
01,200 views
Funnel · from draft to published
Drafts · 128
In review · 100
Approved · 78
Published · 67

Each step is a subset of the one above. If a step can be skipped, this is not a funnel — it is four numbers.

Target · posts per month
Target 12
9Jun
11Jul
14Aug
PostTrendViews
Why EU hosting matters18,402
Pricing page 202612,480

No axis, and never without the value beside it.

Axis, tooltip, legend

  • At most five ticks on the Y axis, abbreviated in German — Tsd., Mio. — with the unit in the chart title, not repeated on every tick.
  • Fewer X labels rather than rotated ones. Below roughly 60 px per label, drop every second and keep the first and the last.
  • The tooltip opens on hover and on focus, shows the x value, every series with its colour and value, and the delta when comparison is on.
  • No number that matters lives only in a tooltip. A figure someone has to hover to find is a figure that will not appear in their report.
  • The legend states values, not just names, wherever there is room. A legend that only names colours makes the reader look twice.
  • Clicking a legend entry may hide a series; it must not change the axis scale, or the remaining series appear to jump.

When there is nothing to draw

  • Loading is a skeleton with the plot area and the axes, not a spinner — section 12.
  • Empty says “No data yet” inside the plot area, with the axes still drawn. A chart that vanishes leaves the reader unsure whether it failed.
  • On incomplete last bucket is marked — hatched, and named: “August is still running”. Without it, every month-to-date chart reads as a collapse.
  • An error stays in the card: what failed, and a retry. Not a toast — section 13.
  • A comparison with no counterpart says so rather than drawing a flat line at zero.
Four states a chart can be inDraft, not built
Loading
Empty
No data in this period
Last bucket incomplete
9Jun
11Jul
6Aug

August is still running — 12 days.

Error
The query failed.
Your period is kept.

Drilldown and annotations

  • A click on a bar filters the table below it. It does not navigate away — the reader came for the comparison and would lose it.
  • The active filter is visible as a chip and clears in one action, exactly like section 11.
  • Annotations — releases, campaigns — are a thin vertical line with a marker at the axis, at most five in view. Beyond five they are a second chart nobody asked for.
  • An export carries what was on screen: period, filters, comparison. A CSV that silently exports everything is worse than no export.

Motion

  • A chart draws itself once, on arrival, in under 1.2 s. Bars grow from the baseline, lines draw left to right, the donut fills clockwise from twelve.
  • Stagger by 60 ms per element, and stop after that. Nothing loops, nothing pulses, nothing animates on hover. A dashboard that keeps moving cannot be read.
  • The final state is the default. The animation runs from empty to it, never the other way round — so a browser that skips the animation shows the finished chart, not an empty frame.
  • Values never animate on update. A figure that counts up while someone reads it is a figure they read wrong.
  • prefers-reduced-motion removes all of it — section 18.
Reporting, in the desktop applicationDraft, not built

Reports

Last 12 months
Views
84,302
+12% vs. previous month
Posts
128
−4% vs. previous month
Avg. read time
2:14
+9 s vs. previous month
AI-generated
36
28% of all posts
Views per month · Europe/Zurich
20262025
SepOktNovDezJanFebMarAprMaiJunJulAug
By channel · views in thousands
31.2Organic
18.1Newsletter
12.8LinkedIn
8.4Direct
4.3Referral
By language
128
Deutsch · 79 · 62 % English · 29 · 23 % Français · 20 · 15 %
PostLanguageViewsAvg. read timeSource
Why EU hosting mattersDE18,4023:02Editorial
Pricing page 2026DE12,4801:48KI
Assade case studyEN3,9022:31KI
22
Backend 22

The chart engine

One charting library, named here, with the case for it and the one point at which it stops being the right answer.

Recharts, through the shadcn/ui chart wrapper

npx shadcn@latest add chart   /* Recharts plus the token wrapper */
WhyWhat it buys
It is already the stack's defaultSection 03 puts the tokens into shadcn. Its chart component is Recharts with a wrapper that reads those same variables. One system, not two.
SVG, not canvasThe labels are real text: selectable, searchable, reachable by a screen reader, and they survive print and PDF. A canvas chart is one image with no words in it — and this product exports reports.
Colour comes from CSS variablesThe series list of section 21 is the only place colours live. No second palette inside a chart config.
ComposableAxis, grid, tooltip and legend are separate components. The rules in section 21 can be enforced once, in a wrapper, instead of per chart.
MIT, no networkNo CDN, no telemetry, no font fetched at runtime. The rule in section 04 applies to libraries too.
Animation is per seriesisAnimationActive binds to reduced motion in one place — section 18.

Where it stops

  • Recharts draws one SVG node per point. Past roughly 5.000 visible points it becomes slow, and no amount of memoising fixes that.
  • Above that, one chart may use uPlot — canvas, 45 KB, MIT. For that chart, not as a second general library, and the axis, tooltip and colours still follow section 21.
  • No second library for convenience. Two libraries mean two sets of defaults, two tooltips and two legends, and the difference shows on the first screen that has both.

Considered and not taken

LibraryWhy not here
Chart.jsCanvas. No real text, so no selection, no screen reader, and blurred labels in a PDF.
EChartsAround a megabyte, an imperative API against a React tree, and its own theming system that would fight the tokens.
NivoHeavy, and its visual defaults are strong enough to become a second brand.
TremorOpinionated dashboard styling on top of Recharts — a layer whose only job here would be to be overridden.
visxThe most control and the most work: every axis, tooltip and legend hand-built. Right only once the standard set genuinely runs out.
D3 directlyThe same, further along, and it puts a second rendering model beside React.

The configuration, in one file

Colours, axes and motion are set once. A component that sets them again is the beginning of a second chart style.

// chart-theme.ts — the series list of section 21, nothing beside it
export const series = [
  "var(--violet)", "var(--violet-deep)", "var(--violet-lift)", "#6A4394", "#D8CBF2",
] as const;

export const axis = {
  stroke: "var(--line)", tickLine: false, axisLine: false,
  tick: { fill: "var(--ink-3)", fontSize: 11 },
};

// Horizontal lines only, at most five steps — section 21
export const grid = { vertical: false, stroke: "var(--line)" };
// One chart. The height sits on the container: without it
// ResponsiveContainer collapses to zero and nothing appears.
const quiet = useReducedMotion();

<ChartContainer config={config} className="h-[280px] w-full">
  <LineChart data={data} margin={{ top: 8, right: 12, bottom: 0, left: 0 }}>
    <CartesianGrid {...grid} />
    <XAxis dataKey="month" {...axis} tickMargin={8} />
    <YAxis {...axis} width={44} tickCount={5} tickFormatter={shorten} />
    <ChartTooltip content={<ChartTooltipContent />} />
    <Line dataKey="now" stroke={series[0]} strokeWidth={2.5} dot={false}
      isAnimationActive={!quiet} animationDuration={900} />
    <Line dataKey="lastYear" stroke="var(--ink-3)" strokeWidth={2}
      strokeDasharray="5 5" dot={false} isAnimationActive={!quiet} />
  </LineChart>
</ChartContainer>
  • Never a literal hex in a chart component. Colours come from series, in order. A hex in a component is a colour nobody will find when the palette changes.
  • isAnimationActive is bound to reduced motion in the wrapper, not per chart. Bound in twelve places it will be wrong in one of them.
  • One ChartContainer per chart, its config beside it. No chart reaches into another chart's config.
  • Height is explicit. ResponsiveContainer takes its width from the parent and its height from nothing — without a height it renders at zero and the card looks broken rather than empty.
  • No chart on the server. Render the skeleton of section 21, hydrate, then draw.
  • The tooltip is ChartTooltip, once. A custom tooltip per chart is how two charts start disagreeing about what a number means.
  • Number formatting comes from one formatter — German format, tabular figures, the abbreviations of section 21. Recharts will happily print 12480.0000001 if nobody stops it.

Why this page does not load it

The examples in this guide are drawn as plain SVG, not rendered with Recharts. This document is a reference that makes no external request and carries no framework; pulling in React and a charting library to draw six example charts would cost more than the rest of the page together. The drawings match what the configuration above produces — if the two ever disagree, the configuration is right and the drawing is the bug.

What the configuration draws, part by partDraft, not built
Views per month · Europe/Zurich
12 Tsd. 8 Tsd. 4 Tsd. 0
Mar Apr Mai Jun Jul Aug
June 2026
20268,812
20255,140
+71 %
YAxis · tickCount 5 CartesianGrid · vertical false Line · series[0] Line · --ink-3, 5 5 ChartTooltip
20262025
vs. previous year
As of 14:32
23
Backend 23

Glow and emphasis

Glow says here. It never says what. Four places carry it and nothing else does — a screen where three things glow has nothing emphasised.

:root{
  --glow-v:      0 3px 16px rgba(130,80,230,.34);   /* violet, resting */
  --glow-v-lg:   0 5px 26px rgba(130,80,230,.48);   /* violet, hover */
  --glow-g:      0 3px 16px rgba(237,169,56,.32);   /* AI */
  --glow-focus:  0 0 0 4px rgba(130,80,230,.20);    /* halo behind the 2px ring */
}
WhereWhichWhy there
Active sidebar item--glow-vThe answer to "where am I", on the darkest surface in the product, where a glow actually reads.
The one primary action--glow-v, --glow-v-lg on hoverOne per view — section 09. The glow is what makes that rule visible.
Anything from the model--glow-gThe gold marking of section 05, given weight. Never on anything a person wrote.
Focus--glow-focusBehind the 2 px ring, never instead of it. The ring is the state; the halo only makes it findable.
The live point in a chartdrop-shadow on the last data pointSection 21. One point, the current one.
  • Glow lies outside the element and changes no contrast value. That is what makes it safe, and it is also why it can never carry a state on its own.
  • At most one glowing element in view, plus focus, plus the AI marking. Beyond that it is wallpaper.
  • Never on a card, a field, a table row or a status pill that is not AI. A glowing card is a card claiming an importance it does not have.
  • Never animated. No pulse, no breathing, no shimmer — section 21.
  • Only violet, gold and the destructive red. A glow in a colour outside the palette invents a fifth meaning.
  • It is strongest on the dark sidebar and weakest on white. On --card keep to the resting value; the large one there reads as a printing error.
Glow, and the same elements without itDraft, not built
With — the answer to "where am I"
Overview Studio
Without
Overview Studio
The one primary action
From the model
AI-generated
Focus — halo behind the ring
NoGlow on an ordinary card, a field or a table row.
NoA pulsing glow. Nobody reads a dashboard that keeps moving.
NoGlow as the only carrier of a state — it sits outside the box and counts towards no contrast value.
24
Backend 24

Integration commands

Everything in this part is reachable from a terminal. Versions are pinned where the guide ships a specific one, so the product and this page cannot drift apart.

Components and tokens

The values of section 03 go into the shadcn tokens, not beside them. shadcn expects bare HSL triples without the hsl() wrapper — the hex values have to be converted, not pasted.

# The scaffold and the components this part describes
npx shadcn@latest init
npx shadcn@latest add button input textarea select checkbox switch label \
  dialog sheet dropdown-menu tooltip table tabs skeleton sonner badge

# Charts from section 21 (shadcn/ui charts, on Recharts)
npx shadcn@latest add chart

Assets from this guide

# Icons — the same library /icons/ comes from
npm i lucide-react

# Flags, exactly the version in Frontend section 06
npm i flag-icons@7.5.0

# Type: no package, no request to Google. Fetch the four subsets
# this page uses itself.
mkdir -p public/fonts && cd public/fonts
for f in InstrumentSans-latin InstrumentSans-latin-ext \
         InstrumentSans-Italic-latin InstrumentSans-Italic-latin-ext; do
  curl -fsSLO "https://ci.teisenda.com/fonts/$f.woff2"
done

# The butterfly. One file, no build, no dependency.
curl -fsSLO https://ci.teisenda.com/effects/butterfly-field.js

Checks that belong in CI

Each one corresponds to a rule in this part. A rule nobody checks is a rule that lasts until the next sprint.

# No request to a Google domain (section 04)
! grep -rn "fonts.googleapis\|fonts.gstatic" src/ public/

# No outline:none without a replacement (section 09)
! grep -rn "outline:\s*none" src/ --include=*.css

# No logo with a live text layer (Frontend section 02)
! grep -l "<text" public/logos/*.svg

# The butterfly carries the same attributes everywhere (section 06)
test "$(grep -rho '<butterfly-field[^>]*>' src/ | sort -u | wc -l)" -le 1

# Operability against the rules of section 18
npx @axe-core/cli http://localhost:3000 --exit

The product's own API

The key comes from Settings · API and is shown once — section 16.

# Read posts
curl -fsS https://api.teisenda.com/v1/posts \
  -H "Authorization: Bearer $TEISENDA_API_KEY" \
  -H "Accept: application/json"

# Verify the webhook signature before reading the body
printf '%s' "$RAW_BODY" \
  | openssl dgst -sha256 -hmac "$TEISENDA_WEBHOOK_SECRET" -binary \
  | base64
  • The key in the repository, in an image, or in a screenshot for support. It is shown once for that reason — section 16.
  • Reading a webhook body before the signature has been checked.

Host and paths above are the shape, not a promise — the API is not among the things this guide fixes. Confirm them against the product before writing against them.

25
Backend 25

Stages and open questions

The order is a recommendation. If the existing code suggests another, say so before starting.

AreaSectionsDone when
E0Tokens, fonts, spacing and type scale03, 08The values apply project-wide, one scale is in use, and no request reaches a Google domain
E1Controls in every state09Every control has all six states and a visible focus ring, including the custom ones
E2App shell and navigation04, 14Checked at 1280/1440/1920, the same role sees the same sidebar everywhere, and the "Soon" question is decided
E3Login, including the butterfly and mobile06, 20Password managers work and the legal links are reachable on a phone
E4Forms10Errors say what to do, submit is never disabled to express validation, and unsaved work warns
E5Lists, tables, empty and waiting states11, 12All four empty states exist and are distinguishable, and every view is a URL
E6Overlays and notifications13Focus enters and returns, Escape closes one layer, and no error the user must act on is a toast
E7Studio: the AI surface15One free-text field, the model row on the composer, and nothing generated publishes without a human step
E8Settings, roles, danger zone16The navigation matches the role, the key is shown once, and deletion requires the name
E9Onboarding and import17Import sits beside create on every empty list, and skipped rows come with their reason
E10Reporting21, 22Every chart states its unit, its period and its timezone, and each one draws itself once and stops
—Motion, keyboard, interface copy, figures, glow18, 19, 21, 23Not a stage. Each of the ten above is checked against them before it is called done
—The commands for all of it24Not a stage either. The checks belong in CI on the first day, not after the first regression

Still open

Not to be invented. Ask.

OpenThe question
Detail pageHeader with tabs, or a two-column layout with a side panel. Where the actions live, and whether editing happens in place or in a form.
Dark mode for the work areaThe sidebar tokens exist. Whether the work area follows, and whether it is a setting or the system preference.
ProjectsWhether a project is an object at all, or whether the workspace is the only level.
NotificationsOn inbox with a history, or only toasts. Toasts alone mean anything missed is gone.
Audit logWhether who changed what is visible in the product. For a tool sold on data residency the answer is usually yes, but it is not a design decision.
Files and assetsUpload, storage, reuse across posts. Nothing here covers it.

Three decisions block work and belong to the product, not to the design: the global AI bar in the studio, the fallback for chat titles, and how the "Soon" entries are handled.