← back to Gallery Agentabrams
builds/routes-residencies-rituals/index.html
924 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Routes, Residencies & Rituals — Geographies of a Traveling Music</title>
<style>
:root{
--navy:#0b1a2b; --navy2:#102437; --navy3:#16324a;
--paper:#f4ece0; --paper2:#ece0cd; --paper3:#e3d4bd;
--ink:#1b1611; --ink2:#463a2c;
--coral:#e8735e; --coral-soft:#d98a76;
--cyan:#3ac6d6; --cyan-deep:#1f8fa0;
--gold:#ffca3a; --amber:#f4a259; --green:#8ac926; --violet:#9b5de5;
--muted:#8a8578;
--west:#3ac6d6; --mtn:#f4a259; --mid:#8ac926; --ne:#9b5de5; --south:#e8735e;
--line:rgba(244,236,224,.14);
--shadow:0 8px 24px rgba(0,0,0,.28);
--shadow-h:0 16px 48px rgba(0,0,0,.42);
--serif:'Playfair Display','Didot','Bodoni MT',Georgia,'Times New Roman',serif;
--sans:-apple-system,'Segoe UI','Helvetica Neue',Arial,sans-serif;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{
margin:0; background:var(--navy); color:var(--paper);
font-family:var(--sans); line-height:1.6; -webkit-font-smoothing:antialiased;
overflow-x:hidden;
}
.label{
font-family:var(--sans); text-transform:uppercase; letter-spacing:.15em;
font-size:12px; color:var(--muted); font-weight:600;
}
.wrap{max-width:1180px; margin:0 auto; padding:0 24px}
section{padding:72px 0}
h1,h2,h3{font-family:var(--serif); font-weight:700; line-height:1.1; margin:0}
a{color:var(--cyan)}
/* ---------- ambient / hero ---------- */
.hero{
position:relative; padding:96px 0 64px; overflow:hidden;
background:
radial-gradient(1200px 600px at 15% -10%, rgba(58,198,214,.16), transparent 60%),
radial-gradient(900px 500px at 90% 0%, rgba(232,115,94,.14), transparent 60%),
linear-gradient(180deg,#081522,#0b1a2b 70%);
}
.hero::before{
content:""; position:absolute; inset:0; opacity:.5; pointer-events:none;
background:
repeating-linear-gradient(90deg,transparent 0 79px,var(--line) 79px 80px),
repeating-linear-gradient(0deg,transparent 0 79px,var(--line) 79px 80px);
-webkit-mask-image:radial-gradient(70% 70% at 50% 30%,#000,transparent);
mask-image:radial-gradient(70% 70% at 50% 30%,#000,transparent);
animation:drift 22s ease-in-out infinite alternate;
}
@keyframes drift{from{transform:translate3d(0,0,0)}to{transform:translate3d(-40px,20px,0)}}
.kicker{display:flex; gap:14px; align-items:center; flex-wrap:wrap; margin-bottom:22px}
.kicker .dotline{flex:1; height:1px; background:var(--line); min-width:40px}
.hero h1{font-size:clamp(40px,7vw,74px); letter-spacing:-.5px}
.hero h1 .amp{font-style:italic; color:var(--coral)}
.hero .sub{
font-family:var(--serif); font-style:italic; font-size:clamp(19px,2.6vw,26px);
color:var(--paper2); max-width:760px; margin:20px 0 0;
}
.hero .lede{max-width:660px; margin:22px 0 0; color:#cdc4b4}
.badges{display:flex; gap:10px; flex-wrap:wrap; margin-top:30px}
.badge{
border:1px solid var(--line); border-radius:999px; padding:7px 14px;
font-size:12px; letter-spacing:.12em; text-transform:uppercase; color:var(--paper2);
background:rgba(255,255,255,.03);
}
.badge.warn{border-color:rgba(255,202,58,.5); color:var(--gold)}
.spectral{
display:inline-block; height:6px; width:180px; border-radius:6px; margin-top:34px;
background:linear-gradient(90deg,var(--violet),var(--cyan),var(--green),var(--gold),var(--coral));
}
/* ---------- disclaimer ---------- */
.note-illustrative{
border-left:3px solid var(--gold); background:rgba(255,202,58,.07);
padding:14px 18px; border-radius:8px; font-size:14.5px; color:#e9dcc6;
}
/* ---------- controls ---------- */
.controls{
position:sticky; top:0; z-index:40;
background:rgba(8,20,33,.9); backdrop-filter:blur(10px);
border-bottom:1px solid var(--line); padding:14px 0;
}
.controls .wrap{display:flex; gap:22px 28px; align-items:center; flex-wrap:wrap}
.ctl-group{display:flex; flex-direction:column; gap:7px}
.ctl-group .label{font-size:11px}
.chips{display:flex; gap:6px; flex-wrap:wrap}
.chip{
cursor:pointer; border:1px solid var(--line); background:transparent; color:var(--paper2);
border-radius:999px; padding:6px 12px; font-size:12.5px; font-family:var(--sans);
letter-spacing:.03em; transition:all .18s ease;
}
.chip:hover{border-color:var(--paper2)}
.chip[aria-pressed="true"]{background:var(--paper); color:var(--ink); border-color:var(--paper); font-weight:600}
.chip .swatch{display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; vertical-align:middle}
.viewtabs{display:flex; border:1px solid var(--line); border-radius:10px; overflow:hidden}
.viewtabs button{
cursor:pointer; background:transparent; border:0; color:var(--paper2);
padding:8px 16px; font-family:var(--sans); font-size:13px; letter-spacing:.04em;
}
.viewtabs button[aria-pressed="true"]{background:var(--cyan); color:#04222a; font-weight:700}
.scrub{display:flex; align-items:center; gap:12px; min-width:260px; flex:1}
.scrub input[type=range]{flex:1; accent-color:var(--coral); cursor:pointer}
.yearbox{
font-family:var(--serif); font-size:30px; font-weight:700; color:var(--gold);
min-width:78px; text-align:center;
}
.playbtn{
cursor:pointer; display:inline-flex; align-items:center; gap:8px;
background:var(--coral); color:#2a0f09; border:0; border-radius:8px;
padding:9px 16px; font-weight:700; font-family:var(--sans); font-size:13px;
letter-spacing:.03em; transition:transform .15s ease, background .2s ease;
}
.playbtn:hover{transform:translateY(-1px); background:#ef8570}
.playbtn .tri{font-size:12px}
/* ---------- map ---------- */
.mapstage{padding-top:40px}
.stage-head{display:flex; justify-content:space-between; align-items:flex-end; gap:20px; flex-wrap:wrap; margin-bottom:20px}
.stage-head h2{font-size:clamp(26px,4vw,38px)}
.mapshell{
position:relative; border:1px solid var(--line); border-radius:16px; overflow:hidden;
background:linear-gradient(180deg,#0a1826,#0d2033); box-shadow:var(--shadow);
}
.mapgrid-bg{position:absolute; inset:0; opacity:.5; pointer-events:none;
background:
repeating-linear-gradient(90deg,transparent 0 59px,rgba(244,236,224,.05) 59px 60px),
repeating-linear-gradient(0deg,transparent 0 59px,rgba(244,236,224,.05) 59px 60px);
}
svg.usmap{display:block; width:100%; height:auto}
.land{fill:rgba(244,236,224,.045); stroke:rgba(244,236,224,.28); stroke-width:1.1}
.graticule{stroke:rgba(58,198,214,.10); stroke-width:.6}
.city{cursor:pointer; transition:opacity .3s ease, transform .2s ease}
.city.hidden{opacity:0; pointer-events:none}
.city .halo{opacity:0; transition:opacity .25s ease}
.city.active .halo{opacity:1}
.city:hover .core, .city:focus .core{stroke:#fff; stroke-width:1.6}
.city .core{stroke:rgba(11,26,43,.7); stroke-width:1}
.city:focus{outline:none}
.city:focus .halo{opacity:1}
.citylabel{
font-family:var(--sans); font-size:9px; letter-spacing:.14em; text-transform:uppercase;
fill:#cdbfa8; pointer-events:none; font-weight:600;
}
.route{fill:none; stroke:var(--gold); stroke-width:2.4; stroke-linecap:round; stroke-linejoin:round;
filter:drop-shadow(0 0 6px rgba(255,202,58,.5));}
.routeghost{fill:none; stroke:rgba(255,202,58,.18); stroke-width:2; stroke-dasharray:2 6; stroke-linecap:round}
.marker{fill:#fff; filter:drop-shadow(0 0 8px rgba(255,255,255,.9))}
.res-arc{fill:none; stroke-linecap:round; opacity:.55}
/* tooltip */
.tip{
position:absolute; z-index:20; pointer-events:none; opacity:0; transform:translateY(4px);
transition:opacity .15s ease, transform .15s ease; max-width:240px;
background:var(--paper); color:var(--ink); border-radius:10px; padding:12px 14px;
box-shadow:var(--shadow-h); font-size:13px;
}
.tip.show{opacity:1; transform:translateY(0)}
.tip h4{font-family:var(--serif); font-size:17px; margin:0 0 3px}
.tip .region-tag{font-size:10.5px; letter-spacing:.14em; text-transform:uppercase; font-weight:700}
.tip dl{margin:8px 0 0; display:grid; grid-template-columns:auto 1fr; gap:2px 10px; font-size:12px}
.tip dt{color:var(--muted); text-transform:uppercase; letter-spacing:.08em; font-size:10px; align-self:center}
.tip dd{margin:0; color:var(--ink2); font-weight:600}
.tip .il{margin-top:8px; font-size:10.5px; color:#8a7c63; font-style:italic}
/* legend */
.legend{display:flex; gap:26px 34px; flex-wrap:wrap; margin-top:22px; align-items:center}
.legend .grp{display:flex; gap:14px; flex-wrap:wrap; align-items:center}
.leg-item{display:flex; gap:8px; align-items:center; font-size:12.5px; color:var(--paper2)}
.leg-dot{width:12px; height:12px; border-radius:50%}
.leg-line{width:26px; height:0; border-top:2.4px solid var(--gold); box-shadow:0 0 6px rgba(255,202,58,.5)}
.leg-size{display:flex; gap:5px; align-items:flex-end}
.leg-size i{display:inline-block; background:var(--paper2); border-radius:50%}
/* ---------- list / cards ---------- */
.view{display:none}
.view.on{display:block}
.cardgrid{display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:22px}
.citycard{
background:var(--navy2); border:1px solid var(--line); border-radius:14px; padding:22px 22px 20px;
box-shadow:var(--shadow); transition:transform .2s ease, box-shadow .2s ease, border-color .2s ease;
position:relative; overflow:hidden;
}
.citycard::before{content:""; position:absolute; left:0; top:0; bottom:0; width:4px; background:var(--west)}
.citycard:hover{transform:translateY(-4px); box-shadow:var(--shadow-h); border-color:rgba(244,236,224,.28)}
.citycard .rtag{font-size:10.5px; letter-spacing:.14em; text-transform:uppercase; font-weight:700}
.citycard h3{font-size:24px; margin:6px 0 2px}
.citycard .venue{font-family:var(--serif); font-style:italic; color:var(--coral-soft); font-size:16px}
.citycard .meta{display:flex; gap:20px; margin:16px 0 10px}
.citycard .meta .m{display:flex; flex-direction:column}
.citycard .meta .num{font-family:var(--serif); font-size:26px; font-weight:700}
.citycard .bar{height:6px; border-radius:6px; background:rgba(244,236,224,.12); overflow:hidden; margin-top:4px}
.citycard .bar i{display:block; height:100%; border-radius:6px}
.citycard .note{color:#c3b9a7; font-size:13.5px; margin-top:12px}
/* ---------- table ---------- */
.tablewrap{overflow-x:auto; border:1px solid var(--line); border-radius:14px; background:var(--navy2)}
table{border-collapse:collapse; width:100%; min-width:680px; font-size:14px}
caption{text-align:left; padding:16px 18px; color:var(--muted); font-size:13px}
th,td{padding:11px 16px; text-align:left; border-bottom:1px solid var(--line)}
thead th{
position:sticky; top:0; background:var(--navy3); color:var(--paper);
text-transform:uppercase; letter-spacing:.1em; font-size:11px; font-weight:700;
}
tbody tr:hover{background:rgba(58,198,214,.06)}
td .rd{width:9px; height:9px; border-radius:50%; display:inline-block; margin-right:8px; vertical-align:middle}
.num-cell{font-variant-numeric:tabular-nums; font-weight:600}
/* ---------- ribbons + seasonal ---------- */
.panel{background:linear-gradient(180deg,#0a1725,#0c1e30); border-top:1px solid var(--line); border-bottom:1px solid var(--line)}
.panel h2{font-size:clamp(26px,4vw,38px)}
.panel .intro{max-width:720px; color:#cdc4b4; margin:14px 0 30px}
.vizshell{border:1px solid var(--line); border-radius:14px; background:rgba(0,0,0,.16); padding:20px 12px 8px; overflow-x:auto; box-shadow:var(--shadow)}
svg.ribbon,svg.seasonal{display:block; width:100%; min-width:720px; height:auto}
.ribbon text.vlabel{font-family:var(--serif); font-size:13px; fill:var(--paper2); font-style:italic}
.ribbon text.vcity{font-family:var(--sans); font-size:9.5px; letter-spacing:.1em; text-transform:uppercase; fill:var(--muted)}
.ribbon text.axis, .seasonal text.axis{font-family:var(--sans); font-size:10px; letter-spacing:.12em; text-transform:uppercase; fill:var(--muted)}
.ribbon .rib{cursor:default; transition:opacity .2s ease}
.ribbon .rib:hover{opacity:.85}
.seasonal text.rlab{font-family:var(--sans); font-size:11px; letter-spacing:.06em; fill:var(--paper2)}
.seasonal text.mo{font-family:var(--sans); font-size:9.5px; fill:var(--muted); text-transform:uppercase; letter-spacing:.08em}
/* ---------- essays ---------- */
.essays{background:var(--paper); color:var(--ink)}
.essays .label{color:#9a8d78}
.essays h2{color:var(--ink); font-size:clamp(28px,4.4vw,42px)}
.essays .intro{color:var(--ink2); max-width:720px; margin:16px 0 44px; font-size:17px}
.essaygrid{display:grid; grid-template-columns:repeat(auto-fit,minmax(300px,1fr)); gap:34px 40px}
.essay{position:relative}
.essay .no{font-family:var(--serif); font-size:15px; color:var(--coral); letter-spacing:.1em; font-weight:700}
.essay h3{font-size:26px; margin:6px 0 4px; color:var(--ink)}
.essay .dek{font-family:var(--serif); font-style:italic; color:#6b5b45; font-size:16px; margin-bottom:12px}
.essay p{margin:0 0 12px; color:#40362a; font-size:15.5px}
.essay p:first-letter{}
.essay.lead p:first-of-type:first-letter{
font-family:var(--serif); font-size:56px; float:left; line-height:.8; padding:6px 10px 0 0; color:var(--coral);
}
.essay .rule{height:1px; background:linear-gradient(90deg,var(--coral),transparent); margin:0 0 16px; width:120px}
/* ---------- footer ---------- */
footer{background:#07131f; padding:56px 0 44px; border-top:1px solid var(--line)}
footer .cols{display:flex; justify-content:space-between; gap:30px; flex-wrap:wrap; align-items:flex-start}
footer h3{font-size:22px; margin-bottom:8px}
footer p{color:#a99e8c; font-size:13.5px; max-width:520px}
footer .spectral{margin-top:20px}
@media (max-width:640px){
section{padding:52px 0}
.controls{position:static}
.hero h1 .amp{display:inline}
.legend{gap:16px}
.citycard .meta{gap:14px}
}
.sr-only{position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); border:0}
</style>
</head>
<body>
<!-- ============ HERO ============ -->
<header class="hero">
<div class="wrap">
<div class="kicker">
<span class="label">A Data-Driven Cultural Feature</span>
<span class="dotline"></span>
<span class="label">Fictionalized · Illustrative</span>
</div>
<h1>Routes, Residencies <span class="amp">&</span> Rituals</h1>
<p class="sub">The geography and recurring patterns of a traveling musical community — how a caravan of players and pilgrims wrote itself across a continent.</p>
<p class="lede">For three decades a band and its congregation kept moving: the same rooms in the same seasons, the same rites in every parking lot. This feature reads that movement as geography — a stylized map, a set of venue ribbons, and five short essays on the culture that formed in the aisles and the asphalt.</p>
<div class="badges">
<span class="badge warn">All numbers illustrative</span>
<span class="badge">27 stylized cities</span>
<span class="badge">Map · List · Table</span>
<span class="badge">Route playback</span>
</div>
<span class="spectral" aria-hidden="true"></span>
</div>
</header>
<!-- ============ CONTROLS ============ -->
<div class="controls" role="region" aria-label="Gallery controls">
<div class="wrap">
<div class="viewtabs" role="tablist" aria-label="View mode">
<button role="tab" data-view="map" aria-pressed="true">Map</button>
<button role="tab" data-view="list" aria-pressed="false">List</button>
<button role="tab" data-view="table" aria-pressed="false">Table</button>
</div>
<div class="ctl-group">
<span class="label" id="regionlbl">Filter by region</span>
<div class="chips" role="group" aria-labelledby="regionlbl" id="regionChips">
<button class="chip" data-region="all" aria-pressed="true">All</button>
<button class="chip" data-region="West" aria-pressed="false"><span class="swatch" style="background:var(--west)"></span>West</button>
<button class="chip" data-region="Mountain" aria-pressed="false"><span class="swatch" style="background:var(--mtn)"></span>Mountain & SW</button>
<button class="chip" data-region="Midwest" aria-pressed="false"><span class="swatch" style="background:var(--mid)"></span>Midwest</button>
<button class="chip" data-region="South" aria-pressed="false"><span class="swatch" style="background:var(--south)"></span>South</button>
<button class="chip" data-region="Northeast" aria-pressed="false"><span class="swatch" style="background:var(--ne)"></span>Northeast</button>
</div>
</div>
<div class="ctl-group scrub">
<span class="label" id="yearlbl">Year scrubber & route</span>
<div class="scrub" aria-labelledby="yearlbl">
<input type="range" id="yearRange" min="1965" max="1995" step="1" value="1977"
aria-label="Select year" aria-valuetext="1977">
<span class="yearbox" id="yearBox">1977</span>
<button class="playbtn" id="playBtn" aria-pressed="false">
<span class="tri" id="playIcon">▶</span><span id="playTxt">Play route</span>
</button>
</div>
</div>
</div>
</div>
<!-- ============ MAP VIEW ============ -->
<section class="mapstage">
<div class="wrap">
<div class="view on" id="view-map">
<div class="stage-head">
<div>
<span class="label">The Continent as a Setlist</span>
<h2>A Stylized Tour Map</h2>
</div>
<p class="label" id="tourCaption" style="max-width:340px; text-align:right">Route shown: <strong style="color:var(--gold)">1977 leg</strong> — press play to trace it</p>
</div>
<div class="mapshell">
<div class="mapgrid-bg" aria-hidden="true"></div>
<div class="tip" id="tip" role="status" aria-live="polite"></div>
<svg class="usmap" id="usmap" viewBox="0 0 990 600" role="group" aria-label="Stylized United States map of tour cities. A data table alternative is available in the Table view.">
<!-- graticule -->
<g class="graticule" aria-hidden="true">
<line x1="0" y1="150" x2="990" y2="150"/><line x1="0" y1="300" x2="990" y2="300"/><line x1="0" y1="450" x2="990" y2="450"/>
<line x1="250" y1="0" x2="250" y2="600"/><line x1="500" y1="0" x2="500" y2="600"/><line x1="750" y1="0" x2="750" y2="600"/>
</g>
<!-- stylized landmass -->
<path class="land" d="M55,73 L62,206 L68,268 L88,293 L111,352 L169,400 L218,404 L332,418 L386,477 L439,508 L470,539 L508,477 L584,473 L675,460 L695,502 L732,556 L721,477 L752,398 L805,341 L813,300 L828,248 L859,220 L889,200 L935,143 L920,95 L798,143 L691,127 L508,60 L370,60 L81,60 Z"/>
<g class="res-layer" id="resLayer" aria-hidden="true"></g>
<path class="routeghost" id="routeGhost" d=""></path>
<path class="route" id="routePath" d=""></path>
<g id="cityLayer"></g>
<circle class="marker" id="marker" r="4.5" cx="-20" cy="-20" aria-hidden="true"></circle>
</svg>
</div>
<div class="legend" aria-label="Map legend">
<div class="grp">
<span class="label" style="font-size:11px">Region</span>
<span class="leg-item"><span class="leg-dot" style="background:var(--west)"></span>West</span>
<span class="leg-item"><span class="leg-dot" style="background:var(--mtn)"></span>Mountain & SW</span>
<span class="leg-item"><span class="leg-dot" style="background:var(--mid)"></span>Midwest</span>
<span class="leg-item"><span class="leg-dot" style="background:var(--south)"></span>South</span>
<span class="leg-item"><span class="leg-dot" style="background:var(--ne)"></span>Northeast</span>
</div>
<div class="grp">
<span class="leg-item"><span class="leg-line"></span>Animated route leg</span>
<span class="leg-item"><span class="leg-size"><i style="width:6px;height:6px"></i><i style="width:10px;height:10px"></i><i style="width:15px;height:15px"></i></span> Illustrative show density</span>
<span class="leg-item"><svg width="26" height="14"><path d="M2,12 Q13,-2 24,12" fill="none" stroke="var(--violet)" stroke-width="2.4" opacity=".6"/></svg>Residency arc</span>
</div>
</div>
<p class="note-illustrative" style="margin-top:26px">
<strong>A note on the numbers.</strong> Every figure here — show counts, residency nights, seasonal intensities, route order — is <em>illustrative sample content</em> invented to demonstrate how a traveling music community <em>might</em> be explored. Nothing on this page should be read as a verified statistic. City positions are stylized, not surveyed.
</p>
</div>
<!-- ============ LIST VIEW ============ -->
<div class="view" id="view-list">
<div class="stage-head">
<div><span class="label">The Rooms, One by One</span><h2>City Cards</h2></div>
<p class="label" style="max-width:340px;text-align:right" id="listCount"></p>
</div>
<div class="cardgrid" id="cardGrid"></div>
</div>
<!-- ============ TABLE VIEW ============ -->
<div class="view" id="view-table">
<div class="stage-head">
<div><span class="label">Accessible Data Fallback</span><h2>Full Data Table</h2></div>
</div>
<div class="tablewrap">
<table id="dataTable">
<caption>Illustrative touring data by city — filtered by the region chips and the year scrubber above. All values are fictional sample content.</caption>
<thead>
<tr>
<th scope="col">City</th><th scope="col">Region</th><th scope="col">Anchor venue</th>
<th scope="col">First year (illus.)</th><th scope="col">Shows (illus.)</th><th scope="col">Character</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
</div>
</div>
</section>
<!-- ============ RESIDENCY RIBBONS ============ -->
<section class="panel">
<div class="wrap">
<span class="label">Where the Caravan Rested</span>
<h2>Venue-Residency Ribbons</h2>
<p class="intro">Certain rooms became annual rites: a band that toured constantly still <em>returned</em>, booking multi-night stands in the same halls year after year. Each ribbon below spans a venue's illustrative active window; its thickness suggests the accumulated weight of nights played there. Hover a ribbon for detail.</p>
<div class="vizshell">
<svg class="ribbon" id="ribbonSvg" viewBox="0 0 1000 430" role="img" aria-label="Ribbon chart of illustrative venue residencies across years 1965 to 1996. Thicker and longer ribbons indicate more accumulated nights. Values are illustrative.">
</svg>
</div>
</div>
</section>
<!-- ============ SEASONAL BANDS ============ -->
<section>
<div class="wrap">
<span class="label">The Year Has a Shape</span>
<h2 style="font-size:clamp(26px,4vw,38px)">Seasonal Movement Bands</h2>
<p class="intro" style="color:#cdc4b4;max-width:720px;margin:14px 0 26px">
A touring life obeys weather and ritual. Winter pulls the community home to the Bay for the turning of the year; spring greens the Northeast; summer opens the mountains and stadiums; autumn folds back through the Midwest. The heat-band below reads this illustrative rhythm — darker cells mean a region drew more of the year's attention that month.
</p>
<div class="vizshell">
<svg class="seasonal" id="seasonalSvg" viewBox="0 0 1000 340" role="img" aria-label="Heat-band chart showing illustrative seasonal touring emphasis by region across twelve months. Darker cells indicate greater emphasis. Values are illustrative.">
</svg>
</div>
</div>
</section>
<!-- ============ ESSAYS ============ -->
<section class="essays">
<div class="wrap">
<span class="label">Five Short Essays</span>
<h2>The Culture in the Aisles & the Asphalt</h2>
<p class="intro">Maps and ribbons chart the <em>where</em>. These pieces reach for the <em>why</em> — the humane, recurring patterns that turned a concert tour into a moving town. They are interpretive sample essays, not reportage.</p>
<div class="essaygrid">
<article class="essay lead">
<div class="rule"></div>
<span class="no">Ⅰ · Pilgrimage</span>
<h3>The Road as Devotion</h3>
<p class="dek">Movement as a form of belief.</p>
<p>To follow a band across a summer is to convert distance into meaning. The route becomes a rosary — each city a bead, each night a station on a circuit that repeats until repetition itself feels like prayer. People quit jobs, sold vans, slept in fields, not to arrive but to keep arriving.</p>
<p>What pilgrimage offers is a structure for wonder: a fixed liturgy (the doors, the tuning, the first chord) inside an unfixed landscape. The sameness of the ritual makes the strangeness of the geography bearable, and the strangeness of the geography keeps the ritual from calcifying. Devotion, here, is simply the willingness to go again.</p>
</article>
<article class="essay">
<div class="rule"></div>
<span class="no">Ⅱ · Parking-Lot Exchange</span>
<h3>An Economy of Gift</h3>
<p class="dek">Shakedown Street as marketplace and commons.</p>
<p>Before the doors opened, a second show ran in the lot: grilled cheese and hand-screened shirts, stickers and stones, whispered directions to the next town. It looked like commerce and behaved like a gift economy — prices soft, barter common, the point less profit than participation.</p>
<p>The lot was where a diffuse crowd became a neighborhood for an afternoon. You could enter with nothing and leave fed, housed for the night, folded into a caravan. Its illustrative currency was reputation and reciprocity, and its real product was belonging — the sense that showing up was itself a contribution.</p>
</article>
<article class="essay">
<div class="rule"></div>
<span class="no">Ⅲ · Taping Culture</span>
<h3>The Archive Made of Air</h3>
<p class="dek">Permission, and the citizen archive it built.</p>
<p>A section behind the soundboard bristling with microphones did something quietly radical: it treated the audience as archivists. Tapes were traded, never sold — dubbed onto cassettes, mailed in padded envelopes, ranked by the warmth of a particular night's second set.</p>
<p>Because every show was improvised, no two were interchangeable, and the archive mattered: a citizen record of a music that refused to sit still. Taping turned ephemerality into inheritance. The performance vanished into the air the moment it ended, and a network of strangers spent decades catching it before it fell.</p>
</article>
<article class="essay">
<div class="rule"></div>
<span class="no">Ⅳ · Regional Identity</span>
<h3>A Nation of Local Weathers</h3>
<p class="dek">The same band, refracted by place.</p>
<p>A tour reveals a country as a set of local weathers — social as much as meteorological. A Bay Area crowd carried the founding myth; a Northeast run in spring felt collegiate and dense; a mountain night in summer was thinner, higher, stranger; a Southern date moved to a different humidity of attention.</p>
<p>The band was a constant thrown against these variables, and the interaction produced identity. Regions "owned" certain nights the way towns own festivals, and returning acts learned each place's temperament — where to stretch a jam, where to lean on the anthem. Geography, it turns out, has taste.</p>
</article>
<article class="essay">
<div class="rule"></div>
<span class="no">Ⅴ · Venue & Set</span>
<h3>The Room Writes the Setlist</h3>
<p class="dek">How architecture becomes music.</p>
<p>A cavernous arena, a red-rock amphitheatre, a ski-hill lawn at dusk — each room proposes a different music. Reverb, sightlines, altitude, the way sound returns off stone all press on the choices a band makes in the moment. The setlist is partly written by the walls.</p>
<p>Residencies deepened this: play the same hall enough years and the room becomes a collaborator with memory of its own. Openers that only work outdoors, closers that need a low ceiling, a ballad that belongs to one particular balcony. The illustrative lesson of the ribbons above is that <em>where</em> a night happens is never neutral — place is an instrument.</p>
</article>
</div>
</div>
</section>
<!-- ============ FOOTER ============ -->
<footer>
<div class="wrap">
<div class="cols">
<div>
<span class="label">Routes, Residencies & Rituals</span>
<h3>A stylized data story</h3>
<p>An exhibited cultural-journalism feature built as a single, self-contained page. Everything here — the map, the ribbons, the seasonal bands, and the figures inside the tooltips — is <strong>fictionalized, illustrative sample content</strong> designed to show <em>how</em> a traveling musical community might be read as geography. It asserts no verified statistics and names no real dates.</p>
</div>
<div>
<span class="label">Legend of colors</span>
<p style="margin-top:8px">Deep navy for the ground beneath the map · warm paper for the essays · faded coral for the pilgrims · signal cyan for the routes we trace · and small flashes of spectral color where a region insists on being itself.</p>
</div>
</div>
<span class="spectral" aria-hidden="true"></span>
</div>
</footer>
<script>
(function(){
"use strict";
/* ---------------- DATA (all illustrative) ---------------- */
var REGION_COLOR = {West:'#3ac6d6', Mountain:'#f4a259', Midwest:'#8ac926', South:'#e8735e', Northeast:'#9b5de5'};
var REGION_LABEL = {West:'West', Mountain:'Mountain & SW', Midwest:'Midwest', South:'South', Northeast:'Northeast'};
var CITIES = [
{id:'sf', name:'San Francisco', region:'West', x:89, y:293, venue:'Winterland', year:1965, shows:92, note:'The hearth. New Year runs and the founding congregation.', label:1},
{id:'oak',name:'Oakland', region:'West', x:98, y:299, venue:'Oakland Coliseum', year:1979, shows:40, note:'Later years’ Bay Area anchor for the turning of the year.', label:0},
{id:'la', name:'Los Angeles', region:'West', x:153, y:372, venue:'The Forum / Shrine', year:1966, shows:55, note:'A warmer, larger congregation down the coast.', label:1},
{id:'eug',name:'Eugene', region:'West', x:76, y:163, venue:'Field near Veneta', year:1972, shows:12, note:'Legendary hot-afternoon outdoor rites in the Oregon fields.', label:0},
{id:'pdx',name:'Portland', region:'West', x:85, y:133, venue:'Memorial Coliseum', year:1972, shows:18, note:'A dependable Northwest stop on the westerly loop.', label:0},
{id:'sea',name:'Seattle', region:'West', x:91, y:89, venue:'Center Arena', year:1973, shows:15, note:'The far corner — top of the map, edge of the circuit.', label:1},
{id:'den',name:'Denver', region:'Mountain', x:352, y:255, venue:'Red Rocks', year:1967, shows:30, note:'Red-rock amphitheatre nights, thin mountain air, long jams.', label:1},
{id:'tel',name:'Telluride', region:'Mountain', x:312, y:290, venue:'Town Park', year:1987, shows:4, note:'A rare high-altitude box-canyon rite; whispered-about.', label:0},
{id:'lv', name:'Las Vegas', region:'Mountain', x:200, y:327, venue:'Silver Bowl', year:1983, shows:14, note:'Late-era desert stadium runs; heat and neon.', label:1},
{id:'phx',name:'Phoenix', region:'Mountain', x:247, y:384, venue:'Compton Terrace', year:1980, shows:9, note:'Southwestern spring dates before the heat closes in.', label:0},
{id:'chi',name:'Chicago', region:'Midwest', x:620, y:208, venue:'Soldier Field', year:1971, shows:45, note:'The great Midwestern hinge; stadium summers and civic halls.', label:1},
{id:'alp',name:'Alpine Valley', region:'Midwest', x:607, y:193, venue:'Alpine Valley', year:1980, shows:24, note:'Ski-hill lawn amphitheatre — a summer residency of its own.', label:1},
{id:'min',name:'Minneapolis', region:'Midwest', x:534, y:144, venue:'Civic Center', year:1971, shows:12, note:'Upper-Midwest anchor on the northern swing.', label:0},
{id:'stl',name:'St. Louis', region:'Midwest', x:581, y:276, venue:'Fox / Kiel', year:1971, shows:16, note:'River-city rooms; a crossroads on the way east.', label:0},
{id:'det',name:'Detroit', region:'Midwest', x:690, y:199, venue:'Cobo / Pontiac', year:1971, shows:20, note:'Industrial Midwest stop with a loyal, loud crowd.', label:1},
{id:'buf',name:'Buffalo', region:'Northeast', x:754, y:187, venue:'Memorial Aud.', year:1970, shows:14, note:'Cold-weather northeastern room; a spring gateway.', label:0},
{id:'ith',name:'Ithaca', region:'Northeast', x:790, y:197, venue:'Barton Hall', year:1977, shows:3, note:'A single celebrated campus gymnasium night, endlessly retold.', label:0},
{id:'nyc',name:'New York', region:'Northeast', x:828, y:233, venue:'Madison Square Garden', year:1967, shows:70, note:'The eastern cathedral; the archetypal multi-night residency.', label:1},
{id:'phl',name:'Philadelphia', region:'Northeast', x:810, y:249, venue:'The Spectrum', year:1968, shows:52, note:'A perennial northeastern stand across two decades.', label:1},
{id:'dc', name:'Washington', region:'Northeast', x:782, y:270, venue:'Capital Centre', year:1970, shows:30, note:'Mid-Atlantic hub; suburban arena residencies.', label:1},
{id:'bos',name:'Boston', region:'Northeast', x:873, y:198, venue:'Boston Garden', year:1969, shows:44, note:'Old-barn acoustics; a college-town devotion.', label:1},
{id:'hfd',name:'Hartford', region:'Northeast', x:848, y:211, venue:'Civic Center', year:1977, shows:26, note:'Reliable New England civic-hall stand.', label:0},
{id:'atl',name:'Atlanta', region:'South', x:670, y:378, venue:'The Omni / Fox', year:1969, shows:22, note:'The Southern capital of the circuit; humid attention.', label:1},
{id:'nol',name:'New Orleans', region:'South', x:583, y:457, venue:'City Park', year:1970, shows:10, note:'A different rhythm entirely; the deep-south outlier.', label:1},
{id:'mia',name:'Miami', region:'South', x:734, y:544, venue:'Sportatorium', year:1974, shows:8, note:'The far tip of the peninsula — end of the line, south.', label:1},
{id:'aus',name:'Austin', region:'South', x:466, y:450, venue:'Manor Downs', year:1972, shows:6, note:'A Texas hill-country stop with its own local weather.', label:1},
{id:'eng',name:'Englishtown', region:'Northeast', x:825, y:239, venue:'Raceway', year:1977, shows:2, note:'A one-off megashow on a raceway; folklore-scale crowd.', label:0}
];
var CITY_BY_ID = {}; CITIES.forEach(function(c){CITY_BY_ID[c.id]=c;});
/* Anchor tour legs (illustrative order) */
var TOURS = {
1970:['sf','buf','bos','phl','nyc','dc'],
1972:['sf','pdx','den','min','chi','det','nyc'],
1974:['sf','la','pdx','sea','eug'],
1977:['sf','ith','buf','hfd','bos','phl','nyc','eng'],
1980:['sf','nol','atl','dc','phl','nyc','bos'],
1982:['sf','den','alp','chi','stl'],
1985:['sf','oak','tel','den','chi'],
1987:['sf','tel','alp','chi','phl','nyc','bos'],
1989:['sf','lv','phx','den','chi','atl','phl','nyc'],
1991:['oak','lv','den','alp','chi','bos','nyc','dc'],
1993:['sf','lv','phx','den','chi','dc','phl','nyc'],
1995:['sf','lv','stl','chi','det','nyc','bos']
};
var ANCHORS = Object.keys(TOURS).map(Number).sort(function(a,b){return a-b;});
function tourForYear(y){ var pick=ANCHORS[0]; for(var i=0;i<ANCHORS.length;i++){ if(ANCHORS[i]<=y) pick=ANCHORS[i]; } return pick; }
/* Residency ribbons (illustrative) */
var VENUES = [
{v:'Winterland', city:'San Francisco', region:'West', s:1966, e:1978, nights:62},
{v:'Fillmore West', city:'San Francisco', region:'West', s:1968, e:1971, nights:26},
{v:'Red Rocks', city:'Denver', region:'Mountain', s:1972, e:1978, nights:20},
{v:'The Spectrum', city:'Philadelphia', region:'Northeast', s:1968, e:1989, nights:45},
{v:'Madison Square Garden', city:'New York', region:'Northeast', s:1979, e:1994, nights:58},
{v:'Capital Centre', city:'Washington', region:'Northeast', s:1973, e:1988, nights:29},
{v:'Boston Garden', city:'Boston', region:'Northeast', s:1979, e:1994, nights:41},
{v:'Alpine Valley', city:'East Troy, WI', region:'Midwest', s:1980, e:1989, nights:24},
{v:'Oakland Coliseum', city:'Oakland', region:'West', s:1979, e:1994, nights:31},
{v:'Frost Amphitheatre', city:'Stanford', region:'West', s:1982, e:1989, nights:17}
];
/* Seasonal emphasis matrix (illustrative, 0-1) */
var SEASON = {
West: [.80,.50,.32,.30,.42,.70,.60,.62,.70,.40,.30,.90],
Mountain: [.10,.10,.20,.32,.52,.82,.72,.62,.50,.22,.10,.10],
Midwest: [.10,.12,.22,.34,.44,.72,.84,.74,.60,.42,.20,.12],
South: [.22,.32,.55,.62,.42,.20,.10,.10,.32,.52,.62,.30],
Northeast: [.20,.30,.60,.84,.60,.40,.32,.42,.72,.84,.60,.30]
};
var MONTHS=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
/* ---------------- STATE ---------------- */
var state = { year:1977, region:'all', view:'map' };
var SVGNS='http://www.w3.org/2000/svg';
var $=function(s){return document.querySelector(s);};
/* ---------------- MAP: build cities + residency arcs ---------------- */
var cityLayer=$('#cityLayer'), resLayer=$('#resLayer');
// residency arcs between residency venue-cities (visual flourish)
VENUES.forEach(function(vn){
var c = CITIES.filter(function(c){return c.venue.indexOf(vn.v)>=0 || c.name===vn.city;})[0];
if(!c) return;
var span = vn.e - vn.s;
var r = 14 + span*1.6;
var col = REGION_COLOR[vn.region];
var p=document.createElementNS(SVGNS,'path');
var x=c.x, y=c.y;
p.setAttribute('d','M'+(x-r)+','+y+' A'+r+','+r+' 0 0 1 '+(x+r)+','+y);
p.setAttribute('class','res-arc');
p.setAttribute('stroke',col);
p.setAttribute('stroke-width', Math.max(2, vn.nights/14));
resLayer.appendChild(p);
});
CITIES.forEach(function(c){
var g=document.createElementNS(SVGNS,'g');
g.setAttribute('class','city');
g.setAttribute('data-id',c.id);
g.setAttribute('data-region',c.region);
g.setAttribute('tabindex','0');
g.setAttribute('role','img');
g.setAttribute('aria-label', c.name+', '+REGION_LABEL[c.region]+'. Anchor venue '+c.venue+'. Illustrative first year '+c.year+', illustrative '+c.shows+' shows.');
var r = 3 + c.shows/70*6.5;
var halo=document.createElementNS(SVGNS,'circle');
halo.setAttribute('class','halo'); halo.setAttribute('cx',c.x); halo.setAttribute('cy',c.y);
halo.setAttribute('r', r+7); halo.setAttribute('fill','none');
halo.setAttribute('stroke',REGION_COLOR[c.region]); halo.setAttribute('stroke-width','1.5'); halo.setAttribute('opacity','.6');
var core=document.createElementNS(SVGNS,'circle');
core.setAttribute('class','core'); core.setAttribute('cx',c.x); core.setAttribute('cy',c.y);
core.setAttribute('r', r); core.setAttribute('fill',REGION_COLOR[c.region]);
g.appendChild(halo); g.appendChild(core);
if(c.label){
var t=document.createElementNS(SVGNS,'text');
t.setAttribute('class','citylabel');
var above = c.y>110;
t.setAttribute('x', c.x + (c.x>820? -6 : 9));
t.setAttribute('y', c.y + (above? -8 : 16));
if(c.x>820) t.setAttribute('text-anchor','end');
t.textContent=c.name;
g.appendChild(t);
}
cityLayer.appendChild(g);
var show=function(){ showTip(c, core); };
var hide=function(){ hideTip(); };
g.addEventListener('mouseenter',show);
g.addEventListener('mousemove',show);
g.addEventListener('mouseleave',hide);
g.addEventListener('focus',show);
g.addEventListener('blur',hide);
});
/* ---------------- TOOLTIP ---------------- */
var tip=$('#tip'), mapshell=$('.mapshell');
function showTip(c, coreEl){
var svg=$('#usmap');
var vb=svg.viewBox.baseVal;
var rect=svg.getBoundingClientRect();
var scaleX=rect.width/vb.width, scaleY=rect.height/vb.height;
var px=c.x*scaleX, py=c.y*scaleY;
tip.innerHTML='<h4>'+c.name+'</h4>'+
'<span class="region-tag" style="color:'+REGION_COLOR[c.region]+'">'+REGION_LABEL[c.region]+'</span>'+
'<dl><dt>Venue</dt><dd>'+c.venue+'</dd>'+
'<dt>First yr</dt><dd>'+c.year+'</dd>'+
'<dt>Shows</dt><dd>~'+c.shows+'</dd></dl>'+
'<p style="margin:8px 0 0;font-size:12px;color:#5c4f3c">'+c.note+'</p>'+
'<div class="il">Illustrative sample figures</div>';
tip.classList.add('show');
var tw=tip.offsetWidth, th=tip.offsetHeight;
var left=px+16, top=py-th-10;
if(left+tw>rect.width-8) left=px-tw-16;
if(left<8) left=8;
if(top<8) top=py+18;
tip.style.left=left+'px'; tip.style.top=top+'px';
}
function hideTip(){ tip.classList.remove('show'); }
/* ---------------- RENDER: apply state ---------------- */
function visible(c){ return (state.region==='all'||c.region===state.region) && c.year<=state.year; }
function applyState(){
// dots
document.querySelectorAll('.city').forEach(function(g){
var c=CITY_BY_ID[g.getAttribute('data-id')];
g.classList.toggle('hidden', !visible(c));
});
// route
buildRoute();
// list + table
renderCards();
renderTable();
// caption
var ty=tourForYear(state.year);
$('#tourCaption').innerHTML='Route shown: <strong style="color:var(--gold)">'+ty+' leg</strong>'+(ty!==state.year?' (nearest ≤ '+state.year+')':'')+' — press play to trace it';
$('#yearBox').textContent=state.year;
$('#yearRange').setAttribute('aria-valuetext', String(state.year));
var vis=CITIES.filter(visible).length;
$('#listCount').textContent=vis+' cities active by '+state.year+(state.region!=='all'?' · '+REGION_LABEL[state.region]:'');
}
/* ---------------- CARDS ---------------- */
function renderCards(){
var grid=$('#cardGrid'); grid.innerHTML='';
CITIES.filter(visible).sort(function(a,b){return b.shows-a.shows;}).forEach(function(c){
var col=REGION_COLOR[c.region];
var el=document.createElement('article');
el.className='citycard';
el.style.setProperty('--west',col);
el.innerHTML=
'<span class="rtag" style="color:'+col+'">'+REGION_LABEL[c.region]+'</span>'+
'<h3>'+c.name+'</h3>'+
'<div class="venue">'+c.venue+'</div>'+
'<div class="meta">'+
'<div class="m"><span class="label" style="font-size:10px">First yr</span><span class="num">'+c.year+'</span></div>'+
'<div class="m" style="flex:1"><span class="label" style="font-size:10px">Shows (illus.)</span><span class="num">~'+c.shows+'</span>'+
'<div class="bar"><i style="width:'+Math.min(100,c.shows/92*100)+'%;background:'+col+'"></i></div></div>'+
'</div>'+
'<p class="note">'+c.note+'</p>';
el.querySelector('.citycard, &::before');
el.style.setProperty('--rc',col);
// color the left bar via ::before override
el.insertAdjacentHTML('afterbegin','<style>#tmp{}</style>');
el.querySelector('style').remove();
el.firstChild;
el.setAttribute('style','--west:'+col);
grid.appendChild(el);
});
}
/* ---------------- TABLE ---------------- */
function renderTable(){
var tb=$('#tableBody'); tb.innerHTML='';
CITIES.filter(visible).sort(function(a,b){return a.name.localeCompare(b.name);}).forEach(function(c){
var tr=document.createElement('tr');
tr.innerHTML=
'<th scope="row"><span class="rd" style="background:'+REGION_COLOR[c.region]+'"></span>'+c.name+'</th>'+
'<td>'+REGION_LABEL[c.region]+'</td>'+
'<td>'+c.venue+'</td>'+
'<td class="num-cell">'+c.year+'</td>'+
'<td class="num-cell">~'+c.shows+'</td>'+
'<td>'+c.note+'</td>';
tb.appendChild(tr);
});
}
/* ---------------- ROUTE PLAYBACK ---------------- */
var routePath=$('#routePath'), routeGhost=$('#routeGhost'), marker=$('#marker');
var routeLen=0, playing=false, rafId=null, lastT=0, dist=0;
var SPEED=170; // px per second along the path
function tourPoints(){
var ids=TOURS[tourForYear(state.year)];
return ids.map(function(id){ return CITY_BY_ID[id]; }).filter(Boolean);
}
function buildRoute(){
var pts=tourPoints();
if(pts.length<2){ routePath.setAttribute('d',''); routeGhost.setAttribute('d',''); return; }
var d='M'+pts[0].x+','+pts[0].y;
for(var i=1;i<pts.length;i++) d+=' L'+pts[i].x+','+pts[i].y;
routePath.setAttribute('d',d);
routeGhost.setAttribute('d',d);
routeLen=routePath.getTotalLength();
resetRoute();
}
function resetRoute(){
dist=0;
routePath.style.strokeDasharray=routeLen;
routePath.style.strokeDashoffset=routeLen;
var p0=routePath.getPointAtLength(0);
marker.setAttribute('cx',p0.x); marker.setAttribute('cy',p0.y);
}
function frame(t){
if(!playing) return;
if(!lastT) lastT=t;
var dt=(t-lastT)/1000; lastT=t;
dist += SPEED*dt;
if(dist>=routeLen){ dist=routeLen; }
routePath.style.strokeDashoffset=(routeLen-dist);
var p=routePath.getPointAtLength(dist);
marker.setAttribute('cx',p.x); marker.setAttribute('cy',p.y);
if(dist>=routeLen){ pause(); return; }
rafId=requestAnimationFrame(frame);
}
function play(){
if(routeLen<=0) return;
if(dist>=routeLen) resetRoute();
playing=true; lastT=0;
$('#playIcon').textContent='❚❚'; $('#playTxt').textContent='Pause'; $('#playBtn').setAttribute('aria-pressed','true');
rafId=requestAnimationFrame(frame);
}
function pause(){
playing=false; if(rafId) cancelAnimationFrame(rafId);
$('#playIcon').textContent='▶'; $('#playTxt').textContent='Play route'; $('#playBtn').setAttribute('aria-pressed','false');
}
$('#playBtn').addEventListener('click',function(){ playing?pause():play(); });
/* ---------------- CONTROLS WIRING ---------------- */
$('#yearRange').addEventListener('input',function(e){
state.year=parseInt(e.target.value,10);
pause();
applyState();
});
$('#regionChips').addEventListener('click',function(e){
var b=e.target.closest('.chip'); if(!b) return;
state.region=b.getAttribute('data-region');
Array.prototype.forEach.call(this.querySelectorAll('.chip'),function(c){ c.setAttribute('aria-pressed', c===b?'true':'false'); });
applyState();
});
document.querySelector('.viewtabs').addEventListener('click',function(e){
var b=e.target.closest('button'); if(!b) return;
state.view=b.getAttribute('data-view');
Array.prototype.forEach.call(this.querySelectorAll('button'),function(x){ x.setAttribute('aria-pressed', x===b?'true':'false'); });
['map','list','table'].forEach(function(v){ $('#view-'+v).classList.toggle('on', v===state.view); });
});
/* ---------------- RIBBON CHART ---------------- */
(function buildRibbons(){
var svg=$('#ribbonSvg');
var W=1000,H=430, padL=200, padR=30, top=34, rowH=36;
var yA=1965, yB=1996, innerW=W-padL-padR;
function xOf(y){ return padL + (y-yA)/(yB-yA)*innerW; }
// axis grid
for(var yr=1965; yr<=1995; yr+=5){
var gx=xOf(yr);
var ln=document.createElementNS(SVGNS,'line');
ln.setAttribute('x1',gx); ln.setAttribute('x2',gx); ln.setAttribute('y1',top-8); ln.setAttribute('y2', top+VENUES.length*rowH+4);
ln.setAttribute('stroke','rgba(244,236,224,.10)'); svg.appendChild(ln);
var tx=document.createElementNS(SVGNS,'text');
tx.setAttribute('class','axis'); tx.setAttribute('x',gx); tx.setAttribute('y',top-14); tx.setAttribute('text-anchor','middle');
tx.textContent="’"+String(yr).slice(2); svg.appendChild(tx);
}
// defs gradient
var defs=document.createElementNS(SVGNS,'defs');
defs.innerHTML='<linearGradient id="ribG" x1="0" x2="1" y1="0" y2="0">'+
'<stop offset="0" stop-color="#9b5de5" stop-opacity=".95"/>'+
'<stop offset=".5" stop-color="#3ac6d6" stop-opacity=".95"/>'+
'<stop offset="1" stop-color="#ffca3a" stop-opacity=".95"/></linearGradient>';
svg.appendChild(defs);
VENUES.forEach(function(vn,i){
var y=top + i*rowH + rowH/2;
var x1=xOf(vn.s), x2=xOf(vn.e);
var h= 7 + vn.nights/62*22;
var col=REGION_COLOR[vn.region];
// ribbon body
var rect=document.createElementNS(SVGNS,'rect');
rect.setAttribute('class','rib');
rect.setAttribute('x',x1); rect.setAttribute('y', y-h/2);
rect.setAttribute('width', Math.max(6,x2-x1)); rect.setAttribute('height',h);
rect.setAttribute('rx', h/2);
rect.setAttribute('fill', col); rect.setAttribute('opacity','.82');
var tt=document.createElementNS(SVGNS,'title');
tt.textContent=vn.v+' — '+vn.city+' · '+vn.s+'–'+vn.e+' · ~'+vn.nights+' nights (illustrative)';
rect.appendChild(tt);
svg.appendChild(rect);
// spectral flash cap at end
var cap=document.createElementNS(SVGNS,'circle');
cap.setAttribute('cx',x2); cap.setAttribute('cy',y); cap.setAttribute('r', Math.max(3,h/2));
cap.setAttribute('fill','url(#ribG)'); cap.setAttribute('opacity','.9'); svg.appendChild(cap);
// labels
var vl=document.createElementNS(SVGNS,'text');
vl.setAttribute('class','vlabel'); vl.setAttribute('x',padL-16); vl.setAttribute('y',y-2); vl.setAttribute('text-anchor','end');
vl.textContent=vn.v; svg.appendChild(vl);
var vc=document.createElementNS(SVGNS,'text');
vc.setAttribute('class','vcity'); vc.setAttribute('x',padL-16); vc.setAttribute('y',y+11); vc.setAttribute('text-anchor','end');
vc.textContent=vn.city; svg.appendChild(vc);
});
})();
/* ---------------- SEASONAL HEAT-BAND ---------------- */
(function buildSeasonal(){
var svg=$('#seasonalSvg');
var W=1000,H=340, padL=150, padT=44, cellW=(W-padL-30)/12, rowH=44;
var regions=['West','Mountain','Midwest','South','Northeast'];
// month labels
MONTHS.forEach(function(m,i){
var tx=document.createElementNS(SVGNS,'text');
tx.setAttribute('class','mo'); tx.setAttribute('x', padL+i*cellW+cellW/2); tx.setAttribute('y', padT-14);
tx.setAttribute('text-anchor','middle'); tx.textContent=m; svg.appendChild(tx);
});
var seasonNames=[['Winter',0,2],['Spring',2,5],['Summer',5,8],['Autumn',8,11]];
seasonNames.forEach(function(s){
var x=padL+s[1]*cellW; var w=(s[2]-s[1])*cellW;
var tx=document.createElementNS(SVGNS,'text');
tx.setAttribute('class','axis'); tx.setAttribute('x', x+w/2); tx.setAttribute('y', padT-30);
tx.setAttribute('text-anchor','middle'); tx.textContent=s[0]; svg.appendChild(tx);
});
regions.forEach(function(rg,ri){
var y=padT + ri*rowH;
var col=REGION_COLOR[rg];
// row label
var lab=document.createElementNS(SVGNS,'text');
lab.setAttribute('class','rlab'); lab.setAttribute('x',padL-16); lab.setAttribute('y', y+rowH/2+4); lab.setAttribute('text-anchor','end');
lab.textContent=REGION_LABEL[rg]; svg.appendChild(lab);
var dot=document.createElementNS(SVGNS,'circle');
dot.setAttribute('cx',padL-8); dot.setAttribute('cy',y+rowH/2); dot.setAttribute('r',4); dot.setAttribute('fill',col); svg.appendChild(dot);
SEASON[rg].forEach(function(val,mi){
var x=padL+mi*cellW;
var cell=document.createElementNS(SVGNS,'rect');
cell.setAttribute('x',x+2); cell.setAttribute('y',y+3);
cell.setAttribute('width',cellW-4); cell.setAttribute('height',rowH-6);
cell.setAttribute('rx',5);
cell.setAttribute('fill',col); cell.setAttribute('opacity', 0.12+val*0.8);
var tt=document.createElementNS(SVGNS,'title');
tt.textContent=REGION_LABEL[rg]+' · '+MONTHS[mi]+' · emphasis '+Math.round(val*100)+'% (illustrative)';
cell.appendChild(tt);
svg.appendChild(cell);
});
});
// caption
var cap=document.createElementNS(SVGNS,'text');
cap.setAttribute('class','mo'); cap.setAttribute('x',padL); cap.setAttribute('y', padT+5*rowH+22);
cap.textContent='Darker = greater illustrative seasonal emphasis'; svg.appendChild(cap);
})();
/* ---------------- INIT ---------------- */
// keyboard: space toggles play when focus not on a control
document.addEventListener('keydown',function(e){
if(e.code==='Space' && state.view==='map' && document.activeElement && document.activeElement.tagName!=='INPUT' && document.activeElement.tagName!=='BUTTON'){
e.preventDefault(); playing?pause():play();
}
});
applyState();
})();
</script>
</body>
</html>