From f2c01a4b9a2addce9bfefb7a6061868f14df38fc Mon Sep 17 00:00:00 2001 From: makearmy Date: Thu, 11 Sep 2025 23:07:27 -0400 Subject: [PATCH] Fix mobile edge rounding: precise content-size grid + epsilon; keep center alignment; captions under icons --- configs/social.json | 11 -- configs/support.json | 4 +- index.html | 75 ++++++----- index.html.bak | 305 +++++++++++++++++++++++++++++++++++++++++++ social.html | 75 ++++++----- styles.css | 90 ++++++------- styles.css.bak | 204 +++++++++++++++++++++++++++++ support.html | 75 ++++++----- 8 files changed, 686 insertions(+), 153 deletions(-) create mode 100644 index.html.bak create mode 100644 styles.css.bak 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'));