Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash
set -euo pipefail

# Only run in Claude Code web sessions.
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
exit 0
fi

cd "$CLAUDE_PROJECT_DIR"

# 1. Node dependencies.
if [ ! -d node_modules ] || [ package-lock.json -nt node_modules ] || [ package.json -nt node_modules ]; then
npm install --no-audit --no-fund
fi

# 2. Playwright browser + OS libs for headless Chromium.
# Prefer --with-deps (installs apt packages Chromium needs).
# If that fails (e.g. broken third-party PPA), fall back to a plain
# browser install so tests can still run when libs are already present.
if ! npx --yes playwright install --with-deps chromium; then
echo "Warning: --with-deps failed, retrying without system deps..." >&2
npx --yes playwright install chromium
fi
14 changes: 14 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
}
]
}
]
}
}
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ dist/
playwright-report/
test-results/
tests/screenshots/
.claude/
.claude/*
!.claude/settings.json
!.claude/hooks/
.claude/settings.local.json
2 changes: 1 addition & 1 deletion src/components/SectionHead.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div style="font-size:10px;letter-spacing:0.3em;text-transform:uppercase;color:var(--accent);margin-bottom:12px">{{ eyebrow }}</div>
<h2 class="serif" style="font-size:clamp(40px,5vw,72px);margin:0;line-height:0.95;letter-spacing:-0.02em;font-weight:400">{{ title }}</h2>
</div>
<div v-if="subtitle" style="max-width:360px;font-size:12px;line-height:1.6;opacity:0.7;text-align:right">
<div v-if="subtitle" style="max-width:360px;font-size:12px;line-height:1.6;opacity:0.88;text-align:right">
{{ subtitle }}
</div>
</div>
Expand Down
10 changes: 5 additions & 5 deletions src/components/TheDenominations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
>
<div v-if="sel.value === d.value" style="position:absolute;top:8px;left:10px;width:6px;height:6px;background:var(--accent)" />
<TheCoin :denom="d" :size="92" />
<div class="serif" style="font-size:22px;margin-top:4px">{{ d.value }} <span style="opacity:0.5;font-size:14px">bral</span></div>
<div class="mono upper" style="font-size:9px;letter-spacing:0.15em;opacity:0.6;text-align:center;white-space:normal;line-height:1.4">{{ d.name }}</div>
<div class="serif" style="font-size:22px;margin-top:4px">{{ d.value }} <span style="opacity:0.78;font-size:14px">bral</span></div>
<div class="mono upper" style="font-size:9px;letter-spacing:0.15em;opacity:0.78;text-align:center;white-space:normal;line-height:1.4">{{ d.name }}</div>
</button>
</div>
</div>
Expand All @@ -36,20 +36,20 @@
<div class="denom-detail">
<div style="flex-shrink:0;text-align:center">
<TheCoin :denom="sel" :size="220" />
<div class="mono upper" style="font-size:9px;letter-spacing:0.2em;margin-top:10px;opacity:0.55;text-align:center">
<div class="mono upper" style="font-size:9px;letter-spacing:0.2em;margin-top:10px;opacity:0.78;text-align:center">
Obverse · actual size × 1.4
</div>
</div>
<div style="flex:1;min-width:0">
<div class="mono upper" style="font-size:10px;letter-spacing:0.2em;color:var(--accent)">Specimen · {{ sel.name }}</div>
<div class="serif" style="font-size:clamp(48px,6vw,72px);line-height:0.95;letter-spacing:-0.02em;margin:10px 0">
{{ sel.value }}<span style="font-size:24px;opacity:0.5;margin-left:8px">bral</span>
{{ sel.value }}<span style="font-size:24px;opacity:0.78;margin-left:8px">bral</span>
</div>

<table class="mono" style="width:100%;font-size:11.5px;border-collapse:collapse;margin-top:18px">
<tbody>
<tr v-for="row in specsFor(sel)" :key="row[0]" style="border-top:1px solid var(--ink)">
<td style="padding:10px 8px 10px 0;text-transform:uppercase;font-size:10px;letter-spacing:0.15em;opacity:0.6;width:140px">{{ row[0] }}</td>
<td style="padding:10px 8px 10px 0;text-transform:uppercase;font-size:10px;letter-spacing:0.15em;opacity:0.78;width:140px">{{ row[0] }}</td>
<td style="padding:10px 0">
<span v-if="row[0] === 'Redemption'">
<strong style="color:var(--accent)">{{ sel.value }}</strong> standard beer{{ sel.value === 1 ? '' : 's' }}
Expand Down
29 changes: 19 additions & 10 deletions src/components/TheExchange.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,55 @@

<!-- Calculator -->
<div class="rule-r exchange-calc" style="padding:36px 40px">
<div style="font-size:10px;letter-spacing:0.3em;text-transform:uppercase;opacity:0.6;margin-bottom:8px">Official Conversion Engine</div>
<div style="font-size:10px;letter-spacing:0.3em;text-transform:uppercase;opacity:0.78;margin-bottom:8px">Official Conversion Engine</div>
<div class="serif" style="font-size:34px;line-height:1.1;letter-spacing:-0.01em;margin-bottom:28px">
Convert between <em>Bral</em> and <em>Beer</em>.
</div>

<div style="display:flex;align-items:stretch;border:1px solid var(--ink);margin-bottom:20px">
<div role="group" aria-label="Conversion direction" style="display:flex;align-items:stretch;border:1px solid var(--ink);margin-bottom:20px">
<button
@click="mode = 'bralToBeer'"
:aria-pressed="mode === 'bralToBeer'"
:style="{ flex:1, padding:'12px', border:'none', background: mode==='bralToBeer' ? 'var(--ink)' : 'transparent', color: mode==='bralToBeer' ? 'var(--paper)' : 'var(--ink)', cursor:'pointer', fontSize:'10px', letterSpacing:'0.2em', textTransform:'uppercase' }"
>Bral ▸ Beer</button>
<button
@click="mode = 'beerToBral'"
:aria-pressed="mode === 'beerToBral'"
:style="{ flex:1, padding:'12px', border:'none', borderLeft:'1px solid var(--ink)', background: mode==='beerToBral' ? 'var(--ink)' : 'transparent', color: mode==='beerToBral' ? 'var(--paper)' : 'var(--ink)', cursor:'pointer', fontSize:'10px', letterSpacing:'0.2em', textTransform:'uppercase' }"
>Beer ▸ Bral</button>
</div>

<div style="border:1px solid var(--ink);padding:18px 20px;background:var(--paper-2);margin-bottom:14px">
<div style="font-size:10px;letter-spacing:0.2em;text-transform:uppercase;opacity:0.55">You deposit</div>
<label for="exchange-amount" style="font-size:10px;letter-spacing:0.2em;text-transform:uppercase;opacity:0.78;display:block">You deposit</label>
<div style="display:flex;align-items:baseline;gap:12px">
<input
id="exchange-amount"
type="number"
:value="amount"
@input="onAmountInput"
min="0"
step="1"
style="border:none;background:transparent;font-size:56px;width:100%;font-family:'Instrument Serif',serif;outline:none;padding:0;color:var(--ink);min-width:0"
:aria-label="`Amount in ${from}`"
style="border:none;background:transparent;font-size:56px;width:100%;font-family:'Instrument Serif',serif;padding:0;color:var(--ink);min-width:0"
/>
<div class="upper" style="font-size:14px;letter-spacing:0.2em">{{ from }}</div>
</div>
</div>

<div style="text-align:center;font-size:20px;opacity:0.5;margin:4px 0">↓</div>
<div aria-hidden="true" style="text-align:center;font-size:20px;opacity:0.78;margin:4px 0">↓</div>

<div style="border:1px solid var(--ink);padding:18px 20px;background:var(--ink);color:var(--paper)">
<div style="font-size:10px;letter-spacing:0.2em;text-transform:uppercase;opacity:0.55">You receive</div>
<div
role="status"
aria-live="polite"
:aria-label="`${fmt(result)} ${to}`"
style="border:1px solid var(--ink);padding:18px 20px;background:var(--ink);color:var(--paper)"
>
<div style="font-size:10px;letter-spacing:0.2em;text-transform:uppercase;opacity:0.78">You receive</div>
<div style="display:flex;align-items:baseline;gap:12px">
<div style="font-size:56px;font-family:'Instrument Serif',serif;width:100%;line-height:1.05">{{ fmt(result) }}</div>
<div class="upper" style="font-size:14px;letter-spacing:0.2em">{{ to }}</div>
</div>
<div class="mono" style="font-size:10px;margin-top:8px;opacity:0.6;letter-spacing:0.08em">
<div class="mono" style="font-size:10px;margin-top:8px;opacity:0.78;letter-spacing:0.08em">
≡ {{ fmt(result * 330) }} mL · {{ (result * 0.33).toFixed(2) }} L
</div>
</div>
Expand All @@ -63,7 +72,7 @@

<!-- Chart -->
<div class="exchange-chart" style="padding:36px 40px;position:relative">
<div style="font-size:10px;letter-spacing:0.3em;text-transform:uppercase;opacity:0.6;margin-bottom:8px">Historical Rate · BRAL / BEER</div>
<div style="font-size:10px;letter-spacing:0.3em;text-transform:uppercase;opacity:0.78;margin-bottom:8px">Historical Rate · BRAL / BEER</div>
<div class="serif" style="font-size:34px;line-height:1.1;margin-bottom:28px">Stability, <em>by design</em>.</div>

<svg viewBox="0 0 400 180" width="100%" height="200" style="border:1px solid var(--ink);background:var(--paper-2)">
Expand All @@ -87,7 +96,7 @@

<div style="display:grid;grid-template-columns:repeat(3,1fr);margin-top:22px;border:1px solid var(--ink)">
<div v-for="(stat, i) in chartStats" :key="stat.label" :style="{ padding:'12px 14px', borderLeft: i===0 ? 'none' : '1px solid var(--ink)' }">
<div style="font-size:9px;letter-spacing:0.15em;text-transform:uppercase;opacity:0.55">{{ stat.label }}</div>
<div style="font-size:9px;letter-spacing:0.15em;text-transform:uppercase;opacity:0.78">{{ stat.label }}</div>
<div class="serif" style="font-size:22px;line-height:1.1;margin-top:2px">{{ stat.value }}</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/TheHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown))
text-decoration: none;
color: inherit;
}
.header-tagline { opacity: 0.55; white-space: nowrap; }
.header-tagline { opacity: 0.78; white-space: nowrap; }
.header-nav {
margin-left: auto;
display: flex;
Expand Down
10 changes: 5 additions & 5 deletions src/components/TheHero.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<div class="rule-r hero-left" style="position:relative">
<div style="display:flex;gap:14px;align-items:baseline;margin-bottom:26px;font-size:11px;letter-spacing:0.2em;text-transform:uppercase">
<span style="color:var(--accent)">◆ Nº 001</span>
<span style="opacity:0.55">— A Communiqué From The Treasury —</span>
<span style="opacity:0.78">— A Communiqué From The Treasury —</span>
</div>

<h1 class="serif" style="font-size:clamp(48px,9vw,156px);line-height:0.88;margin:0;letter-spacing:-0.035em;font-weight:400">
Expand All @@ -30,8 +30,8 @@
<!-- stamp + signature -->
<div class="hero-stamp" style="text-align:right">
<div class="stamp">Ratified · MMXXIV</div>
<div class="serif" style="margin-top:14px;font-style:italic;font-size:22px;opacity:0.7">M. Linnet</div>
<div class="mono upper" style="font-size:9px;opacity:0.55;margin-top:2px">Governor, Banco·Bral</div>
<div class="serif" style="margin-top:14px;font-style:italic;font-size:22px;opacity:0.85">M. Linnet</div>
<div class="mono upper" style="font-size:9px;opacity:0.78;margin-top:2px">Governor, Banco·Bral</div>
</div>

<!-- stats strip -->
Expand All @@ -42,7 +42,7 @@
class="stats-item"
:style="{ borderLeft: i === 0 ? 'none' : '1px solid var(--ink)' }"
>
<div style="font-size:9px;letter-spacing:0.2em;text-transform:uppercase;opacity:0.55">{{ stat.key }}</div>
<div style="font-size:9px;letter-spacing:0.2em;text-transform:uppercase;opacity:0.78">{{ stat.key }}</div>
<div class="serif" style="font-size:26px;line-height:1.1;margin-top:4px">
{{ stat.value }}
</div>
Expand Down Expand Up @@ -72,7 +72,7 @@
<div :style="cornerDotStyle(c)" />
</div>

<div style="position:absolute;bottom:18px;left:0;right:0;text-align:center;font-size:10px;letter-spacing:0.3em;text-transform:uppercase;opacity:0.55">
<div style="position:absolute;bottom:18px;left:0;right:0;text-align:center;font-size:10px;letter-spacing:0.3em;text-transform:uppercase;opacity:0.78">
Fig. 001 — Ten Bral, obverse
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/TheManifesto.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
>
<div style="display:flex;justify-content:space-between;align-items:baseline">
<div class="serif" style="font-size:48px;line-height:1;color:var(--accent)">{{ a.num }}</div>
<div class="mono upper" style="font-size:10px;opacity:0.55">Article</div>
<div class="mono upper" style="font-size:10px;opacity:0.78">Article</div>
</div>
<h3 class="serif" style="font-size:28px;margin:12px 0 10px;font-weight:400">{{ a.title }}</h3>
<p style="font-size:12.5px;line-height:1.7;margin:0;max-width:420px">{{ a.body }}</p>
Expand Down
36 changes: 29 additions & 7 deletions src/components/TheMembers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,17 @@
/>

<!-- Member tabs -->
<div class="member-tabs" style="margin-top:48px;border:1px solid var(--ink)">
<div class="member-tabs" role="tablist" aria-label="Mint roster" style="margin-top:48px;border:1px solid var(--ink)">
<button
v-for="(m, i) in members"
:key="m.id"
:id="`member-tab-${m.id}`"
role="tab"
:aria-selected="selIdx === i"
:aria-controls="`member-panel-${m.id}`"
:tabindex="selIdx === i ? 0 : -1"
@click="selIdx = i"
@keydown="onTabKey"
class="member-tab-btn"
:style="{
background: selIdx === i ? 'var(--ink)' : 'var(--paper)',
Expand Down Expand Up @@ -54,25 +60,31 @@
<div v-if="selIdx === i" style="position:absolute;bottom:6px;right:6px;width:8px;height:8px;background:var(--accent)" />
</div>

<div class="mono upper" style="font-size:9px;letter-spacing:0.2em;opacity:0.6">{{ m.role }}</div>
<div class="mono upper" style="font-size:9px;letter-spacing:0.2em;opacity:0.78">{{ m.role }}</div>
<div class="serif" style="font-size:20px;line-height:1.1;margin-top:4px">{{ m.name.split(' ')[0] }}</div>
</button>
</div>

<!-- Detail panels -->
<div class="member-panels" style="border:1px solid var(--ink);border-top:none">
<div
class="member-panels"
role="tabpanel"
:id="`member-panel-${sel.id}`"
:aria-labelledby="`member-tab-${sel.id}`"
style="border:1px solid var(--ink);border-top:none"
>

<!-- Bio -->
<div class="rule-r member-panel" style="padding:32px">
<div class="mono upper" style="font-size:10px;letter-spacing:0.2em;color:var(--accent)">Licensed Minter · {{ sel.id }}</div>
<div class="serif" style="font-size:44px;line-height:1;margin:10px 0;letter-spacing:-0.02em">{{ sel.name }}</div>
<div class="mono upper" style="font-size:10px;letter-spacing:0.2em;opacity:0.6;margin-top:6px">{{ sel.role }}</div>
<div class="mono upper" style="font-size:10px;letter-spacing:0.2em;opacity:0.78;margin-top:6px">{{ sel.role }}</div>
<div class="serif" style="font-size:22px;font-style:italic;margin-top:22px;opacity:0.85">"{{ sel.motto }}"</div>
</div>

<!-- Stats -->
<div class="rule-r member-panel" style="padding:32px">
<div class="mono upper" style="font-size:10px;letter-spacing:0.2em;opacity:0.6;margin-bottom:18px">Statistics, to Date</div>
<div class="mono upper" style="font-size:10px;letter-spacing:0.2em;opacity:0.78;margin-bottom:18px">Statistics, to Date</div>
<div v-for="[label, val, max] in statsFor(sel)" :key="label" style="margin-bottom:16px">
<div style="display:flex;justify-content:space-between;font-size:10px;letter-spacing:0.15em;text-transform:uppercase;margin-bottom:4px">
<span>{{ label }}</span>
Expand All @@ -86,15 +98,15 @@

<!-- Certificate -->
<div class="member-panel" style="padding:32px">
<div class="mono upper" style="font-size:10px;letter-spacing:0.2em;opacity:0.6;margin-bottom:18px">Certification of Authority</div>
<div class="mono upper" style="font-size:10px;letter-spacing:0.2em;opacity:0.78;margin-bottom:18px">Certification of Authority</div>
<div style="border:1px solid var(--ink);padding:22px 20px;background:var(--paper);position:relative">
<div class="serif" style="font-size:22px;line-height:1.2;font-style:italic;margin-bottom:14px">
Let it be known that <strong style="font-style:normal">{{ sel.name }}</strong> is duly licensed to strike, assay, and countersign Bral coin in the name of the Banco·Bral.
</div>
<div style="display:flex;justify-content:space-between;align-items:flex-end;margin-top:18px">
<div>
<div class="serif" style="font-style:italic;font-size:18px">M. Linnet</div>
<div class="mono upper" style="font-size:8px;letter-spacing:0.2em;opacity:0.55">Governor</div>
<div class="mono upper" style="font-size:8px;letter-spacing:0.2em;opacity:0.78">Governor</div>
</div>
<TheSeal :size="44" />
</div>
Expand Down Expand Up @@ -123,6 +135,16 @@ function statsFor(m) {
['Cans Contributed', m.cans, 500],
]
}

function onTabKey(e) {
const max = members.length - 1
if (e.key === 'ArrowRight') { selIdx.value = selIdx.value === max ? 0 : selIdx.value + 1; e.preventDefault() }
else if (e.key === 'ArrowLeft') { selIdx.value = selIdx.value === 0 ? max : selIdx.value - 1; e.preventDefault() }
else if (e.key === 'Home') { selIdx.value = 0; e.preventDefault() }
else if (e.key === 'End') { selIdx.value = max; e.preventDefault() }
else return
document.getElementById(`member-tab-${members[selIdx.value].id}`)?.focus()
}
</script>

<style scoped>
Expand Down
6 changes: 3 additions & 3 deletions src/components/TheMerch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
</div>

<div style="display:flex;justify-content:space-between;align-items:baseline">
<div class="mono upper" style="font-size:9px;letter-spacing:0.2em;opacity:0.55">{{ item.sku }}</div>
<div class="mono upper" style="font-size:9px;letter-spacing:0.2em;opacity:0.78">{{ item.sku }}</div>
<div class="serif" style="font-size:32px;letter-spacing:-0.01em;color:var(--accent)">
{{ item.price }}<span style="font-size:12px;opacity:0.6;margin-left:4px">bral</span>
{{ item.price }}<span style="font-size:12px;opacity:0.78;margin-left:4px">bral</span>
</div>
</div>
<div class="serif" style="font-size:24px;line-height:1.15;margin-top:6px">{{ item.name }}</div>
<div style="font-size:11.5px;opacity:0.7;margin-top:6px;line-height:1.55">{{ item.desc }}</div>
<div style="font-size:11.5px;opacity:0.88;margin-top:6px;line-height:1.55">{{ item.desc }}</div>
<div style="margin-top:16px;display:flex;gap:10px">
<button class="btn" style="padding:8px 12px;font-size:10px">Add to Cart</button>
<button class="btn btn-ghost" style="padding:8px 12px;font-size:10px">Details</button>
Expand Down
Loading
Loading