PageSourceSearch

https://dearindiegames.com/steam-next-fest/

html dearindiegames.com collected 2026-10-04 07:43:17 UTC 58,021 bytes, 1,585 lines download raw bytes

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("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&#039;");
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()">&times;</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, "&quot;");
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.