Tạp Chí Phố WallTCPW

Agent guide

Rules that travel with the prompt.

A SKILL.md for AI agents. Ten non-negotiables, type and component recipes, Vue conventions, the legacy list, and a self-check.

01

Install

Using the skill

The file design/SKILL.md is written for Claude Code and any agent that reads a skill or system prompt. It compresses this site into rules, recipes and a self-check.

  1. Claude Code. Copy it to .claude/skills/tcpw-design/SKILL.md in the repo. Invoke with /tcpw-design, or reference it from CLAUDE.md so it loads for every UI task.
  2. Any other agent. Paste the body below into the system prompt before asking for a component or page.
  3. Review. Ask the agent to run the self-check at the end of the file before it returns code.
Prompt prefix
You are adding UI to tapchiphowall.com. Follow the TCPW design language in SKILL.md exactly:
square corners (border-radius: 0 on every control), tokens from app/assets/theme.css only,
Playfair Display titles, Be Vietnam Pro body, hairline rules instead of cards, colour only for state,
tabular numerals, house easing with a reduced-motion block, 768px mobile behaviour written on purpose,
all copy through $t(). Run the self-check before returning code. Task: ...
02

design/SKILL.md

The skill file

---
name: tcpw-design
description: Design language for tapchiphowall.com (Tạp Chí Phố Wall). Load before writing or editing any Vue component, page, CSS, or HTML for this app so the output matches the editorial, square-cornered, ink-and-paper system.
---

# TCPW design language

You are building UI for a Vietnamese financial newspaper set with a ruler. Paper background, hairline rules, serif titles, tabular figures, controls cut square. Colour is reserved for state. Read `app/assets/theme.css` for tokens and `design/` for the reference site.

## Non-negotiables

1. **No corner radius.** Write `border-radius: 0` explicitly on every button, input, select, tab, panel, sheet, dialog, table, card, chip and stamp. Allowed exceptions: 2px on small stamps and the plan CTA, 3px on login fields and the segmented frame, 4px on images, 50% on circles only (avatars, live dots, spinners), 100px on metadata pills that are never clickable. Never 6, 8, 12, 16 or 24px.
2. **Tokens only.** Every colour comes from a `--token` in `theme.css`. No hex literal that has a token. Dark mode is `[data-theme="dark"]`; never `prefers-color-scheme` in CSS. Add a dark override only for a literal you had to introduce (plan inks, shadows).
3. **Three fonts, one job each.** `var(--font-serif)` (Playfair Display 600/700) for titles, big figures and italic asides. `var(--font-sans)` (Be Vietnam Pro 400 to 700) for body, labels, tables, buttons. `'Andada Pro'` only for the wordmark. `var(--font-mono)` for small readouts. Never Inter as a literal in new code.
4. **Rules, not boxes.** Separate with `1px solid var(--border-color)`. Close a block with `1px solid var(--text-primary)`. Inner list rows use `--border-lighter`. Post cards use dashed rules. Grids that share rules use `gap: 1px` on a `--border-color` backdrop. No box-shadow on the page plane. No hover lift.
5. **Colour is state.** `--accent-color` for live, available, active tab underline, focus ring. `--warning-color` for low. `--negative-color` for depleted, error, loss. `--positive-color` for gain. Gains and losses are bare coloured text at weight 600, not chips.
6. **Numbers are tabular.** `font-variant-numeric: tabular-nums` on any element that can hold a number.
7. **Motion.** `cubic-bezier(0.16, 1, 0.3, 1)` for entries, 0.15s ease for hovers, 0.35s for sheets. Every animated file ends with a `@media (prefers-reduced-motion: reduce)` block that removes it.
8. **Focus.** `outline: 2px solid var(--accent-color); outline-offset: 2px` on `:focus-visible`. Masthead links use ink with `-2px` offset. Never remove an outline without replacing it.
9. **Mobile.** Desktop-first `max-width` queries. 768px is the mobile line, 960px the masthead line. Tables scroll inside their frame, never collapse to cards. Inputs are 16px on mobile. Ink buttons go full width inside sheets and notices. Touch targets 44px.
10. **Vietnamese first.** All copy through `$t()`. Keep source strings sentence case; uppercase is applied by CSS. Labels must survive 0.12em tracking.

## Type recipes

| Role | CSS |
|---|---|
| Eyebrow | `font-size:.62rem; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--text-tertiary)`; prefix `◆` as a text node in `--text-primary` |
| Micro-label / table header | `.6 to .66rem; 700; .1 to .14em; uppercase` (table header 10.5px, .05em) |
| Serif title | `var(--font-serif); 600; letter-spacing:-.01em; line-height:1.25; text-wrap:balance`; 1.55rem sheet, 1.35 section, 1.3 notice, 1.2 progress |
| Figure | `var(--font-serif); 700; 3rem / 1.75rem; line-height:.95; letter-spacing:-.02em; tabular-nums lining-nums` |
| Voice | `var(--font-serif); italic; 600; 1 to 1.05rem; line-height:1.6; color:var(--text-secondary)` |
| Body | `1rem; line-height:1.75` prose, `.92rem; 1.6` notes, 12px tables, 14px nav |
| Button | `.76rem; 700; .12em; uppercase` (sm .72rem/.1em, xs .66rem) |

## Component recipes

**Ink button**: `height:44px; padding:0 1.5rem; border:1px solid var(--text-primary); background:var(--text-primary); color:var(--bg-color)`. Hover darkens to `--primary-hover` on note surfaces, inverts to transparent in the masthead and plan CTA. Ghost: transparent, `--border-color` border, ink border on hover. Icon button: 34px square, hairline, ink on hover. One ink button per view.

**Segmented**: divided box, 30px, hairline frame, `border-right` between, active = ink fill. Binary with sliding thumb: 3px padding, 3px frame radius, 2px thumb radius, `transform 0.35s cubic-bezier(0.22,1,0.36,1)`.

**Tabs**: `border-bottom:2px solid transparent; margin-bottom:-1px` over a hairline track; active = accent (feature tabs) or ink (page views).

**Input**: 44px, `--input-bg`, hairline, no radius; hover `--text-tertiary` border, focus `--text-primary` border, no glow. Select trigger is the one control with a focus glow: `0 0 0 3px rgba(16,185,129,.12)`. Affixes ($, %, sh) sit outside the input in tertiary.

**Notice**: `max-width:560px; padding:1.25rem 1.25rem 1.25rem 1.5rem; border:1px solid var(--border-color); background:var(--card-bg)`; `::before` 3px bar in `--notice-ink` bled 1px past the frame. Kinds repoint `--notice-ink`: error/network negative, limit warning, tier/auth ink. Eyebrow, serif title, secondary body, actions row that stacks under 480px.

**Dialog**: `width:min(440px,100%); padding:1.75rem; background:var(--card-bg); border:1px solid var(--border-color); box-shadow:0 24px 48px -28px rgba(0,0,0,.45)`; scrim `--overlay-background` + `blur(2px)`; actions right-aligned over a hairline, column-reverse under 480px. Focus trapped and restored.

**Sheet**: fixed right, `min(640px,100vw)`, `100dvh`, `border-left` hairline, `-24px 0 48px -28px rgba(0,0,0,.45)`, `z-index:var(--z-ai-sheet)`, slides from `translateX(100%)` over .35s. Head (eyebrow + close + serif title + subtitle), ledger strip on `--surface-2`, scrolling body, disclaimer inside the body. Full width under 768, still a right sheet.

**Progress**: 3px track with segment ticks (`repeating-linear-gradient` over `--border-color`), ink fill, paper sheen, mono percent. Time-driven to 95, snap to 100.

**Table**: header 10.5px uppercase tertiary on sticky `--surface-2`; cells 12px, `9px 12px`, `--border-lighter` rows, odd rows `--surface-2`, hover `color-mix(in srgb, var(--accent-color) 6%, var(--card-bg))`; numerics right-aligned.

**Statement figures**: no cards. `display:grid; grid-template-rows:subgrid; grid-row:span 2` per figure so labels and figures align. Ledger below with hairlines top and bottom and a `border-left` between columns.

**Plan grid**: `gap:1px` grid on `--border-color`; each plan sets `--ink`/`--ink-rgb`, gets a 2px cap (4px featured), featured tint 4.5% of ink, square tag, serif 2.6rem price with a half-size raised currency mark.

**Post card**: no chrome; dashed rules between cells; image 16:10 at 4px with a .55s zoom; sans 1rem/700 title that turns accent on hover; meta .72rem with accent author and tertiary date; container queries at 560 and 820px.

**Masthead**: 64px (56 under 960), paper, hairline bottom, `z-index:10000`, shadow only when scrolled; links 14px/500 secondary with a 2px ink underline that `scaleX`es in; dropdown panel with a 2px ink cap, hairline frame, `0 24px 48px -28px` shadow, rows 9px 16px with a 2px ink bar when active.

**Empty / loading**: dashed hairline frame with a line icon and serif title; shimmer over zebra rows; three-dot pulse; square spinner. Never a rounded skeleton card.

## Vue conventions

- `<script setup>`, Composition API, auto-imported components and composables.
- Scoped `<style>`; namespace classes per component (`.sheet__head`, `.tcpw-btn--ghost`).
- Consume tokens; define a local custom property only as an ink pointer (`--notice-ink`, `--ub-ink`, `--ink`) or a layout knob (`--segments`, `--cols`).
- `◆` is a text node inside `<span aria-hidden="true">`, never an SVG or an emoji.
- Dialog semantics for every overlay: `role="dialog" aria-modal="true" aria-labelledby`, focus moved in, trapped, restored; ESC closes; body scroll locked with a reference count.
- Stream AI text into the rendered ref as deltas arrive. Never buffer.
- New AI analyzers are a data entry in `shared/config/ai-insights.ts` and render through `AISidebar` and `app/components/ai/*`. Do not hand-roll a spinner, markdown block, sources list, error box or confirm overlay.
- Check `nuxt.config.ts` `routeRules` before adding cache headers.

## Legacy idiom, do not extend

`financial-tables.css`, `UnifiedFinancialTable`, `MetricsGrid`, `MetricGroup`, `ValueChange`, `TierBadge`, `RangeSlider`, `ToastNotifications` still use 12 to 24px radii, shadows, hover lifts and accent-filled pills. When you touch one, migrate it to the rules above. Never copy from it into new work. `var(--token)30` alpha concatenation in `financial-tables.css` is invalid CSS; use `color-mix()` or `rgba(var(--primary-rgb), .3)`.

## Self-check before returning code

- [ ] Every control declares `border-radius: 0` (or an allowed exception with a reason).
- [ ] No hex literal that has a token. Dark override present for any literal introduced.
- [ ] Titles serif, labels sans uppercase tracked, numbers tabular.
- [ ] Hairlines separate; no page-plane shadow; no hover lift; no gradient.
- [ ] Colour appears only for state or plan ink.
- [ ] House easing; reduced-motion block; focus-visible ring.
- [ ] 768px and 480px behaviour written on purpose; inputs 16px on mobile; tables scroll.
- [ ] Copy through `$t()`, Vietnamese default.