1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 5 <title>Steam Next Fest</title> 6 <meta charset="utf-8"> 7 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 8 9 <link rel="preload" as="style" href="https://dearindiegames.com/assets/built/screen.css?v=xPGozQS2SFMZDlmQ"> 10 <link rel="preload" as="script" href="https://dearindiegames.com/assets/built/source.js?v=33sa4-qy5thAFPQH"> 11 12 <link rel="preload" as="font" type="font/woff2" href="https://dearindiegames.com/assets/fonts/inter-roman.woff2?v=OecsB5TBLy27FKD2" crossorigin="anonymous"> 13<style> 14 @font-face { 15 font-family: "Inter"; 16 font-style: normal; 17 font-weight: 100 900; 18 font-display: optional; 19 src: url(https://dearindiegames.com/assets/fonts/inter-roman.woff2?v=OecsB5TBLy27FKD2) format("woff2"); 20 unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 21 } 22</style> 23 24 <link rel="stylesheet" type="text/css" href="https://dearindiegames.com/assets/built/screen.css?v=xPGozQS2SFMZDlmQ"> 25 26 <style> 27 :root { 28 --background-color: #232323 29 } 30 </style> 31 32
32<script> 33 /* The script for calculating the color contrast has been taken from 34 https://gomakethings.com/dynamically-changing-the-text-color-based-on-background-color-contrast-with-vanilla-js/ */ 35 var accentColor = getComputedStyle(document.documentElement).getPropertyValue('--background-color'); 36 accentColor = accentColor.trim().slice(1); 37 38 if (accentColor.length === 3) { 39 accentColor = accentColor[0] + accentColor[0] + accentColor[1] + accentColor[1] + accentColor[2] + accentColor[2]; 40 } 41 42 var r = parseInt(accentColor.substr(0, 2), 16); 43 var g = parseInt(accentColor.substr(2, 2), 16); 44 var b = parseInt(accentColor.substr(4, 2), 16); 45 var yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000; 46 var textColor = (yiq >= 128) ? 'dark' : 'light'; 47 48 document.documentElement.className = `has-${textColor}-text`; 49 </script>
49 50 51 <meta name="description" content="From emotional journeys to wonderfully strange experiments, Iâve curated my Steam Next Fest highlights into one easy page. Watch the trailers, sort by genre, and dive straight into the demos."> 52 <link rel="icon" href="https://storage.ghost.io/c/ef/27/ef273632-f447-4457-b65f-a3da589d0492/content/images/size/w256h256/2024/04/THE-DIG_ProfilePic_Alpha-1.png" type="image/png"> 53 <link rel="canonical" href="https://dearindiegames.com/steam-next-fest/"> 54 <meta name="referrer" content="no-referrer-when-downgrade"> 55 56 <meta property="og:site_name" content="Dear IndieGames"> 57 <meta property="og:type" content="website"> 58 <meta property="og:title" content="Steam Next Fest"> 59 <meta property="og:description" content="From emotional journeys to wonderfully strange experiments, Iâve curated my Steam Next Fest highlights into one easy page. Watch the trailers, sort by genre, and dive straight into the demos."> 60 <meta property="og:url" content="https://dearindiegames.com/steam-next-fest/"> 61 <meta property="og:image" content="https://storage.ghost.io/c/ef/27/ef273632-f447-4457-b65f-a3da589d0492/content/images/size/w1200/2026/05/Steam-Next-Fest-Dear-IndieGames.png"> 62 <meta property="article:published_time" content="2026-02-23T12:24:33.000Z"> 63 <meta property="article:modified_time" content="2026-06-30T22:15:04.000Z"> 64 <meta property="article:publisher" content="https://www.facebook.com/dearindiegames"> 65 <meta name="twitter:card" content="summary_large_image"> 66 <meta name="twitter:title" content="Steam Next Fest"> 67 <meta name="twitter:description" content="From emotional journeys to wonderfully strange experiments, Iâve curated my Steam Next Fest highlights into one easy page. Watch the trailers, sort by genre, and dive straight into the demos."> 68 <meta name="twitter:url" content="https://dearindiegames.com/steam-next-fest/"> 69 <meta name="twitter:image" content="https://storage.ghost.io/c/ef/27/ef273632-f447-4457-b65f-a3da589d0492/content/images/size/w1200/2026/05/Steam-Next-Fest-Dear-IndieGames.png"> 70 <meta name="twitter:label1" content="Written by"> 71 <meta name="twitter:data1" content="Vasco De Mello"> 72 <meta name="twitter:site" content="@DearIndieGames"> 73 <meta property="og:image:width" content="1200"> 74 <meta property="og:image:height" content="675"> 75 76
76<script type="application/ld+json"> 77{ 78 "@context": "https://schema.org", 79 "@type": "Article", 80 "publisher": { 81 "@type": "Organization", 82 "name": "Dear IndieGames", 83 "url": "https://dearindiegames.com/", 84 "logo": { 85 "@type": "ImageObject", 86 "url": "https://storage.ghost.io/c/ef/27/ef273632-f447-4457-b65f-a3da589d0492/content/images/2024/04/Dear-IndieGames-Logo-White-2.png" 87 } 88 }, 89 "author": { 90 "@type": "Person", 91 "name": "Vasco De Mello", 92 "image": { 93 "@type": "ImageObject", 94 "url": "https://storage.ghost.io/c/ef/27/ef273632-f447-4457-b65f-a3da589d0492/content/images/2026/01/THE-DIG_ProfilePic_Alpha.png", 95 "width": 1200, 96 "height": 1200 97 }, 98 "url": "https://dearindiegames.com/author/vasco/", 99 "sameAs": [ 100 "https://www.threads.net/@dearindiegames", 101 "https://bsky.app/profile/dearindiegames.bsky.social", 102 "https://www.tiktok.com/@dearindiegames" 103 ] 104 }, 105 "headline": "Steam Next Fest", 106 "url": "https://dearindiegames.com/steam-next-fest/", 107 "datePublished": "2026-02-23T12:24:33.000Z", 108 "dateModified": "2026-06-30T22:15:04.000Z", 109 "image": { 110 "@type": "ImageObject", 111 "url": "https://storage.ghost.io/c/ef/27/ef273632-f447-4457-b65f-a3da589d0492/content/images/size/w1200/2026/05/Steam-Next-Fest-Dear-IndieGames.png", 112 "width": 1200, 113 "height": 675 114 }, 115 "description": "From emotional journeys to wonderfully strange experiments, Iâve curated my Steam Next Fest highlights into one easy page. Watch the trailers, sort by genre, and dive straight into the demos.", 116 "mainEntityOfPage": "https://dearindiegames.com/steam-next-fest/" 117} 118 </script>
118 119 120 <meta name="generator" content="Ghost 6.68"> 121 <link rel="alternate" type="application/rss+xml" title="Dear IndieGames" href="https://dearindiegames.com/rss/"> 122
122<script defer src="https://cdn.jsdelivr.net/ghost/portal@~2.71/umd/portal.min.js" data-i18n="true" data-ghost="https://dearindiegames.com/" data-key="6569ccb67c54c2da05e258be48" data-api="https://dear-indiegames.ghost.io/ghost/api/content/" data-locale="en" crossorigin="anonymous"></script>
122<style id="gh-members-styles">.gh-post-upgrade-cta-content, 123.gh-post-upgrade-cta { 124 display: flex; 125 flex-direction: column; 126 align-items: center; 127 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 128 text-align: center; 129 width: 100%; 130 color: #ffffff; 131 font-size: 16px; 132} 133 134.gh-post-upgrade-cta-content { 135 border-radius: 8px; 136 padding: 40px 4vw; 137} 138 139.gh-post-upgrade-cta h2 { 140 color: #ffffff; 141 font-size: 28px; 142 letter-spacing: -0.2px; 143 margin: 0; 144 padding: 0; 145} 146 147.gh-post-upgrade-cta p { 148 margin: 20px 0 0; 149 padding: 0; 150} 151 152.gh-post-upgrade-cta small { 153 font-size: 16px; 154 letter-spacing: -0.2px; 155} 156 157.gh-post-upgrade-cta a { 158 color: #ffffff; 159 cursor: pointer; 160 font-weight: 500; 161 box-shadow: none; 162 text-decoration: underline; 163} 164 165.gh-post-upgrade-cta a:hover { 166 color: #ffffff; 167 opacity: 0.8; 168 box-shadow: none; 169 text-decoration: underline; 170} 171 172.gh-post-upgrade-cta a.gh-btn { 173 display: block; 174 background: #ffffff; 175 text-decoration: none; 176 margin: 28px 0 0; 177 padding: 8px 18px; 178 border-radius: 4px; 179 font-size: 16px; 180 font-weight: 600; 181} 182 183.gh-post-upgrade-cta a.gh-btn:hover { 184 opacity: 0.92; 185}</style> 186
186<script defer src="https://cdn.jsdelivr.net/ghost/sodo-search@~1.8/umd/sodo-search.min.js" data-key="6569ccb67c54c2da05e258be48" data-styles="https://cdn.jsdelivr.net/ghost/sodo-search@~1.8/umd/main.css" data-sodo-search="https://dear-indiegames.ghost.io/" data-locale="en" crossorigin="anonymous"></script>
186 187 188 <link href="https://dearindiegames.com/webmentions/receive/" rel="webmention"> 189
189<script defer src="/public/cards.min.js?v=ShRHxgy4po8zN-Wf"></script>
189 190 <link rel="stylesheet" type="text/css" href="/public/cards.min.css?v=WwnU9jw5ancNC8Gc"> 191
191<script defer src="/public/comment-counts.min.js?v=oFYkaGLdiMqB8VN9" data-ghost-comments-counts-api="https://dearindiegames.com/members/api/comments/counts/"></script>
191 192
192<script defer src="/public/member-attribution.min.js?v=AKG4hWena9j3yX3I"></script>
192<style>:root {--ghost-accent-color: #ff5858;}</style> 193 <style> 194.kg-cta-content-inner > div > p > b > strong:first-child { 195 font-size: 1.7em; 196 font-weight: 700; 197 display: block; 198 line-height: 0.0; 199} 200 .kg-cta-bg-green { 201 background: rgba(52,183,67,.3) !important; 202} 203</style>
204<script defer src="https://analytics.eu.umami.is/script.js" data-website-id="5e16f1d8-d207-40d6-87f5-fc1231c9862e"></script>
204 205 206<!-- Google tag (gtag.js) -->
vendor: 64 bytes, lines 206-207
206 207<script async src="https://www.googletagmanager.com/gtag/js?id=
207G-VYXMRC6XLR
vendor: 12 bytes, line 207
207"></script>
208<script> 209
vendor: 134 bytes, lines 209-213
209window.dataLayer = window.dataLayer || []; 210 function gtag(){dataLayer.push(arguments);} 211 gtag('js', new Date()); 212 213 gtag('config', '
213G-VYXMRC6XLR
vendor: 4 bytes, line 213
213');
214</script>
214 215 216<style> 217/* New Trailers This Week */ 218.ntw{ 219 position:relative; 220 --bg:#11131a;--bd:rgba(255,255,255,.12);--tx:rgba(255,255,255,.92);--mut:rgba(255,255,255,.7); 221 --sh:0 10px 30px rgba(0,0,0,.35);--r:16px;--gap:14px; 222 223 /* Font sizes (edit these) */ 224 --fs-title:1.4rem; 225 --fs-card:1.1rem; 226 --fs-date:1.1rem; 227 228 color:var(--tx);margin:18px 0 22px;width:100%; 229} 230#ntwInjectedWrap{width:100%;max-width:none;justify-self:stretch;align-self:stretch;grid-column:1/-1} 231 232/* Header */ 233.ntw__header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 10px} 234.ntw__header h2{margin:0;font-size:var(--fs-title);letter-spacing:.2px} 235 236/* Frame & grid */ 237.ntw__frame{position:relative; overflow:visible; border:1px solid var(--bd);border-radius:var(--r);background:rgba(255,255,255,.03);padding:var(--gap);box-shadow:var(--sh)} 238.ntw__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gap);width:100%} 239@media (max-width:860px){.ntw__grid{grid-template-columns:repeat(2,minmax(0,1fr))}} 240@media (max-width:560px){.ntw__grid{grid-template-columns:1fr}} 241 242/* Cards */ 243.ntw__card{border:1px solid var(--bd);border-radius:14px;overflow:hidden;background:var(--bg);cursor:pointer;text-align:left;padding:0;transition:transform .14s ease,border-color .14s ease} 244.ntw__card:hover{transform:translateY(2px);border-color:rgba(255,255,255,.2)} 245.ntw__thumb{position:relative;aspect-ratio:16/9;background:#0a0b0f} 246.ntw__thumb img{width:100%;height:100%;object-fit:cover;display:block} 247.ntw__meta{padding:10px 12px 12px} 248.ntw__title{font-size:var(--fs-card);line-height:1.25;margin:0 0 6px;color:var(--tx);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} 249.ntw__date{font-size:var(--fs-date);color:var(--mut)} 250 251/* Big centered play button */ 252.ntw__playBadge{display:none!important} 253.ntw__playCenter{position:absolute;inset:0;display:grid;place-items:center;pointer-events:none} 254 255.ntw__playIcon{ 256 width:64px; 257 height:64px; 258 border-radius:50%; /* FORCE circle */ 259 display:grid; 260 place-items:center; 261 262 background:#ff5858 !important; /* pink circle */ 263 border:1px solid rgba(255,255,255,.25); 264 color:rgba(255,255,255,.75); /* grey-ish play icon */ 265 266 font-size:28px; 267 line-height:1; 268 transform:translateX(2px); 269 transition:transform .14s ease, filter .14s ease; 270} 271 272 /* Pulse animation */ 273@keyframes ntw-play-pulse { 274 0% { box-shadow: 0 0 0 0 rgba(255, 88, 88, 0.55); } 275 70% { box-shadow: 0 0 0 18px rgba(255, 88, 88, 0); } 276 100% { box-shadow: 0 0 0 0 rgba(255, 88, 88, 0); } 277} 278 279/* Trigger pulse on hover */ 280.ntw__card:hover .ntw__playIcon{ 281 animation: ntw-play-pulse 1.2s ease-out infinite; 282} 283 284/* Arrows: vertically centered to the whole carousel (frame) */ 285.ntw__controls{ 286 position:absolute; 287 left:-70px;right:-70px; 288 top:50%;transform:translateY(-50%); 289 display:flex;justify-content:space-between;gap:10px; 290 pointer-events:none; 291 z-index:5; 292} 293.ntw__btn{ 294 pointer-events:auto; 295 width:54px;height:54px;border-radius:999px; 296 border:1px solid rgba(255,255,255,.22); 297 background:#ff5858;color:var(--tx); 298 box-shadow: 0 10px 24px rgba(0,0,0,.45); 299 font-size:28px;line-height:1;cursor:pointer; 300 transition:transform .12s ease,background .12s ease,opacity .12s ease; 301} 302.ntw__btn:hover{transform:translateY(-1px) scale(1.03);background:#f59a9a} 303.ntw__btn:disabled{opacity:.35;cursor:not-allowed;transform:none} 304 305/* Modal */ 306.ntw__modal{position:fixed;inset:0;display:none;z-index:9999} 307.ntw__modal[aria-hidden="false"]{display:grid;place-items:center} 308.ntw__backdrop{position:absolute;inset:0;background:rgba(0,0,0,.7)} 309.ntw__modalCard{position:relative;width:min(980px,calc(100vw - 24px));border-radius:18px;border:1px solid rgba(255,255,255,.16);background:#0e1016;box-shadow:0 25px 80px rgba(0,0,0,.6);overflow:hidden} 310.ntw__modalTop{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:14px 16px 10px;border-bottom:1px solid rgba(255,255,255,.1)} 311.ntw__modalTitle{font-size:1.8rem;line-height:1.25} 312.ntw__modalDate{margin-top:4px;font-size:1.6rem;color:rgba(255,255,255,.7)} 313.ntw__modalStore{color: var(--ghost-accent-color, #ff5858); text-decoration: underline; font-weight: 600;} 314.ntw__modalStore:hover{ opacity:.9; } 315.ntw__modalSep{ color: rgba(255,255,255,.7); } 316.ntw__close{width:40px;height:40px;border-radius:999px;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.06);color:rgba(255,255,255,.92);cursor:pointer} 317.ntw__videoWrap{position:relative;aspect-ratio:16/9;background:#000} 318.ntw__videoWrap iframe{position:absolute;inset:0;width:100%;height:100%;border:0} 319 320 /* Tablet + mobile: swipe carousel with peek + fade */ 321@media (max-width: 860px){ 322 .ntw__frame{ padding: 12px; } /* a touch tighter on mobile/tablet */ 323 324 .ntw__grid{ 325 display:flex; 326 overflow-x:auto; 327 scroll-snap-type:x mandatory; 328 -webkit-overflow-scrolling:touch; 329 gap:14px; 330 331 /* Peek: small right padding so next card shows */ 332 padding-right: 18%; 333 scrollbar-width:none; 334 } 335 .ntw__grid::-webkit-scrollbar{ display:none; } 336 337 .ntw__card{ 338 flex: 0 0 78%; /* tablet: show most of card + peek next */ 339 scroll-snap-align:start; 340 } 341 342 /* Fade edge to hint more content */ 343 .ntw__grid{ 344 -webkit-mask-image: linear-gradient(to right, #000 0%, #000 86%, transparent 100%); 345 mask-image: linear-gradient(to right, #000 0%, #000 86%, transparent 100%); 346 } 347 348 /* Arrows: hide on touch layouts */ 349 .ntw__controls{ display:none; } 350} 351 352@media (max-width: 560px){ 353 .ntw__grid{ padding-right: 22%; } 354 .ntw__card{ flex-basis: 86%; } /* mobile: bigger card, still peeks */ 355} 356 357</style> 358
359<script> 360(()=> { 361 const FEED="https://opensheet.elk.sh/1Noigoiz76CWPT1MapoUWFi7-ARHyHIdxY19PkatfC_s/1"; 362 const home=()=>document.body?.classList.contains("home-template"); 363 const ytId=u=>(u?.match(/youtu\.be\/([a-zA-Z0-9_-]{6,})/)||u?.match(/[?&]v=([a-zA-Z0-9_-]{6,})/)||u?.match(/youtube\.com\/embed\/([a-zA-Z0-9_-]{6,})/))?.[1]||null; 364 const esc=s=>String(s||"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'"); 365 const thumb=id=>`https://img.youtube.com/vi/${id}/hqdefault.jpg`; 366 const isTouchLayout=()=>matchMedia("(max-width: 860px)").matches; 367 368 const shell=()=>{ 369 const el=document.createElement("section"); 370 el.id="ntwInjectedWrap"; 371 el.className="ntw"; 372 el.innerHTML=` 373 <div class="ntw__header"><h2 id="ntw-title">New Trailers This Week</h2></div> 374 375 <div class="ntw__frame" role="region" aria-label="Trailer carousel"> 376 <div class="ntw__controls" aria-label="Trailer carousel controls"> 377 <button class="ntw__btn" id="ntwPrev" type="button" aria-label="Previous trailers">â¹</button> 378 <button class="ntw__btn" id="ntwNext" type="button" aria-label="Next trailers">âº</button> 379 </div> 380 <div class="ntw__grid" id="ntwGrid" aria-live="polite"></div> 381 </div> 382 383 <div class="ntw__modal" id="ntwModal" aria-hidden="true"> 384 <div class="ntw__backdrop" data-close="true"></div> 385 <div class="ntw__modalCard" role="dialog" aria-modal="true" aria-labelledby="ntwModalTitle"> 386 <div class="ntw__modalTop"> 387 <div> 388 <div class="ntw__modalTitle" id="ntwModalTitle"></div> 389 <div class="ntw__modalDate"> <span id="ntwModalDate"></span> <span class="ntw__modalSep" aria-hidden="true"> â </span> 390 <a id="ntwModalStore" class="ntw__modalStore" href="#" target="_blank" rel="noopener noreferrer">Steam Store</a></div> 391 </div> 392 <button class="ntw__close" type="button" aria-label="Close video" data-close="true">â</button> 393 </div> 394 <div class="ntw__videoWrap"> 395 <iframe id="ntwPlayer" title="YouTube trailer player" src="" 396 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 397 allowfullscreen></iframe> 398 </div> 399 </div> 400 </div>`; 401 return el; 402 }; 403 404 const inject=()=>{ 405 if(document.getElementById("ntwInjectedWrap")) return true; 406 const latest=[...document.querySelectorAll("section.gh-container.is-list h2.gh-container-title")] 407 .find(h=>(h.textContent||"").trim().toLowerCase()==="latest"); 408 if(!latest) return false; 409 latest.insertAdjacentElement("beforebegin", shell()); 410 return true; 411 }; 412 413 const init=async()=>{ 414 const $=id=>document.getElementById(id); 415 const grid=$("ntwGrid"), prev=$("ntwPrev"), next=$("ntwNext"), 416 modal=$("ntwModal"), mt=$("ntwModalTitle"), md=$("ntwModalDate"), 417 store=$("ntwModalStore"), player=$("ntwPlayer"); 418 const frame = grid.closest(".ntw__frame"); 419 420 let items=[], start=0, per=3; 421 422 const calcPer=()=>{ 423 const gtc=getComputedStyle(grid).gridTemplateColumns; 424 const cols=(gtc && gtc!=="none") ? gtc.split(" ").length : 3; 425 per=Math.max(1, Math.min(3, cols)); 426 }; 427 428 const open=it=>{ 429 mt.textContent=it.title||"Trailer"; 430 md.textContent=it.date||""; 431 432 if(it.storeUrl){ 433 store.href=it.storeUrl; 434 store.style.display=""; 435 store.setAttribute("aria-label","Open Steam store page (opens in a new tab)"); 436 }else{ 437 store.href="#"; 438 store.style.display="none"; // hide link if missing 439 } 440 441 player.src=`https://www.youtube-nocookie.com/embed/${it.id}?autoplay=1&rel=0`; 442 modal.setAttribute("aria-hidden","false"); 443 document.documentElement.style.overflow="hidden"; 444}; 445 446 const close=()=>{ 447 modal.setAttribute("aria-hidden","true"); 448 player.src=""; 449 store.href="#"; 450 store.style.display="none"; 451 document.documentElement.style.overflow=""; 452}; 453 454 const cardEl=(it)=>{ 455 const b=document.createElement("button"); 456 b.type="button"; b.className="ntw__card"; b.setAttribute("aria-label","Open trailer: "+it.title); 457 b.innerHTML=` 458 <div class="ntw__thumb"> 459 <img loading="lazy" src="${it.thumb}" alt=""> 460 <div class="ntw__playCenter" aria-hidden="true"><span class="ntw__playIcon">â¶</span></div> 461 </div> 462 <div class="ntw__meta"> 463 <p class="ntw__title">${esc(it.title)}</p> 464 <div class="ntw__date">${esc(it.date||"")}</div> 465 </div>`; 466 b.addEventListener("click",()=>open(it)); 467 return b; 468 }; 469 470 const setBtnsDesktop=()=>{ 471 prev.disabled=start<=0; 472 next.disabled=start+per>=items.length; 473 }; 474 475 const render=()=>{ 476 const touch = isTouchLayout(); 477 478 grid.innerHTML=""; 479 480 if(touch){ 481 // Touch layout: render ALL items and let CSS scroll-snap handle swipe/peek
482 items.forEach(it=>grid.appendChild(cardEl(it))); 483 prev.disabled = true; next.disabled = true; // arrows hidden via CSS anyway 484 return; 485 } 486 487 // Desktop: paged grid 488 calcPer(); 489 start=Math.min(start, Math.max(0, items.length - per)); 490 const slice=items.slice(start, start+per); 491 492 if(!slice.length){ 493 grid.innerHTML="<div style='color:rgba(255,255,255,.75);padding:6px 2px;'>No trailers found.</div>"; 494 prev.disabled=next.disabled=true; 495 return; 496 } 497 498 slice.forEach(it=>grid.appendChild(cardEl(it))); 499 setBtnsDesktop(); 500 }; 501 502 // Desktop swipe (optional): allow trackpad/touch swipe to page 503 const attachSwipeDesktop=()=>{ 504 let x0=null, y0=null; 505 const onStart=(e)=>{ 506 if(isTouchLayout()) return; 507 const t=e.touches?.[0]; if(!t) return; 508 x0=t.clientX; y0=t.clientY; 509 }; 510 const onEnd=(e)=>{ 511 if(isTouchLayout()) return; 512 const t=e.changedTouches?.[0]; if(!t || x0===null) return; 513 const dx=t.clientX-x0, dy=t.clientY-y0; 514 x0=y0=null; 515 if(Math.abs(dx)<50 || Math.abs(dx)<Math.abs(dy)) return; 516 if(dx<0) next.click(); else prev.click(); 517 }; 518 frame.addEventListener("touchstart", onStart, {passive:true}); 519 frame.addEventListener("touchend", onEnd, {passive:true}); 520 }; 521 522 try{ 523 const res=await fetch(FEED,{cache:"no-store"}); 524 const data=await res.json(); 525 items=(Array.isArray(data)?data:[]).map(r=>{ 526 const title=(r.Title||"").trim(), date=(r["Date Posted"]||"").trim(), url=(r.Trailer||"").trim(), storeUrl=(r["Steam Store"]||"").trim(); 527 const id=ytId(url); if(!title||!id) return null; 528 const t=Date.parse(date); return {title,date,id,storeUrl,thumb:thumb(id),sort:isNaN(t)?0:t}; 529 }).filter(Boolean).sort((a,b)=>b.sort-a.sort); 530 render(); 531 }catch(e){ 532 grid.innerHTML="<div style='color:rgba(255,255,255,.75);padding:6px 2px;'>Couldnât load trailers.</div>"; 533 prev.disabled=next.disabled=true; 534 } 535 536 prev.addEventListener("click",()=>{ 537 if(isTouchLayout()) return; 538 calcPer(); start=Math.max(0,start-per); render(); 539 }); 540 next.addEventListener("click",()=>{ 541 if(isTouchLayout()) return; 542 calcPer(); start=Math.min(Math.max(0,items.length-per), start+per); render(); 543 }); 544 545 modal.addEventListener("click",e=>{ if(e.target?.dataset?.close==="true") close(); }); 546 addEventListener("keydown",e=>{ if(e.key==="Escape" && modal.getAttribute("aria-hidden")==="false") close(); }); 547 548 // Re-render on resize / breakpoint changes 549 let rt; 550 addEventListener("resize",()=>{ clearTimeout(rt); rt=setTimeout(render,120); }); 551 if("ResizeObserver" in window){ 552 const ro=new ResizeObserver(()=>{ clearTimeout(rt); rt=setTimeout(render,60); }); 553 ro.observe(frame); 554 } 555 556 attachSwipeDesktop(); 557 }; 558 559 const boot=()=>{ 560 if(!home()) return; 561 let i=0; const t=setInterval(()=>{ if(inject()||++i>40){ clearInterval(t); if(document.getElementById("ntwGrid")) init(); } },150); 562 }; 563 564 document.readyState==="loading" ? addEventListener("DOMContentLoaded",boot) : boot(); 565})(); 566</script>
566 567 568</head> 569<body class="page-template page-steam-next-fest has-sans-title has-sans-body"> 570 571<div class="gh-viewport"> 572 573 <header id="gh-navigation" class="gh-navigation is-left-logo has-accent-color gh-outer"> 574 <div class="gh-navigation-inner gh-inner"> 575 576 <div class="gh-navigation-brand"> 577 <a class="gh-navigation-logo is-title" href="https://dearindiegames.com"> 578 <img src="https://storage.ghost.io/c/ef/27/ef273632-f447-4457-b65f-a3da589d0492/content/images/2024/04/Dear-IndieGames-Logo-White-2.png" alt="Dear IndieGames"> 579 </a> 580 <button class="gh-search gh-icon-button" aria-label="Search this site" data-ghost-search> 581 <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" width="20" height="20"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg></button> <button class="gh-burger gh-icon-button" aria-label="Menu"> 582 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path></svg> </button> 583 </div> 584 585 <nav class="gh-navigation-menu"> 586 <ul class="nav"> 587 <li class="nav-home"><a href="https://dearindiegames.com/">Home</a></li> 588 <li class="nav-magazine"><a href="https://dearindiegames.com/magazines/">Magazine</a></li> 589 <li class="nav-release-date-calendar"><a href="https://dearindiegames.com/release-date-calendar/">Release Date Calendar</a></li> 590 <li class="nav-latest-trailers"><a href="https://dearindiegames.com/latest-trailers/">Latest Trailers</a></li> 591 <li class="nav-steam-next-fest-june-2026 nav-current"><a href="https://dearindiegames.com/steam-next-fest/">Steam Next Fest June 2026</a></li> 592</ul> 593 594 </nav> 595 596 <div class="gh-navigation-actions"> 597 <button class="gh-search gh-icon-button" aria-label="Search this site" data-ghost-search> 598 <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" width="20" height="20"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg></button> <div class="gh-navigation-members"> 599 <a href="#/portal/signin" data-portal="signin">Sign in</a> 600 <a class="gh-button" href="#/portal/signup" data-portal="signup">Subscribe</a> 601 </div> 602 </div> 603 604 </div> 605</header> 606 607 608 609<main class="gh-main"> 610 <article class="gh-article post featured"> 611 612 613 <section class="gh-content gh-canvas is-body"> 614 615<!--kg-card-begin: html--> 616<!DOCTYPE html> 617<html lang="en"> 618<head> 619 <meta charset="UTF-8"> 620 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 621 <title>DIG For Hidden Gems</title> 622 <style> 623 624 /* Nav Bar */ 625 .month-nav{ 626 display:flex; 627 gap:10px; 628 flex-wrap:wrap; 629 justify-content:center; 630 margin: 10px 0 24px; 631 width: 100%; 632 max-width: 100vw; 633} 634.month-nav button{ 635 background:#1a1f3a; 636 color:#fff; 637 border:3px solid rgba(255,255,255,0.12); 638 border-radius:12px; 639 padding:10px 12px; 640 cursor:pointer; 641 font-size:1.8rem; 642 font-weight: bold; 643 letter-spacing:0.02em; 644 transition: transform 0.15s ease, box-shadow 0.15s ease; 645 white-space: nowrap; 646} 647 648.month-nav button.active{ 649 background: #ff5858; 650 border-color: rgba(255,255,255,0.45); 651 box-shadow: 0 6px 18px rgba(0,0,0,0.35); 652} 653 654.month-nav button:hover { 655 transform: translateY(-2px); 656 background: #ff5858; 657} 658 659.month-nav button:active { 660 transform: translateY(0); 661} 662 663@media (max-width: 820px) { 664 .month-nav button { 665 font-size: 1.35rem; 666 } 667} 668 669@media (max-width: 430px) { 670 .month-nav button { 671 font-size: 1.0rem; 672 } 673} 674 675 * { margin: 0; padding: 0; box-sizing: border-box; } 676 body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #232323; color: #fff; } 677 678 /* Card sizing variables (edit these in ONE place) */ 679 :root{ --card-min: 220px; --gap: 20px; --stamp-url: url("https://drive.google.com/thumbnail?id=1p2lI_WOjq7yfRyTKserc9TpbmMnhN1FS&sz=w300");} 680 .dig-calendar-container { width:100%;
680 max-width: calc((var(--card-min) * 5) + (var(--gap) * 4)); margin: 0 auto; padding: 20px; } 681 h1 { font-size: 2.5rem; margin-bottom: 10px; } 682 .container { width:100%; max-width: calc((var(--card-min) * 5) + (var(--gap) * 4)); margin: 0 auto; padding: 20px 20px 60px; } 683 .calendar-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gap); } 684 .game-card { border-radius: 14px; overflow: visible; cursor: pointer; transition: transform 0.3s, box-shadow 0.3s; position: relative; aspect-ratio: 2 / 3; } 685 .game-card:hover { transform: translateY(-5px); box-shadow: 0 10px 30px rgba(0,0,0,0.5); } 686 .game-card.favourite { outline: 4px solid #ff5858; outline-offset: -4px; 687 box-shadow: 0 0 0 4px #ff5858, 0 0 18px 4px rgba(255,88,88,0.45); } 688 .game-card.favourite:hover { transform: translateY(-5px); 689 box-shadow: 0 0 0 4px #ff5858, 0 8px 28px rgba(255,88,88,0.55), 0 10px 30px rgba(0,0,0,0.5); } 690 .game-card.favourite::after { content: ""; position: absolute; top: -20px; right: -20px; width: 80px; height: 80px; background-image: var(--stamp-url); background-size: contain; background-repeat: no-repeat; background-position: center; pointer-events: none; } 691 .game-card img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 14px; } 692 .game-card-info { display: none; } 693 694 695 /* 3 cards */ 696@media (min-width: 700px){ 697 .calendar-grid{ 698 grid-template-columns: repeat(3, minmax(var(--card-min), 1fr)); 699 justify-content: center; /* keeps the block centered */ 700 } 701} 702 703/* 4 cards */ 704@media (min-width: 980px){ 705 .calendar-grid{ 706 grid-template-columns: repeat(4, minmax(var(--card-min), 1fr)); 707 } 708} 709 710/* 5 cards */ 711@media (min-width: 1260px){ 712 .calendar-grid{ 713 grid-template-columns: repeat(5, minmax(var(--card-min), 1fr)); 714 } 715} 716 717.global-hover-preview{ 718 position: fixed; 719 z-index: 3000; 720 721 width: 520px; 722 aspect-ratio: 16 / 9; 723 724 display: none; 725 background: #000; 726 border-radius: 12px; 727 overflow: hidden; 728 729 border: 1px solid rgba(255,255,255,0.16); 730 box-shadow: 0 12px 30px rgba(0,0,0,0.55); 731 732 pointer-events: none; 733} 734 735.global-hover-preview video{ 736 width: 100%; 737 height: 100%; 738 object-fit: cover; 739 display: block; 740} 741 742 /* Lightbox Modal Styles */ 743 /* Overlay (same role as current modal background) */ 744/* Make overlay stable and prevent page scroll behind it */ 745.modal{ 746 display: none; 747 position: fixed; 748 inset: 0; 749 width: 100vw; 750 height: 100dvh; 751 background: rgba(0,0,0,0.78); 752 overflow: hidden; 753 z-index: 9000; 754 pointer-events: none; 755} 756 757 /* When active, show overlay */ 758.modal.active { display: block; pointer-events: auto; } 759 760/* Drawer: fixed to viewport, full height */ 761.drawer{ 762 position: fixed; /* was absolute */ 763 top: 0; 764 right: 0; 765 height: 100dvh; /* full visible height on mobile */ 766 height: 100vh; /* fallback */ 767 width: min(540px, 92vw); 768 max-width: 92vw; 769 background: #1a1f3a; 770 771 transform: translateX(100%); 772 transition: transform 0.25s ease; 773 z-index: 1; 774 775 overflow-y: auto; 776 overflow-x: hidden; /* prevent sideways scroll */ 777 -webkit-overflow-scrolling: touch; 778} 779 /* Slide drawer in when modal is active */ 780 .modal.active .drawer{ 781 transform: translateX(0); 782 } 783 /* Ensure padding doesn't create overflow */ 784.drawer, .modal-body, .screenshots-grid, .trailer-section { 785 box-sizing: border-box; 786} 787 788/* Never allow content to force horizontal scroll */ 789.drawer * { 790 max-width: 100%; 791} 792 @media (max-width: 599px) { 793 794 /* Centered âcardâ modal instead of slide-in drawer */ 795 .drawer{ 796 left: 50%; 797 right: auto; 798 top: 50%; 799 bottom: auto; 800 801 /* 3:4-ish lightbox rectangle */ 802 width: min(92vw, 420px); 803 max-height: min(88vh, calc((92vw) * 4 / 3)); /* keeps it near 3:4 */ 804 height: auto; 805 806 border-radius: 18px; 807 box-shadow: 0 18px 60px rgba(0,0,0,.6); 808 border: 1px solid rgba(255,255,255,.08); 809 810 /* Start hidden (slightly down + transparent) */ 811 transform: translate(-50%, -46%) scale(0.98); 812 opacity: 0; 813 transition: transform .18s ease, opacity .18s ease; 814 } 815 816 /* When active, pop into center */ 817 .modal.active .drawer{ 818 transform: translate(-50%, -50%) scale(1); 819 opacity: 1; 820 } 821 822 /* Make the close button âstickyâ inside the card */ 823 .modal-close{ 824 position: sticky; 825 top: 12px; 826 right: 12px; 827 margin-left: auto; 828 z-index: 3; 829 } 830 831 /* Header image should NOT force weird cropping */ 832 .modal-header-image{ 833 height: auto; 834 max-height: 32vh; 835 width: 100%; 836 object-fit: contain; /* keeps full image visible */ 837 background: #0a0e27; 838 } 839 840 /* Give content breathing room */ 841 .modal-body{ 842 padding: 16px; 843 } 844 845 /* Ensure no horizontal scrolling inside the card */ 846 .drawer, .drawer *{ 847 max-width: 100%; 848 } 849} 850 851 852/* Make trailer iframe responsive */ 853.trailer-video{ 854 width: 100%; 855 max-width: 100%; 856 aspect-ratio: 16/9; 857 height: auto; 858 display: block; 859} 860 861/* Make screenshots grid behave on narrow screens */ 862.screenshots-grid{ 863 display: grid; 864 grid-template-columns: 1fr; /* single column on mobile */ 865 gap: 12px; 866} 867 868/* Ensure screenshot images fit */ 869.screenshot{ 870 width: 100%; 871 height: auto; /* let it scale down */ 872 max-height: 260px; /* optional cap */ 873 object-fit: cover; 874 display: block; 875} 876 @media (min-width: 900px){ 877 .drawer{ 878 width: min(760px, 46vw); 879 max-width: 760px; 880 } 881 } 882 @media (max-width: 420px){ 883 .drawer{ 884 width: calc(100vw - 44px); 885 max-width: calc(100vw - 44px); 886 } 887 } 888 .modal-body{ 889 padding: 16px; /* was 30px */ 890 } 891 .modal-title{ 892 font-size: 1.8rem; /* was 3rem */ 893 line-height: 1.2; 894 } 895 .modal-meta{ 896 font-size: 1rem; /* was 2rem */ 897 gap: 12px; 898 flex-wrap: wrap; 899 } 900 .modal-header-image{ 901 height: 200px; /* slightly smaller */ 902 } 903} 904html, body{ 905 overflow-x: hidden; 906} 907
908 .modal-content { background-color: #1a1f3a; margin: 30px auto; padding: 0; width: 50%; max-width: 900px; border-radius: 10px; position: relative; animation: slideIn 0.3s ease-out; } 909 @keyframes slideIn { from { opacity: 0; transform: translateY(-50px); } to { opacity: 1; transform: translateY(0); } } 910 .modal-close { position: absolute; right: 20px; top: 20px; font-size: 2rem; font-weight: bold; color: #fff; cursor: pointer; z-index: 1001; background: rgba(0, 0, 0, 0.2); width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: background 0.3s; } 911 .modal-close:hover { background: rgba(0, 0, 0, 0.8); } 912 913 /* Header image inside drawer */ 914 .modal-header-image { 915 width: 100%; 916 height: 215px; /* smaller fits drawer better */ 917 object-fit: contain; 918 background: #0a0e27; 919 border-radius: 0; 920} 921 922 .modal-body { padding: 30px; } 923 .modal-title a { color: inherit; text-decoration: none;} 924 .modal-title { font-size: 3rem; margin-bottom: 15px; } 925 .modal-title a:hover { text-decoration: underline; } 926 .modal-title-hint{ font-size: 2rem; margin-left: 10px; opacity: 0.75; letter-spacing: 0.05em; white-space: nowrap; display: infinite-flex; align-items: center; } 927 .modal-title a:hover .modal-title-hint{ opacity: 1; } 928 .modal-meta { display: flex; gap: 20px; margin-bottom: 20px; color: #bfbdbd; font-size: 2rem; } 929 .modal-description { line-height: 1.6; margin-bottom: 30px; color: #ccc; } 930 .screenshots-section { margin-bottom: 30px; } 931 .screenshots-section h3 { margin-bottom: 15px; font-size: 1.3rem; } 932 .screenshot { width: 100%; height: 270px; object-fit: cover; border-radius: 5px; cursor: pointer; transition: transform 0.3s; } 933 .screenshot:hover { transform: scale(1.05); } 934 .trailer-section { margin-bottom: 20px; } 935 .trailer-section h3 { margin-bottom: 15px; font-size: 1.3rem; } 936 .trailer-video { width: 100%; aspect-ratio: 16/9; border-radius: 5px; } 937 938 /* Icon hidden by default (desktop) */ 939 .modal-title-hint .hint-icon{ 940 display: none; 941 font-size: 1.2em; 942 margin-left: 2px; } 943 944 /* Mobile: icon only */ 945@media (max-width: 600px){ 946 .modal-title-hint .hint-text{ 947 display: none; } 948 .modal-title-hint .hint-icon{ 949 display: inline; } 950} 951 952 /* Nice hover feedback */ 953 .modal-title a:hover .modal-title-hint{ 954 opacity: 1; } 955 956 /*IMAGE LIGHTBOX*/ 957 .img-lightbox { 958 display: none; 959 position: fixed; 960 inset: 0; 961 z-index: 9999; 962 background: rgba(0,0,0,0.9); 963 align-items: center; 964 justify-content: center; 965 padding: 20px; 966} 967 968/* Screenshot lightbox */ 969 .img-lightbox { 970 display: none; 971 position: fixed; 972 inset: 0; 973 z-index: 9999; 974 background: rgba(0,0,0,0.9); 975 align-items: center; 976 justify-content: center; 977 padding: 20px; 978} 979 980 .img-lightbox.active { 981 display: flex; 982} 983 984 .img-lightbox img { 985 max-width: 85vw; 986 max-height: 85vh; 987 border-radius: 10px; 988 box-shadow: 0 10px 40px rgba(0,0,0,0.6); 989 cursor: zoom-out; 990} 991 992 .modal-backdrop{ 993 position: absolute; 994 inset: 0; 995 z-index: 0; 996} 997 998</style> 999</head> 1000 1001<body> 1002 <div id="imageLightbox" class="img-lightbox" onclick="closeImageLightbox()"> 1003 <img id="imageLightboxImg" alt=""> 1004 </div> 1005 1006 <div class="container" id="indiegame-release-date-calendar"> 1007 <header> 1008 <h1>Steam Next Fest June 2026 ð </h1> 1009 <p class="page-intro"> 1010 From emotional journeys to wonderfully strange experiments, Iâve curated my Steam Next Fest highlights into one easy page. Watch the trailers, sort by genre, and dive straight into the demos. 1011 </p> 1012 <!-- same styling, but now it's genre buttons --> 1013 <div class="month-nav" id="genreNav"></div> 1014 </header> 1015 1016 <div id="calendarContent"> 1017 <div class="loading">Loading IndieGame releases...</div> 1018 </div> 1019 </div> 1020 1021 <!-- Lightbox Modal --> 1022 <div id="gameModal" class="modal"> 1023 <div class="drawer"> 1024 <span class="modal-close" onclick="closeModal()">×</span> 1025 <img id="modalHeaderImage" class="modal-header-image" src="" alt=""> 1026 <div class="modal-body"> 1027 <h2 class="modal-title"> 1028 <a id="modalTitle" href="" target="_blank" rel="noopener">
1029 <span class="modal-title-text"></span> 1030 <span class="modal-title-hint" aria-hidden="true"> 1031 <span class="hint-text">[CLICK FOR STORE PAGE]</span> 1032 <span class="hint-icon">ð</span> 1033 </span> 1034 </a> 1035 </h2> 1036 1037 <div class="modal-meta"> 1038 <span id="modalDate"></span> 1039 <span id="modalGenres"></span> 1040 </div> 1041 1042 <p id="modalDescription" class="modal-description"></p> 1043 1044 <div class="trailer-section" id="trailerSection"> 1045 <h3>Trailer</h3> 1046 <iframe id="trailerVideo" 1047 class="trailer-video" 1048 src="" 1049 frameborder="0" 1050 allowfullscreen></iframe> 1051 </div> 1052 1053 <div class="screenshots-section" id="screenshotsSection"> 1054 <h3>Screenshots</h3> 1055 <div class="screenshots-grid" id="screenshotsGrid"></div> 1056 </div> 1057 </div> 1058 </div> 1059 </div> 1060 1061 <div id="globalHoverPreview" class="global-hover-preview" aria-hidden="true"></div> 1062 1063
1063<script> 1064(() => { 1065 // ========================= 1066 // Config 1067 // ========================= 1068 const DATA_URL = "https://opensheet.elk.sh/1s2ruYwFr-uIS5xSPM5sz1AAQBVpgRzcQ7VJWeLPpYfk/3"; 1069 const GRID_IMAGE_COL = "Poster"; // 600x900 portrait 1070 const HEADER_IMAGE_COL = "Header Image"; // wide banner for the drawer 1071 1072 // If your genres are stored in a different column name, change this: 1073 const GENRES_COL = "Tags"; // (you already use game.Genres in the modal) 1074 1075 // â genre buttons: derived from your sheet, excluding "Indie" 1076 // (No hardcoded genre list needed.) 1077 1078 // ========================= 1079 // State 1080 // ========================= 1081 let allData = []; 1082 let currentGenre = ""; 1083 const hoverTimers = new WeakMap(); 1084 1085 // ========================= 1086 // Helpers 1087 // ========================= 1088 function isWeekDivider(row) { 1089 return !row.Name?.trim() && (row["Short description"] || "").toUpperCase().startsWith("WEEK"); 1090 } 1091 1092 function escapeHtml(text) { 1093 const div = document.createElement("div"); 1094 div.textContent = text ?? ""; 1095 return div.innerHTML; 1096 } 1097 1098 function escapeAttr(text) { 1099 return (text ?? "").toString().replace(/"/g, """); 1100 } 1101 1102 function convertToEmbedUrl(url) { 1103 const patterns = [ 1104 /youtube\.com\/watch\?v=([^&]+)/i, 1105 /youtube\.com\/embed\/([^?]+)/i, 1106 /youtu\.be\/([^?]+)/i 1107 ]; 1108 for (const pattern of patterns) { 1109 const match = url.match(pattern); 1110 if (match) return `https://www.youtube.com/embed/${match[1]}`; 1111 } 1112 return url; 1113 } 1114 1115 function sortAlpha(a, b) { 1116 return String(a).localeCompare(String(b), undefined, { sensitivity: "base" }); 1117} 1118 1119function splitGenres(genresStr) { 1120 // Sheet values look like: "Action, Adventure" etc. 1121 return String(genresStr || "") 1122 .split(",") 1123 .map(s => s.trim()) 1124 .filter(Boolean); 1125} 1126 1127function getRowGenres(game) { 1128 return splitGenres(game[GENRES_COL]); 1129} 1130 1131function getRowGenres(game) { 1132 return splitGenres(game[GENRES_COL]); 1133} 1134 1135function gameHasGenre(game, genre) { 1136 const target = String(genre).toLowerCase(); 1137 return getRowGenres(game).some(g => String(g).toLowerCase() === target); 1138} 1139 1140function isFavourite(game) { 1141 return String(game["Favourite"] || "").trim().toUpperCase() === "YES"; 1142} 1143 1144 window.handleHoverStart = function(card){ 1145 if (!window.matchMedia('(min-width: 1200px) and (hover: hover) and (pointer: fine)').matches) return; 1146 1147 const previewUrl = card.dataset.preview; 1148 if (!previewUrl) return; 1149 1150 const timer = setTimeout(() => { 1151 const global = document.getElementById("globalHoverPreview"); 1152 if (!global) return; 1153 1154 // position near the card 1155 const r = card.getBoundingClientRect(); 1156 const gap = 12; 1157 1158 // default: to the right of the card 1159 let left = r.right + gap; 1160 let top = r.top + gap; 1161 1162 // keep inside viewport 1163 const w = 520; 1164 const h = Math.round(w * 9 / 16); 1165 1166 if (left + w > window.innerWidth - gap) left = r.left - w - gap; // move left side 1167 if (left < gap) left = gap; 1168 1169 if (top + h > window.innerHeight - gap) top = window.innerHeight - h - gap; 1170 if (top < gap) top = gap; 1171 1172 global.style.left = `${left}px`; 1173 global.style.top = `${top}px`; 1174 1175 global.innerHTML = ` 1176 <video muted autoplay playsinline loop preload="metadata"> 1177 <source src="${previewUrl}" type="video/mp4"> 1178 </video> 1179 `; 1180 global.style.display = "block"; 1181 }, 500); 1182 1183 hoverTimers.set(card, timer); 1184}; 1185 1186window.handleHoverEnd = function(card){ 1187 const timer = hoverTimers.get(card); 1188 if (timer) clearTimeout(timer); 1189 1190 const global = document.getElementById("globalHoverPreview"); 1191 if (global){ 1192 global.style.display = "none"; 1193 global.innerHTML = ""; 1194 } 1195} 1196 1197 1198// ========================= 1199// Lightbox (screenshots) 1200// ========================= 1201 window.openImageLightbox = function(url) { 1202 const lb = document.getElementById("imageLightbox"); 1203 const img = document.getElementById("imageLightboxImg"); 1204 if (!lb || !img) return; 1205 img.src = url; 1206 lb.classList.add("active"); 1207 }; 1208 1209 window.closeImageLightbox = function() { 1210 const lb = document.getElementById("imageLightbox"); 1211 const img = document.getElementById("imageLightboxImg"); 1212 if (!lb || !img) return; 1213 lb.classList.remove("active"); 1214 img.src = ""; 1215 }; 1216 1217// ========================= 1218// Render: alphabetical list by selected genre 1219// ========================= 1220function renderGenre(genre) { 1221 const container = document.getElementById("calendarContent"); 1222 if (!container) return; 1223 1224 const headerLabel = (genre === "__ALL__") ? "All" : genre; 1225 1226 const games = allData 1227 .filter(g => g.Name && g.Name.trim() !== "") 1228 .filter(g => !isWeekDivider(g)) 1229 .filter(g => (genre === "__ALL__") ? true : gameHasGenre(g, genre)) 1230 .slice() 1231 .sort((a, b) => sortAlpha(a.Name, b.Name)); 1232 1233 if (!games.length) { 1234 container.innerHTML = `<div class="loading">No games found for: ${escapeHtml(headerLabel)}.</div>`; 1235 return; 1236 } 1237 1238 let html = ` 1239 <div class="date-section"> 1240 <div class="calendar-grid"> 1241 `; 1242
1243 games.forEach(game => { 1244 const imageUrl = (game[GRID_IMAGE_COL] || "").trim(); 1245 const idx = allData.indexOf(game); 1246 const favClass = isFavourite(game) ? " favourite" : ""; 1247 1248 html += ` 1249 <div class="game-card${favClass}" 1250 data-preview="${escapeAttr(game["Mini Trailer"] || "")}" 1251 onmouseenter="handleHoverStart(this)" 1252 onmouseleave="handleHoverEnd(this)" 1253 onclick="openGameDetails(${idx})"> 1254 <img loading="lazy" 1255 src="${escapeAttr(imageUrl)}" 1256 alt="${escapeAttr(game.Name)}" 1257 onerror="this.src='https://via.placeholder.com/460x215/1a1f3a/ffffff?text=${encodeURIComponent(game.Name)}'"> 1258 <div class="game-card-info"> 1259 <div class="game-title">${escapeHtml(game.Name)}</div> 1260 </div> 1261 <div class="hover-preview"></div> 1262 </div> 1263 `; 1264 }); 1265 1266 html += `</div></div>`; 1267 container.innerHTML = html; 1268} 1269 1270// ========================= 1271// Build genre nav 1272// ========================= 1273function buildGenreNav() { 1274 const nav = document.getElementById("genreNav"); 1275 if (!nav) return; 1276 1277 const genreSet = new Set(); 1278 1279 allData 1280 .filter(g => g.Name && g.Name.trim() !== "") 1281 .filter(g => !isWeekDivider(g)) 1282 .forEach(g => { 1283 getRowGenres(g).forEach(genre => { const key = genre.toLowerCase(); 1284 genreSet.add(genre); 1285 }); 1286 }); 1287 1288 const genres = Array.from(genreSet).sort(sortAlpha); 1289 1290 // â Add "All" as the first button 1291 genres.unshift("__ALL__"); 1292 1293 nav.innerHTML = genres 1294 .map(g => { 1295 const label = (g === "__ALL__") ? "All" : g; 1296 const count = allData 1297 .filter(game => game.Name && game.Name.trim() !== "") 1298 .filter(game => !isWeekDivider(game)) 1299 .filter(game => (g === "__ALL__") ? true : gameHasGenre(game, g)) 1300 .length; 1301 return `<button type="button" data-genre="${escapeAttr(g)}">${escapeHtml(label)} (${count})</button>`; 1302 }) 1303 .join(""); 1304 1305 const buttons = nav.querySelectorAll("button"); 1306 1307 buttons.forEach(btn => { 1308 btn.addEventListener("click", () => { 1309 currentGenre = btn.dataset.genre; 1310 1311 buttons.forEach(b => b.classList.remove("active")); 1312 btn.classList.add("active"); 1313 1314 renderGenre(currentGenre); 1315 }); 1316 }); 1317 1318 // â Default to "All" 1319 currentGenre = "__ALL__"; 1320 if (buttons[0]) buttons[0].classList.add("active"); 1321} 1322 1323 // ========================= 1324 // Modal / drawer (unchanged, except genre source already ok) 1325 // ========================= 1326 window.openGameDetails = function(rowIndex) { 1327 const game = allData[rowIndex]; 1328 if (!game) return; 1329 1330 const modal = document.getElementById("gameModal"); 1331 if (!modal) return; 1332 1333 // header image 1334 const headerImage = (game[HEADER_IMAGE_COL] || "").trim(); 1335 const headerEl = document.getElementById("modalHeaderImage"); 1336 if (headerEl) { 1337 headerEl.src = headerImage || "https://via.placeholder.com/900x400/1a1f3a/ffffff?text=No+Image"; 1338 headerEl.onerror = function () { 1339 this.src = "https://via.placeholder.com/900x400/1a1f3a/ffffff?text=No+Image"; 1340 }; 1341 } 1342 1343 // title link 1344 const titleEl = document.getElementById("modalTitle"); 1345 const titleTextEl = titleEl?.querySelector(".modal-title-text"); 1346 const steamUrl = (game["steam url"] || game["Steam Store Page"] || "").trim(); 1347 1348 if (titleTextEl) titleTextEl.textContent = game.Name || ""; 1349 1350 if (titleEl) { 1351 if (steamUrl) { 1352 titleEl.href = steamUrl; 1353 titleEl.style.pointerEvents = "auto"; 1354 } else { 1355 titleEl.removeAttribute("href"); 1356 titleEl.style.pointerEvents = "none"; 1357 } 1358 } 1359 1360 // date 1361 const dateEl = document.getElementById("modalDate"); 1362 if (dateEl) { 1363 dateEl.textContent = `Release: ${(game["Release Date"] || game["Estimated Release Date"] || "TBC").trim()}`; 1364 } 1365 1366 // genres 1367 const genresEl = document.getElementById("modalGenres"); 1368 if (genresEl) { 1369 const genres = (game[GENRES_COL] || game.Genres || game["genres[*].description"] || "").trim(); 1370 genresEl.textContent = genres || "No genres listed"; 1371 } 1372 1373 // description
1374 const descEl = document.getElementById("modalDescription"); 1375 if (descEl) { 1376 descEl.textContent = game["Short description"] || "No description available."; 1377 } 1378 1379 // trailer 1380 const trailerUrl = (game["Main Trailer"] || game["Trailer"] || "").trim(); 1381 const trailerSection = document.getElementById("trailerSection"); 1382 const trailerVideo = document.getElementById("trailerVideo"); 1383 if (trailerUrl) { 1384 if (trailerSection) trailerSection.style.display = "block"; 1385 if (trailerVideo) trailerVideo.src = convertToEmbedUrl(trailerUrl); 1386 } else { 1387 if (trailerSection) trailerSection.style.display = "none"; 1388 if (trailerVideo) trailerVideo.src = ""; 1389 } 1390 1391 // screenshots 1392 const screenshotsGrid = document.getElementById("screenshotsGrid"); 1393 const screenshotsSection = document.getElementById("screenshotsSection"); 1394 const screenshots = [game["Screenshot 0"], game["Screenshot 1"], game["Screenshot 2"]].filter(u => u && u.trim()); 1395 1396 if (screenshots.length) { 1397 if (screenshotsSection) screenshotsSection.style.display = "block"; 1398 if (screenshotsGrid) { 1399 screenshotsGrid.innerHTML = screenshots.map(url => ` 1400 <img loading="lazy" 1401 src="${escapeAttr(url)}" 1402 class="screenshot" 1403 alt="Screenshot" 1404 onclick="openImageLightbox('${escapeAttr(url)}')"> 1405 `).join(""); 1406 } 1407 } else { 1408 if (screenshotsSection) screenshotsSection.style.display = "none"; 1409 if (screenshotsGrid) screenshotsGrid.innerHTML = ""; 1410 } 1411 1412 modal.classList.add("active"); 1413 document.body.style.overflow = "hidden"; 1414 }; 1415 1416 window.closeModal = function() { 1417 const modal = document.getElementById("gameModal"); 1418 if (!modal) return; 1419 modal.classList.remove("active"); 1420 document.body.style.overflow = "auto"; 1421 const trailerVideo = document.getElementById("trailerVideo"); 1422 if (trailerVideo) trailerVideo.src = ""; 1423 }; 1424 1425 window.addEventListener("click", (event) => { 1426 const modal = document.getElementById("gameModal"); 1427 if (event.target === modal) window.closeModal(); 1428 }); 1429 1430 document.addEventListener("keydown", (e) => { 1431 if (e.key === "Escape") { 1432 window.closeImageLightbox(); 1433 window.closeModal(); 1434 } 1435 }); 1436 1437 // ========================= 1438 // Boot 1439 // ========================= 1440 (async function init() { 1441 try { 1442 const res = await fetch(DATA_URL, { headers: { "Accept": "application/json" }, mode: "cors" }); 1443 if (!res.ok) throw new Error(`HTTP ${res.status}`); 1444 allData = await res.json(); 1445 1446 // Move overlays out of the Ghost/content wrapper 1447 const gameModal = document.getElementById("gameModal"); 1448 const imageLightbox = document.getElementById("imageLightbox"); 1449 const globalHoverPreview = document.getElementById("globalHoverPreview"); 1450 1451 if (gameModal) document.body.appendChild(gameModal); 1452 if (imageLightbox) document.body.appendChild(imageLightbox); 1453 if (globalHoverPreview) document.body.appendChild(globalHoverPreview); 1454 1455 buildGenreNav(); 1456 renderGenre(currentGenre); 1457 } catch (err) { 1458 console.error(err); 1459 const container = document.getElementById("calendarContent"); 1460 if (container) container.innerHTML = '<div class="loading">Error loading games. Please try again.</div>'; 1461 } 1462 })(); 1463})(); 1464 </script>
1464 1465</body> 1466</html> 1467<!--kg-card-end: html--> 1468 1469 </section> 1470 1471 </article> 1472</main> 1473 1474 1475 1476 <footer class="gh-footer has-accent-color gh-outer"> 1477 <div class="gh-footer-inner gh-inner"> 1478 1479 <section class="gh-footer-signup"> 1480 <h2 class="gh-footer-signup-header is-title"> 1481 Dear IndieGames 1482 </h2> 1483 <p class="gh-footer-signup-subhead is-body"> 1484 DIG into your next IndieGame 1485 </p> 1486 <form class="gh-form" data-members-form> 1487 <input class="gh-form-input" id="footer-email" name="email" type="email" placeholder="[email protected]" required data-members-email> 1488 <button class="gh-button" type="submit" aria-label="Subscribe"> 1489 <span><span>Subscribe</span> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M224.49,136.49l-72,72a12,12,0,0,1-17-17L187,140H40a12,12,0,0,1,0-24H187L135.51,64.48a12,12,0,0,1,17-17l72,72A12,12,0,0,1,224.49,136.49Z"></path></svg></span> 1490 <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"> 1491 <g stroke-linecap="round" stroke-width="2" fill="currentColor" stroke="none" stroke-linejoin="round" class="nc-icon-wrapper"> 1492 <g class="nc-loop-dots-4-24-icon-o"> 1493 <circle cx="4" cy="12" r="3"></circle> 1494 <circle cx="12" cy="12" r="3"></circle> 1495 <circle cx="20" cy="12" r="3"></circle> 1496 </g> 1497 <style data-cap="butt"> 1498 .nc-loop-dots-4-24-icon-o{--animation-duration:0.8s} 1499 .nc-loop-dots-4-24-icon-o *{opacity:.4;transform:scale(.75);animation:nc-loop-dots-4-anim var(--animation-duration) infinite} 1500 .nc-loop-dots-4-24-icon-o :nth-child(1){transform-origin:4px 12px;animation-delay:-.3s;animation-delay:calc(var(--animation-duration)/-2.666)} 1501 .nc-loop-dots-4-24-icon-o :nth-child(2){transform-origin:12px 12px;animation-delay:-.15s;animation-delay:calc(var(--animation-duration)/-5.333)} 1502 .nc-loop-dots-4-24-icon-o :nth-child(3){transform-origin:20px 12px} 1503 @keyframes nc-loop-dots-4-anim{0%,100%{opacity:.4;transform:scale(.75)}50%{opacity:1;transform:scale(1)}} 1504 </style> 1505 </g> 1506</svg> <span>Email sent</span> 1507 </button> 1508 <p data-members-error></p> 1509</form> 1510 </section> 1511 1512 <div class="gh-social-links"> 1513 <a href="https://x.com/DearIndieGames" target="_blank" rel="noopener" aria-label="X"> 1514<svg viewBox="0 0 24 24" fill="currentColor"><g><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></g></svg> </a> 1515 <a href="https://www.facebook.com/dearindiegames" target="_blank" rel="noopener" aria-label="Facebook"> 1516<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M23.9981 11.9991C23.9981 5.37216 18.626 0 11.9991 0C5.37216 0 0 5.37216 0 11.9991C0 17.9882 4.38789 22.9522 10.1242 23.8524V15.4676H7.07758V11.9991H10.1242V9.35553C10.1242 6.34826 11.9156 4.68714 14.6564 4.68714C15.9692 4.68714 17.3424 4.92149 17.3424 4.92149V7.87439H15.8294C14.3388 7.87439 13.8739 8.79933 13.8739 9.74824V11.9991H17.2018L16.6698 15.4676H13.8739V23.8524C19.6103 22.9522 23.9981 17.9882 23.9981 11.9991Z"/></svg> </a> 1517 </div> 1518 1519 <div class="gh-footer-bar"> 1520 <span class="gh-footer-logo is-title"> 1521 <img src="https://storage.ghost.io/c/ef/27/ef273632-f447-4457-b65f-a3da589d0492/content/images/2024/04/Dear-IndieGames-Logo-White-2.png" alt="Dear IndieGames"> 1522 </span> 1523 <nav class="gh-footer-menu"> 1524 <ul class="nav"> 1525 <li class="nav-sign-up"><a href="#/portal/">Sign up</a></li> 1526</ul> 1527 1528 </nav> 1529 <div class="gh-footer-copyright"> 1530 Powered by <a href="https://ghost.org/" target="_blank" rel="noopener">Ghost</a> 1531 </div> 1532 </div> 1533 1534 </div> 1535</footer> 1536 1537</div> 1538 1539 <div class="pswp" tabindex="-1" role="dialog" aria-hidden="true"> 1540 <div class="pswp__bg"></div> 1541 1542 <div class="pswp__scroll-wrap"> 1543 <div class="pswp__container"> 1544 <div class="pswp__item"></div> 1545 <div class="pswp__item"></div> 1546 <div class="pswp__item"></div> 1547 </div> 1548 1549 <div class="pswp__ui pswp__ui--hidden"> 1550 <div class="pswp__top-bar"> 1551 <div class="pswp__counter"></div> 1552 1553 <button class="pswp__button pswp__button--close" title="Close (Esc)"></button> 1554 <button class="pswp__button pswp__button--share" title="Share"></button> 1555 <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button> 1556 <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button> 1557 1558 <div class="pswp__preloader"> 1559 <div class="pswp__preloader__icn"> 1560 <div class="pswp__preloader__cut"> 1561 <div class="pswp__preloader__donut"></div> 1562 </div> 1563 </div> 1564 </div> 1565 </div> 1566 1567 <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap"> 1568 <div class="pswp__share-tooltip"></div> 1569 </div> 1570 1571 <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"></button> 1572 <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)"></button> 1573 1574 <div class="pswp__caption"> 1575 <div class="pswp__caption__center"></div> 1576 </div> 1577 </div> 1578 </div> 1579</div>
1580<script src="https://dearindiegames.com/assets/built/source.js?v=33sa4-qy5thAFPQH"></script>
1580 1581 1582 1583 1584</body> 1585</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.