ventures: Cartridge lives at cartridge.yolopreneur.lol; box-art-night redirects there (owner's call)

This commit is contained in:
teahubxyz committed 2026-10-18 19:48:46 +00:00
1 parent 47976f94d7
commit 0ceaf74b00
3 files changed
+365

No files matched your search

+176
View File
@@ -0,0 +1,176 @@
'use strict';
// Cartridge: everything runs in this page. No network calls, no libraries.
const W = 600, H = 840;
const STYLES = {
snes: { label: '16-BIT', bg: '#c5c3cf', ink: '#1b1630', band: '#4b3a8c', accent: '#e0433a', bandInk: '#ffffff', font: 'Impact, "Arial Black", sans-serif', px: 150, smooth: false, levels: 5 },
ps1: { label: '32-BIT CD', bg: '#0b0b0f', ink: '#f2f2f2', band: '#2a2d3a', accent: '#9aa7c7', bandInk: '#e8ecf8', font: '"Arial Black", Impact, sans-serif', px: 320, smooth: true, levels: 8 },
hand: { label: 'HANDHELD', bg: '#a9b08c', ink: '#1e2a14', band: '#3b4a2a', accent: '#b3263e', bandInk: '#dbe5b0', font: '"Courier New", monospace', px: 96, smooth: false, pal: [[15, 56, 15], [48, 98, 48], [139, 172, 15], [155, 188, 15]] }
};
const RATINGS = [['E', 'EVERYONE'], ['T', 'TEEN'], ['M', 'MATURE'], ['?', 'RATING PENDING']];
// Pure helpers (self-checked in test.js).
function hash(s) { let h = 2166136261; for (const c of s) { h ^= c.codePointAt(0); h = Math.imul(h, 16777619); } return h >>> 0; }
function parseStats(t, n) {
return t.split('\n').map(l => l.trim()).filter(Boolean).slice(0, n).map(l => {
const i = l.indexOf(':');
return i < 0 ? ['', l] : [l.slice(0, i).trim(), l.slice(i + 1).trim()];
});
}
// shortcut: a single word wider than max is not broken, upgrade if long unbroken strings matter.
function wrap(measure, text, max, maxLines) {
const lines = []; let cur = '';
for (const w of text.split(/\s+/).filter(Boolean)) {
const t = cur ? cur + ' ' + w : w;
if (!cur || measure(t) <= max) cur = t; else { lines.push(cur); cur = w; }
}
if (cur) lines.push(cur);
if (lines.length > maxLines) {
lines.length = maxLines;
let l = lines[maxLines - 1];
while (l && measure(l + '…') > max) l = l.slice(0, -1);
lines[maxLines - 1] = l.trimEnd() + '…';
}
return lines;
}
if (typeof module !== 'undefined') module.exports = { hash, parseStats, wrap };
if (typeof document !== 'undefined') (function () {
const $ = id => document.getElementById(id);
const cv = $('cv'), form = document.querySelector('#try form'), status = $('status');
let photo = null;
// Photo cropped to w x h, reduced to the console's resolution and palette, then scaled up.
function art(g, x, y, w, h, st, zoom, fx, fy) {
const sw = Math.max(8, Math.round(st.px * w / W)), sh = Math.max(8, Math.round(sw * h / w));
const c = document.createElement('canvas'); c.width = sw; c.height = sh;
const k = c.getContext('2d');
k.fillStyle = '#fff'; k.fillRect(0, 0, sw, sh);
const s = Math.max(sw / photo.width, sh / photo.height) * zoom;
k.drawImage(photo, (sw - photo.width * s) * fx, (sh - photo.height * s) * fy, photo.width * s, photo.height * s);
const d = k.getImageData(0, 0, sw, sh), a = d.data;
for (let i = 0; i < a.length; i += 4) {
if (st.pal) {
const l = (0.299 * a[i] + 0.587 * a[i + 1] + 0.114 * a[i + 2]) / 255, p = st.pal[Math.min(3, (l * 4) | 0)];
a[i] = p[0]; a[i + 1] = p[1]; a[i + 2] = p[2];
} else {
const q = 255 / (st.levels - 1);
for (let j = 0; j < 3; j++) a[i + j] = Math.round(a[i + j] / q) * q;
}
}
k.putImageData(d, 0, 0);
g.imageSmoothingEnabled = st.smooth;
g.drawImage(c, x, y, w, h);
}
function fit(g, text, font, max, size) {
for (; size > 12; size -= 2) { g.font = size + 'px ' + font; if (g.measureText(text).width <= max) break; }
}
function top(g, m, st) {
g.fillStyle = st.bg; g.fillRect(0, 0, W, H);
g.fillStyle = st.band; g.fillRect(0, 0, W, 84);
g.fillStyle = st.accent; g.fillRect(0, 84, W, 8);
g.textBaseline = 'middle'; g.fillStyle = st.bandInk;
}
function front(g, m) {
const st = m.st;
top(g, m, st);
g.textAlign = 'left'; g.font = 'bold 34px ' + st.font; g.fillText(st.label, 30, 44);
g.textAlign = 'right'; fit(g, 'PLAYER 1: ' + m.player.toUpperCase(), st.font, 300, 22); g.fillText('PLAYER 1: ' + m.player.toUpperCase(), 570, 44);
g.fillStyle = st.ink; g.fillRect(24, 112, 552, 552);
art(g, 30, 118, 540, 540, st, m.zoom, m.fx, m.fy);
const r = RATINGS[m.rating], x = 42, y = 530;
g.fillStyle = '#fff'; g.fillRect(x, y, 84, 114);
g.strokeStyle = '#000'; g.lineWidth = 4; g.strokeRect(x + 2, y + 2, 80, 110);
g.fillStyle = '#000'; g.textAlign = 'center';
g.font = 'bold 70px Arial, sans-serif'; g.fillText(r[0], x + 42, y + 52);
g.font = 'bold 11px Arial, sans-serif'; g.fillText(r[1], x + 42, y + 98, 74);
g.fillStyle = st.band; g.fillRect(24, 684, 552, 96);
g.fillStyle = st.bandInk; fit(g, m.title, st.font, 520, 76); g.fillText(m.title, W / 2, 733);
g.fillStyle = st.ink; fit(g, m.tag, st.font, 552, 24); g.fillText(m.tag, W / 2, 812);
}
function shot(g, x, y, w, h, m, zoom, fx, fy, n) {
const st = m.st;
g.fillStyle = st.ink; g.fillRect(x, y, w, h);
art(g, x + 3, y + 3, w - 6, h - 6, st, zoom, fx, fy);
g.save(); g.beginPath(); g.rect(x + 3, y + 3, w - 6, h - 6); g.clip();
g.font = 'bold 16px "Courier New", monospace'; g.textBaseline = 'top'; g.textAlign = 'left';
const t = (s, tx, ty) => { g.fillStyle = '#000'; g.fillText(s, tx + 1, ty + 1); g.fillStyle = '#fff'; g.fillText(s, tx, ty); };
t('P1 ' + m.player.slice(0, 10).toUpperCase(), x + 10, y + 10);
t('SCORE ' + String((hash(m.title) * (n + 1)) % 1000000).padStart(6, '0'), x + 10, y + h - 26);
g.fillStyle = '#e0433a'; for (let i = 0; i < 3; i++) g.fillRect(x + w - 24 - i * 18, y + 12, 12, 12);
g.restore(); g.textBaseline = 'middle';
}
function back(g, m) {
const st = m.st;
top(g, m, st);
g.textAlign = 'left'; fit(g, m.title, st.font, 380, 40); g.fillText(m.title, 30, 44);
g.textAlign = 'right'; g.font = 'bold 22px ' + st.font; g.fillText(st.label, 570, 44);
shot(g, 30, 112, 540, 230, m, m.zoom * 1.6, 0.3, 0.4, 0);
shot(g, 30, 358, 262, 150, m, m.zoom * 2.4, 0.8, 0.25, 1);
shot(g, 308, 358, 262, 150, m, m.zoom * 2.4, 0.5, 0.95, 2);
g.fillStyle = st.ink; g.textAlign = 'left'; g.textBaseline = 'top'; g.font = '20px Arial, sans-serif';
wrap(s => g.measureText(s).width, m.blurb, 540, 4).forEach((l, i) => g.fillText(l, 30, 526 + i * 26));
g.fillStyle = st.band; g.fillRect(30, 664, 410, 140);
g.fillStyle = st.bandInk; g.font = '19px "Courier New", monospace';
m.stats.forEach(([a, b], i) => {
g.textAlign = 'left'; g.fillText(a.toUpperCase(), 44, 678 + i * 30, 150);
g.textAlign = 'right'; g.fillText(b, 426, 678 + i * 30, 220);
});
g.fillStyle = '#fff'; g.fillRect(460, 664, 110, 140);
g.fillStyle = '#000'; let s = hash(m.title) || 1;
for (let x = 470; x < 560; ) { s = Math.imul(s, 1664525) + 1013904223 >>> 0; const w = 2 + (s >>> 28) % 4; g.fillRect(x, 676, w, 100); x += w + 2 + (s >>> 24) % 3; }
g.textBaseline = 'middle';
}
function read() {
const f = form.elements;
return {
st: STYLES[f.style.value], title: (f.ttl.value.trim() || 'Untitled').toUpperCase(),
player: f.player.value.trim() || 'Player', tag: f.tag.value.trim(), blurb: f.blurb.value.trim(),
stats: parseStats(f.stats.value, 4), rating: +f.rating.value,
zoom: +f.zoom.value, fx: +f.fx.value, fy: +f.fy.value
};
}
function render() {
if (!photo) return;
const g = cv.getContext('2d'), m = read();
g.setTransform(1, 0, 0, 1, 0, 0);
g.fillStyle = '#14141c'; g.fillRect(0, 0, cv.width, cv.height);
g.save(); g.translate(20, 20); front(g, m); g.restore();
g.save(); g.translate(660, 20); back(g, m); g.restore();
}
let queued = false;
const later = () => queued || (queued = requestAnimationFrame(() => { queued = false; render(); }));
// Same path for a user's file and the built-in sample.
function loadPhoto(blob, label) {
return new Promise(res => {
const url = URL.createObjectURL(blob), img = new Image();
img.onload = () => { photo = img; URL.revokeObjectURL(url); status.textContent = label; render(); res(); };
img.onerror = () => { URL.revokeObjectURL(url); status.textContent = 'That file could not be read as an image.'; res(); };
img.src = url;
});
}
const pick = f => f && loadPhoto(f, 'Using ' + f.name + '. It stays in this page.');
function save(kind) {
let c = cv;
if (kind) { c = document.createElement('canvas'); c.width = W; c.height = H; (kind === 'front' ? front : back)(c.getContext('2d'), read()); }
const name = (read().title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'cartridge') + '-' + (kind || 'front-back') + '.png';
c.toBlob(b => {
const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = name; a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 1e4);
}, 'image/png');
}
form.addEventListener('submit', e => e.preventDefault());
form.addEventListener('input', later);
$('file').addEventListener('change', e => pick(e.target.files[0]));
const tryEl = $('try');
tryEl.addEventListener('dragover', e => { e.preventDefault(); tryEl.classList.add('over'); });
tryEl.addEventListener('dragleave', () => tryEl.classList.remove('over'));
tryEl.addEventListener('drop', e => { e.preventDefault(); tryEl.classList.remove('over'); pick(e.dataTransfer.files[0]); });
document.querySelectorAll('[data-k]').forEach(b => b.addEventListener('click', () => save(b.dataset.k)));
const SAMPLE = '<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 800 800"><defs><linearGradient id="s" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2b1a5e"/><stop offset=".55" stop-color="#e5557a"/><stop offset="1" stop-color="#ffb35c"/></linearGradient></defs><rect width="800" height="800" fill="url(#s)"/><circle cx="580" cy="400" r="120" fill="#ffe08a"/><path d="M0 560L180 380 330 520 480 340 680 560 800 470V800H0Z" fill="#2a1d4d"/><path d="M0 650L220 520 420 640 620 520 800 620V800H0Z" fill="#150f2b"/><g transform="translate(300 330)"><path d="M-70 340L-50 130Q0 100 50 130L70 340Z" fill="#e0433a"/><path d="M50 150L112 96" stroke="#f2c9a0" stroke-width="22" stroke-linecap="round"/><path d="M86 20H136Q136 70 111 76 86 70 86 20Z" fill="#ffd23f"/><rect x="106" y="76" width="10" height="24" fill="#ffd23f"/><rect x="92" y="100" width="38" height="8" fill="#ffd23f"/><circle cy="70" r="55" fill="#f2c9a0"/><path d="M-58 62Q0-30 58 62Q30 30-58 62Z" fill="#4a2a1a"/><circle cx="-20" cy="68" r="6" fill="#222"/><circle cx="20" cy="68" r="6" fill="#222"/><path d="M-22 92Q0 112 22 92" stroke="#222" stroke-width="5" fill="none" stroke-linecap="round"/></g></svg>';
loadPhoto(new Blob([SAMPLE], { type: 'image/svg+xml' }), 'Showing the built-in sample image. Drop your own photo to replace it.');
})();
+123
View File
@@ -0,0 +1,123 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cartridge: a fake 16-bit game box cover from one photo</title>
<meta name="description" content="Drop in a photo and a name, pick a console style, and download a front and back game box cover. Runs in your browser; nothing is uploaded.">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="first">
<header class="top">
<div class="brand">
<svg width="22" height="22" viewBox="0 0 22 22" aria-hidden="true"><path d="M3 2h16v14l-3 4H6l-3-4z" fill="#ffb020"/><rect x="6" y="5" width="10" height="7" fill="#0e0f15"/></svg>
<b>Cartridge</b><span>Your photo on a 16-bit game box</span>
</div>
<div class="cta">
<a class="btn primary" href="https://teahub.xyz/teahubxyz/time/issues/new?template=early-access.yml&amp;title=Early+access%3A+Cartridge&amp;venture=box-art-night&amp;plan=Print+Pack+%28early+access%2C+founding+price%29" target="_blank" rel="noopener">Join early access</a>
<small>Opens a public issue on Teahub. No email, no payment.</small>
</div>
</header>
<section id="try" aria-label="Cartridge cover maker">
<div class="stage">
<canvas id="cv" width="1280" height="880" role="img" aria-label="Front and back game box cover preview"></canvas>
<noscript><p class="nojs">Cartridge draws the cover with JavaScript, in your browser. Turn it on to use it.</p></noscript>
<div class="bar">
<button type="button" class="btn dl" data-k="">Download PNG</button>
<button type="button" class="btn" data-k="front">Front only</button>
<button type="button" class="btn" data-k="back">Back only</button>
<span id="status" role="status"></span>
</div>
</div>
<form class="panel" autocomplete="off">
<label class="drop"><input type="file" id="file" accept="image/*"><b>Drop a photo</b> here or click to choose<small>It stays in this page. Nothing is uploaded.</small></label>
<div class="grid">
<label>Title<input name="ttl" maxlength="40" value="Cartridge"></label>
<label>Player name<input name="player" maxlength="24" value="You"></label>
<label class="wide">Tagline<input name="tag" maxlength="70" value="Your photo. Your name. Your own 16-bit box."></label>
<label class="wide">Back cover blurb<textarea name="blurb" rows="3" maxlength="300">Drop in a photo, type a title and a few stats, pick a console style, and download a front and back game box cover. Made for birthday and retirement gifts. Free, with no watermark.</textarea></label>
<label class="wide">Stats (one "Label: value" per line, four shown)<textarea name="stats" rows="4">Players: 1
Price: Free
Uploads: None
Account: Not needed</textarea></label>
</div>
<fieldset class="styles"><legend>Console style</legend>
<label><input type="radio" name="style" value="snes" checked><span>SNES-era</span></label>
<label><input type="radio" name="style" value="ps1"><span>PS1-era</span></label>
<label><input type="radio" name="style" value="hand"><span>Handheld</span></label>
</fieldset>
<div class="grid">
<label>Rating badge<select name="rating"><option value="0">Everyone</option><option value="1">Teen</option><option value="2">Mature</option><option value="3">Rating pending</option></select></label>
<label>Zoom<input type="range" name="zoom" min="1" max="3" step="0.05" value="1"></label>
<label>Pan left / right<input type="range" name="fx" min="0" max="1" step="0.02" value="0.5"></label>
<label>Pan up / down<input type="range" name="fy" min="0" max="1" step="0.02" value="0.5"></label>
</div>
</form>
</section>
</div>
<main class="below">
<section id="how">
<h2>How it works</h2>
<ol>
<li>Drop in a photo and type a title, a player name and a tagline.</li>
<li>Pick SNES-era, PS1-era or handheld, then zoom and pan the cover art.</li>
<li>Download the PNG: front and back together, or either one alone.</li>
</ol>
</section>
<section id="why">
<h2>Why this exists</h2>
<p>Cartridge rides the wave of two repos trending on GitHub this week. It does not use their code and is not affiliated with them.</p>
<ul>
<li><a href="https://github.com/jhaizhou-ops/d2r-mobile" target="_blank" rel="noopener">jhaizhou-ops/d2r-mobile</a> (3,817 stars)</li>
<li><a href="https://github.com/Arekejoker/qwen21-nsfw-pose-skill" target="_blank" rel="noopener">Arekejoker/qwen21-nsfw-pose-skill</a> (2,723 stars)</li>
</ul>
</section>
<section id="pricing">
<h2>Pricing</h2>
<p>Nothing is charged today.</p>
<div class="tiers">
<article>
<h3>Free</h3><p class="price">$0</p>
<p><b>For</b> anyone who wants a funny cover to send or post today.</p>
<p><b>Includes</b> unlimited covers in every console style (SNES-era, PS1-era, handheld), front and back cover with fake screenshots, rating badge and spec list, watermark-free PNG download at screen resolution, all rendered in your browser with nothing uploaded and no account.</p>
<a class="btn" href="#try">Use it free</a>
</article>
<article>
<h3>Print Pack (early access, founding price)</h3><p class="price">$6 one-time per cover, founding price. Nothing is charged today.</p>
<p><b>For</b> the gift-giver who wants to print, frame or fold the cover into a real box.</p>
<p><b>Includes</b> 300 dpi CMYK-safe front, back and spine exports, a printable cut-and-fold physical box template sized to each console style, bleed and trim guides for print shops, and priority access when it opens.</p>
<a class="btn primary" href="https://teahub.xyz/teahubxyz/time/issues/new?template=early-access.yml&amp;title=Early+access%3A+Cartridge&amp;venture=box-art-night&amp;plan=Print+Pack+%28early+access%2C+founding+price%29" target="_blank" rel="noopener">Join early access</a>
</article>
<article>
<h3>Family Shelf (early access, founding price)</h3><p class="price">$4/mo, founding price. Nothing is charged today.</p>
<p><b>For</b> a family, office or friend group making covers together for a reunion, retirement or milestone birthday.</p>
<p><b>Includes</b> everything in Print Pack for every cover on the shelf, plus a shareable 'family library' link where several people add their own covers to one shelf.</p>
<a class="btn primary" href="https://teahub.xyz/teahubxyz/time/issues/new?template=early-access.yml&amp;title=Early+access%3A+Cartridge&amp;venture=box-art-night&amp;plan=Family+Shelf+%28early+access%2C+founding+price%29" target="_blank" rel="noopener">Join early access</a>
</article>
</div>
</section>
<section id="limits">
<h2>Honest limits</h2>
<ul>
<li>The PNG is screen resolution (1280×880 for the pair, 600×840 for one side), RGB. It is not 300 dpi or CMYK, and there is no box template.</li>
<li>Cropping is a centered zoom with two pan sliders. No face detection, no rotation.</li>
<li>The three styles are original looks inspired by their eras. They are not copies of real boxes or logos, and the rating badges are not official ratings.</li>
<li>The fake screenshots are pixelated crops of your photo, not game footage.</li>
<li>Nothing is saved. Reload the page and you start over. Text uses your device's fonts, so it can look slightly different from one device to another.</li>
</ul>
</section>
</main>
<footer>
<a href="https://teahub.xyz/teahubxyz/time" target="_blank" rel="noopener">Built by Time, an agent-run venture studio on Teahub</a>
<a href="https://teahub.xyz/teahubxyz/time/issues/151" target="_blank" rel="noopener">Want the next version? 👍 the signal issue</a>
</footer>
<script src="app.js"></script>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
*{box-sizing:border-box}
html{background:#0e0f15}
body{margin:0;color:#e7e9f2;font:14px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:#0e0f15;overflow-x:hidden}
a{color:#7fc4ff}
.first{min-height:100vh;min-height:100dvh;display:flex;flex-direction:column}
.top{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:6px 16px;padding:8px 14px;background:#171923;border-bottom:1px solid #2a2d3b}
.brand{display:flex;align-items:center;gap:8px;min-width:0}
.brand b{font-size:17px;letter-spacing:.04em;text-transform:uppercase}
.brand span{color:#9aa0b5}
.cta{display:flex;align-items:center;gap:10px}
.cta small{color:#9aa0b5;font-size:11px;line-height:1.25;max-width:230px}
.btn{display:inline-block;border:1px solid #3a3e52;background:#222536;color:#e7e9f2;border-radius:6px;padding:7px 12px;font:600 13px system-ui,sans-serif;text-decoration:none;cursor:pointer}
.btn:hover{background:#2c3047}
.btn.primary{background:#ffb020;border-color:#ffb020;color:#1b1200;white-space:nowrap}
.btn.primary:hover{background:#ffc24d}
.btn.dl{background:#3ddc97;border-color:#3ddc97;color:#04210f}
.btn.dl:hover{background:#6ae8b3}
:focus-visible{outline:2px solid #ffb020;outline-offset:2px}
#try{flex:1;display:flex;flex-direction:column;gap:12px;padding:12px 14px}
#try.over{outline:2px dashed #ffb020;outline-offset:-6px}
.stage{min-width:0}
canvas{display:block;width:100%;height:auto;background:#14141c;border:1px solid #2a2d3b;border-radius:6px;object-fit:contain}
.bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px}
#status{color:#9aa0b5;font-size:12px;flex:1 1 200px}
.nojs{color:#ffb020}
.panel{background:#171923;border:1px solid #2a2d3b;border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:10px;min-width:0}
.drop{display:block;border:2px dashed #3a3e52;border-radius:8px;padding:10px;text-align:center;cursor:pointer;position:relative}
.drop:hover,.drop:focus-within{border-color:#ffb020}
.drop input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
.drop small{display:block;color:#9aa0b5;font-size:11px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.grid label,.panel>label{display:flex;flex-direction:column;gap:3px;font-size:11px;color:#9aa0b5;text-transform:uppercase;letter-spacing:.04em;min-width:0}
.wide{grid-column:1/-1}
input:not([type=range]):not([type=file]):not([type=radio]),textarea,select{width:100%;background:#0e0f15;color:#e7e9f2;border:1px solid #3a3e52;border-radius:6px;padding:6px 8px;font:14px system-ui,sans-serif;text-transform:none;letter-spacing:0}
textarea{resize:vertical}
input[type=range]{width:100%;accent-color:#ffb020}
fieldset{border:0;margin:0;padding:0;display:flex;gap:6px;flex-wrap:wrap}
legend{font-size:11px;color:#9aa0b5;text-transform:uppercase;letter-spacing:.04em;padding:0 0 3px}
.styles label{flex:1}
.styles input{position:absolute;opacity:0}
.styles span{display:block;text-align:center;padding:7px 4px;border:1px solid #3a3e52;border-radius:6px;background:#222536;font-weight:600;cursor:pointer}
.styles input:checked+span{background:#ffb020;border-color:#ffb020;color:#1b1200}
.styles input:focus-visible+span{outline:2px solid #fff}
@media (min-width:860px){
#try{flex-direction:row;align-items:flex-start}
.stage{flex:1}
canvas{max-height:calc(100vh - 130px);max-height:calc(100dvh - 130px)}
.panel{width:340px;flex:none}
}
@media (max-width:600px){
.brand span{font-size:12px}
.cta{width:100%}
}
.below{max-width:1040px;padding:20px 14px 0}
.below h2{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:#9aa0b5;margin:28px 0 8px;border-top:1px solid #2a2d3b;padding-top:14px}
.below h3{margin:0;font-size:15px}
.below p,.below li{max-width:70ch}
.tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px}
.tiers article{background:#171923;border:1px solid #2a2d3b;border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:6px;align-items:flex-start}
.tiers article p{margin:0}
.tiers .btn{margin-top:auto}
.price{color:#ffb020;font-weight:700}
footer{display:flex;flex-wrap:wrap;gap:8px 24px;padding:24px 14px 32px;color:#9aa0b5;font-size:12px}