diff --git a/configs/social.json b/configs/social.json
index 94b6688..9f85654 100644
--- a/configs/social.json
+++ b/configs/social.json
@@ -83,17 +83,6 @@
"bgFit": "cover",
"bgPos": "center",
"description": "Discord is where you want to be. Active, friendly and intelligent community members await you."
- },
- {
- "title": "Fediverse",
- "href": "https://makearmy.io",
- "icon": "/icons/fediverse.png",
- "image": "",
- "color": "#1289a1",
- "size": "2x1",
- "bgFit": "cover",
- "bgPos": "center",
- "description": "We run a wide range of fediverse instances. Check out the list at MakeArmy!"
}
]
}
diff --git a/configs/support.json b/configs/support.json
index 17c7970..88d34e8 100644
--- a/configs/support.json
+++ b/configs/support.json
@@ -39,7 +39,7 @@
"href": "https://www.patreon.com/LaserEverything",
"icon": "/icons/patreon.png",
"color": "#f39a31",
- "size": 1,
+ "size": "2x1",
"bgFit": "cover",
"bgPos": "center",
"description": "Mainstream supporter platform for creators."
@@ -60,7 +60,7 @@
"icon": "/icons/liberapay.png",
"image": "",
"color": "#134823",
- "size": "2x1",
+ "size": "1",
"bgFit": "cover",
"bgPos": "center",
"description": "FOSS and Privacy First supporter platform for creators."
diff --git a/index.html b/index.html
index 18cdb73..7533c52 100644
--- a/index.html
+++ b/index.html
@@ -10,6 +10,7 @@
@@ -20,7 +21,7 @@
Config default: /configs/index.json (override with ?config=name)
====================================================== */
(async () => {
- /* ---------- tiny seeded RNG (deterministic) ---------- */
+ /* ---------- tiny seeded RNG ---------- */
function xmur3(str){
let h = 1779033703 ^ str.length;
for (let i=0;i{
@@ -217,27 +217,34 @@
grid.textContent = '';
grid.appendChild(frag2);
- /* ---------- layout (header-aware) ---------- */
+ /* ---------- layout (header-aware, edge-rounding safe) ---------- */
function layout(){
+ // viewport and header height
const W = innerWidth;
const H = innerHeight;
- const VV = visualViewport;
- const vh = (VV && VV.height) ? VV.height : H;
const header = document.getElementById('siteHeader');
- const HH = Math.max(0, vh - (header ? header.getBoundingClientRect().height : 0));
+ const HH = Math.max(1, H - (header ? header.getBoundingClientRect().height : 0));
+ // gap
const gap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--gap')) || 0;
+
+ // USE CONTENT BOX SIZES (padding removed) to avoid last-column overflow
+ const contentW = W - gap * 2;
+ const contentH = HH - gap * 2;
+
const fit = (cfg.grid?.fit || 'stretch').toLowerCase();
+ // total tile area
const S = ordered.reduce((s, el)=> s + (+el.dataset.w * +el.dataset.h), 0) || 1;
- const A = W / (HH || 1);
+ const A = contentW / (contentH || 1);
let cols = Math.max(1, Math.round(Math.sqrt(S * A)));
let rows = Math.max(1, Math.ceil(S / cols));
+ // square unit that fits both axes, using content sizes
const unitFor = (c, r) => Math.max(1, Math.min(
- Math.floor((W - gap*(c+1)) / c),
- Math.floor((HH - gap*(r+1)) / r)
+ Math.floor((contentW - gap * (c - 1)) / c),
+ Math.floor((contentH - gap * (r - 1)) / r)
));
let best = { cols, rows, unit: unitFor(cols, rows), holes: Math.max(0, cols*rows - S) };
@@ -252,25 +259,29 @@
grid.style.gap = gap + 'px';
grid.style.padding = gap + 'px';
+ // sub-pixel epsilon to guarantee no overflow in final column/row
+ const EPS = 0.75;
+
if (fit === 'stretch'){
- const unitW = (W - gap * (best.cols + 1)) / best.cols;
- const unitH = (HH - gap * (best.rows + 1)) / best.rows;
+ const unitW = (contentW - gap * (best.cols - 1)) / best.cols - EPS;
+ const unitH = (contentH - gap * (best.rows - 1)) / best.rows - EPS;
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
- grid.style.gridAutoRows = `${unitH}px`;
+ grid.style.gridAutoRows = `${unitH}px`;
} else {
- grid.style.gridTemplateColumns = `repeat(${best.cols}, ${best.unit}px)`;
- grid.style.gridAutoRows = `${best.unit}px`;
+ const unit = best.unit - EPS;
+ grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unit}px)`;
+ grid.style.gridAutoRows = `${unit}px`;
}
ordered.forEach(n=>{
- /* Phone-only span clamp to keep everything on one page */
+ // phone span clamp
const isPhone = innerWidth <= 520;
const w = Math.min(+n.dataset.w, isPhone ? 2 : +n.dataset.w);
const h = Math.min(+n.dataset.h, isPhone ? 2 : +n.dataset.h);
n.style.gridColumn = `span ${w}`;
n.style.gridRow = `span ${h}`;
- /* Deterministic wipe dir (prod = 140%) */
+ // deterministic wipe
const title = n.querySelector('.title')?.textContent || '';
const R = rngFrom('wipe|'+title);
const dirs = [[1,0],[-1,0],[0,-1],[0,1],[1,-1],[-1,-1],[-1,1],[1,1]];
@@ -278,7 +289,7 @@
n.style.setProperty('--dx', (dx*140)+'%');
n.style.setProperty('--dy', (dy*140)+'%');
- /* Recompute hint row offsets using current tile width */
+ // hint offsets
const tw = n.getBoundingClientRect().width;
n.querySelectorAll('.hint-row').forEach(row=>{
const scale = parseFloat(row.dataset.offsetScale || '0');
@@ -291,8 +302,8 @@
addEventListener('resize', resched);
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
- // JS-assisted reveal class only
- ordered.forEach(tile=>{
+ // js-assisted reveal class
+ document.querySelectorAll('.tile').forEach(tile=>{
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
diff --git a/index.html.bak b/index.html.bak
new file mode 100644
index 0000000..18cdb73
--- /dev/null
+++ b/index.html.bak
@@ -0,0 +1,305 @@
+
+
+
+
+
+ Laser Everything
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/social.html b/social.html
index 286e3d3..823f3b8 100644
--- a/social.html
+++ b/social.html
@@ -10,6 +10,7 @@
@@ -20,7 +21,7 @@
Config default: /configs/index.json (override with ?config=name)
====================================================== */
(async () => {
- /* ---------- tiny seeded RNG (deterministic) ---------- */
+ /* ---------- tiny seeded RNG ---------- */
function xmur3(str){
let h = 1779033703 ^ str.length;
for (let i=0;i{
@@ -217,27 +217,34 @@
grid.textContent = '';
grid.appendChild(frag2);
- /* ---------- layout (header-aware) ---------- */
+ /* ---------- layout (header-aware, edge-rounding safe) ---------- */
function layout(){
+ // viewport and header height
const W = innerWidth;
const H = innerHeight;
- const VV = visualViewport;
- const vh = (VV && VV.height) ? VV.height : H;
const header = document.getElementById('siteHeader');
- const HH = Math.max(0, vh - (header ? header.getBoundingClientRect().height : 0));
+ const HH = Math.max(1, H - (header ? header.getBoundingClientRect().height : 0));
+ // gap
const gap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--gap')) || 0;
+
+ // USE CONTENT BOX SIZES (padding removed) to avoid last-column overflow
+ const contentW = W - gap * 2;
+ const contentH = HH - gap * 2;
+
const fit = (cfg.grid?.fit || 'stretch').toLowerCase();
+ // total tile area
const S = ordered.reduce((s, el)=> s + (+el.dataset.w * +el.dataset.h), 0) || 1;
- const A = W / (HH || 1);
+ const A = contentW / (contentH || 1);
let cols = Math.max(1, Math.round(Math.sqrt(S * A)));
let rows = Math.max(1, Math.ceil(S / cols));
+ // square unit that fits both axes, using content sizes
const unitFor = (c, r) => Math.max(1, Math.min(
- Math.floor((W - gap*(c+1)) / c),
- Math.floor((HH - gap*(r+1)) / r)
+ Math.floor((contentW - gap * (c - 1)) / c),
+ Math.floor((contentH - gap * (r - 1)) / r)
));
let best = { cols, rows, unit: unitFor(cols, rows), holes: Math.max(0, cols*rows - S) };
@@ -252,25 +259,29 @@
grid.style.gap = gap + 'px';
grid.style.padding = gap + 'px';
+ // sub-pixel epsilon to guarantee no overflow in final column/row
+ const EPS = 0.75;
+
if (fit === 'stretch'){
- const unitW = (W - gap * (best.cols + 1)) / best.cols;
- const unitH = (HH - gap * (best.rows + 1)) / best.rows;
+ const unitW = (contentW - gap * (best.cols - 1)) / best.cols - EPS;
+ const unitH = (contentH - gap * (best.rows - 1)) / best.rows - EPS;
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
- grid.style.gridAutoRows = `${unitH}px`;
+ grid.style.gridAutoRows = `${unitH}px`;
} else {
- grid.style.gridTemplateColumns = `repeat(${best.cols}, ${best.unit}px)`;
- grid.style.gridAutoRows = `${best.unit}px`;
+ const unit = best.unit - EPS;
+ grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unit}px)`;
+ grid.style.gridAutoRows = `${unit}px`;
}
ordered.forEach(n=>{
- /* Phone-only span clamp to keep everything on one page */
+ // phone span clamp
const isPhone = innerWidth <= 520;
const w = Math.min(+n.dataset.w, isPhone ? 2 : +n.dataset.w);
const h = Math.min(+n.dataset.h, isPhone ? 2 : +n.dataset.h);
n.style.gridColumn = `span ${w}`;
n.style.gridRow = `span ${h}`;
- /* Deterministic wipe dir (prod = 140%) */
+ // deterministic wipe
const title = n.querySelector('.title')?.textContent || '';
const R = rngFrom('wipe|'+title);
const dirs = [[1,0],[-1,0],[0,-1],[0,1],[1,-1],[-1,-1],[-1,1],[1,1]];
@@ -278,7 +289,7 @@
n.style.setProperty('--dx', (dx*140)+'%');
n.style.setProperty('--dy', (dy*140)+'%');
- /* Recompute hint row offsets using current tile width */
+ // hint offsets
const tw = n.getBoundingClientRect().width;
n.querySelectorAll('.hint-row').forEach(row=>{
const scale = parseFloat(row.dataset.offsetScale || '0');
@@ -291,8 +302,8 @@
addEventListener('resize', resched);
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
- // JS-assisted reveal class only
- ordered.forEach(tile=>{
+ // js-assisted reveal class
+ document.querySelectorAll('.tile').forEach(tile=>{
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));
diff --git a/styles.css b/styles.css
index c458064..2438a5a 100644
--- a/styles.css
+++ b/styles.css
@@ -24,14 +24,14 @@ body{
font:16px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial;
overflow:hidden; /* tiles view */
}
-/* make sure tiles never pick UA link blue */
+/* prevent UA link blue on tiles */
a{ color:inherit; text-decoration:none }
/* ===== Header ===== */
.site-header{
position:fixed; left:0; right:0; top:0;
height:calc(var(--header-h) + var(--safe-top));
- display:flex; align-items:center; justify-content:center;
+ display:flex; align-items:center; justify-content:center; gap:12px;
padding:0 clamp(12px,3vw,24px);
background:rgba(0,0,0,.55); backdrop-filter:blur(6px);
border-bottom:1px solid rgba(255,255,255,.10);
@@ -42,6 +42,10 @@ a{ color:inherit; text-decoration:none }
font-size:clamp(12px,1.4vw,14px); opacity:.95; text-decoration:none;
}
+/* Optional search bar in header */
+.search-bar{ flex:0 1 480px; max-width:480px; }
+.search-bar input{ width:100%; }
+
/* ===== Grid ===== */
.grid{
position:fixed; inset:0;
@@ -79,13 +83,29 @@ a{ color:inherit; text-decoration:none }
position:absolute; inset:0; display:grid; place-items:center; justify-items:center;
text-align:center; padding:clamp(12px,3vh,28px);
transition:opacity .55s ease, transform .55s ease; pointer-events:none;
+
+ /* NEW: keep centering based on visible interior to avoid edge rounding issues */
+ max-width:100%;
+ max-height:100%;
}
.text-layer{ z-index:1; opacity:0; transform:translateY(8px) }
-.hint-layer{ z-index:3; opacity:var(--hint-opacity); }
+.hint-layer{ z-index:3; opacity:var(--hint-opacity) }
.icon-layer{ z-index:4; opacity:1 }
+/* Icon + small bold caption under it */
.icon{ width:clamp(36px,9vh,110px); height:auto; object-fit:contain }
.icon.icon-text{ width:auto; font-size:clamp(24px,10vh,120px); line-height:1 }
+
+.icon-caption{
+ margin-top:.4rem;
+ font-weight:800;
+ font-size:12px;
+ line-height:1.1;
+ max-width:92%;
+ white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
+ pointer-events:none;
+}
+
.kicker{ font-weight:900; letter-spacing:.22em; text-transform:uppercase; font-size:12px; opacity:.95; margin:0 0 .25em }
.title{ margin:0; font-weight:1000; letter-spacing:.2px; font-size:clamp(16px,2.2vw,28px); line-height:1.1 }
.desc{ margin:.45em 0 0; opacity:.92; font-size:14px; max-width:min(72ch,90%) }
@@ -104,7 +124,6 @@ a{ color:inherit; text-decoration:none }
.tile:focus-visible .text-layer,
.tile.js-reveal .text-layer{ opacity:1; transform:none }
-/* Hide hint during reveal for the flip */
.tile:hover .hint-layer,
.tile:focus-visible .hint-layer,
.tile.js-reveal .hint-layer{ opacity:0 }
@@ -113,11 +132,9 @@ a{ color:inherit; text-decoration:none }
.tile,.cover,.layer{ transition:none }
}
-/* ===== Hint layer geometry (unchanged from prod) ===== */
+/* ===== Hint layer geometry ===== */
.hint-layer{
- position:absolute; inset:0; pointer-events:none;
- display:block;
- /* optional soft fade at extreme sides */
+ position:absolute; inset:0; pointer-events:none; display:block;
-webkit-mask-image:linear-gradient(to right,
rgba(0,0,0,0) var(--hint-feather),
#000 calc(var(--hint-feather) + 1px),
@@ -135,8 +152,7 @@ a{ color:inherit; text-decoration:none }
width:calc(var(--hint-plane) * 100%);
height:calc(var(--hint-plane) * 100%);
display:flex; flex-direction:column; justify-content:center; align-items:center;
- gap:var(--hint-row-gap);
- overflow:hidden; /* clip rows to plane; tile clips plane */
+ gap:var(--hint-row-gap); overflow:hidden;
}
.hint-row{
position:relative; display:block;
@@ -144,41 +160,19 @@ a{ color:inherit; text-decoration:none }
font-weight:900; text-transform:uppercase; font-size:var(--hint-size);
transform:translateX(var(--row-offset-px, 0px));
}
-.hint-line{ opacity:1 } /* keep lines solid */
+.hint-line{ opacity:1 }
-/* ===== Static pages (from prod) ===== */
-body.page{
- min-height:100vh; margin:0; color:var(--fg); background:var(--bg);
- overflow-y:auto; overflow-x:hidden;
-}
-.page-wrap{
- box-sizing:border-box; min-height:100svh;
- padding:var(--gap);
- padding-top:calc(var(--gap) + var(--header-h) + var(--safe-top));
- padding-bottom:calc(var(--gap) + var(--safe-bottom));
- display:grid; grid-template-rows:auto 1fr; gap:var(--gap);
-}
+/* ===== Static pages preserved ===== */
+body.page{ min-height:100vh; margin:0; color:var(--fg); background:var(--bg); overflow-y:auto; overflow-x:hidden }
+.page-wrap{ box-sizing:border-box; min-height:100svh; padding:var(--gap); padding-top:calc(var(--gap) + var(--header-h) + var(--safe-top)); padding-bottom:calc(var(--gap) + var(--safe-bottom)); display:grid; grid-template-rows:auto 1fr; gap:var(--gap) }
.page-header{ display:flex; align-items:center; justify-content:space-between; gap:var(--gap) }
-.back-btn,.btn{
- display:inline-flex; align-items:center; gap:.6rem;
- border-radius:999px; padding:.6rem .9rem;
- border:1px solid rgba(255,255,255,0.10);
- background:rgba(255,255,255,0.04);
- color:inherit; text-decoration:none; outline:none;
- transition:background 300ms, transform 160ms;
-}
+.back-btn,.btn{ display:inline-flex; align-items:center; gap:.6rem; border-radius:999px; padding:.6rem .9rem; border:1px solid rgba(255,255,255,0.10); background:rgba(255,255,255,0.04); color:inherit; text-decoration:none; outline:none; transition:background 300ms, transform 160ms }
.btn.primary{ background:rgba(255,255,255,0.08) }
.back-btn:hover,.btn:hover{ background:rgba(255,255,255,0.07); transform:translateY(-1px) }
.back-btn:focus-visible,.btn:focus-visible{ box-shadow:0 0 0 3px rgba(154,168,255,.45) }
.back-btn svg{ width:18px; height:18px; flex:0 0 auto; opacity:.95 }
.page-main{ display:grid; place-items:start center }
-.article{
- width:min(90ch,100%); line-height:1.6;
- background:rgba(255,255,255,0.04);
- border:1px solid rgba(255,255,255,0.10);
- border-radius:16px;
- padding:clamp(16px,3vw,28px);
-}
+.article{ width:min(90ch,100%); line-height:1.6; background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.10); border-radius:16px; padding:clamp(16px,3vw,28px) }
.article h1{ margin:0 0 .35em; font-size:clamp(22px,3.2vw,36px); letter-spacing:.2px; text-align:center }
.subtitle{ margin:0 0 1.1em; opacity:.85; text-align:center; font-size:clamp(14px,1.6vw,16px) }
.cta-row{ display:flex; flex-wrap:wrap; gap:.6rem; justify-content:center; margin:0 0 1.25em }
@@ -195,10 +189,18 @@ body.page{
.party-list{ display:grid; gap:.35rem; margin:.6rem 0 1rem; padding-left:1.2rem }
/* ===== Mobile ===== */
-@media (max-width:480px){
- :root{ --gap:8px; --header-h:48px }
- .kicker{ font-size:11px }
- .title{ font-size:clamp(16px,5.2vw,22px) }
- .desc{ font-size:13px }
- .icon{ width:clamp(28px,11vh,90px) }
+@media (max-width:520px){
+ :root{ --gap:10px; --header-h:48px }
+
+ /* let header wrap and push search under brand to avoid horizontal scroll */
+ .site-header{ flex-wrap:wrap; justify-content:center }
+ .search-bar{ flex:1 1 100%; max-width:100%; width:100%; margin-top:8px }
+ .search-bar input{ width:100% }
+
+ /* smaller icons so they center inside 1x1 / 2x2 tiles on phones */
+ .icon{ width:clamp(28px,14vw,56px); height:auto }
+ .icon.icon-text{ font-size:clamp(22px,12vw,56px) }
+
+ /* keep caption readable but compact */
+ .icon-caption{ font-size:11px; max-width:95% }
}
diff --git a/styles.css.bak b/styles.css.bak
new file mode 100644
index 0000000..c458064
--- /dev/null
+++ b/styles.css.bak
@@ -0,0 +1,204 @@
+:root{
+ --bg:#121212;
+ --fg:#eceff3;
+ --gap:12px;
+ --reveal-ms:2600ms;
+ --header-h:56px;
+ --safe-top: env(safe-area-inset-top, 0px);
+ --safe-bottom: env(safe-area-inset-bottom, 0px);
+
+ /* Hint knobs (wired from config.hint) */
+ --hint-opacity:.12;
+ --hint-angle:-12deg;
+ --hint-size:16px;
+ --hint-row-gap:8px;
+ --hint-plane:3;
+ --hint-feather:0%;
+}
+
+/* ===== Base ===== */
+*{ box-sizing:border-box }
+html,body{ height:100% }
+body{
+ margin:0; color:var(--fg); background:var(--bg);
+ font:16px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Arial;
+ overflow:hidden; /* tiles view */
+}
+/* make sure tiles never pick UA link blue */
+a{ color:inherit; text-decoration:none }
+
+/* ===== Header ===== */
+.site-header{
+ position:fixed; left:0; right:0; top:0;
+ height:calc(var(--header-h) + var(--safe-top));
+ display:flex; align-items:center; justify-content:center;
+ padding:0 clamp(12px,3vw,24px);
+ background:rgba(0,0,0,.55); backdrop-filter:blur(6px);
+ border-bottom:1px solid rgba(255,255,255,.10);
+ z-index:2000;
+}
+.site-header .brand{
+ font-weight:900; letter-spacing:.22em; text-transform:uppercase;
+ font-size:clamp(12px,1.4vw,14px); opacity:.95; text-decoration:none;
+}
+
+/* ===== Grid ===== */
+.grid{
+ position:fixed; inset:0;
+ top:calc(var(--header-h) + var(--safe-top));
+ display:grid; grid-auto-flow:dense;
+ grid-template-columns:repeat(12, 80px);
+ grid-auto-rows:80px;
+ gap:var(--gap); padding:var(--gap);
+}
+
+/* ===== Tiles ===== */
+.tile{
+ position:relative; display:block; overflow:hidden;
+
+ /* Prod aesthetic: bright config color is the actual tile bg */
+ background:var(--color,#1f2937);
+
+ /* subtle frame */
+ border:1px solid rgba(255,255,255,.08);
+
+ transition:opacity .55s ease, transform .55s ease;
+}
+
+/* Cover sits ABOVE text (to hide it until reveal) and BELOW hint+icon */
+.cover{
+ position:absolute; inset:0; z-index:2; background:#242424;
+ transform:translate(0,0);
+ transition:transform var(--reveal-ms) cubic-bezier(.22,.8,.18,1);
+ pointer-events:none; will-change:transform;
+}
+
+/* Layer stack matches prod look:
+ * 1 = text, 2 = cover, 3 = hint (visible when hidden), 4 = icon (visible when hidden) */
+.layer,.cover{
+ position:absolute; inset:0; display:grid; place-items:center; justify-items:center;
+ text-align:center; padding:clamp(12px,3vh,28px);
+ transition:opacity .55s ease, transform .55s ease; pointer-events:none;
+}
+.text-layer{ z-index:1; opacity:0; transform:translateY(8px) }
+.hint-layer{ z-index:3; opacity:var(--hint-opacity); }
+.icon-layer{ z-index:4; opacity:1 }
+
+.icon{ width:clamp(36px,9vh,110px); height:auto; object-fit:contain }
+.icon.icon-text{ width:auto; font-size:clamp(24px,10vh,120px); line-height:1 }
+.kicker{ font-weight:900; letter-spacing:.22em; text-transform:uppercase; font-size:12px; opacity:.95; margin:0 0 .25em }
+.title{ margin:0; font-weight:1000; letter-spacing:.2px; font-size:clamp(16px,2.2vw,28px); line-height:1.1 }
+.desc{ margin:.45em 0 0; opacity:.92; font-size:14px; max-width:min(72ch,90%) }
+.text-layer>div{ display:flex; flex-direction:column; align-items:center }
+
+/* Reveal: slide the cover off; fade icon out; fade text in; hide hint */
+.tile:hover .cover,
+.tile:focus-visible .cover,
+.tile.js-reveal .cover{ transform:translate(var(--dx),var(--dy)) }
+
+.tile:hover .icon-layer,
+.tile:focus-visible .icon-layer,
+.tile.js-reveal .icon-layer{ opacity:0; transform:scale(.965) }
+
+.tile:hover .text-layer,
+.tile:focus-visible .text-layer,
+.tile.js-reveal .text-layer{ opacity:1; transform:none }
+
+/* Hide hint during reveal for the flip */
+.tile:hover .hint-layer,
+.tile:focus-visible .hint-layer,
+.tile.js-reveal .hint-layer{ opacity:0 }
+
+@media (prefers-reduced-motion:reduce){
+ .tile,.cover,.layer{ transition:none }
+}
+
+/* ===== Hint layer geometry (unchanged from prod) ===== */
+.hint-layer{
+ position:absolute; inset:0; pointer-events:none;
+ display:block;
+ /* optional soft fade at extreme sides */
+ -webkit-mask-image:linear-gradient(to right,
+ rgba(0,0,0,0) var(--hint-feather),
+ #000 calc(var(--hint-feather) + 1px),
+ #000 calc(100% - var(--hint-feather) - 1px),
+ rgba(0,0,0,0) calc(100% - var(--hint-feather)));
+ mask-image:linear-gradient(to right,
+ rgba(0,0,0,0) var(--hint-feather),
+ #000 calc(var(--hint-feather) + 1px),
+ #000 calc(100% - var(--hint-feather) - 1px),
+ rgba(0,0,0,0) calc(100% - var(--hint-feather)));
+}
+.hint-stack{
+ position:absolute; left:50%; top:50%;
+ transform:translate(-50%,-50%) rotate(var(--hint-angle));
+ width:calc(var(--hint-plane) * 100%);
+ height:calc(var(--hint-plane) * 100%);
+ display:flex; flex-direction:column; justify-content:center; align-items:center;
+ gap:var(--hint-row-gap);
+ overflow:hidden; /* clip rows to plane; tile clips plane */
+}
+.hint-row{
+ position:relative; display:block;
+ white-space:nowrap; letter-spacing:.22em; line-height:1;
+ font-weight:900; text-transform:uppercase; font-size:var(--hint-size);
+ transform:translateX(var(--row-offset-px, 0px));
+}
+.hint-line{ opacity:1 } /* keep lines solid */
+
+/* ===== Static pages (from prod) ===== */
+body.page{
+ min-height:100vh; margin:0; color:var(--fg); background:var(--bg);
+ overflow-y:auto; overflow-x:hidden;
+}
+.page-wrap{
+ box-sizing:border-box; min-height:100svh;
+ padding:var(--gap);
+ padding-top:calc(var(--gap) + var(--header-h) + var(--safe-top));
+ padding-bottom:calc(var(--gap) + var(--safe-bottom));
+ display:grid; grid-template-rows:auto 1fr; gap:var(--gap);
+}
+.page-header{ display:flex; align-items:center; justify-content:space-between; gap:var(--gap) }
+.back-btn,.btn{
+ display:inline-flex; align-items:center; gap:.6rem;
+ border-radius:999px; padding:.6rem .9rem;
+ border:1px solid rgba(255,255,255,0.10);
+ background:rgba(255,255,255,0.04);
+ color:inherit; text-decoration:none; outline:none;
+ transition:background 300ms, transform 160ms;
+}
+.btn.primary{ background:rgba(255,255,255,0.08) }
+.back-btn:hover,.btn:hover{ background:rgba(255,255,255,0.07); transform:translateY(-1px) }
+.back-btn:focus-visible,.btn:focus-visible{ box-shadow:0 0 0 3px rgba(154,168,255,.45) }
+.back-btn svg{ width:18px; height:18px; flex:0 0 auto; opacity:.95 }
+.page-main{ display:grid; place-items:start center }
+.article{
+ width:min(90ch,100%); line-height:1.6;
+ background:rgba(255,255,255,0.04);
+ border:1px solid rgba(255,255,255,0.10);
+ border-radius:16px;
+ padding:clamp(16px,3vw,28px);
+}
+.article h1{ margin:0 0 .35em; font-size:clamp(22px,3.2vw,36px); letter-spacing:.2px; text-align:center }
+.subtitle{ margin:0 0 1.1em; opacity:.85; text-align:center; font-size:clamp(14px,1.6vw,16px) }
+.cta-row{ display:flex; flex-wrap:wrap; gap:.6rem; justify-content:center; margin:0 0 1.25em }
+.callout{ margin:1.2em 0; padding:1rem; border:1px solid rgba(255,255,255,0.10); border-radius:12px; background:rgba(255,255,255,.03) }
+.callout.warn{ border-color:rgba(255,99,99,.5); background:rgba(255,0,0,.06) }
+.callout h3{ margin:.2em 0 .6em; font-size:1rem; letter-spacing:.08em; text-transform:uppercase; opacity:.9 }
+.article h2{ margin:1.6em 0 .6em; font-size:clamp(18px,2.4vw,24px) }
+.article h3{ margin:1.2em 0 .4em; font-size:clamp(16px,2vw,20px) }
+.article p{ margin:.8em 0 }
+.article ul,.article ol{ margin:.8em 0 .8em 1.2em }
+.article a{ color:inherit; text-decoration:underline; text-underline-offset:2px }
+.legal{ border-color:rgba(255,255,255,.18) }
+.sig{ margin-top:1.5em; padding-top:1em; border-top:1px solid rgba(255,255,255,0.10); display:grid; gap:.25rem }
+.party-list{ display:grid; gap:.35rem; margin:.6rem 0 1rem; padding-left:1.2rem }
+
+/* ===== Mobile ===== */
+@media (max-width:480px){
+ :root{ --gap:8px; --header-h:48px }
+ .kicker{ font-size:11px }
+ .title{ font-size:clamp(16px,5.2vw,22px) }
+ .desc{ font-size:13px }
+ .icon{ width:clamp(28px,11vh,90px) }
+}
diff --git a/support.html b/support.html
index 1157c82..fa84274 100644
--- a/support.html
+++ b/support.html
@@ -10,6 +10,7 @@
@@ -20,7 +21,7 @@
Config default: /configs/index.json (override with ?config=name)
====================================================== */
(async () => {
- /* ---------- tiny seeded RNG (deterministic) ---------- */
+ /* ---------- tiny seeded RNG ---------- */
function xmur3(str){
let h = 1779033703 ^ str.length;
for (let i=0;i{
@@ -217,27 +217,34 @@
grid.textContent = '';
grid.appendChild(frag2);
- /* ---------- layout (header-aware) ---------- */
+ /* ---------- layout (header-aware, edge-rounding safe) ---------- */
function layout(){
+ // viewport and header height
const W = innerWidth;
const H = innerHeight;
- const VV = visualViewport;
- const vh = (VV && VV.height) ? VV.height : H;
const header = document.getElementById('siteHeader');
- const HH = Math.max(0, vh - (header ? header.getBoundingClientRect().height : 0));
+ const HH = Math.max(1, H - (header ? header.getBoundingClientRect().height : 0));
+ // gap
const gap = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--gap')) || 0;
+
+ // USE CONTENT BOX SIZES (padding removed) to avoid last-column overflow
+ const contentW = W - gap * 2;
+ const contentH = HH - gap * 2;
+
const fit = (cfg.grid?.fit || 'stretch').toLowerCase();
+ // total tile area
const S = ordered.reduce((s, el)=> s + (+el.dataset.w * +el.dataset.h), 0) || 1;
- const A = W / (HH || 1);
+ const A = contentW / (contentH || 1);
let cols = Math.max(1, Math.round(Math.sqrt(S * A)));
let rows = Math.max(1, Math.ceil(S / cols));
+ // square unit that fits both axes, using content sizes
const unitFor = (c, r) => Math.max(1, Math.min(
- Math.floor((W - gap*(c+1)) / c),
- Math.floor((HH - gap*(r+1)) / r)
+ Math.floor((contentW - gap * (c - 1)) / c),
+ Math.floor((contentH - gap * (r - 1)) / r)
));
let best = { cols, rows, unit: unitFor(cols, rows), holes: Math.max(0, cols*rows - S) };
@@ -252,25 +259,29 @@
grid.style.gap = gap + 'px';
grid.style.padding = gap + 'px';
+ // sub-pixel epsilon to guarantee no overflow in final column/row
+ const EPS = 0.75;
+
if (fit === 'stretch'){
- const unitW = (W - gap * (best.cols + 1)) / best.cols;
- const unitH = (HH - gap * (best.rows + 1)) / best.rows;
+ const unitW = (contentW - gap * (best.cols - 1)) / best.cols - EPS;
+ const unitH = (contentH - gap * (best.rows - 1)) / best.rows - EPS;
grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unitW}px)`;
- grid.style.gridAutoRows = `${unitH}px`;
+ grid.style.gridAutoRows = `${unitH}px`;
} else {
- grid.style.gridTemplateColumns = `repeat(${best.cols}, ${best.unit}px)`;
- grid.style.gridAutoRows = `${best.unit}px`;
+ const unit = best.unit - EPS;
+ grid.style.gridTemplateColumns = `repeat(${best.cols}, ${unit}px)`;
+ grid.style.gridAutoRows = `${unit}px`;
}
ordered.forEach(n=>{
- /* Phone-only span clamp to keep everything on one page */
+ // phone span clamp
const isPhone = innerWidth <= 520;
const w = Math.min(+n.dataset.w, isPhone ? 2 : +n.dataset.w);
const h = Math.min(+n.dataset.h, isPhone ? 2 : +n.dataset.h);
n.style.gridColumn = `span ${w}`;
n.style.gridRow = `span ${h}`;
- /* Deterministic wipe dir (prod = 140%) */
+ // deterministic wipe
const title = n.querySelector('.title')?.textContent || '';
const R = rngFrom('wipe|'+title);
const dirs = [[1,0],[-1,0],[0,-1],[0,1],[1,-1],[-1,-1],[-1,1],[1,1]];
@@ -278,7 +289,7 @@
n.style.setProperty('--dx', (dx*140)+'%');
n.style.setProperty('--dy', (dy*140)+'%');
- /* Recompute hint row offsets using current tile width */
+ // hint offsets
const tw = n.getBoundingClientRect().width;
n.querySelectorAll('.hint-row').forEach(row=>{
const scale = parseFloat(row.dataset.offsetScale || '0');
@@ -291,8 +302,8 @@
addEventListener('resize', resched);
if (visualViewport){ visualViewport.addEventListener('resize', resched); visualViewport.addEventListener('scroll', resched); }
- // JS-assisted reveal class only
- ordered.forEach(tile=>{
+ // js-assisted reveal class
+ document.querySelectorAll('.tile').forEach(tile=>{
tile.addEventListener('mouseenter',()=>tile.classList.add('js-reveal'));
tile.addEventListener('mouseleave',()=>tile.classList.remove('js-reveal'));
tile.addEventListener('focusin', ()=>tile.classList.add('js-reveal'));