Tạp Chí Phố WallTCPW

Foundations

Ink, paper, rule, figure.

Every value below is lifted from app/assets/theme.css and the revamped components. Switch the theme in the masthead to see both palettes applied live.

01

Tokens

Colour

Two palettes, one set of names. Each swatch shows the light value on the left and the dark value on the right. Greys carry the interface. Green, amber and red are reserved for state.

Semantics

TokenUseDo not use for
--text-primaryInk. Body text, ink buttons, ink rules, active underlines, the notice bar default.Backgrounds larger than a button.
--text-secondaryBody copy in secondary position, nav links at rest, tab labels, micro-labels on data.Headings.
--text-tertiaryEyebrows, dates, table headers, denominators, index numbers, placeholder text.Anything that must pass AA on its own as body text.
--border-colorThe default hairline. Frames, dividers, table rows, dashed card rules.Text.
--border-lighterInner list rows, meter tracks, citation rows.Outer frames.
--surface-2Ledger strips, table header rows, zebra rows, code, hover rows.Page background.
--card-bgFloating layers (panel, dialog, sheet head) and plan cells.Decorative cards on the page plane.
--accent-colorLive dots, available quota, active tab underline, post-title hover, focus ring, select focus glow.Large fills. Button backgrounds (the quiet button hover is the one exception).
--positive-color / --negative-colorGains and losses as bare coloured text at weight 600. Error notices use negative.Success or failure decoration unrelated to money or errors.
--warning-colorLow quota, stalled progress, upcoming earnings.Emphasis.
--primary-colorLegacy grey-blue control colour. Kept for older components.New work. Use ink.

Identity inks

Plans and tiers get a personal ink via a local --ink and --ink-rgb. It colours a 2px cap rule, the name, the checkmarks and a tag. It never colours the background beyond a 4.5% tint on the featured plan.

PlanLightDarkRGB
Freevar(--text-tertiary)var(--text-tertiary)107, 114, 128
Plus#059669#34d39916, 185, 129
Pro#2563eb#60a5fa59, 130, 246
Max#b45309#fbbf24245, 158, 11
02

Type

Typography

Three families, one job each. Loaded in a single Google Fonts request from theme.css. Weights are limited to the ones listed; do not request more.

Wordmark--font-brand
Andada Pro 700
uppercase · 0.08em
21px nav · clamp(26, 2.6vw, 40)px footer
Tạp Chí Phố Wall
Display and titles--font-serif
Playfair Display 600 / 700
-0.01em to -0.03em
text-wrap: balance
Rates, revisited.
Gamma exposure has flipped negative below 5,400
Voice--font-serif italic 600
1rem to 1.05rem · lh 1.6
reasoning, quotes, taglines
“The dealer book is short gamma into Friday. Any move gets amplified; the tape is not going to be kind to late entries.”
Figures--font-serif 700
tabular-nums lining-nums
3rem · 2.6rem · 1.75rem
lh 0.95 · -0.02em
$142,309.18+3.42%218.54
Body--font-sans
Be Vietnam Pro 400 / 500 / 600 / 700
1rem · lh 1.75 (prose)
0.92rem · lh 1.6 (notes)
Lợi suất trái phiếu kỳ hạn 10 năm tăng lên 4,32% sau dữ liệu việc làm mạnh hơn dự báo. Thị trường đang định giá lại khả năng Fed giữ nguyên lãi suất đến hết quý. Cổ phiếu công nghệ vốn hóa lớn chịu áp lực trong phiên chiều.
Eyebrow--font-sans 700
0.62rem · 0.14em · uppercase
--text-tertiary · ◆ in ink

AI research note Beta

Micro-label--font-sans 600 / 700
0.6 to 0.66rem · 0.1 to 0.14em
uppercase
Total valueDay changeMarkets
Mono readouts--font-mono
0.7 to 0.85rem · tabular
percent, citation index, quota
64%01 02 037/10

Scale

RoleFamilySizeWeightTrackingLine height
Display (share page symbol)serifclamp(2.6rem, 7vw, 4rem)700-0.02em1
Sheet titleserif1.55rem600-0.01em1.2
Prose h1 / h2 / h3serif1.5 / 1.35 / 1.15rem600-0.01em1.25
Dialog titleserif1.4rem600-0.01em1.3
Notice titleserif1.3rem600-0.01em1.3
Plan nameserif1.3rem700-0.01em1
Post card titlesans1rem70001.35
Nav linksans14px5000.01em
Body prosesans1rem (0.97 mobile)40001.75
Note bodysans0.92rem40001.6
Table cellsans12px4000
Buttonsans0.76rem (0.72 sm)7000.12em
Table headersans10.5px7000.05em
Eyebrowsans0.62rem7000.14em
Disclaimersans0.74rem40001.55
Numbers. Any element that can contain a number sets font-variant-numeric: tabular-nums. Prices, changes, percentages, counts, dates, list markers, table cells. Without it columns wobble as values update.
03

Structure

Rules and lines

A rule is always 1px solid unless it is one of three named exceptions: the 2px ink cap on dropdown panels and active tabs, the 3px ink bar on notices and accent cap on the overview card, and the 3px folio rule that opens the footer.

Rule weights
Default · --border-color
Faint · --border-lighter · inner list rows
Dashed · between post cards
Strong · --text-primary · closes a block, masthead, table bookends
Folio · 3px ink + 1px hairline · footer opening
.rule          { border: 0; border-top: 1px solid var(--border-color); }
.rule--faint   { border-top-color: var(--border-lighter); }
.rule--dashed  { border-top-style: dashed; }
.rule--strong  { border-top-color: var(--text-primary); }

/* footer opening: 3px ink then 1px hairline 3px below */
.folio { border-top: 3px solid var(--text-primary); position: relative; }
.folio::before { content: ''; position: absolute; left: 0; right: 0; top: 3px; height: 1px; background: var(--border-color); }

1px-gap grids

A grid of cells that must share rules is drawn with gap: 1px on a --border-color backdrop. No cell has its own border, so rules never double.

.plan-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px;
  border: 1px solid var(--border-color);
  background: var(--border-color);
}
.plan { background: var(--card-bg); }
04

Shape

Radius

See the summary on the overview. In practice:

  • Write border-radius: 0 on every button, input, panel, sheet, dialog, table, card and chip you create. Do not rely on omission.
  • Image containers may use 4px (cards, hero) or 2 to 3px (thumbnails).
  • Form fields inside the login popup use 3px. Form fields elsewhere use 0.
  • Only an element whose width equals its height and which reads as a dot or avatar may use 50%.
  • A 100px pill is a metadata tag only (sector, benchmark, result count). It is never a button and never appears in the masthead, footer or a table header.
  • The scrollbar thumb keeps its 99px radius. It is browser chrome, not ours.
05

Grid

Layout and spacing

ContainerMax widthPaddingNotes
Masthead, footer1600px0 28px → 20px @1140 → 16px @960Grid auto 1fr auto, gap 24px. Bar height 64px, 56px under 960.
Default layout main1600px20pxFlex column, footer pushed down.
Homepage1440px24px → 16px @768 → 12px @480Two columns 1fr 310px, gap 56px; single column under 1024.
Reading column (AI share, prose)760pxProse has no max-width of its own. Measure comes from the container.
Note frames (notice, progress, dialog)560px / 440px1.25rem / 1.75remCentered with margin: 0 auto.
Right sheetmin(640px, 100vw)1.5rem → 1.15rem @768Full viewport height, 100dvh.

Spacing rhythm

No spacing tokens exist. The app uses rem for note surfaces and px for chrome. The values that recur:

4 · 6 · 8Gaps inside a control: icon to label, chip padding, meter to figure.
10 · 12 · 14 · 16Control padding, table cells (9/11 by 12), panel rows (9 by 16), post card gaps.
20 · 24 · 28Card padding (28/24/24 plan), section padding, page gutters.
32 · 44 · 48 · 56 · 72Between sections, footer bands (44/32), homepage column gap (56), footer top margin (72).
1rem · 1.25 · 1.5 · 2.1Note surfaces: body padding 1.5rem, notice 1.25rem, prose heading top margin 2.1rem, rule margins 2rem.

Control heights

30pxFooter segmented control and back-to-top. 36px on mobile.
34pxMasthead login button, sheet close button.
38pxSmall ink button (notice, dialog), billing toggle, select trigger.
40pxBare icon button in the masthead.
44pxPrimary ink button, search box, plan CTA. Also the minimum touch target.
46pxFull-width mobile buttons in sheets.
06

Viewports

Responsive

Desktop-first with max-width queries. 768px is the mobile line and matches useDeviceType. 960px is the masthead line. Post cards use container queries instead.

BreakpointCountWhat changes
1320pxWordmark shortens, nav gap tightens, search collapses to an icon.
1200px7Homepage sidebar 310 → 290px.
1100pxPlan grid 4 → 2 columns.
1024px25Homepage single column. Stock tabs vertical → horizontal. Sidebar ad hidden.
960pxMasthead switch. Bar 64 → 56px, desktop nav hidden, burger and icon search appear, right drawer sheet.
768px182Mobile. Bottom tab bar, footer accordion, hero 2 columns, ledger 1 column, tooltips hidden, inputs 16px, sheet full width, sticky plan CTA, comparison table shows one tier via picker, ink buttons full width, tables scroll horizontally with edge fades.
640px23Toasts edge to edge. Usage ledger 1 column. AI rebalance card wraps.
480px89Tightest padding. Notice and dialog actions stack, full-width buttons. Section margins 40px.
380pxHero figure 2.125rem. Wordmark shortest.

Rules for mobile

  • Tables scroll inside their own container. They do not collapse into cards. Add JS-toggled 32px edge fades to signal overflow.
  • Text inputs are 16px on mobile so iOS does not zoom.
  • Ink buttons become full width inside sheets and notices. Dialog actions reverse to a column so the primary action sits at the bottom.
  • Hover tooltips are removed with display: none !important; dotted underlines go with them.
  • The right sheet stays a right sheet at full width. It is not a bottom sheet.
  • Tap highlight is transparent on every control. Touch targets stay at least 44px.
Container query · drag the corner

Fed giữ lãi suất, nhưng biểu đồ dot plot nói điều khác

Ba thành viên FOMC hạ dự báo cắt giảm cho 2027.

NVDA sau earnings: dealer gamma và vùng 5,400

Cấu trúc options đang nói gì về tuần tới.

Danh mục 13F quý 2: ai đang mua năng lượng

Bốn quỹ lớn tăng tỷ trọng XLE.

.post-grid-wrap { container-type: inline-size; }   /* query container = ancestor */
.post-grid { display: grid; grid-template-columns: 1fr; }
@container (min-width: 560px) { .post-grid { grid-template-columns: 1fr 1fr; } }
@container (min-width: 820px) { .post-grid { grid-template-columns: 1fr 1fr 1fr; } }
/* cells own their top and left dashed rule; first row and column drop theirs */
07

Time

Motion

NameValueUsed for
House easecubic-bezier(0.16, 1, 0.3, 1)Panels, sheets, ink underlines, chevrons, search modal, dialog panel.
Thumb easecubic-bezier(0.22, 1, 0.36, 1)Sliding segmented thumb, usage meter width, login popup, sticky CTA.
Image easecubic-bezier(0.25, 0.46, 0.45, 0.94)Card image zoom (0.55s), hero shadow (0.35s).
Micro hover0.12 to 0.18s easeColour, border, background on hover.
Standard0.2 to 0.25sChevron rotate, tab underline, progress fill (linear).
Layer entry0.22s panel · 0.22s dialog · 0.35s sheetPanel drops from -6px. Dialog rises from 12px. Sheet slides from 100%.
Ambient1.8s breathe · 2.2s sheen · 2.4s pulseLive dot, progress sheen, composing diamond. Ease-in-out, infinite.
@media (prefers-reduced-motion: reduce) {
  .live-dot, .progress-sheen, .mark { animation: none; }
  .sheet, .sheet-scrim, .dialog, .progress-fill { transition: none; }
}

Every file that animates ends with this block. The masthead uses a blanket kill on all descendants. A banner may keep a 0.15s opacity fade.

Focus

Content and controls: outline: 2px solid var(--accent-color); outline-offset: 2px. Masthead links use an inset ink ring, outline: 2px solid var(--text-primary); outline-offset: -2px. Never remove the outline without replacing it.

08

Depth

Layers and elevation

Shadows exist only on floating layers and are long, soft and mostly negative-spread, so they read as a cast shadow at the edge, not a glow.

Layerz-indexShadowBackdrop
Mobile tab bar900none
Masthead100000 10px 30px -24px rgba(0,0,0,.35) when scrolled
Dropdown panelinside masthead0 24px 48px -28px rgba(0,0,0,.35)
Toast, banner100030 10px 30px rgba(0,0,0,.12)
Nav mobile sheet10010-24px 0 48px -32px rgba(0,0,0,.45)rgba(10,10,10,.45) + blur(2px)
AI sheet10015 (--z-ai-sheet)-24px 0 48px -28px rgba(0,0,0,.45)--overlay-background, no blur
AI dialog10016 (--z-ai-dialog)0 24px 48px -28px rgba(0,0,0,.45)--overlay-background + blur(2px)
Search modal100200 24px 64px rgba(0,0,0,.22), 0 8px 24px rgba(0,0,0,.12)rgba(0,0,0,.65) + blur(8px)

Dark theme shadows are deeper: 0 28px 56px -28px rgba(0,0,0,.8) for panels. Nothing on the page plane casts a shadow.

09

Implementation

Theme

Light is the default. The choice lives in a theme cookie (light | dark | system) and is resolved to a data-theme attribute on <html> by an inline script in <head>, before first paint. CSS never reads prefers-color-scheme directly.

<script>
(function () {
  try {
    var m = document.cookie.match(/(?:^|; )theme=([^;]*)/);
    var pref = m ? decodeURIComponent(m[1]) : 'light';
    var theme = 'light';
    if (pref === 'dark') theme = 'dark';
    else if (pref === 'system') theme = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    document.documentElement.setAttribute('data-theme', theme);

    /* status bar colour must follow the attribute, not the media query */
    var meta = document.createElement('meta');
    meta.name = 'theme-color';
    meta.content = theme === 'dark' ? '#0e0d0c' : '#ffffff';
    document.head.insertBefore(meta, document.head.firstChild);
  } catch (e) {
    document.documentElement.setAttribute('data-theme', 'light');
  }
})();
</script>
  • Style with tokens only. A component needs a dark override only when it uses a literal, such as a plan ink or a shadow. Write it as [data-theme="dark"] .plan-plus { --ink: #34d399 }.
  • The logo is a dark PNG. In dark theme it is inverted with filter: invert(1).
  • Body colour and background transition over 0.3s so a toggle does not flash.
  • The store at app/stores/theme.js owns the cookie and the meta tag. Do not set data-theme from a component.