Tạp Chí Phố WallTCPW

Components

Cut square, set in ink.

Every demo is live and follows the theme toggle. Press Code on a frame for the CSS. Class names match assets/ds.css; the originals are named in each note.

01

Controls

Buttons

One shape, four inks. Uppercase, 700, tracked 0.12em, 1px border, no radius. The primary is solid ink. Hover either darkens (note surfaces) or inverts (masthead, plan CTA). Source: AISidebar.vue .ai-btn, Navbar.vue .login-btn, Footer.vue .top-btn.

Ink buttons
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.6rem;
  height: 44px; padding: 0 1.5rem;
  border: 1px solid var(--text-primary);
  border-radius: 0;
  background: var(--text-primary);
  color: var(--bg-color);
  font-family: var(--font-sans);
  font-size: 0.76rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.btn:hover:not(:disabled) { background: var(--primary-hover); border-color: var(--primary-hover); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn:focus-visible { outline: 2px solid var(--accent-color); outline-offset: 2px; }

/* masthead and plan CTA invert instead */
.btn--invert:hover { background: transparent; color: var(--text-primary); }

/* ghost: hairline, ink on hover */
.btn--ghost { background: transparent; color: var(--text-primary); border-color: var(--border-color); }
.btn--ghost:hover { border-color: var(--text-primary); }

/* outline: ink frame, fills on hover (footer) */
.btn--outline { background: transparent; color: var(--text-primary); }
.btn--outline:hover { background: var(--text-primary); color: var(--bg-color); }
Sizes, icons, links
See all
.btn--sm { height: 38px; padding: 0 1.1rem; font-size: 0.72rem; letter-spacing: 0.1em; }
.btn--xs { height: 30px; padding: 0 12px; font-size: 0.66rem; }
.btn svg  { width: 14px; height: 14px; }

.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px;
  background: transparent; border: 1px solid var(--border-color); border-radius: 0;
  color: var(--text-secondary); cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.icon-btn:hover { color: var(--text-primary); border-color: var(--text-primary); }
.icon-btn--bare { width: 40px; height: 40px; border-color: transparent; color: var(--text-primary); }
.icon-btn--bare:hover { background: var(--surface-2); }

.link-btn { color: var(--text-tertiary); font-size: 0.66rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
.link-btn::after { content: '→'; transition: transform 0.25s ease; }
.link-btn:hover { color: var(--accent-color); }
.link-btn:hover::after { transform: translateX(3px); }
Do

One ink button per view. Pair it with a ghost. Full width under 768px inside a sheet or notice.

Don't

Accent-filled buttons, gradients, drop shadows, hover lifts, rounded corners, sentence-case labels.

02

Selection

Segmented controls and tabs

Three idioms. The divided box (footer, theme picker) for 2 to 4 short options. The sliding thumb (billing cycle) for a binary. Underline tabs for views. Source: Footer.vue .seg, BillingCycleToggle.vue, pages/index.vue .calendar-tab.

Divided box
.seg { display: inline-flex; height: 30px; border: 1px solid var(--border-color); }
.seg-btn {
  height: 100%; padding: 0 11px;
  background: none; border: 0; border-right: 1px solid var(--border-color); border-radius: 0;
  font-size: 11px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-secondary); cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.seg-btn:last-child { border-right: 0; }
.seg-btn:hover     { background: var(--surface-2); color: var(--text-primary); }
.seg-btn.is-active { background: var(--text-primary); color: var(--bg-color); }
Sliding thumb
.segmented {
  position: relative; display: inline-grid; grid-template-columns: 1fr 1fr;
  padding: 3px; background: var(--card-bg);
  border: 1px solid var(--border-color); border-radius: 3px; isolation: isolate;
}
.segmented__thumb {
  position: absolute; top: 3px; bottom: 3px; left: 3px; width: calc(50% - 3px);
  background: var(--text-primary); border-radius: 2px; z-index: 0;
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.segmented[data-active="b"] .segmented__thumb { transform: translateX(100%); }
.segmented button {
  position: relative; z-index: 1; min-height: 38px; padding: 0 20px;
  border: none; border-radius: 2px; background: transparent;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-secondary); transition: color 0.25s ease;
}
.segmented button.active { color: var(--bg-color); }
Underline tabs
.tabs { display: flex; border-bottom: 1px solid var(--border-color); }
.tab {
  padding: 10px 20px; margin-bottom: -1px;
  background: transparent; border: none; border-bottom: 2px solid transparent;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-secondary); cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease;
}
.tab.active { color: var(--accent-color); border-bottom-color: var(--accent-color); }
/* ink variant for page-level views */
.tab.active--ink { color: var(--text-primary); border-bottom-color: var(--text-primary); }
03

Input

Forms

Fields sit on --input-bg inside a hairline. Focus turns the border to ink; there is no glow ring except on the select trigger. Labels are 0.5625rem uppercase micro-labels. Source: StockSearch.vue, WatchlistPositionEditor.vue, ScreenerSelect.vue.

Text, number, select
Sector
Press ⌘K to search
.input {
  width: 100%; min-height: 44px; padding: 0 0.875rem;
  background: var(--input-bg); border: 1px solid var(--border-color); border-radius: 0;
  color: var(--text-primary); font-size: 15px; letter-spacing: 0.01em; outline: none;
  transition: border-color 0.2s ease, background 0.2s ease;
}
.input::placeholder { color: var(--text-tertiary); }
.input:hover { border-color: var(--text-tertiary); }
.input:focus { border-color: var(--text-primary); background: var(--bg-color); }
@media (max-width: 768px) { .input { font-size: 16px; } }

.field-label { font-size: 0.5625rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-tertiary); }

/* affixed number: the $ and % live outside the input */
.input-wrap { display: inline-flex; align-items: center; gap: 4px; padding: 0 0.6rem; min-height: 36px; border: 1px solid var(--border-color); background: var(--surface-1); }
.input-wrap:focus-within { border-color: var(--text-primary); }
.input-wrap input { border: 0; background: transparent; outline: none; width: 5.5rem; font-size: 0.8rem; font-variant-numeric: tabular-nums; -moz-appearance: textfield; }
.input-wrap input::-webkit-inner-spin-button { -webkit-appearance: none; }

.select-trigger { display: flex; justify-content: space-between; min-height: 38px; padding: 8px 11px; background: var(--input-bg); border: 1px solid var(--border-color); border-radius: 0; font-size: 13px; }
.select-trigger.open { border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(16,185,129,0.12); }
.select-trigger.has-value { border-color: var(--accent-color); background: color-mix(in srgb, var(--accent-color) 8%, var(--input-bg)); }
.select-menu { margin-top: 6px; padding: 4px; background: var(--card-bg); border: 1px solid var(--border-light); box-shadow: 0 12px 32px rgba(0,0,0,0.16), 0 2px 8px rgba(0,0,0,0.06); }
.select-option.selected { background: rgba(16,185,129,0.1); color: var(--accent-color); font-weight: 600; }
04

Marks

Chips and stamps

Stamps are square and carry status: beta, plan tag, free, short side. Chips are 100px pills and carry metadata: sector, benchmark, counts. A pill is never clickable and never sits in chrome. Source: TierCard.vue .plan-tag, AISidebar.vue .ai-sheet-beta, portfolio.vue .sector-tag.

Stamps and chips
Beta Most popular Plus Free Short Technology SPY vs QQQ Live
.stamp {
  display: inline-flex; padding: 3px 7px;
  border: 1px solid var(--border-color); border-radius: 0;
  font-size: 0.56rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-tertiary);
}
.stamp--mono  { font-family: var(--font-mono); }
.stamp--ink   { color: var(--text-primary); border-color: var(--text-primary); }
.stamp--fill  { background: var(--accent-color); color: #fff; border-color: var(--accent-color); border-radius: 2px; }
.stamp--short { color: #ef4444; border-color: currentColor; border-radius: 3px; }

.chip {
  display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.15rem 0.5rem;
  border: 1px solid var(--border-color); border-radius: 100px;
  font-size: 0.6875rem; font-weight: 500; color: var(--text-secondary);
}
.live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-color); animation: breathe 1.8s ease-in-out infinite; }
05

Feedback

Notice

One neutral frame. The kind repoints a single --notice-ink that colours a 3px left bar and the diamond. Titles are serif. The CTA is an ink button. Source: components/ai/AINotice.vue.

Kinds

AI research note

Daily limit reached

You have used all 10 Pro requests for today.

◷ Resets in 6h 12m · Purchased credits are used after the daily pool

AI research note

Available on Pro and Max

AI analysis is part of the Pro plan. Compare plans to unlock it.

.notice {
  --notice-ink: var(--text-primary);
  position: relative; max-width: 560px; margin: 0 auto;
  padding: 1.25rem 1.25rem 1.25rem 1.5rem;
  border: 1px solid var(--border-color); background: var(--card-bg);
}
.notice::before { content: ''; position: absolute; left: -1px; top: -1px; bottom: -1px; width: 3px; background: var(--notice-ink); }
.notice.kind-error, .notice.kind-network { --notice-ink: var(--negative-color); }
.notice.kind-limit { --notice-ink: var(--warning-color); }
.notice .mark { color: var(--notice-ink); }
.notice .title { font-size: 1.3rem; line-height: 1.3; margin: 0.5rem 0 0.4rem; }
.notice p { font-size: 0.92rem; line-height: 1.6; color: var(--text-secondary); }
.notice-actions { display: flex; gap: 0.6rem; margin-top: 1.1rem; }
@media (max-width: 480px) { .notice-actions { flex-direction: column; } }
06

Confirm

Dialog

440px, 1.75rem padding, hairline frame, cast shadow, blurred scrim. Actions right-aligned above an ink-less hairline, reversed to a column under 480px. Focus is trapped and returned. Source: components/ai/AIDialog.vue.

Confirm
.dialog-scrim {
  position: fixed; inset: 0; z-index: var(--z-ai-dialog);
  display: flex; align-items: center; justify-content: center; padding: 16px;
  background: var(--overlay-background);
  backdrop-filter: blur(2px);
}
.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, 0.45);
}
.dialog-actions {
  display: flex; justify-content: flex-end; gap: 0.6rem;
  margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border-color);
}
/* enter: scrim opacity 0.18s ease, panel translateY(12px) 0.22s cubic-bezier(0.16,1,0.3,1) */
07

Composing

Progress

An index bar: 3px track with segment ticks punched out by a repeating gradient, ink fill, paper sheen. Time-driven to 95% then snapped to 100 on completion. When the model streams reasoning, an italic serif excerpt replaces the step copy. Source: components/ai/AIProgress.vue.

Index bar

Composing Step 1 of 4

0%

Reading the options chain

Pulling dealer positioning and key strikes.

Reasoning

Net gamma flips near 5,400; below that dealers are short and must sell into weakness, so the first support is thin.

.progress-track {
  position: relative; height: 3px; overflow: hidden;
  background:
    repeating-linear-gradient(90deg,
      transparent 0,
      transparent calc(100% / var(--segments) - 3px),
      var(--bg-color) calc(100% / var(--segments) - 3px),
      var(--bg-color) calc(100% / var(--segments))),
    var(--border-color);
}
.progress-fill { height: 100%; background: var(--text-primary); transition: width 0.25s linear; position: relative; overflow: hidden; }
.progress-sheen {
  position: absolute; inset: 0; opacity: 0.55;
  background: linear-gradient(90deg, transparent, var(--bg-color) 50%, transparent);
  animation: sheen 2.2s ease-in-out infinite;
}
@keyframes sheen { 0% { transform: translateX(-100%); } 60%, 100% { transform: translateX(100%); } }
.progress-pct { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 600; font-variant-numeric: tabular-nums; }
08

Quota

Usage ledger

The only quota display. Colour is state: accent means available, warning means low, negative means depleted, ink means credits or unlimited. Figures are mono and tabular. Source: AIUsageBadge.vue.

Badge states
7/10today·resets in 6h 12m
2/10today·resets in 6h 12m
0/10Daily limit reached·resets in 6h 12m
14credits
Unlimited
.usage { --ub-ink: var(--accent-color); display: inline-flex; gap: 0.45rem; font-size: 0.76rem; color: var(--text-secondary); }
.usage.state-low { --ub-ink: var(--warning-color); }
.usage.state-depleted { --ub-ink: var(--negative-color); }
.usage.state-credits, .usage.state-unlimited { --ub-ink: var(--text-primary); }
.usage .mark { font-size: 0.6rem; color: var(--ub-ink); }
.usage-figure { font-family: var(--font-mono); font-size: 0.82rem; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text-primary); }
.usage-figure span { color: var(--text-tertiary); font-weight: 500; }
.usage-meter { width: 56px; height: 3px; background: var(--border-color); }
.usage-meter i { display: block; height: 100%; background: var(--ub-ink); transition: width 0.4s cubic-bezier(0.22, 1, 0.36, 1); }
09

Layer

Right sheet

The AI surface. Fixed right, min(640px, 100vw), full height, slides in over 0.35s. Header with eyebrow, close, serif title, subtitle. A ledger strip on --surface-2 carries the usage badge and a live dot. The body scrolls; the disclaimer scrolls with it. Source: AISidebar.vue.

Sheet
.sheet {
  position: fixed; top: 0; right: 0;
  width: min(640px, 100vw); height: 100dvh;
  display: flex; flex-direction: column;
  background: var(--bg-color); border-left: 1px solid var(--border-color);
  box-shadow: -24px 0 48px -28px rgba(0,0,0,0.45);
  z-index: var(--z-ai-sheet);
  transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);   /* from translateX(100%) */
}
.sheet-head   { padding: 1.1rem 1.5rem; border-bottom: 1px solid var(--border-color); }
.sheet-ledger { min-height: 42px; padding: 0.5rem 1.5rem; background: var(--surface-2); border-bottom: 1px solid var(--border-color); }
.sheet-body   { flex: 1; min-height: 0; overflow-y: auto; padding: 1.5rem; overscroll-behavior: contain; }
@media (max-width: 768px) { .sheet { width: 100vw; border-left: none; } }
10

Reading

Prose and citations

AI markdown renders through one scale. Serif h1 to h3 with a hairline under each; h2 carries a faint diamond. h4 and below become micro-labels. Tables are bookended by two ink rules. Links are underlined with a tertiary hairline. Source: components/ai/AIMarkdown.vue, AICitations.vue.

Prose scale

NVDA into earnings

Dealers are short gamma below 5,400. Positioning is crowded on the call side and the put wall at 5,200 is thin.

Key levels

StrikeGEXRole
5,500+1.2BCall wall
5,4000Gamma flip
5,200-0.6BPut wall

Risk

A move through the flip removes the dealer bid. Size accordingly.

Volatility is likely to expand on the print

.prose { font-size: 1rem; line-height: 1.75; overflow-wrap: anywhere; }
.prose h1, .prose h2, .prose h3 {
  font-family: var(--font-serif); font-weight: 600; letter-spacing: -0.01em; line-height: 1.25;
  margin: 2.1rem 0 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border-color);
}
.prose h1 { font-size: 1.5rem; } .prose h2 { font-size: 1.35rem; } .prose h3 { font-size: 1.15rem; }
.prose h2::before { content: '◆'; font-size: 0.5em; vertical-align: 0.35em; margin-right: 0.6em; color: var(--text-tertiary); }
.prose h4 { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-secondary); }
.prose blockquote { border-left: 2px solid var(--text-primary); padding-left: 1.1rem; font-family: var(--font-serif); font-style: italic; font-size: 1.05rem; color: var(--text-secondary); }
.prose a { border-bottom: 1px solid var(--text-tertiary); }
.prose a:hover { border-bottom-color: var(--text-primary); }

.prose-table th { font-size: 0.66rem; letter-spacing: 0.1em; text-transform: uppercase; border-bottom: 1px solid var(--text-primary); }
.prose-table td { padding: 0.55rem 0.75rem; border-bottom: 1px solid var(--border-color); }
.prose-table tr:last-child td { border-bottom-color: var(--text-primary); }

.citation { display: flex; align-items: baseline; gap: 0.75rem; padding: 0.5rem 0; border-top: 1px solid var(--border-lighter); font-size: 0.82rem; color: var(--text-secondary); }
.citation-num { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-tertiary); }   /* padStart(2, '0') */
.citation-host { color: var(--text-primary); font-weight: 500; }
11

Data

Tables

Dense, flat, zero radius. Uppercase 10.5px headers in tertiary, sticky on --surface-2. 12px cells with tabular numerals, zebra on odd rows, accent-tinted hover. Numbers right-aligned. Overflow scrolls inside the frame. Source: pages/screener.vue.

Screener table
#SymbolCompanyPrice ↓ChangeMkt capP/ESector
1NVDANVIDIA Corp218.54+3.42%5.31T48.2Technology
2MSFTMicrosoft Corp512.10-0.88%3.80T36.1Technology
3XOMExxon Mobil118.72+1.14%512B14.0Energy
4JPMJPMorgan Chase242.36-0.21%690B13.5Financials
5MPMP Materials62.08+7.90%10.1BMaterials
.table-scroll { overflow-x: auto; border: 1px solid var(--border-light); }
.table { width: 100%; border-collapse: collapse; font-size: 12px; }
.table thead { position: sticky; top: 0; background: var(--surface-2); }
.table th {
  padding: 11px 12px; text-align: left; white-space: nowrap;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--text-tertiary); border-bottom: 1px solid var(--border-color);
}
.table th.sortable:hover { color: var(--text-primary); background: var(--hover-bg); }
.table th.sorted { color: var(--accent-color); }
.table td { padding: 9px 12px; white-space: nowrap; border-bottom: 1px solid var(--border-lighter); font-variant-numeric: tabular-nums; }
.table tbody tr:nth-child(odd) { background: var(--surface-2); }
.table tbody tr:hover { background: color-mix(in srgb, var(--accent-color) 6%, var(--card-bg)); }
.pos { color: var(--positive-color); font-weight: 600; }
.neg { color: var(--negative-color); font-weight: 600; }
Financial statements (financial-tables.css) still use the 16px rounded wrapper with a shadow and a 4px left accent bar on hover. Do not copy it for new tables. Note also that its var(--input-bg)30 alpha concatenations are invalid CSS and silently drop.
12

Figures

Statement figures

KPIs are not cards. They are borderless figures on a subgrid so labels share a line and figures share a baseline. A two-column ledger sits beneath, bounded by hairlines. Source: watchlist/WatchlistPortfolioSummary.vue.

Portfolio hero
Total value$142,309.18
Day+$1,204.55
Total return+18.4%
Cost basis
$120,180
Cash
$8,420
Positions
14
Beta
1.12
Sharpe
1.48
Max DD
-9.8%
vs SPY
+4.1%
Short exposure
12%
Dividend yld
1.3%
.hero { display: grid; grid-template-columns: repeat(3, auto); column-gap: 3rem; padding: 0.25rem 0 1.5rem; justify-content: start; }
.hero-figure { display: grid; grid-template-rows: subgrid; grid-row: span 2; row-gap: 0.5rem; align-items: baseline; }
.hero-label { font-size: 0.625rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-tertiary); }
.figure { font-family: var(--font-serif); font-weight: 700; line-height: 0.95; letter-spacing: -0.02em; font-variant-numeric: tabular-nums lining-nums; }
.figure--lg { font-size: 3rem; }      /* 2.5rem @768, 2.125rem @380 */
.figure--md { font-size: 1.75rem; }

.ledger { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); border-top: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color); }
.ledger > div + div { border-left: 1px solid var(--border-color); padding-left: 1.75rem; }
.metric-label { font-size: 0.625rem; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-tertiary); }
.metric-value { font-size: 1rem; font-weight: 600; font-variant-numeric: tabular-nums; }
@media (max-width: 768px) { .hero { grid-template-columns: 1fr 1fr; } .hero-figure:first-child { grid-column: 1 / -1; } .ledger { grid-template-columns: 1fr; } }
13

Stock header

Overview card

The one framed card on the page plane, and it earns it: a 3px accent cap, a faint 28px ledger grid masked from the corner, and a mood glow that follows the day's direction. Serif symbol, sans price. Source: StockOverview.vue.

Positive mood
NVDA
NVIDIA Corporation · NASDAQ
218.54
+7.22 (+3.42%)
TechnologySemiconductorsMarket open
.overview {
  --mood-rgb: 16, 185, 129;
  position: relative; isolation: isolate; overflow: hidden;
  border: 1px solid var(--border-color); border-top: 3px solid var(--accent-color); border-radius: 0;
  padding: 16px 20px 18px;
}
.overview.mood-positive { --mood-rgb: 45, 125, 50; }   /* dark: 76,175,80 */
.overview.mood-negative { --mood-rgb: 204, 51, 51; }   /* dark: 244,67,54 */
.overview::before {   /* ledger grid, masked from the top-left corner */
  content: ''; position: absolute; inset: 0; z-index: -1; opacity: 0.5;
  background:
    linear-gradient(var(--border-color) 1px, transparent 1px) 0 0 / 28px 28px,
    linear-gradient(90deg, var(--border-color) 1px, transparent 1px) 0 0 / 28px 28px;
  mask-image: radial-gradient(420px 220px at 0 0, #000, transparent 70%);
}
.overview::after {    /* mood glow */
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: radial-gradient(560px 280px at 3% 0%, rgba(var(--mood-rgb), 0.14), transparent 70%);
}
.overview-sym   { font-family: var(--font-serif); font-size: 2rem; font-weight: 700; letter-spacing: -0.02em; }
.overview-price { font-size: 1.75rem; font-weight: 700; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
14

Commerce

Plan grid

Four cells share rules through a 1px gap. Each plan carries its ink in a 2px cap; the featured plan gets a 4px cap, a 4.5% tint and a square tag. No borders, no shadow, no scale. Source: PricingTiers.vue, TierCard.vue.

Plans
Free
$0
forever
  • Market news and calendar
  • Watchlist
Plus
$4.99
per month
one coffee a month
  • + Paid articles
  • Everything in Free
  • Screener
Max
$29.99
per month
  • + 20 notes a day, Steve AI
  • Everything in Pro
.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 { --ink: var(--text-tertiary); --ink-rgb: 107,114,128; position: relative; display: flex; flex-direction: column; padding: 28px 24px 24px; background: var(--card-bg); }
.plan::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: var(--ink); }
.plan-plus { --ink: #059669; --ink-rgb: 16,185,129; }
.plan-pro  { --ink: #2563eb; --ink-rgb: 59,130,246; }
.plan-max  { --ink: #b45309; --ink-rgb: 245,158,11; }
[data-theme="dark"] .plan-plus { --ink: #34d399; }
[data-theme="dark"] .plan-pro  { --ink: #60a5fa; }
[data-theme="dark"] .plan-max  { --ink: #fbbf24; }
.plan.is-featured { background: linear-gradient(rgba(var(--ink-rgb), .045), rgba(var(--ink-rgb), .045)), var(--card-bg); }
.plan.is-featured::before { height: 4px; }
.plan-name   { font-family: var(--font-serif); font-size: 1.3rem; font-weight: 700; color: var(--ink); }
.plan-amount { font-family: var(--font-serif); font-size: 2.6rem; font-weight: 700; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
.plan-amount small { font-size: 0.5em; font-weight: 600; vertical-align: 0.75em; }
.plan-list li svg { width: 14px; height: 14px; color: var(--ink); }   /* path M5 12.5l4.2 4L19 7, stroke 2.4 */
.plan .btn { width: 100%; border-radius: 2px; font-size: 0.7rem; }
15

Editorial

Post cards

No card chrome. Dashed rules between cells, 16:10 image at 4px, sans title in 700 that turns accent on hover, meta line with an accent author and a tertiary date. Layout is by container query. Source: PostGrid.vue, PostCardImage.vue, PostMeta.vue.

Grid · drag the corner
Free

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

Ba thành viên FOMC hạ dự báo cắt giảm cho 2027. Thị trường phản ứng bằng cách bán trái phiếu dài hạn.

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 trong quý.

.post-card { padding: 20px; border-top: 1px dashed var(--border-color); border-left: 1px dashed var(--border-color); }
/* first row and first column drop their rule, see foundations for the nth-child math */
.post-img { aspect-ratio: 16 / 10; border-radius: 4px; overflow: hidden; background: var(--input-bg); margin-bottom: 14px; }
.post-img img { transition: transform 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94); }
.post-card:hover .post-img img { transform: scale(1.06); }
.post-title { font-size: 1rem; font-weight: 700; line-height: 1.35; text-wrap: balance; -webkit-line-clamp: 3; transition: color 0.25s ease; }
.post-card:hover .post-title { color: var(--accent-color); }
.post-desc { font-size: 0.8rem; line-height: 1.5; color: var(--text-secondary); -webkit-line-clamp: 2; }
.post-meta { font-size: 0.72rem; letter-spacing: 0.02em; font-variant-numeric: tabular-nums; }
.post-meta .author { color: var(--accent-color); font-weight: 500; }
.post-meta .date { color: var(--text-tertiary); }
.post-meta .sep { opacity: 0.4; }
17

Absence

Empty and loading

Empty states sit inside a dashed hairline with a line icon and a serif title. Loading is a shimmer over zebra rows, a three-dot pulse, or a square spinner. Never a rounded skeleton card. Source: portfolio.vue, screener.vue.

States

No positions yet

Add a ticker or import a broker statement to start the ledger.

Loading 8,214 rows
.empty { border: 1px dashed var(--border-color); padding: 3.5rem 2rem; text-align: center; }
.empty svg { width: 40px; height: 40px; color: var(--text-tertiary); }
.empty .title { font-size: 1.25rem; font-weight: 700; }

.skeleton-row {
  height: 44px; border-bottom: 1px solid var(--border-lighter);
  background: linear-gradient(90deg, transparent 25%, var(--surface-2) 50%, transparent 75%);
  background-size: 200% 100%; animation: shimmer 1.5s infinite;
}
.skeleton-row:nth-child(odd) { background-color: var(--surface-2); }
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

.pulse i { width: 7px; height: 7px; border-radius: 50%; background: var(--border-color); animation: blink 1.4s infinite; }
.spinner { width: 28px; height: 28px; border: 2px solid var(--border-color); border-top-color: var(--accent-color); animation: spin 0.85s linear infinite; }  /* square */
18

Transient

Toast

The shipped toast (ToastNotifications.vue) is the one chrome element still rounded at 12px with hardcoded gradients. The form below is the target when it is next touched: squared, hairline, a 3px state bar, cast shadow, slide from the right.

Target form
Saved to watchlist
NVDA · 120 sh @ 118.40
.toast {
  display: flex; gap: 12px; padding: 12px 14px; max-width: 360px; font-size: 0.85rem;
  background: var(--card-bg); border: 1px solid var(--border-color); border-left: 3px solid var(--accent-color);
  box-shadow: 0 10px 30px rgba(0,0,0,0.12);
}
.toast.is-error   { border-left-color: var(--negative-color); }
.toast.is-warning { border-left-color: var(--warning-color); }
/* position: fixed top-right, z-index 10003; enter translateX(100%) 0.3s ease-out; ≤640px left:10px right:10px */