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.
◆ Components
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.
Controls
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.
.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); }
.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); }
One ink button per view. Pair it with a ghost. Full width under 768px inside a sheet or notice.
Accent-filled buttons, gradients, drop shadows, hover lifts, rounded corners, sentence-case labels.
Selection
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.
.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); }
.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); }
.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); }
Input
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.
.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; }
Marks
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.
.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; }
Feedback
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.
◆ AI research note
The model did not return a usable answer. Your quota was not charged.
◆ AI research note
You have used all 10 Pro requests for today.
◆ AI research note
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; } }
Confirm
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.
.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) */
Composing
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.
◆ Composing Step 1 of 4
0%Pulling dealer positioning and key strikes.
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; }
Quota
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.
.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); }
Layer
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 {
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; } }
Reading
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.
Dealers are short gamma below 5,400. Positioning is crowded on the call side and the put wall at 5,200 is thin.
| Strike | GEX | Role |
|---|---|---|
| 5,500 | +1.2B | Call wall |
| 5,400 | 0 | Gamma flip |
| 5,200 | -0.6B | Put wall |
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; }
Data
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.
| # | Symbol | Company | Price ↓ | Change | Mkt cap | P/E | Sector |
|---|---|---|---|---|---|---|---|
| 1 | NVDA | NVIDIA Corp | 218.54 | +3.42% | 5.31T | 48.2 | Technology |
| 2 | MSFT | Microsoft Corp | 512.10 | -0.88% | 3.80T | 36.1 | Technology |
| 3 | XOM | Exxon Mobil | 118.72 | +1.14% | 512B | 14.0 | Energy |
| 4 | JPM | JPMorgan Chase | 242.36 | -0.21% | 690B | 13.5 | Financials |
| 5 | MP | MP Materials | 62.08 | +7.90% | 10.1B | – | Materials |
.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-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.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.
.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; } }
Stock header
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.
.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; }
Commerce
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.
.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; }
Editorial
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.
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.
Cấu trúc options đang nói gì về tuần tới.
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; }
Absence
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.
Add a ticker or import a broker statement to start the ledger.
.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 */
Transient
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.
.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 */
◆ AI research note
This uses 1 of your 10 daily requests. The current note will be replaced.