/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain) */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; } * { margin: 0; padding: 0; box-sizing: border-box; } html { font-size: 1.25rem; /* 25% larger base font size */ } body { font-family: 'degular', sans-serif; background: #ffffff; min-height: 100vh; /*padding: 2rem;*/ color: #1a1a1a; } .container { max-width: 1400px; margin: 0 auto; padding: 0 1rem 4rem 1rem; } header { text-align: center; margin-bottom: 3rem; } header h1 { font-size: 3rem; font-weight: 700; margin: 1.5rem auto 0.5rem auto ; color: #1a1a1a; text-wrap: balance; max-width: 70%; } #scroll-to-about { color: #3b82f6; text-decoration: none; } #scroll-to-about:hover { color: #2563eb; text-decoration: none; } header h2 { font-size: 1.5rem; font-weight: 400; margin-bottom: 1.5rem; color: #666666; } .chart-meta { margin: 1.25rem auto 0; max-width: 420px; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 14px; padding: 0.85rem 1.25rem; display: flex; flex-direction: column; gap: 0.35rem; box-shadow: 0 1px 2px rgba(0,0,0,0.02); } .chart-meta .meta-row { display: flex; justify-content: space-between; align-items: baseline; font-size: 0.85rem; } .chart-meta .meta-label { color: #6b7280; letter-spacing: 0.02em; } .chart-meta .meta-value { font-weight: 700; color: #1a1a1a; font-variant-numeric: tabular-nums; } .chart-main { position: relative; padding-bottom: 3rem; } #chart-wrap { position: relative; width: 100%; /* Reserve the chart's final height (560 SVG units + ~24px wrap padding) up front so the page doesn't jump down when history.json loads and the SVG is sized. */ min-height: 584px; background: #fafafa; border-radius: 16px; padding: 1rem 42px 0.5rem 1rem; box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 1px 1px rgba(0,0,0,0.03); overflow-x: auto; overflow-y: visible; scroll-behavior: smooth; } /* Right-side y-axis, pinned outside the scrolling SVG. */ #chart-yaxis-pin { position: absolute; top: 1rem; right: 0; width: 42px; pointer-events: none; z-index: 3; background: linear-gradient(to right, rgba(250, 250, 250, 0) 0, #fafafa 45%); } #chart-yaxis-pin .pin-label { position: absolute; right: 8px; font-size: 12px; color: #6b7280; white-space: nowrap; line-height: 16px; } #chart { display: block; height: auto; overflow: visible; /* Bleed the chart a bit into the right-axis pin area so the lines visibly pass under the gradient fade. */ margin-right: -12px; } /* Safari computes the SVG's intrinsic width slightly differently than Chrome, so the chart ends a bit short of the right-axis pin. Push it back. */ @supports (background: -webkit-named-image(i)) { #chart { margin-right: 28px; } } #chart .dot { cursor: pointer; /* Decouple pointer events from paint: otherwise an opacity-0 dot stops catching events (SVG default = visiblePainted), which causes mouseenter/ mouseleave to bounce unpredictably on the .hovered class. */ pointer-events: all; /* Shorter, simpler transition — mobile was jittering on the bouncy cubic-bezier while many dots animated at once. */ transition: r 0.15s ease, stroke-width 0.15s ease, transform 0.16s ease-out; transform-origin: center; } /* Hidden while hovered so the photo overlay can take its place. */ #chart .dot.hovered { opacity: 0; } #chart .cluster.fanned .dot, #chart .cluster.fanned .stripe-ring { /* will-change hints the browser to promote these to their own compositor layer so the fan animates without repainting the whole chart. */ will-change: transform; transform: translate(calc(var(--fx) * 1px), calc(var(--fy) * 1px)); } #chart .stripe-ring { transition: transform 0.16s ease-out; transform-origin: center; } #chart polyline.city-line.hidden { display: none; } #chart polyline.city-line { pointer-events: none; } #chart polyline.city-line.emphasized { stroke-width: 4; stroke-opacity: 1; } #chart polyline.city-line.emphasized.hidden { display: inline; } /* Single overlay dims everything beneath it. Way cheaper than transitioning opacity on ~1500 dots + lines every hover. */ #dim-overlay { opacity: 0; transition: opacity 0.15s ease; pointer-events: none; } #chart.has-hover #dim-overlay, #chart.has-fanned #dim-overlay { opacity: 0.7; } /* While any cluster is fanned, lock out every OTHER cluster so the fan can't collapse because the cursor momentarily hit a neighbor's dot (which happens with overlapping hit-zones). */ #chart.has-fanned .cluster:not(.fanned) { pointer-events: none; } .hover-scale { transform: scale(0.15); transform-origin: 0 0; opacity: 0; transition: transform 0.18s cubic-bezier(.34, 1.6, .64, 1), opacity 0.1s ease-out; pointer-events: none; } .hover-scale.on { transform: scale(1); opacity: 1; } /* The "me Brian" photo (at the dot's rank) reads as half the size of the top-Brian photo so the top Brian feels like the star. Exception: when the dot IS 1st place (gold state), the me-Brian photo goes full size because he's the winner — no other Brian in frame. */ #hover-main-scale.on { transform: scale(0.5); } #hover-main.gold #hover-main-scale.on { transform: scale(1); } #hover-main-ring, #hover-top-ring { animation: ring-spin 6s linear infinite; transform-origin: 0 0; } @keyframes ring-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* Gold (1st place) treatment — no resting animation on the dot, all the sparkle happens only on hover. */ #chart .gold-dot { transform-origin: center; } /* Sparkle stars are hidden by default; shown only when hover-main is .gold */ .sparkle { opacity: 0; pointer-events: none; } #hover-main.gold .sparkle { opacity: 1; } .sparkle-star { animation: sparkle-twinkle 1.4s ease-in-out infinite; transform-origin: center; transform-box: fill-box; filter: drop-shadow(0 0 2px #fde047); } @keyframes sparkle-twinkle { 0%, 100% { opacity: 0; transform: scale(0.6) rotate(0deg); } 50% { opacity: 1; transform: scale(1.2) rotate(45deg); } } /* Gold tooltip: gold background, black text, with a diagonal sheen that sweeps across once when it appears. */ #chart-hover.gold .hover-label { background: linear-gradient(135deg, #fde047, #facc15 55%, #f59e0b); color: #111827; box-shadow: 0 6px 20px rgba(234, 179, 8, 0.35), 0 2px 6px rgba(0,0,0,0.08); position: relative; overflow: hidden; isolation: isolate; } #chart-hover.gold .hover-label::after { content: ''; position: absolute; inset: 0; background: linear-gradient(110deg, transparent 35%, rgba(255, 255, 255, 0.7) 50%, transparent 65%); transform: translateX(-120%); animation: tooltip-sheen 900ms ease-out forwards; pointer-events: none; } @keyframes tooltip-sheen { to { transform: translateX(120%); } } #chart-hover.gold .hover-label .top-line, #chart-hover.gold .hover-label .dim { color: #111827; } #chart-hover.gold .hover-label strong { color: #111827; } #chart-hover { position: absolute; pointer-events: none; opacity: 0; transition: opacity 0.15s ease; transform: translate(-50%, 30px); z-index: 5; } #chart-hover .hover-label { background: #1a1a1a; color: #fff; padding: 0.5rem 0.75rem; border-radius: 8px; font-size: 0.72rem; line-height: 1.35; text-align: center; white-space: nowrap; box-shadow: 0 4px 14px rgba(0,0,0,0.2); } #chart-hover .hover-label .hover-place { margin-bottom: 2px; } #chart-hover .hover-label .dim { color: #9ca3af; font-weight: 400; } #chart-hover .hover-label strong { font-weight: 700; } #chart-hover .hover-label .hover-place { font-weight: 700; } #chart-hover .hover-label .top-line { margin-top: 2px; color: #9ca3af; font-style: italic; font-weight: 400; } #chart-hover .hover-label .top-line strong { font-weight: 700; font-style: italic; } #chart-legend { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 0.4rem 0.8rem; margin-top: 1.25rem; } /* Brian legend swatch: small SVG circle with the Brian's chart stroke. */ #chart-legend .legend-item .legend-swatch.brian-swatch { width: 18px; height: 18px; border-radius: 0; background: none; overflow: visible; } .legend-item { position: relative; display: flex; align-items: center; gap: 0.55rem; padding: 0.35rem 0.5rem; background: none; border: none; border-radius: 6px; font-family: inherit; font-size: 0.75rem; color: inherit; text-align: left; cursor: pointer; transition: background 0.15s ease, opacity 0.15s ease; } /* Hover styles only for devices that actually have hover. On touch the first tap would otherwise just "show the hover state" and require a second tap to actually fire the click handler. */ @media (hover: hover) { .legend-item:hover { background: rgba(0,0,0,0.04); } } .legend-item.off { opacity: 0.35; } .legend-item .legend-only { margin-left: auto; background: none; border: none; font-family: inherit; font-size: 0.7rem; color: #3b82f6; text-decoration: underline; border-radius: 4px; cursor: pointer; /* Collapsed by default — takes no width so the row stays compact. The hover/tap-active rules below expand it. */ max-width: 0; padding: 2px 0; overflow: hidden; opacity: 0; white-space: nowrap; transition: max-width 0.18s ease, padding 0.18s ease, opacity 0.15s ease; } @media (hover: hover) { .legend-item:hover .legend-only { opacity: 1; max-width: 60px; padding: 2px 6px; } } /* Mobile: tapping a row reveals its "only" link. */ .legend-item.tap-active .legend-only { opacity: 1; max-width: 60px; padding: 2px 6px; } @media (hover: hover) { .legend-item .legend-only:hover { color: #1d4ed8; background: rgba(59, 130, 246, 0.08); } } #chart polyline.city-line { transition: stroke-width 0.15s ease, stroke-opacity 0.15s ease; } .legend-swatch { width: 14px; height: 14px; border-radius: 50%; flex: 0 0 auto; } .legend-text { display: flex; flex-direction: column; line-height: 1.15; } .legend-city { font-weight: 600; color: #1a1a1a; } .legend-country { color: #6b7280; font-size: 0.7rem; } footer { text-align: center; padding-top: 2rem; border-top: 1px solid rgba(0, 0, 0, 0.1); } footer p { margin: 0.5rem 0; opacity: 0.9; font-size: 0.9rem; text-wrap: balance; } /* About overlay for blur effect */ #about-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255, 255, 255, 0); backdrop-filter: blur(0px); -webkit-backdrop-filter: blur(0px); z-index: 98; pointer-events: none; transition: backdrop-filter 0.3s ease, -webkit-backdrop-filter 0.3s ease, background 0.3s ease; } /* About section styles */ #about { position: relative; z-index: 99; max-width: 800px; margin: 0 auto 10vw auto; padding: 2rem; /* Give the about panel some breathing room at the top when the anchor link or scrollIntoView lands on it. */ scroll-margin-top: 6vh; background: rgba(248, 249, 250, 0.95); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border-radius: 12px; font-family: 'Degular', sans-serif; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); } #about h2 { font-size: 2.5rem; font-weight: 700; margin-bottom: 2rem; color: #1a1a1a; text-align: center; } #about .photo-gallery { display: flex; justify-content: center; align-items: flex-end; margin: 2rem 0 3rem 0; perspective: 1000px; } #about .photo-gallery .photo-wrapper { position: relative; transition: z-index 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease; } #about .photo-gallery .photo-wrapper .label { position: absolute; bottom: -35px; left: 50%; transform: translateX(-50%); white-space: nowrap; font-size: 0.9rem; /*font-weight: 600;*/ color: #000; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; /* Stack above the ::after border ring and above neighboring photos. */ z-index: 20; background: #DBFF00; padding: 0.4em 1em; } #about .photo-gallery .photo-wrapper:hover .label { opacity: 1; } #about .photo-gallery .photo-wrapper:hover { z-index: 10; } /* Photo: just the photo. No border on the img itself. */ #about .photo-gallery img { width: 160px; height: 160px; object-fit: cover; border-radius: 12px; position: relative; transition: transform 0.3s ease; cursor: pointer; } /* Shadow layer behind the image. Transparent fill so dotted/dashed/dash-dot border gaps (drawn by ::after) show through without a white wash. */ #about .photo-gallery .photo-wrapper::before { content: ''; position: absolute; top: 0; left: 0; width: 160px; height: 160px; border-radius: 12px; box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); transition: box-shadow 0.3s ease, transform 0.3s ease; z-index: -1; pointer-events: none; } /* Border layer drawn on top of the img — one ring per photo, set per nth-child below. Keeps the img clean (no border on it) so there's only ever ONE border. ~5px stroke wraps a 160x160 photo => 170x170 at -5,-5 with border-radius 17 (12 + 5). */ #about .photo-gallery .photo-wrapper::after { content: ''; position: absolute; top: -5px; left: -5px; width: 170px; height: 170px; border-radius: 17px; box-sizing: border-box; pointer-events: none; transition: transform 0.3s ease; } #about .photo-gallery img:hover { transform: scale(1.2); } #about .photo-gallery .photo-wrapper:hover::before, #about .photo-gallery .photo-wrapper.hover-active::before { transform: scale(1.2); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3); } #about .photo-gallery .photo-wrapper:hover::after, #about .photo-gallery .photo-wrapper.hover-active::after { transform: scale(1.2); } #about .photo-gallery .photo-wrapper:nth-child(1) { z-index: 4; margin-right: -20px; top: 15px; transform: rotate(-12deg) translateY(-15px); } #about .photo-gallery .photo-wrapper:nth-child(2) { z-index: 3; margin-right: -20px; transform: rotate(-4deg) translateY(-8px); } #about .photo-gallery .photo-wrapper:nth-child(3) { z-index: 2; margin-right: -20px; transform: rotate(4deg) translateY(-8px); } #about .photo-gallery .photo-wrapper:nth-child(4) { top: 15px; z-index: 1; transform: rotate(12deg) translateY(-15px); } /* Per-Brian border patterns drawn on the wrapper's ::after, all in black. */ #about .photo-gallery .photo-wrapper:nth-child(1)::after { border: 5px dotted #1a1a1a; } #about .photo-gallery .photo-wrapper:nth-child(2)::after { border: 5px dashed #1a1a1a; } /* Commentator: matches the dash-dot pattern used on chart dots. Drawn as a background SVG with a stroked rounded rect, since CSS has no native dash-dot border style. SVG viewBox & inset scaled to match 5px stroke. */ #about .photo-gallery .photo-wrapper:nth-child(3)::after { background-image: url("data:image/svg+xml;utf8,"); background-size: 100% 100%; background-repeat: no-repeat; } #about .photo-gallery .photo-wrapper:nth-child(4)::after { border: 5px solid #1a1a1a; } #about .photo-gallery .photo-wrapper:hover, #about .photo-gallery .photo-wrapper.hover-active { transform: rotate(0deg); } #about .photo-gallery .photo-wrapper:hover img, #about .photo-gallery .photo-wrapper.hover-active img { transform: scale(1.2); box-shadow: 0 12px 24px rgba(0,0,0,0.3); } #about .photo-gallery .photo-wrapper:hover, #about .photo-gallery .photo-wrapper.hover-active { z-index: 10; } #about .photo-gallery .photo-wrapper:hover .label, #about .photo-gallery .photo-wrapper.hover-active .label { opacity: 1; } #about p { font-size: 1.2rem; line-height: 1.6; margin-bottom: 2rem; color: #333; text-wrap: pretty; } #about a { color: #0066cc; text-decoration: none; } #about a:hover { text-decoration: underline; } /* Responsive design */ @media (max-width: 768px) { body { /*padding: 1rem;*/ } header { margin-bottom: 1rem; } header h1 { font-size: 1.75rem; max-width: 100%; margin-top: 1rem; } /* Let the chart bleed out past .container's 1rem horizontal padding so it spans the full viewport width on mobile. */ .chart-main { margin-left: -1rem; margin-right: -1rem; } #chart-wrap { border-radius: 0; /* No top padding on mobile so the chart sits right under the header. */ padding-top: 0; } /* The pin's top offset must match chart-wrap's padding-top so its label y-positions line up with the SVG's gridline y-positions. */ #chart-yaxis-pin { top: 0; } /* Desktop gives the chart a small negative right margin so lines bleed under the gradient — on mobile that pushes the content too far right into the axis pin, so reset it. */ #chart { margin-right: 0; } /* Safari (mobile included) needs the same right-shift the desktop Safari rule provides — otherwise lines fall short of the right-axis pin. */ @supports (background: -webkit-named-image(i)) { #chart { margin-right: 28px; } } #chart-hover .hover-photo { width: 60px; height: 60px; border-width: 3px; } /* Mobile legend: horizontally scrolling single row right under the chart. Each city/Brian is one tap to toggle (no hover-then-click flow). */ #chart-legend { display: flex; flex-wrap: nowrap; overflow-x: auto; gap: 0.5rem; margin-top: 0.5rem; padding: 0.25rem 1rem 0.5rem; -webkit-overflow-scrolling: touch; scroll-snap-type: x proximity; grid-template-columns: none; } #chart-legend .legend-item { flex: 0 0 auto; scroll-snap-align: start; } /* (.legend-item.tap-active .legend-only handled by the global rule above — same expand-on-active behavior on every viewport.) */ .chart-meta { margin-left: 1rem; margin-right: 1rem; } /* Mobile about section styling */ #about { max-width: 95%; padding: 1.5rem; } /* Mobile 2x2 grid for about photos */ #about .photo-gallery { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 0.5rem; justify-items: center; align-items: center; margin: 2rem 0 1rem 0; max-width: 350px; margin-left: auto; margin-right: auto; perspective: 1000px; } #about .photo-gallery .photo-wrapper { margin-right: 0; position: relative; } #about .photo-gallery .photo-wrapper:nth-child(1) { grid-column: 1; grid-row: 1; top: 0; transform: rotate(-3deg) translate(10px, 5px); z-index: 4; } #about .photo-gallery .photo-wrapper:nth-child(2) { grid-column: 2; grid-row: 1; transform: rotate(3deg) translate(-10px, 5px); z-index: 3; } #about .photo-gallery .photo-wrapper:nth-child(3) { grid-column: 1; grid-row: 2; transform: rotate(-3deg) translate(10px, -5px); z-index: 2; margin-top: -30px; } #about .photo-gallery .photo-wrapper:nth-child(4) { grid-column: 2; grid-row: 2; top: 0; transform: rotate(3deg) translate(-10px, -5px); z-index: 1; margin-top: -30px; } #about .photo-gallery img { width: 160px; height: 160px; } #about .photo-gallery .photo-wrapper:hover { z-index: 10; } #about .photo-gallery .photo-wrapper:hover img, #about .photo-gallery .photo-wrapper.hover-active img { transform: scale(1.2) rotate(0deg); } }