sdetProfile/css/app.css
Builder c7fa2e0f98 Fix dangling separator in hero subtitle on narrow Safari viewports
The " · " before test.describe() was orphaned when .mono became
display:block on mobile.  Wrap it in .hero__sub-sep and hide it at
≤900px.  Also tighten topbar padding at ≤480px for iPhone-mini widths.
Add a responsive test asserting the separator is hidden on mobile.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 16:45:28 -04:00

1007 lines
41 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* app.css — Playwright runner UI */
body {
display: grid;
grid-template-rows: 36px 1fr 22px;
height: 100vh;
overflow: hidden;
}
/* ===================== TOP BAR =====================
* Two-column layout: brand + crumb on the left, run controls + theme/help on
* the right. The status pill + workers/headed toggles moved out of here (down
* into the editor bar) so this strip stays focused on global app chrome. */
.topbar {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
background: var(--topbar-bg);
border-bottom: 1px solid var(--line);
padding: 0 10px;
gap: 12px;
font-size: var(--text-xs);
}
.topbar__left, .topbar__right { display: flex; align-items: center; gap: 10px; }
.topbar__right { justify-content: flex-end; }
.brand { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; color: var(--text); }
.brand__logo { width: 22px; height: 22px; color: var(--text-2); }
.brand__name { font-family: var(--font-mono); letter-spacing: .2px; }
.crumb { color: var(--text-3); font-family: var(--font-mono); font-size: var(--text-xs); }
.crumb__sep { color: var(--text-4); padding: 0 6px; }
.crumb strong { color: var(--text); font-weight: 500; }
/* Status pill lives in the editor bar now; height matches the slimmer strip. */
.status-pill {
display: inline-flex; align-items: center; gap: 8px;
height: 20px; padding: 0 10px;
background: var(--bg-3); border: 1px solid var(--line-2);
border-radius: 999px; font-family: var(--font-mono); font-size: 11px;
color: var(--text-2);
}
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-4); display: inline-block; }
.dot--idle { background: var(--text-4); }
.dot--running { background: var(--accent); box-shadow: 0 0 0 0 rgba(78,201,176,.6); animation: pulse 1.2s infinite; }
.dot--pass { background: var(--pass); }
.dot--fail { background: var(--fail); }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(78,201,176,.55);} 70%{box-shadow:0 0 0 8px rgba(78,201,176,0);} 100%{box-shadow:0 0 0 0 rgba(78,201,176,0);} }
.status-counts { display: inline-flex; gap: 8px; padding-left: 8px; border-left: 1px solid var(--line-2); font-variant-numeric: tabular-nums; }
.cnt { display: inline-flex; align-items: baseline; gap: 3px; }
.cnt em { font-style: normal; font-weight: 600; }
.cnt--pass { color: var(--pass); }
.cnt--fail { color: var(--fail); }
.cnt--skip { color: var(--skip); }
.btn {
display: inline-flex; align-items: center; gap: 6px;
height: 24px; padding: 0 10px;
background: var(--bg-3); color: var(--text);
border: 1px solid var(--line-2); border-radius: var(--radius-sm);
cursor: pointer; transition: background .15s var(--ease), border-color .15s;
}
.btn:hover { background: var(--hover); }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn--run {
background: var(--pass); color: #0b1f1a; border-color: transparent; font-weight: 600;
}
.btn--run:hover { background: var(--pass-2); }
:root[data-theme='hc'] .btn--run { color: #000000; }
.btn--ghost { background: transparent; border-color: transparent; color: var(--text-2); }
.btn--ghost:hover { background: var(--hover); color: var(--text); }
.iconbtn { background: transparent; border: 0; color: var(--text-3); cursor: pointer; padding: 4px; border-radius: 3px; }
.iconbtn:hover { background: var(--hover); color: var(--text); }
.toggle { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); color: var(--text-3); font-size: var(--text-xs); user-select: none; cursor: pointer; }
.toggle input { accent-color: var(--accent); }
.toggle.workers { gap: 0; }
.toggle.workers select {
appearance: none;
background: var(--bg-3); color: var(--text);
border: 1px solid var(--line-2); border-radius: 3px;
font-family: var(--font-mono); font-size: var(--text-xs);
padding: 1px 16px 1px 4px; margin-left: 2px;
cursor: pointer;
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
background-position: calc(100% - 9px) 50%, calc(100% - 5px) 50%;
background-size: 4px 4px, 4px 4px;
background-repeat: no-repeat;
}
.toggle.workers select:hover { border-color: var(--text-3); color: var(--text); }
.toggle.workers select:focus { outline: 0; border-color: var(--accent); color: var(--text); }
/* ===================== LAYOUT ===================== */
.layout {
display: grid;
grid-template-columns: 400px 1fr;
min-height: 0;
overflow: hidden;
}
/* ===================== SIDEBAR ===================== */
.sidebar {
display: flex; flex-direction: column; min-height: 0;
background: var(--bg-2);
border-right: 1px solid var(--line);
}
.sidebar__head {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 12px 6px;
}
.sidebar__title {
font-size: 10.5px; letter-spacing: .14em; color: var(--text-3); text-transform: uppercase; font-weight: 600;
}
.filter {
display: flex; align-items: center; gap: 6px;
margin: 4px 10px 8px;
background: var(--bg-3); border: 1px solid var(--line-2);
border-radius: 3px; padding: 4px 8px;
}
.filter:focus-within { border-color: var(--accent); }
.filter__icon { color: var(--text-4); font-size: 13px; }
.filter input {
flex: 1; background: transparent; border: 0; outline: 0;
color: var(--text); font-family: var(--font-mono); font-size: var(--text-xs);
}
.tags {
display: flex; flex-wrap: wrap; gap: 4px;
padding: 0 10px 8px;
}
.tag {
display: inline-flex; align-items: center;
font-family: var(--font-mono); font-size: 10.5px;
background: var(--tag-bg); color: var(--tag-fg);
padding: 2px 6px; border-radius: 3px; cursor: pointer; user-select: none;
border: 1px solid transparent;
transition: background .12s, border-color .12s;
}
.tag:hover { background: var(--hover); }
.tag.is-active { background: var(--accent); color: #0b1f1a; }
:root[data-theme='light'] .tag.is-active { color: #fff; }
:root[data-theme='hc'] .tag.is-active { color: #000000; }
/* Tag bar shows the first 6 tags by default; "+N more" expands the rest
* inline so the visual mass stays small until a visitor needs the full list. */
.tag.is-collapsed { display: none; }
.tags.is-expanded .tag.is-collapsed { display: inline-flex; }
.tags-more {
display: inline-flex; align-items: center; gap: 4px;
font-family: var(--font-mono); font-size: 10.5px;
background: transparent; color: var(--text-3);
padding: 1px 7px; border-radius: 3px; cursor: pointer;
border: 1px dashed var(--line-2);
transition: color .12s, border-color .12s, background .12s;
}
.tags-more:hover { color: var(--text); border-color: var(--text-4); background: var(--hover); }
.tags.is-expanded .tags-more { display: none; }
.tags-clear {
display: inline-flex; align-items: center; gap: 3px;
font-family: var(--font-mono); font-size: 10px;
background: transparent; color: var(--text-4);
padding: 1px 6px; border-radius: 3px; cursor: default;
border: 1px solid transparent;
opacity: .4;
pointer-events: none;
transition: opacity .12s, background .12s, color .12s;
}
.tags-clear svg { width: 10px; height: 10px; }
.tags.has-active .tags-clear {
color: var(--fail); opacity: .85; cursor: pointer; pointer-events: auto;
}
.tags.has-active .tags-clear:hover { opacity: 1; background: var(--hover); }
.tree { flex: 1; overflow: auto; padding: 4px 0 16px; font-size: var(--text-sm); }
.suite { padding: 6px 10px 2px; }
.suite + .suite { margin-top: 4px; padding-top: 8px; border-top: 1px dashed var(--line); }
.suite__head {
display: flex; align-items: center; gap: 6px;
color: var(--text-3); font-family: var(--font-mono); font-size: var(--text-xs);
cursor: pointer;
padding: 2px 4px; margin: 0 -4px;
border-radius: 3px;
transition: background .12s var(--ease);
}
.suite__head:hover { background: var(--hover); }
.suite.is-active > .suite__head { background: rgba(78,201,176,.06); }
:root[data-theme='light'] .suite.is-active > .suite__head { background: rgba(12,138,111,.07); }
/* Caret defaults to "expanded" (▾ pointing down). Collapsed rows snap back
* to the natural ▸ — matches VS Code's Test Explorer and is what visitors
* intuit on first glance. */
.suite__caret {
width: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 14px;
color: var(--text-3);
transition: transform .15s var(--ease);
transform: rotate(90deg);
}
.suite__caret svg { width: 12px; height: 12px; display: block; }
.suite.collapsed .suite__caret { transform: rotate(0deg); }
.suite.collapsed .test { display: none; }
/* Suite name truncates with ellipsis instead of wrapping — a wrapped row in
* a tree view always reads as a layout bug, even when intentional. */
.suite__name {
color: var(--text-2);
flex: 1 1 auto;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.suite__name em { color: var(--accent); font-style: normal; }
/* Counts are "metrics" — right-aligned, fixed min-width, tabular numerals,
* outlined box rather than the rounded pill we use for @tags. */
.suite__count {
margin-left: auto;
display: inline-flex;
align-items: center;
justify-content: flex-end;
min-width: 22px;
height: 16px;
padding: 0 6px;
font-family: var(--font-mono);
font-size: 10px;
font-variant-numeric: tabular-nums;
color: var(--text-3);
background: rgba(255,255,255,.04);
border: 1px solid var(--line-2);
border-radius: 3px;
line-height: 1;
}
:root[data-theme='light'] .suite__count { background: rgba(0,0,0,.03); }
.suite.is-active .suite__count {
color: var(--text);
background: rgba(78,201,176,.12);
border-color: rgba(78,201,176,.35);
}
:root[data-theme='light'] .suite.is-active .suite__count {
background: rgba(12,138,111,.10);
border-color: rgba(12,138,111,.30);
}
.test {
display: grid;
grid-template-columns: 22px 14px 1fr auto;
gap: 6px; align-items: center;
padding: 4px 10px 4px 22px;
cursor: pointer; border-left: 2px solid transparent;
transition: background .1s;
position: relative;
}
.test:hover { background: var(--hover); }
.test.is-selected { background: var(--active); border-left-color: var(--accent); }
.test.is-running { background: var(--hover); }
.test.is-skipped { opacity: .55; }
.test.is-skipped .test__run { visibility: hidden; }
.test__run {
width: 18px; height: 18px; border-radius: 3px;
display: inline-flex; align-items: center; justify-content: center;
color: var(--green-arrow); background: transparent;
border: 0; cursor: pointer; opacity: .85;
}
.test__run:hover { background: var(--hover); opacity: 1; }
.test__run svg { width: 10px; height: 10px; }
.test.is-running .test__run, .test.is-passed .test__run { opacity: 1; }
.test__icon { width: 14px; height: 14px; display: inline-flex; align-items: center; justify-content: center; }
.test__icon svg { width: 12px; height: 12px; }
.icon--idle { color: var(--text-4); }
.icon--run { color: var(--accent); }
.icon--pass { color: var(--pass); }
.icon--fail { color: var(--fail); }
.icon--skip { color: var(--skip); }
.spin { animation: spin 1s linear infinite; transform-origin: center; }
@keyframes spin { from{transform:rotate(0)} to{transform:rotate(360deg)} }
.test__title { color: var(--text); font-family: var(--font-mono); font-size: var(--text-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.test__title span.kw { color: var(--info); }
.test__title span.str { color: #ce9178; }
:root[data-theme='light'] .test__title span.str { color: #a31515; }
:root[data-theme='hc'] .test__title span.str { color: #ffd700; }
.test__dur { color: var(--text-4); font-family: var(--font-mono); font-size: 10.5px; }
.test__tags { grid-column: 3 / span 2; padding-top: 2px; display: flex; flex-wrap: wrap; gap: 3px; }
.test__tags .tag { font-size: 10px; padding: 1px 5px; }
.sidebar__foot {
padding: 8px 12px; border-top: 1px solid var(--line);
color: var(--text-4); font-family: var(--font-mono); font-size: 10.5px;
}
/* ===================== MAIN PANE ===================== */
.main { display: grid; grid-template-rows: 28px 34px 1fr; min-height: 0; background: var(--bg); }
/* ----- EDITOR BAR (spec tabs + status pill + overflow menu) -----
* The bar itself fits in 28px (slimmer than VS Code's ~35px tab strip to
* compensate for our smaller font size). It hosts:
* 1. .editor-strip — scrollable list of open .spec.ts files
* 2. .editor-bar__right — sticky status pill + a "⋯" overflow that
* exposes --workers= and --headed (used to live in the top bar). */
.editor-bar {
display: flex;
align-items: stretch;
background: var(--bg-2);
border-bottom: 1px solid var(--line);
min-height: 0;
}
.editor-strip {
display: flex;
align-items: stretch;
flex: 1 1 auto;
min-width: 0;
overflow-x: auto;
scrollbar-width: thin;
}
.editor-strip::-webkit-scrollbar { height: 0; }
.editor-bar__right {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
padding: 0 8px 0 10px;
border-left: 1px solid var(--line);
background: var(--bg-2);
}
.espec {
position: relative;
display: inline-flex;
align-items: center;
gap: 7px;
padding: 0 12px;
background: transparent;
border: 0;
border-right: 1px solid var(--line);
color: var(--text-3);
font-family: var(--font-mono);
font-size: 11.5px;
cursor: pointer;
white-space: nowrap;
transition: background .12s var(--ease), color .12s;
}
.espec:hover { background: var(--hover); color: var(--text); }
.espec.is-active { background: var(--bg); color: var(--text); }
.espec.is-active::before {
content: '';
position: absolute;
left: 0; right: 0; top: 0;
height: 2px;
background: var(--accent);
}
/* TS file-type badge — 12px tighter than before so the slimmer tabs don't
* end up dominated by the icon. */
.espec__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px; height: 14px;
background: #3178c6;
color: #fff;
border-radius: 2px;
font-size: 8.5px;
font-weight: 700;
letter-spacing: .04em;
}
:root[data-theme='light'] .espec__icon { background: #2168bd; }
:root[data-theme='hc'] .espec__icon { background: #ffff00; color: #000000; }
.espec__name { /* filename — inherits .espec font */ }
.espec__count {
/* "metric" treatment: tabular numerals, fixed min-width, right-aligned,
* subtle outline so it reads as a number rather than another tag. */
display: inline-flex;
align-items: center;
justify-content: flex-end;
min-width: 18px;
height: 16px;
padding: 0 6px;
font-family: var(--font-mono);
font-size: 10px;
font-variant-numeric: tabular-nums;
color: var(--text-3);
background: rgba(255,255,255,.04);
border: 1px solid var(--line-2);
border-radius: 3px;
line-height: 1;
}
:root[data-theme='light'] .espec__count { background: rgba(0,0,0,.03); }
.espec.is-active .espec__count {
color: var(--text);
background: rgba(78,201,176,.12);
border-color: rgba(78,201,176,.35);
}
:root[data-theme='light'] .espec.is-active .espec__count {
background: rgba(12,138,111,.10);
border-color: rgba(12,138,111,.30);
}
/* ----- OVERFLOW MENU (⋯) -----
* Dropdown anchored to the bar; clicks outside close it (handled in app.js).
* Holds --workers= and --headed so the editor bar stays uncluttered. */
.overflow { position: relative; }
.overflow__btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px; height: 22px;
background: transparent;
color: var(--text-3);
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
transition: background .12s var(--ease), color .12s, border-color .12s;
}
.overflow__btn:hover { background: var(--hover); color: var(--text); }
.overflow.is-open .overflow__btn {
background: var(--bg);
color: var(--text);
border-color: var(--line-2);
}
.overflow__menu {
position: absolute;
top: calc(100% + 4px);
right: 0;
z-index: 30;
display: grid;
gap: 10px;
padding: 10px 12px;
background: var(--panel);
border: 1px solid var(--line-2);
border-radius: var(--radius);
box-shadow: var(--shadow);
min-width: 180px;
animation: ksAppear .12s var(--ease);
}
.overflow__menu[hidden] { display: none; }
.overflow__menu .toggle { font-size: var(--text-xs); }
.tabs {
display: flex; align-items: stretch;
background: var(--bg-2);
border-bottom: 1px solid var(--line);
padding-left: 6px;
}
.tab {
background: transparent; border: 0; cursor: pointer;
padding: 0 14px; color: var(--text-3); font-size: var(--text-xs); font-family: var(--font-mono);
border-top: 2px solid transparent;
position: relative;
}
.tab:hover { color: var(--text); }
.tab.is-active { color: var(--text); background: var(--bg); border-top-color: var(--accent); }
.tabs__spacer { flex: 1; }
.pane { display: none; height: 100%; overflow: auto; padding: 0; }
.pane.is-active { display: block; }
/* HERO */
.hero { padding: 36px 36px 18px; border-bottom: 1px solid var(--line); transition: padding .15s var(--ease); }
.hero__tag { display: inline-block; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .12em; color: var(--accent); background: rgba(78,201,176,.1); padding: 2px 8px; border-radius: 3px; text-transform: uppercase; margin-bottom: 12px; }
.hero__title { margin: 0; font-size: clamp(34px, 4vw, 52px); font-weight: 700; letter-spacing: -.02em; line-height: 1.05; }
.hero__sub { color: var(--text-3); font-family: var(--font-mono); font-size: var(--text-sm); margin: 8px 0 0; }
.hero__hint { color: var(--text-3); margin: 18px 0 0; font-size: var(--text-sm); }
/* Slim variant for non-portfolio specs — keeps the runner aesthetic but
stops a 52px hero from drowning a 1-test spec like playground.spec.ts. */
.hero--slim { padding: 22px 36px 14px; }
.hero--slim .hero__tag { margin-bottom: 8px; }
.hero--slim .hero__title { font-size: clamp(22px, 2.4vw, 28px); }
.hero--slim .hero__sub { font-size: var(--text-xs); margin-top: 4px; }
.hero--slim .hero__hint { margin-top: 10px; font-size: var(--text-xs); }
.kbd { font-family: var(--font-mono); background: var(--kbd-bg); border: 1px solid var(--line-2); border-bottom-width: 2px; border-radius: 3px; padding: 1px 6px; font-size: 11px; color: var(--text); }
/* Tiny run-history banner above the results list. Pulls the eye to dynamic
* data — runtime + counts — while taking only one line of vertical space. */
.summary-stripe {
display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
padding: 8px 36px;
font-family: var(--font-mono); font-size: 11px;
color: var(--text-3);
background: linear-gradient(to bottom, rgba(78,201,176,.03), transparent);
border-bottom: 1px solid var(--line);
}
.ss__lbl { color: var(--text-4); letter-spacing: .06em; text-transform: uppercase; font-size: 10px; }
.ss__sep { color: var(--text-4); }
.ss__val { color: var(--text-2); font-variant-numeric: tabular-nums; }
.ss__val--pass { color: var(--pass); }
.ss__val--pending { color: var(--skip); }
.ss__val--skip { color: var(--skip); opacity: .7; }
.ss__val--idle { color: var(--text-4); }
.ss__dot {
width: 6px; height: 6px; border-radius: 50%; display: inline-block;
background: var(--text-4); margin-right: 4px;
}
.ss__dot--pass { background: var(--pass); }
.ss__dot--fail { background: var(--fail); }
.ss__dot--pending { background: var(--skip); }
.ss__dot--running { background: var(--accent); animation: pulse 1.2s infinite; }
/* RESULTS list */
.results { padding: 12px 0 80px; }
.result {
border-bottom: 1px solid var(--line);
padding: 0 36px;
}
.result__head {
display: grid;
grid-template-columns: 18px 18px 1fr auto auto;
gap: 10px; align-items: center;
padding: 12px 0;
cursor: pointer;
}
.result__caret { color: var(--text-4); transition: transform .2s; }
.result.is-open .result__caret { transform: rotate(90deg); }
.result__status { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; }
.result__status svg { width: 14px; height: 14px; }
.result__title { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text); }
.result__title .kw { color: var(--info); }
.result__title .str { color: #ce9178; }
:root[data-theme='light'] .result__title .str { color: #a31515; }
:root[data-theme='hc'] .result__title .str { color: #ffd700; }
.result__dur { color: var(--text-4); font-family: var(--font-mono); font-size: 11px; }
.result__rerun { color: var(--text-3); background: transparent; border: 0; cursor: pointer; padding: 4px; border-radius: 3px; }
.result__rerun:hover { background: var(--hover); color: var(--text); }
.result__body { display: none; padding: 4px 0 24px 46px; animation: slideDown .25s var(--ease); }
.result.is-open .result__body { display: block; }
.result.is-skipped { opacity: .65; }
.result.is-skipped .result__rerun { visibility: hidden; }
@keyframes slideDown { from { opacity: 0; transform: translateY(-4px);} to { opacity: 1; transform: none; } }
/* "Pending" preview — the first idle test is shown expanded with its body
* pre-rendered so visitors see what they'd get without committing to a run.
* Faded look + a centered "click ▶ to run" overlay reads as "preview, not
* result". Click anywhere on the overlay to kick off the run. */
.result.is-pending {
position: relative;
}
.result.is-pending .result__body {
display: block;
position: relative;
opacity: .42;
filter: saturate(.7);
pointer-events: none;
animation: none;
}
.result__pending-overlay {
position: absolute;
left: 50%;
top: 48%;
transform: translate(-50%, -50%);
display: inline-flex; align-items: center; gap: 8px;
padding: 7px 14px;
font-family: var(--font-mono); font-size: 11.5px;
color: var(--text-2);
background: var(--panel);
border: 1px solid var(--line-2);
border-radius: 999px;
box-shadow: var(--shadow);
cursor: pointer;
pointer-events: auto;
z-index: 2;
white-space: nowrap;
transition: background .12s var(--ease), transform .15s var(--ease);
}
.result__pending-overlay:hover {
background: var(--bg-3);
transform: translate(-50%, -50%) translateY(-1px);
}
.result__pending-overlay .kbd { font-size: 10.5px; padding: 0 5px; }
.result.is-pending .result__caret { opacity: .6; }
.progress {
height: 2px; width: 100%; background: transparent; overflow: hidden; position: relative;
margin: -2px 0 0;
}
.progress__bar {
position: absolute; left: 0; top: 0; height: 100%; width: 0; background: var(--accent);
transition: width .12s linear;
}
/* "Test step" rows inside a result */
.step {
display: grid;
grid-template-columns: 18px 1fr auto;
gap: 10px; align-items: center;
padding: 5px 0; font-family: var(--font-mono); font-size: var(--text-xs);
color: var(--text-2);
border-left: 1px dashed var(--line-2);
padding-left: 14px;
}
.step__icon { color: var(--pass); }
.step__icon.is-fail { color: var(--fail); }
.step__icon.is-pass { color: var(--pass); }
.step__icon.is-skip { color: var(--skip); }
.step__icon.is-info { color: var(--info); }
.step__dur { color: var(--text-4); }
.step__title em { color: var(--accent); font-style: normal; }
/* Generic content blocks for test bodies */
.block { padding: 6px 0; }
.block h4 { margin: 14px 0 6px; font-size: var(--text-sm); color: var(--text); font-family: var(--font-mono); }
.block h4::before { content: '// '; color: var(--text-4); }
.block p { margin: 4px 0; color: var(--text-2); max-width: 78ch; }
.block ul { margin: 4px 0; padding-left: 18px; color: var(--text-2); }
.block ul li { padding: 2px 0; }
.block strong { color: var(--text); }
.block a { color: var(--info); border-bottom: 1px dashed var(--info); }
.block a:hover { border-bottom-style: solid; }
.snippet {
font-family: var(--font-mono); font-size: var(--text-xs);
background: var(--bg-2); border: 1px solid var(--line);
border-radius: var(--radius); padding: 14px 16px;
white-space: pre; overflow: auto;
display: grid; grid-template-columns: auto 1fr; gap: 0 14px;
margin: 10px 0;
}
.snippet .ln { color: var(--text-4); user-select: none; text-align: right; }
.snippet .code .kw { color: var(--info); }
.snippet .code .fn { color: #dcdcaa; }
.snippet .code .str { color: #ce9178; }
.snippet .code .num { color: #b5cea8; }
.snippet .code .cm { color: #6a9955; font-style: italic; }
:root[data-theme='hc'] .snippet .code .fn { color: #ffff00; }
:root[data-theme='hc'] .snippet .code .str { color: #ffd700; }
:root[data-theme='hc'] .snippet .code .num { color: #00ff00; }
:root[data-theme='hc'] .snippet .code .cm { color: #7fd5ff; }
.snippet .code .tag { color: var(--accent); }
:root[data-theme='light'] .snippet .code .str { color: #a31515; }
:root[data-theme='light'] .snippet .code .fn { color: #795e26; }
:root[data-theme='light'] .snippet .code .cm { color: #008000; }
:root[data-theme='light'] .snippet .code .num { color: #098658; }
/* Cards (projects) */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px,1fr)); gap: 12px; margin: 8px 0; }
.card { border: 1px solid var(--line); border-radius: var(--radius); padding: 14px; background: var(--panel-2); }
.card h5 { margin: 0 0 4px; font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text); }
.card p { margin: 4px 0 0; color: var(--text-2); font-size: var(--text-xs); }
.card__tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
/* Skills bars */
.skills { display: grid; gap: 10px; margin: 8px 0; }
.skill {
display: grid; grid-template-columns: 1fr 80px; gap: 12px; align-items: center;
padding: 8px 0; border-top: 1px dashed var(--line);
}
.skill:first-child { border-top: 0; }
.skill__name { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-2); }
.skill__name strong { color: var(--text); }
.skill__bar { position: relative; height: 6px; background: var(--bg-2); border-radius: 3px; overflow: hidden; }
.skill__fill { position: absolute; left: 0; top: 0; height: 100%; width: 0; background: linear-gradient(90deg, var(--accent), var(--info)); transition: width .8s var(--ease); }
.skill__pct { font-family: var(--font-mono); font-size: 11px; color: var(--text-3); text-align: right; }
/* Contact grid */
.contact-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); gap: 12px; margin: 8px 0; }
.contact-cell {
border: 1px solid var(--line); border-radius: var(--radius);
padding: 12px 14px; background: var(--panel-2);
font-family: var(--font-mono); font-size: var(--text-xs);
}
.contact-cell label { color: var(--text-4); display: block; font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; margin-bottom: 4px; }
.contact-cell a { color: var(--info); }
.contact-cell a:hover { text-decoration: underline; }
/* CTA row */
.cta-row { display: flex; gap: 10px; flex-wrap: wrap; margin: 16px 0 6px; }
.cta {
display: inline-flex; align-items: center; gap: 8px;
padding: 9px 16px; border-radius: var(--radius);
background: var(--accent); color: #062018; font-weight: 700;
border: 0; cursor: pointer; font-family: var(--font-mono); font-size: var(--text-xs);
text-decoration: none; letter-spacing: .02em;
}
.cta:hover { filter: brightness(1.05); }
.cta--ghost { background: transparent; color: var(--text); border: 1px solid var(--line-2); }
:root[data-theme='light'] .cta { color: #ffffff; }
:root[data-theme='light'] .cta--ghost { color: var(--text); }
:root[data-theme='hc'] .cta { color: #000000; }
:root[data-theme='hc'] .cta--ghost { color: var(--text); }
.projects-foot { margin-top: 18px; font-size: 12px; color: var(--text-3); line-height: 1.55; max-width: 52rem; }
.projects-foot .tab-link {
background: none; border: 0; padding: 0; margin: 0;
color: var(--info); font-family: inherit; font-size: inherit;
cursor: pointer; text-decoration: underline;
}
.projects-foot .tab-link:hover { color: var(--accent); }
/* ===================== TRACE PANE ===================== */
.trace-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 24px; border-bottom: 1px solid var(--line); }
.trace-head__title { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text); }
.trace-head__meta { display: flex; gap: 14px; font-family: var(--font-mono); font-size: 11px; color: var(--text-3); }
.legend { display: inline-flex; align-items: center; gap: 6px; }
.sw { width: 10px; height: 10px; border-radius: 2px; background: var(--text-4); display: inline-block; }
.sw--pass { background: var(--pass); }
.sw--info { background: var(--info); }
.trace { padding: 18px 24px; display: grid; gap: 6px; }
.trace-row { display: grid; grid-template-columns: 220px 1fr 90px; gap: 12px; align-items: center; font-family: var(--font-mono); font-size: 11.5px; }
.trace-row__label { color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.trace-row__label small { color: var(--text-4); }
.trace-row__track { position: relative; height: 16px; background: var(--bg-2); border-radius: 3px; }
.trace-row__bar { position: absolute; top: 2px; bottom: 2px; background: var(--pass); border-radius: 2px; opacity: .85; }
.trace-row__bar:hover { opacity: 1; }
.trace-row__dur { color: var(--text-4); text-align: right; }
.trace-rule { height: 1px; background: var(--line); margin: 8px 0 0; }
.trace-axis { display: grid; grid-template-columns: 220px 1fr 90px; gap: 12px; padding: 6px 24px 16px; font-family: var(--font-mono); font-size: 10.5px; color: var(--text-4); }
.trace-axis__ticks { display: flex; justify-content: space-between; }
/* ===================== NETWORK PANE ===================== */
/* Block layout (not nested flex-shrink) — long repo list stays visible everywhere. */
.network-pane { padding: 0; min-height: 40vh; }
.network-head {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 24px; border-bottom: 1px solid var(--line);
}
.network-head__title { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text); }
.network-head__meta { display: flex; gap: 14px; font-family: var(--font-mono); font-size: 11px; color: var(--text-3); }
.network-scroll { padding: 0 0 32px; max-height: none; overflow: visible; -webkit-overflow-scrolling: touch; }
.network-empty, .network-err {
padding: 20px 24px; font-size: var(--text-xs); color: var(--text-3); line-height: 1.5;
font-family: var(--font-mono);
}
.network-err { color: var(--skip); }
.network-entry { border-bottom: 1px solid var(--line); }
.network-row {
display: grid;
grid-template-columns: 44px minmax(0, 1fr) 36px 38px 56px 48px 18px;
gap: 8px;
align-items: center;
width: 100%;
padding: 8px 24px;
border: 0;
background: transparent;
color: var(--text-2);
font-family: var(--font-mono);
font-size: 11px;
text-align: left;
cursor: pointer;
}
.network-row:hover { background: var(--hover); }
.network-row__method { color: var(--info); font-weight: 600; }
.network-row__url { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.network-row__status { color: var(--pass); }
.network-row__caret { display: flex; align-items: center; justify-content: center; color: var(--text-4); }
.network-row__caret svg { transition: transform .15s var(--ease); }
.network-entry.is-open .network-row__caret svg { transform: rotate(90deg); }
.network-detail { padding: 0 24px 14px 44px; background: var(--bg-2); border-top: 1px solid var(--line); }
.network-detail__hdr { font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--text-4); margin: 10px 0 6px; }
.network-detail__body {
margin: 0; padding: 12px 14px;
background: var(--panel); border: 1px solid var(--line-2); border-radius: var(--radius-sm);
font-size: 11px; line-height: 1.45; overflow-x: auto; white-space: pre-wrap; word-break: break-word;
}
.network-detail__link { display: inline-block; margin-top: 10px; font-size: var(--text-xs); color: var(--info); }
.network-detail__link:hover { text-decoration: underline; }
/* ===================== SOURCE PANE ===================== */
.source { font-family: var(--font-mono); font-size: var(--text-xs); padding: 14px 0; line-height: 1.65; }
.source__line { display: grid; grid-template-columns: 50px 1fr; gap: 14px; padding: 0 24px 0 0; }
.source__line:hover { background: var(--hover); }
.source__ln { color: var(--text-4); text-align: right; user-select: none; padding-left: 12px; border-right: 1px solid var(--line); }
.source__code { color: var(--text); white-space: pre; }
.source .kw { color: var(--info); }
.source .fn { color: #dcdcaa; }
.source .str { color: #ce9178; }
.source .num { color: #b5cea8; }
.source .cm { color: #6a9955; font-style: italic; }
:root[data-theme='hc'] .source .fn { color: #ffff00; }
:root[data-theme='hc'] .source .str { color: #ffd700; }
:root[data-theme='hc'] .source .num { color: #00ff00; }
:root[data-theme='hc'] .source .cm { color: #7fd5ff; }
.source .tag { color: var(--accent); }
:root[data-theme='light'] .source .str { color: #a31515; }
:root[data-theme='light'] .source .fn { color: #795e26; }
:root[data-theme='light'] .source .cm { color: #008000; }
:root[data-theme='light'] .source .num { color: #098658; }
/* ===================== CONSOLE PANE ===================== */
.console { font-family: var(--font-mono); font-size: var(--text-xs); padding: 8px 0; }
.console__line {
display: grid; grid-template-columns: 80px 60px 1fr; gap: 12px;
padding: 3px 24px;
border-bottom: 1px dotted transparent;
}
.console__line:hover { background: var(--hover); }
.console__ts { color: var(--text-4); }
.console__lvl { font-weight: 600; }
.lvl--info { color: var(--info); }
.lvl--ok { color: var(--pass); }
.lvl--warn { color: var(--skip); }
.lvl--err { color: var(--fail); }
.console__msg { color: var(--text-2); white-space: pre-wrap; }
/* ===================== STATUSBAR ===================== */
.statusbar {
display: flex; align-items: center; gap: 14px;
background: var(--statusbar-bg); color: var(--statusbar-fg);
padding: 0 12px; font-family: var(--font-mono); font-size: 11px;
}
.sb__seg { display: inline-flex; align-items: center; gap: 6px; opacity: .95; }
.sb__seg--accent { background: rgba(255,255,255,.15); padding: 0 8px; height: 18px; border-radius: 2px; }
.sb__spacer { flex: 1; }
/* ===================== MOBILE SIDEBAR DRAWER ===================== */
.menu-btn { display: none; }
.sidebar-scrim { display: none; } /* hidden on desktop, escapes grid flow */
/* ===================== RESPONSIVE ===================== */
@media (max-width: 900px) {
body { grid-template-rows: 44px 1fr 22px; overflow: auto; }
.topbar { grid-template-columns: auto 1fr auto; padding: 0 8px; gap: 6px; }
.topbar__left .crumb { display: none; }
.topbar__right { gap: 4px; }
.btn--run span { display: none; }
.btn { padding: 0 8px; min-height: 36px; }
.menu-btn { display: inline-flex; }
.layout { grid-template-columns: 1fr; min-height: 0; }
.sidebar {
position: fixed; top: 44px; bottom: 22px; left: 0;
width: 86%; max-width: 400px; z-index: 50;
transform: translateX(-100%); transition: transform .25s var(--ease);
box-shadow: 4px 0 12px rgba(0,0,0,.4);
}
.sidebar.is-open { transform: translateX(0); }
.sidebar-scrim {
position: fixed; inset: 44px 0 22px 0; background: rgba(0,0,0,.45);
z-index: 49; display: none;
}
.sidebar-scrim.is-open { display: block; }
.tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.tab { min-height: 36px; padding: 0 12px; }
.editor-strip { font-size: 10.5px; }
.espec { padding: 0 10px; gap: 6px; min-height: 36px; }
.espec__count { display: none; }
.espec__icon { width: 14px; height: 12px; font-size: 8px; }
.editor-bar__right { padding: 0 6px; gap: 6px; }
.status-pill { padding: 0 8px; gap: 6px; }
.status-counts { padding-left: 6px; gap: 6px; }
.summary-stripe { padding: 6px 16px; font-size: 10.5px; flex-wrap: wrap; }
.hero { padding: 22px 18px 14px; }
.hero__title { font-size: 32px; }
.hero__sub { font-size: 12px; word-break: break-word; white-space: normal; }
.hero__sub .mono { display: block; margin-top: 4px; }
/* When .mono drops to its own line, the inline " · " in front of it would
dangle as an orphan after "Canadian citizen". Hide it on mobile. */
.hero__sub .hero__sub-sep { display: none; }
.hero__hint { font-size: 12.5px; white-space: normal; word-break: break-word; }
.hero--slim { padding: 14px 18px 10px; }
.hero--slim .hero__title { font-size: 20px; }
.result { padding: 0 16px; }
.result__head { grid-template-columns: 16px 16px 1fr auto; gap: 8px; min-height: 44px; }
.result__rerun { display: none; }
.result__title { font-size: 12px; word-break: break-word; white-space: normal; }
.result__body { padding: 4px 0 18px 22px; }
.step { grid-template-columns: 16px 1fr auto; font-size: 11px; }
.step__title { word-break: break-word; white-space: normal; }
.test { min-height: 40px; padding: 6px 10px 6px 22px; }
.test__run { width: 28px; height: 28px; }
.snippet { white-space: pre-wrap; word-break: break-all; overflow-x: auto; }
.source__code { white-space: pre-wrap; word-break: break-all; }
.cards { grid-template-columns: 1fr; }
.contact-grid { grid-template-columns: 1fr; }
.skill { grid-template-columns: 1fr; gap: 6px; }
.skill__bar { width: 100%; }
.skill__pct { text-align: left; }
.block p { max-width: none; word-wrap: break-word; overflow-wrap: break-word; }
.block ul { padding-left: 14px; }
.block ul li { word-wrap: break-word; overflow-wrap: break-word; }
.cta-row { flex-direction: column; }
.cta { justify-content: center; width: 100%; }
.network-row {
grid-template-columns: 38px minmax(0, 1fr) 32px 44px 16px;
gap: 6px;
padding: 8px 16px;
font-size: 10px;
}
.network-row__mime,
.network-row__size { display: none; }
.network-detail { padding-left: 16px; padding-right: 16px; }
.network-head { padding: 14px 16px; }
.trace { padding: 14px 16px; }
.trace-head { padding: 14px 16px; flex-wrap: wrap; gap: 8px; }
.trace-row, .trace-axis { grid-template-columns: 100px 1fr 56px; gap: 8px; font-size: 10.5px; }
.trace-row__label small { display: none; }
.trace-axis { padding: 6px 16px 16px; }
.source__line { grid-template-columns: 36px 1fr; }
.source { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.console__line { grid-template-columns: 56px 42px 1fr; gap: 8px; padding: 3px 16px; font-size: 11px; }
.statusbar { font-size: 10.5px; gap: 8px; }
.statusbar .sb__seg:nth-child(n+6) { display: none; }
}
/* Phone */
@media (max-width: 480px) {
.hero { padding: 18px 14px 12px; }
.hero__title { font-size: 26px; }
.hero__sub { font-size: 11px; }
.hero__hint { font-size: 11px; }
.hero--slim .hero__title { font-size: 18px; }
/* Tighten the topbar so 5 ghost buttons + brand fit on iPhone-mini widths
without horizontal overflow. We keep all controls reachable; just trim
their padding and shrink the brand wordmark. */
.topbar { gap: 4px; padding: 0 6px; }
.topbar__right { gap: 2px; }
.btn { padding: 0 6px; }
.btn--run { padding: 0 9px; }
.brand__name { font-size: 11.5px; }
.trace-row, .trace-axis { grid-template-columns: 80px 1fr 44px; font-size: 10px; }
.trace-head__meta { display: none; }
.console__line { grid-template-columns: 42px 1fr; }
.console__ts { display: none; }
.network-row { grid-template-columns: 34px minmax(0,1fr) 28px 16px; font-size: 10px; }
.network-row__time { display: none; }
.result__body { padding: 4px 0 14px 12px; }
.step { padding-left: 8px; }
.kshelp__row { grid-template-columns: 90px 1fr; gap: 8px; padding: 6px 12px; }
}
/* ===================== KEYBOARD SHORTCUTS OVERLAY ===================== */
.kshelp[hidden] { display: none; }
.kshelp {
position: fixed; inset: 0; z-index: 100;
display: grid; place-items: center;
}
.kshelp__scrim {
position: absolute; inset: 0;
background: rgba(0, 0, 0, .55);
backdrop-filter: blur(2px);
animation: ksFade .12s var(--ease);
}
.kshelp__panel {
position: relative;
width: min(520px, 92vw);
background: var(--panel);
border: 1px solid var(--line-2);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
outline: 0;
animation: ksAppear .15s var(--ease);
}
@keyframes ksFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes ksAppear { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.kshelp__head {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--line);
}
.kshelp__title {
font-family: var(--font-mono);
font-size: var(--text-xs);
letter-spacing: .12em; text-transform: uppercase;
color: var(--text-2);
}
.kshelp__title::before { content: '// '; color: var(--text-4); }
.kshelp__close {
background: transparent; border: 0; color: var(--text-3);
font-size: 20px; line-height: 1; cursor: pointer;
padding: 2px 8px; border-radius: 3px;
}
.kshelp__close:hover { background: var(--hover); color: var(--text); }
.kshelp__grid { padding: 4px 0; }
.kshelp__row {
display: grid;
grid-template-columns: 130px 1fr;
gap: 14px; align-items: center;
padding: 9px 18px;
font-family: var(--font-mono);
font-size: var(--text-xs);
}
.kshelp__row + .kshelp__row { border-top: 1px dashed var(--line); }
.kshelp__keys { display: inline-flex; gap: 4px; align-items: center; color: var(--text-3); }
.kshelp__desc { color: var(--text-2); }
.kshelp__foot {
padding: 10px 16px;
border-top: 1px solid var(--line);
color: var(--text-3); font-size: 11px;
}
@media (max-width: 900px) {
.kshelp__panel { width: 92vw; }
.kshelp__row { grid-template-columns: 110px 1fr; padding: 8px 14px; }
}