diff --git a/app/styles/back-to-top.css b/app/styles/back-to-top.css index bbb87c6b2..da52e0ae0 100644 --- a/app/styles/back-to-top.css +++ b/app/styles/back-to-top.css @@ -46,6 +46,10 @@ html.light .back-to-top { width: 40px; height: 40px; } + + html[data-facilitator-launcher-visible="true"] .back-to-top { + bottom: calc(148px + env(safe-area-inset-bottom)); + } } @media (prefers-reduced-motion: reduce) { diff --git a/app/styles/monthly-games.css b/app/styles/monthly-games.css index c1b0d8c2a..159dd94c2 100644 --- a/app/styles/monthly-games.css +++ b/app/styles/monthly-games.css @@ -1 +1 @@ -.monthly-games-host{margin-top:14px;scroll-margin-top:92px}.monthly-games-panel{position:relative;margin:0 0 14px;padding:20px;border:1px solid rgba(124,58,237,.24);border-radius:14px;background:linear-gradient(145deg,rgba(12,20,39,.96),rgba(17,28,50,.94));box-shadow:0 16px 40px rgba(2,8,23,.18)}.monthly-games-panel.is-loading{display:flex;align-items:center;justify-content:center;gap:10px;min-height:92px;color:var(--arcade-muted);font-size:.78rem}.monthly-games-panel.is-loading svg{width:18px;height:18px;color:#a78bfa}.monthly-games-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;margin-bottom:17px}.monthly-games-heading>div:first-child{min-width:0}.monthly-games-kicker{display:flex;align-items:center;gap:7px;margin-bottom:7px;color:#a78bfa;font-size:.68rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.monthly-games-kicker svg{width:15px;height:15px}.monthly-games-heading h2{margin:0 0 6px;color:#f8fbff;font-size:1.28rem;line-height:1.25;text-transform:capitalize}.monthly-games-heading p{max-width:700px;margin:0;color:var(--arcade-muted);font-size:.74rem;line-height:1.55}.monthly-progress-summary{display:grid;grid-template-columns:auto auto;align-items:baseline;gap:2px 7px;flex:0 0 150px;padding:10px 12px;border:1px solid rgba(167,139,250,.2);border-radius:10px;background:rgba(9,15,31,.58)}.monthly-progress-summary.is-pending{grid-template-columns:auto 1fr;flex-basis:210px}.monthly-progress-summary strong{color:#fff;font-size:1.3rem}.monthly-progress-summary span{color:var(--arcade-muted);font-size:.68rem}.monthly-progress-summary i{grid-column:1/-1;height:6px;overflow:hidden;border-radius:999px;background:#18233b}.monthly-progress-summary i b{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#7c3aed,#22c55e);transition:width .3s ease}.monthly-games-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;align-items:stretch}.monthly-game-card{display:flex;min-width:0;min-height:100%;overflow:hidden;flex-direction:column;border:1px solid rgba(148,163,184,.14);border-radius:14px;background:linear-gradient(180deg,rgba(10,19,36,.94),rgba(6,14,28,.98));box-shadow:inset 0 1px 0 rgba(255,255,255,.02);transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease}.monthly-game-card:hover{transform:translateY(-2px);border-color:rgba(167,139,250,.38);box-shadow:0 14px 30px rgba(2,8,23,.24)}.monthly-game-card.is-complete{border-color:rgba(34,197,94,.32);box-shadow:inset 0 1px 0 rgba(34,197,94,.05)}.monthly-game-art{position:relative;display:flex;height:178px;align-items:center;justify-content:center;overflow:hidden;padding:14px;border-bottom:1px solid rgba(148,163,184,.08);background:radial-gradient(circle at 50% 44%,rgba(66,85,120,.22),rgba(8,15,30,.98) 70%);color:#a78bfa}.monthly-game-art>img{display:block;width:auto!important;height:auto!important;max-width:72%!important;max-height:150px!important;object-fit:contain!important;object-position:center!important;background:transparent!important;filter:drop-shadow(0 8px 16px rgba(0,0,0,.18))}.monthly-game-art>svg{width:34px;height:34px}.monthly-game-art-backdrop{position:absolute!important;inset:-34px!important;width:calc(100% + 68px)!important;height:calc(100% + 68px)!important;max-width:none!important;max-height:none!important;object-fit:cover!important;opacity:.23;filter:blur(28px) saturate(1.3)!important;transform:scale(1.08);pointer-events:none}.monthly-game-art::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(6,14,28,.2),rgba(6,14,28,.55));pointer-events:none}.monthly-game-art-image{position:relative;z-index:1;max-width:76%!important;max-height:154px!important;filter:drop-shadow(0 10px 20px rgba(0,0,0,.34))!important}.monthly-game-status{position:absolute;z-index:2;top:10px;right:10px;display:flex;align-items:center;gap:5px;padding:5px 8px;border:1px solid rgba(251,191,36,.32);border-radius:999px;background:rgba(8,15,30,.9);color:#fbbf24;font-size:.61rem;font-weight:800;backdrop-filter:blur(8px)}.monthly-game-status svg{width:12px;height:12px}.monthly-game-status.is-complete{border-color:rgba(34,197,94,.42);color:#4ade80}.monthly-game-status.is-pending{border-color:rgba(96,165,250,.34);color:#93c5fd}.monthly-game-body{display:flex;min-height:0;flex:1;flex-direction:column;padding:15px 15px 14px}.monthly-game-body h3{margin:0 0 7px;color:#f4f8ff;font-size:.91rem;line-height:1.38}.monthly-game-description{display:-webkit-box;overflow:hidden;min-height:3.25em;margin:0 0 13px;color:#93a4bd;font-size:.69rem;line-height:1.55;-webkit-box-orient:vertical;-webkit-line-clamp:3}.monthly-game-meta{display:grid;gap:9px;margin-top:auto}.monthly-access-row{display:flex;align-items:stretch;gap:7px}.monthly-access-value{display:flex;min-width:0;flex:1;align-items:center;gap:8px;padding:7px 9px;border:1px solid rgba(167,139,250,.2);border-radius:8px;background:rgba(124,58,237,.09)}.monthly-access-value>span{flex:0 0 auto;color:#a78bfa;font-size:.57rem;font-weight:800;letter-spacing:.05em;text-transform:uppercase}.monthly-access-value code{min-width:0;overflow:hidden;color:#f5f3ff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.7rem;font-weight:800;text-overflow:ellipsis;white-space:nowrap}.monthly-copy-button{display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;gap:5px;min-width:68px;padding:0 10px;border:1px solid rgba(167,139,250,.28);border-radius:8px;background:rgba(124,58,237,.14);color:#c4b5fd;font-size:.61rem;font-weight:800;cursor:pointer;transition:border-color .16s ease,background .16s ease,color .16s ease}.monthly-copy-button:hover{border-color:rgba(196,181,253,.55);background:rgba(124,58,237,.22);color:#ede9fe}.monthly-copy-button svg{width:13px;height:13px}.monthly-copy-button.is-copied{border-color:rgba(34,197,94,.42);background:rgba(34,197,94,.1);color:#86efac}.monthly-game-facts{display:flex;flex-wrap:wrap;gap:7px 14px;padding:0 1px}.monthly-game-facts>span{display:flex;align-items:center;gap:5px;color:#9fb0c6;font-size:.65rem}.monthly-game-facts svg{width:13px;height:13px;color:#8b5cf6}.monthly-game-points{color:#fbbf24!important;font-weight:800}.monthly-game-points svg{color:#f59e0b!important}.monthly-game-deadline{display:grid!important;grid-template-columns:auto auto minmax(0,1fr);align-items:center;gap:6px!important;padding:8px 9px;border:1px solid rgba(251,191,36,.26);border-radius:8px;background:linear-gradient(90deg,rgba(120,53,15,.18),rgba(69,26,3,.08));color:#fde68a!important}.monthly-game-deadline svg{width:13px;height:13px;color:#f59e0b!important}.monthly-game-deadline strong{font-size:.59rem;letter-spacing:.05em;text-transform:uppercase}.monthly-game-deadline time{justify-self:end;overflow:hidden;color:#fef3c7;font-size:.64rem;font-weight:750;text-align:right;text-overflow:ellipsis;white-space:nowrap}.monthly-game-link{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:11px;padding:8px 10px;border:1px solid rgba(124,58,237,.28);border-radius:8px;background:rgba(124,58,237,.11);color:#c4b5fd;font-size:.68rem;font-weight:800;text-decoration:none}.monthly-game-link:hover{border-color:rgba(139,92,246,.52);background:rgba(124,58,237,.18);color:#ede9fe}.monthly-game-link svg{width:13px;height:13px}.light .monthly-games-panel{border-color:rgba(109,40,217,.18);background:linear-gradient(145deg,rgba(255,255,255,.98),rgba(248,250,252,.98));box-shadow:0 14px 34px rgba(15,23,42,.08)}.light .monthly-games-heading h2,.light .monthly-game-body h3,.light .monthly-progress-summary strong{color:#172033}.light .monthly-progress-summary{border-color:rgba(109,40,217,.16);background:#f8fafc}.light .monthly-progress-summary i{background:#e2e8f0}.light .monthly-game-card{border-color:#e2e8f0;background:linear-gradient(180deg,#fff,#fbfdff)}.light .monthly-game-card.is-complete{border-color:rgba(22,163,74,.28)}.light .monthly-game-art{border-bottom-color:#e5e7eb;background:radial-gradient(circle at 50% 42%,#f8fafc,#eef2f7)}.light .monthly-game-art::after{background:linear-gradient(180deg,rgba(255,255,255,.15),rgba(255,255,255,.62))}.light .monthly-game-art-backdrop{opacity:.2}.light .monthly-game-description,.light .monthly-game-facts>span{color:#64748b}.light .monthly-game-points{color:#b45309!important}.light .monthly-game-points svg{color:#d97706!important}.light .monthly-access-value{border-color:#ddd6fe;background:#faf5ff}.light .monthly-access-value>span{color:#7c3aed}.light .monthly-access-value code{color:#4c1d95}.light .monthly-copy-button{border-color:#ddd6fe;background:#f5f3ff;color:#6d28d9}.light .monthly-copy-button:hover{background:#ede9fe}.light .monthly-game-deadline{border-color:#fde68a;background:linear-gradient(90deg,#fffbeb,#fffdf5);color:#92400e!important}.light .monthly-game-deadline time{color:#78350f}.light .monthly-game-link{border-color:rgba(109,40,217,.2);background:#f5f3ff;color:#6d28d9}.light .monthly-game-link:hover{background:#ede9fe;color:#5b21b6}@media(max-width:1050px){.monthly-games-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:680px){.monthly-games-host{margin-top:10px;scroll-margin-top:76px}.monthly-games-panel{padding:13px;border-radius:12px}.monthly-games-heading{display:grid;gap:12px;margin-bottom:14px}.monthly-games-heading h2{font-size:1.12rem}.monthly-games-heading p{font-size:.7rem;line-height:1.5}.monthly-progress-summary{width:100%;box-sizing:border-box;padding:9px 10px}.monthly-progress-summary.is-pending{grid-template-columns:auto 1fr;flex-basis:auto}.monthly-games-grid{grid-template-columns:1fr;gap:12px}.monthly-game-card{border-radius:12px}.monthly-game-art{height:154px;padding:10px}.monthly-game-art-backdrop{inset:-24px!important;width:calc(100% + 48px)!important;height:calc(100% + 48px)!important;opacity:.2;filter:blur(22px) saturate(1.2)!important;transform:scale(1.05)}.monthly-game-art-image{max-width:66%!important;max-height:128px!important;filter:drop-shadow(0 8px 16px rgba(0,0,0,.3))!important}.monthly-game-status{top:8px;right:8px;max-width:58%;padding:4px 7px;font-size:.57rem;line-height:1.2;text-align:right}.monthly-game-status svg{width:11px;height:11px}.monthly-game-body{padding:13px}.monthly-game-body h3{font-size:.88rem;line-height:1.35}.monthly-game-description{min-height:0;margin-bottom:11px;font-size:.67rem;-webkit-line-clamp:2}.monthly-game-meta{gap:8px}.monthly-access-row{display:grid;grid-template-columns:minmax(0,1fr) 44px;gap:6px}.monthly-access-value{padding:8px 9px}.monthly-access-value>span{display:none}.monthly-access-value code{font-size:.69rem}.monthly-copy-button{min-width:44px;min-height:40px;padding:0;border-radius:8px}.monthly-copy-button span{display:none}.monthly-game-facts{display:grid;grid-template-columns:1fr 1fr;gap:7px 10px}.monthly-game-facts>span{font-size:.63rem}.monthly-game-points{justify-content:flex-start}.monthly-game-deadline{grid-template-columns:auto minmax(0,1fr);gap:4px 6px!important;padding:8px}.monthly-game-deadline strong{font-size:.57rem}.monthly-game-deadline time{grid-column:1/-1;justify-self:start;padding-left:19px;font-size:.62rem;text-align:left;white-space:normal}.monthly-game-link{min-height:40px;margin-top:10px;padding:9px 10px;font-size:.68rem}.light .monthly-game-art-backdrop{opacity:.16}}@media(max-width:380px){.monthly-games-panel{padding:10px}.monthly-game-art{height:144px}.monthly-game-art-image{max-width:62%!important;max-height:118px!important}.monthly-game-status{max-width:64%;font-size:.54rem}.monthly-game-facts{grid-template-columns:1fr}.monthly-access-row{grid-template-columns:minmax(0,1fr) 42px}.monthly-copy-button{min-width:42px}.monthly-game-deadline time{padding-left:18px}}.monthly-progress-summary.is-coming-soon{grid-template-columns:1fr;align-content:center;flex-basis:190px}.monthly-progress-summary.is-coming-soon strong{font-size:.86rem;color:#c4b5fd}.monthly-progress-summary.is-coming-soon span{font-size:.65rem;line-height:1.35}.monthly-games-coming-soon{display:flex;align-items:center;gap:16px;min-height:150px;padding:22px;border:1px dashed rgba(167,139,250,.28);border-radius:14px;background:linear-gradient(135deg,rgba(124,58,237,.08),rgba(15,23,42,.16));color:#dbe4f3}.monthly-coming-soon-icon{display:grid;place-items:center;flex:0 0 54px;width:54px;height:54px;border:1px solid rgba(167,139,250,.3);border-radius:16px;background:rgba(124,58,237,.12);color:#c4b5fd}.monthly-coming-soon-icon svg{width:25px;height:25px}.monthly-coming-soon-label{display:inline-block;margin-bottom:5px;color:#a78bfa;font-size:.62rem;font-weight:800;letter-spacing:.07em;text-transform:uppercase}.monthly-games-coming-soon h3{margin:0 0 6px;color:#f8fbff;font-size:1rem;line-height:1.35}.monthly-games-coming-soon p{max-width:700px;margin:0;color:var(--arcade-muted);font-size:.72rem;line-height:1.55}.light .monthly-progress-summary.is-coming-soon strong{color:#6d28d9}.light .monthly-games-coming-soon{border-color:rgba(109,40,217,.2);background:linear-gradient(135deg,#faf5ff,#f8fafc);color:#334155}.light .monthly-coming-soon-icon{border-color:#ddd6fe;background:#f5f3ff;color:#7c3aed}.light .monthly-games-coming-soon h3{color:#172033}.light .monthly-games-coming-soon p{color:#64748b}@media(max-width:680px){.monthly-progress-summary.is-coming-soon{flex-basis:auto}.monthly-games-coming-soon{align-items:flex-start;gap:12px;min-height:0;padding:16px}.monthly-coming-soon-icon{flex-basis:44px;width:44px;height:44px;border-radius:13px}.monthly-coming-soon-icon svg{width:21px;height:21px}.monthly-games-coming-soon h3{font-size:.91rem}.monthly-games-coming-soon p{font-size:.68rem}} +.monthly-games-host{margin-top:14px;scroll-margin-top:92px}.monthly-games-panel{position:relative;margin:0 0 14px;padding:20px;border:1px solid rgba(124,58,237,.24);border-radius:14px;background:linear-gradient(145deg,rgba(12,20,39,.96),rgba(17,28,50,.94));box-shadow:0 16px 40px rgba(2,8,23,.18)}.monthly-games-panel.is-loading{display:flex;align-items:center;justify-content:center;gap:10px;min-height:92px;color:var(--arcade-muted);font-size:.78rem}.monthly-games-panel.is-loading svg{width:18px;height:18px;color:#a78bfa}.monthly-games-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;margin-bottom:17px}.monthly-games-heading>div:first-child{min-width:0}.monthly-games-kicker{display:flex;align-items:center;gap:7px;margin-bottom:7px;color:#a78bfa;font-size:.68rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.monthly-games-kicker svg{width:15px;height:15px}.monthly-games-heading h2{margin:0 0 6px;color:#f8fbff;font-size:1.28rem;line-height:1.25;text-transform:capitalize}.monthly-games-heading p{max-width:700px;margin:0;color:var(--arcade-muted);font-size:.74rem;line-height:1.55}.monthly-progress-summary{display:grid;grid-template-columns:auto auto;align-items:baseline;gap:2px 7px;flex:0 0 150px;padding:10px 12px;border:1px solid rgba(167,139,250,.2);border-radius:10px;background:rgba(9,15,31,.58)}.monthly-progress-summary.is-pending{grid-template-columns:auto 1fr;flex-basis:210px}.monthly-progress-summary strong{color:#fff;font-size:1.3rem}.monthly-progress-summary span{color:var(--arcade-muted);font-size:.68rem}.monthly-progress-summary i{grid-column:1/-1;height:6px;overflow:hidden;border-radius:999px;background:#18233b}.monthly-progress-summary i b{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#7c3aed,#22c55e);transition:width .3s ease}.monthly-games-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;align-items:stretch}.monthly-game-card{display:flex;min-width:0;min-height:100%;overflow:hidden;flex-direction:column;border:1px solid rgba(148,163,184,.14);border-radius:14px;background:linear-gradient(180deg,rgba(10,19,36,.94),rgba(6,14,28,.98));box-shadow:inset 0 1px 0 rgba(255,255,255,.02);transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease}.monthly-game-card:hover{transform:translateY(-2px);border-color:rgba(167,139,250,.38);box-shadow:0 14px 30px rgba(2,8,23,.24)}.monthly-game-card.is-complete{border-color:rgba(34,197,94,.32);box-shadow:inset 0 1px 0 rgba(34,197,94,.05)}.monthly-game-art{position:relative;display:flex;height:178px;align-items:center;justify-content:center;overflow:hidden;padding:14px;border-bottom:1px solid rgba(148,163,184,.08);background:radial-gradient(circle at 50% 44%,rgba(66,85,120,.22),rgba(8,15,30,.98) 70%);color:#a78bfa}.monthly-game-art>img{display:block;width:auto!important;height:auto!important;max-width:72%!important;max-height:150px!important;object-fit:contain!important;object-position:center!important;background:transparent!important;filter:drop-shadow(0 8px 16px rgba(0,0,0,.18))}.monthly-game-art>svg{width:34px;height:34px}.monthly-game-art-backdrop{position:absolute!important;inset:-34px!important;width:calc(100% + 68px)!important;height:calc(100% + 68px)!important;max-width:none!important;max-height:none!important;object-fit:cover!important;opacity:.23;filter:blur(28px) saturate(1.3)!important;transform:scale(1.08);pointer-events:none}.monthly-game-art::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(6,14,28,.2),rgba(6,14,28,.55));pointer-events:none}.monthly-game-art-image{position:relative;z-index:1;max-width:76%!important;max-height:154px!important;filter:drop-shadow(0 10px 20px rgba(0,0,0,.34))!important}.monthly-game-status{position:absolute;z-index:2;top:10px;right:10px;display:flex;align-items:center;gap:5px;padding:5px 8px;border:1px solid rgba(251,191,36,.32);border-radius:999px;background:rgba(8,15,30,.9);color:#fbbf24;font-size:.61rem;font-weight:800;backdrop-filter:blur(8px)}.monthly-game-status svg{width:12px;height:12px}.monthly-game-status.is-complete{border-color:rgba(34,197,94,.42);color:#4ade80}.monthly-game-status.is-pending{border-color:rgba(96,165,250,.34);color:#93c5fd}.monthly-game-body{display:flex;min-height:0;flex:1;flex-direction:column;padding:15px 15px 14px}.monthly-game-body h3{margin:0 0 7px;color:#f4f8ff;font-size:.91rem;line-height:1.38}.monthly-game-description{display:-webkit-box;overflow:hidden;min-height:3.25em;margin:0 0 13px;color:#93a4bd;font-size:.69rem;line-height:1.55;-webkit-box-orient:vertical;-webkit-line-clamp:3}.monthly-game-meta{display:grid;gap:9px;margin-top:auto}.monthly-access-row{display:flex;align-items:stretch;gap:7px}.monthly-access-value{display:flex;min-width:0;flex:1;align-items:center;gap:8px;padding:7px 9px;border:1px solid rgba(167,139,250,.2);border-radius:8px;background:rgba(124,58,237,.09)}.monthly-access-value>span{flex:0 0 auto;color:#a78bfa;font-size:.57rem;font-weight:800;letter-spacing:.05em;text-transform:uppercase}.monthly-access-value code{min-width:0;overflow:hidden;color:#f5f3ff;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.7rem;font-weight:800;text-overflow:ellipsis;white-space:nowrap}.monthly-copy-button{display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;gap:5px;min-width:68px;padding:0 10px;border:1px solid rgba(167,139,250,.28);border-radius:8px;background:rgba(124,58,237,.14);color:#c4b5fd;font-size:.61rem;font-weight:800;cursor:pointer;transition:border-color .16s ease,background .16s ease,color .16s ease}.monthly-copy-button:hover{border-color:rgba(196,181,253,.55);background:rgba(124,58,237,.22);color:#ede9fe}.monthly-copy-button svg{width:13px;height:13px}.monthly-copy-button.is-copied{border-color:rgba(34,197,94,.42);background:rgba(34,197,94,.1);color:#86efac}.monthly-game-facts{display:flex;flex-wrap:wrap;gap:7px 14px;padding:0 1px}.monthly-game-facts>span{display:flex;align-items:center;gap:5px;color:#9fb0c6;font-size:.65rem}.monthly-game-facts svg{width:13px;height:13px;color:#8b5cf6}.monthly-game-points{color:#fbbf24!important;font-weight:800}.monthly-game-points svg{color:#f59e0b!important}.monthly-game-deadline{display:grid!important;grid-template-columns:auto auto minmax(0,1fr);align-items:center;gap:6px!important;padding:8px 9px;border:1px solid rgba(251,191,36,.26);border-radius:8px;background:linear-gradient(90deg,rgba(120,53,15,.18),rgba(69,26,3,.08));color:#fde68a!important}.monthly-game-deadline svg{width:13px;height:13px;color:#f59e0b!important}.monthly-game-deadline strong{font-size:.59rem;letter-spacing:.05em;text-transform:uppercase}.monthly-game-deadline time{justify-self:end;overflow:hidden;color:#fef3c7;font-size:.64rem;font-weight:750;text-align:right;text-overflow:ellipsis;white-space:nowrap}.monthly-game-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:11px}.monthly-game-actions>.monthly-lab-source,.monthly-game-actions>.monthly-game-link{display:flex;width:100%;min-width:0;min-height:40px;align-items:center;justify-content:center;gap:6px;margin-top:0;box-sizing:border-box;padding:8px 10px;border-radius:8px;font-family:inherit;font-size:.68rem;font-weight:800;line-height:1.2;text-decoration:none;white-space:nowrap}.monthly-game-link{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:11px;padding:8px 10px;border:1px solid rgba(124,58,237,.28);border-radius:8px;background:rgba(124,58,237,.11);color:#c4b5fd;font-size:.68rem;font-weight:800;text-decoration:none}.monthly-game-link:hover{border-color:rgba(139,92,246,.52);background:rgba(124,58,237,.18);color:#ede9fe}.monthly-game-link svg{width:13px;height:13px}.light .monthly-games-panel{border-color:rgba(109,40,217,.18);background:linear-gradient(145deg,rgba(255,255,255,.98),rgba(248,250,252,.98));box-shadow:0 14px 34px rgba(15,23,42,.08)}.light .monthly-games-heading h2,.light .monthly-game-body h3,.light .monthly-progress-summary strong{color:#172033}.light .monthly-progress-summary{border-color:rgba(109,40,217,.16);background:#f8fafc}.light .monthly-progress-summary i{background:#e2e8f0}.light .monthly-game-card{border-color:#e2e8f0;background:linear-gradient(180deg,#fff,#fbfdff)}.light .monthly-game-card.is-complete{border-color:rgba(22,163,74,.28)}.light .monthly-game-art{border-bottom-color:#e5e7eb;background:radial-gradient(circle at 50% 42%,#f8fafc,#eef2f7)}.light .monthly-game-art::after{background:linear-gradient(180deg,rgba(255,255,255,.15),rgba(255,255,255,.62))}.light .monthly-game-art-backdrop{opacity:.2}.light .monthly-game-description,.light .monthly-game-facts>span{color:#64748b}.light .monthly-game-points{color:#b45309!important}.light .monthly-game-points svg{color:#d97706!important}.light .monthly-access-value{border-color:#ddd6fe;background:#faf5ff}.light .monthly-access-value>span{color:#7c3aed}.light .monthly-access-value code{color:#4c1d95}.light .monthly-copy-button{border-color:#ddd6fe;background:#f5f3ff;color:#6d28d9}.light .monthly-copy-button:hover{background:#ede9fe}.light .monthly-game-deadline{border-color:#fde68a;background:linear-gradient(90deg,#fffbeb,#fffdf5);color:#92400e!important}.light .monthly-game-deadline time{color:#78350f}.light .monthly-game-link{border-color:rgba(109,40,217,.2);background:#f5f3ff;color:#6d28d9}.light .monthly-game-link:hover{background:#ede9fe;color:#5b21b6}@media(max-width:1050px){.monthly-games-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:680px){.monthly-games-host{margin-top:10px;scroll-margin-top:76px}.monthly-games-panel{padding:13px;border-radius:12px}.monthly-games-heading{display:grid;gap:12px;margin-bottom:14px}.monthly-games-heading h2{font-size:1.12rem}.monthly-games-heading p{font-size:.7rem;line-height:1.5}.monthly-progress-summary{width:100%;box-sizing:border-box;padding:9px 10px}.monthly-progress-summary.is-pending{grid-template-columns:auto 1fr;flex-basis:auto}.monthly-games-grid{grid-template-columns:1fr;gap:12px}.monthly-game-card{border-radius:12px}.monthly-game-art{height:154px;padding:10px}.monthly-game-art-backdrop{inset:-24px!important;width:calc(100% + 48px)!important;height:calc(100% + 48px)!important;opacity:.2;filter:blur(22px) saturate(1.2)!important;transform:scale(1.05)}.monthly-game-art-image{max-width:66%!important;max-height:128px!important;filter:drop-shadow(0 8px 16px rgba(0,0,0,.3))!important}.monthly-game-status{top:8px;right:8px;max-width:58%;padding:4px 7px;font-size:.57rem;line-height:1.2;text-align:right}.monthly-game-status svg{width:11px;height:11px}.monthly-game-body{padding:13px}.monthly-game-body h3{font-size:.88rem;line-height:1.35}.monthly-game-description{min-height:0;margin-bottom:11px;font-size:.67rem;-webkit-line-clamp:2}.monthly-game-meta{gap:8px}.monthly-access-row{display:grid;grid-template-columns:minmax(0,1fr) 44px;gap:6px}.monthly-access-value{padding:8px 9px}.monthly-access-value>span{display:none}.monthly-access-value code{font-size:.69rem}.monthly-copy-button{min-width:44px;min-height:40px;padding:0;border-radius:8px}.monthly-copy-button span{display:none}.monthly-game-facts{display:grid;grid-template-columns:1fr 1fr;gap:7px 10px}.monthly-game-facts>span{font-size:.63rem}.monthly-game-points{justify-content:flex-start}.monthly-game-deadline{grid-template-columns:auto minmax(0,1fr);gap:4px 6px!important;padding:8px}.monthly-game-deadline strong{font-size:.57rem}.monthly-game-deadline time{grid-column:1/-1;justify-self:start;padding-left:19px;font-size:.62rem;text-align:left;white-space:normal}.monthly-game-link{min-height:40px;margin-top:10px;padding:9px 10px;font-size:.68rem}.light .monthly-game-art-backdrop{opacity:.16}}@media(max-width:380px){.monthly-games-panel{padding:10px}.monthly-game-art{height:144px}.monthly-game-art-image{max-width:62%!important;max-height:118px!important}.monthly-game-status{max-width:64%;font-size:.54rem}.monthly-game-facts{grid-template-columns:1fr}.monthly-access-row{grid-template-columns:minmax(0,1fr) 42px}.monthly-copy-button{min-width:42px}.monthly-game-deadline time{padding-left:18px}}.monthly-progress-summary.is-coming-soon{grid-template-columns:1fr;align-content:center;flex-basis:190px}.monthly-progress-summary.is-coming-soon strong{font-size:.86rem;color:#c4b5fd}.monthly-progress-summary.is-coming-soon span{font-size:.65rem;line-height:1.35}.monthly-games-coming-soon{display:flex;align-items:center;gap:16px;min-height:150px;padding:22px;border:1px dashed rgba(167,139,250,.28);border-radius:14px;background:linear-gradient(135deg,rgba(124,58,237,.08),rgba(15,23,42,.16));color:#dbe4f3}.monthly-coming-soon-icon{display:grid;place-items:center;flex:0 0 54px;width:54px;height:54px;border:1px solid rgba(167,139,250,.3);border-radius:16px;background:rgba(124,58,237,.12);color:#c4b5fd}.monthly-coming-soon-icon svg{width:25px;height:25px}.monthly-coming-soon-label{display:inline-block;margin-bottom:5px;color:#a78bfa;font-size:.62rem;font-weight:800;letter-spacing:.07em;text-transform:uppercase}.monthly-games-coming-soon h3{margin:0 0 6px;color:#f8fbff;font-size:1rem;line-height:1.35}.monthly-games-coming-soon p{max-width:700px;margin:0;color:var(--arcade-muted);font-size:.72rem;line-height:1.55}.light .monthly-progress-summary.is-coming-soon strong{color:#6d28d9}.light .monthly-games-coming-soon{border-color:rgba(109,40,217,.2);background:linear-gradient(135deg,#faf5ff,#f8fafc);color:#334155}.light .monthly-coming-soon-icon{border-color:#ddd6fe;background:#f5f3ff;color:#7c3aed}.light .monthly-games-coming-soon h3{color:#172033}.light .monthly-games-coming-soon p{color:#64748b}@media(max-width:680px){.monthly-progress-summary.is-coming-soon{flex-basis:auto}.monthly-games-coming-soon{align-items:flex-start;gap:12px;min-height:0;padding:16px}.monthly-coming-soon-icon{flex-basis:44px;width:44px;height:44px;border-radius:13px}.monthly-coming-soon-icon svg{width:21px;height:21px}.monthly-games-coming-soon h3{font-size:.91rem}.monthly-games-coming-soon p{font-size:.68rem}} diff --git a/app/styles/site-ui.css b/app/styles/site-ui.css index 7e162e410..7e918a2a5 100644 --- a/app/styles/site-ui.css +++ b/app/styles/site-ui.css @@ -215,14 +215,464 @@ html[lang="en"] .hero-heading h1 { font-weight: 400; } .site-content-page .content-primary-action { color: var(--ui-on-accent); } .content-primary-action:hover { filter: brightness(.94); } -.monthly-lab-breadcrumbs { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: 2rem; font-size: .875rem; color: var(--ui-muted); overflow-wrap: anywhere; } +.monthly-lab-breadcrumbs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .5rem; + margin-bottom: 2rem; + font-size: .875rem; + color: var(--ui-muted); + overflow-wrap: anywhere; +} +.monthly-lab-breadcrumbs a { color: var(--ui-muted); transition: color .15s ease; } +.monthly-lab-breadcrumbs a:hover { color: var(--ui-accent); } .monthly-lab-breadcrumbs [aria-current] { color: var(--ui-text); } -.monthly-lab-art { display: flex; align-items: center; justify-content: center; border-radius: var(--ui-radius); background: var(--ui-soft); padding: 1rem; } -.monthly-lab-status { display: inline-flex; align-self: flex-start; margin-top: 1rem; padding: .25rem .65rem; border-radius: var(--ui-radius-sm); font-size: .75rem; font-weight: 600; background: var(--ui-soft); color: var(--ui-muted); } -.monthly-lab-status.labActive { background: var(--ui-accent-soft); color: var(--ui-accent); } -.monthly-lab-facts { display: grid; gap: 1rem; margin-top: 1.25rem; } -.monthly-lab-facts dt { font-size: .75rem; color: var(--ui-muted); margin-bottom: .3rem; } -.monthly-lab-facts dd { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; font-size: .875rem; overflow-wrap: anywhere; } -.monthly-lab-facts code { user-select: all; overflow-wrap: anywhere; } -.monthly-lab-facts [role=status]:empty { display: none; } -.monthly-lab-source { display: inline-flex; align-items: center; gap: .5rem; min-height: 44px; color: var(--ui-accent); font-size: .875rem; } + +.monthly-labs-content { + display: grid; + gap: 1.5rem; +} + +.monthly-lab-archive-grid, +.monthly-lab-game-grid { + display: grid; + gap: 1rem; +} +.monthly-lab-archive-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.monthly-lab-game-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + +.monthly-lab-month-card { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 1rem; + min-height: 104px; + padding: 1.15rem; + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-lg); + background: var(--ui-surface); + color: var(--ui-text); + box-shadow: var(--ui-shadow); + transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease; +} +.monthly-lab-month-card:hover { + transform: translateY(-2px); + border-color: var(--ui-accent-border); + box-shadow: 0 12px 28px hsl(224 50% 3% / .16); +} +.monthly-lab-month-icon { + display: grid; + width: 46px; + height: 46px; + place-items: center; + border-radius: 14px; + background: var(--ui-accent-soft); + color: var(--ui-accent); +} +.monthly-lab-month-icon svg { width: 22px; height: 22px; } +.monthly-lab-month-copy { + display: grid; + min-width: 0; + gap: .3rem; +} +.monthly-lab-month-copy strong { + color: var(--ui-text); + font-size: 1rem; + line-height: 1.35; +} +.monthly-lab-month-copy span { + color: var(--ui-muted); + font-size: .82rem; + line-height: 1.4; +} +.monthly-lab-card-arrow { + width: 18px; + height: 18px; + color: var(--ui-muted); + transition: transform .18s ease, color .18s ease; +} +.monthly-lab-month-card:hover .monthly-lab-card-arrow, +.monthly-lab-game-card:hover .monthly-lab-card-arrow { + transform: translateX(3px); + color: var(--ui-accent); +} + +.monthly-lab-list-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + min-height: 44px; +} +.monthly-lab-list-toolbar p { + margin: 0; + color: var(--ui-muted); + font-size: .875rem; +} +.monthly-lab-back-link { + display: inline-flex; + min-height: 40px; + align-items: center; + gap: .45rem; + padding: .5rem .8rem; + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-sm); + background: var(--ui-surface); + color: var(--ui-text); + font-size: .82rem; + font-weight: 700; + transition: border-color .15s ease, color .15s ease, background .15s ease; +} +.monthly-lab-back-link:hover { + border-color: var(--ui-accent-border); + background: var(--ui-accent-soft); + color: var(--ui-accent); +} +.monthly-lab-back-link svg { width: 16px; height: 16px; } + +.monthly-lab-game-card { + display: flex; + min-width: 0; + min-height: 100%; + flex-direction: column; + overflow: hidden; + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-lg); + background: var(--ui-surface); + color: var(--ui-text); + box-shadow: var(--ui-shadow); + transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease; +} +.monthly-lab-game-card:hover { + transform: translateY(-2px); + border-color: var(--ui-accent-border); + box-shadow: 0 14px 32px hsl(224 50% 3% / .16); +} +.monthly-lab-game-art { + display: flex; + aspect-ratio: 16 / 7; + align-items: center; + justify-content: center; + overflow: hidden; + border-bottom: 1px solid var(--ui-border); + background: + radial-gradient(circle at 20% 20%, var(--ui-hero-glow), transparent 44%), + var(--ui-soft); + padding: 1.25rem; +} +.monthly-lab-game-art img { + transition: transform .2s ease; +} +.monthly-lab-game-card:hover .monthly-lab-game-art img { + transform: scale(1.025); +} +.monthly-lab-placeholder-icon { + width: 64px; + height: 64px; + color: var(--ui-muted); +} +.monthly-lab-game-body { + display: flex; + min-width: 0; + flex: 1; + flex-direction: column; + padding: 1.15rem 1.2rem 1.2rem; +} +.monthly-lab-card-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .5rem; +} +.monthly-lab-status, +.monthly-lab-points-badge { + display: inline-flex; + min-height: 28px; + align-items: center; + gap: .35rem; + padding: .3rem .65rem; + border-radius: 999px; + font-size: .72rem; + font-weight: 800; + line-height: 1; +} +.monthly-lab-status { + background: var(--ui-soft); + color: var(--ui-muted); +} +.monthly-lab-status.labActive { + background: var(--ui-accent-soft); + color: var(--ui-accent); +} +.monthly-lab-status.labUnknown { + color: var(--ui-warning); +} +.monthly-lab-points-badge { + background: var(--ui-info-soft); + color: var(--ui-info); +} +.monthly-lab-points-badge svg { width: 13px; height: 13px; } +.monthly-lab-game-title { + margin-top: .9rem; + color: var(--ui-text); + font-size: 1.05rem; + line-height: 1.45; + overflow-wrap: anywhere; +} +.monthly-lab-game-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: .75rem; + margin-top: auto; + padding-top: 1.15rem; + color: var(--ui-accent); + font-size: .82rem; + font-weight: 800; +} +.monthly-lab-game-footer svg { + width: 17px; + height: 17px; + transition: transform .18s ease; +} +.monthly-lab-game-card:hover .monthly-lab-game-footer svg { + transform: translateX(3px); +} + +.monthly-lab-detail-card { + display: grid; + grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); + overflow: hidden; + padding: 0 !important; +} +.monthly-lab-detail-media { + display: flex; + min-height: 360px; + align-items: center; + justify-content: center; + border-right: 1px solid var(--ui-border); + background: + radial-gradient(circle at 30% 25%, var(--ui-hero-glow), transparent 42%), + var(--ui-soft); + padding: 2rem; +} +.monthly-lab-detail-placeholder { + width: 120px; + height: 120px; + color: var(--ui-muted); +} +.monthly-lab-detail-body { + display: flex; + min-width: 0; + flex-direction: column; + justify-content: center; + padding: 1.75rem; +} +.monthly-lab-fact-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: .8rem; + margin-top: 1.35rem; +} +.monthly-lab-fact { + min-width: 0; + padding: .9rem; + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius); + background: var(--ui-soft); +} +.monthly-lab-fact dt { + display: flex; + align-items: center; + gap: .45rem; + margin-bottom: .45rem; + color: var(--ui-muted); + font-size: .72rem; + font-weight: 700; +} +.monthly-lab-fact dt svg { + width: 14px; + height: 14px; + color: var(--ui-accent); +} +.monthly-lab-fact dd { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .6rem; + margin: 0; + color: var(--ui-text); + font-size: .86rem; + font-weight: 700; + line-height: 1.5; + overflow-wrap: anywhere; +} +.monthly-lab-fact code { + max-width: 100%; + user-select: all; + overflow-wrap: anywhere; +} +.monthly-lab-fact [role=status]:empty { display: none; } +.monthly-lab-detail-actions { + display: flex; + flex-wrap: wrap; + gap: .75rem; + margin-top: 1.25rem; +} +.monthly-lab-source { + display: inline-flex; + min-height: 44px; + align-items: center; + justify-content: center; + gap: .5rem; + padding: .625rem .95rem; + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-sm); + background: var(--ui-surface); + color: var(--ui-accent); + font-size: .875rem; + font-weight: 700; + transition: border-color .15s ease, background .15s ease; +} +.monthly-lab-source:hover { + border-color: var(--ui-accent-border); + background: var(--ui-accent-soft); +} +.monthly-lab-description-card { + padding: 1.4rem; +} +.monthly-lab-section-heading { + display: flex; + align-items: center; + gap: .7rem; +} +.monthly-lab-section-heading svg { + width: 20px; + height: 20px; + color: var(--ui-accent); +} +.monthly-lab-section-heading h2 { + margin: 0; + color: var(--ui-text); + font-size: 1.05rem; +} +.monthly-lab-description-card > p { + margin: .9rem 0 0; + color: var(--ui-muted); + font-size: .92rem; + line-height: 1.75; +} +.monthly-lab-history-note { + display: flex; + align-items: flex-start; + gap: .7rem; + padding: .9rem 1rem; + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius); + background: var(--ui-soft); + color: var(--ui-muted); + font-size: .82rem; + line-height: 1.6; +} +.monthly-lab-history-note svg { + width: 17px; + height: 17px; + flex: 0 0 auto; + margin-top: .15rem; + color: var(--ui-info); +} +.monthly-lab-history-note p { margin: 0; } + +@media (max-width: 900px) { + .monthly-lab-archive-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .monthly-lab-detail-card { grid-template-columns: 1fr; } + .monthly-lab-detail-media { + min-height: 280px; + border-right: 0; + border-bottom: 1px solid var(--ui-border); + } +} + +@media (max-width: 640px) { + .monthly-lab-archive-grid, + .monthly-lab-game-grid { + grid-template-columns: 1fr; + } + .monthly-lab-month-card { min-height: 92px; } + .monthly-lab-list-toolbar { + align-items: flex-start; + flex-direction: column; + } + .monthly-lab-game-art { aspect-ratio: 16 / 8; } + .monthly-lab-detail-media { + height: 176px; + min-height: 0; + padding: .75rem 1rem; + } + .monthly-lab-detail-media :is(img,svg) { + max-height: 150px; + } + .monthly-lab-detail-body { + justify-content: flex-start; + padding: 1rem; + } + .monthly-lab-detail-meta { + margin-bottom: .15rem; + } + .monthly-lab-fact-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: .65rem; + margin-top: .9rem; + } + .monthly-lab-fact { + padding: .75rem; + border-radius: var(--ui-radius-sm); + } + .monthly-lab-fact-access, + .monthly-lab-fact-deadline { + grid-column: 1 / -1; + } + .monthly-lab-fact-points:last-child, + .monthly-lab-fact-places:nth-child(3):last-child { + grid-column: 1 / -1; + } + .monthly-lab-fact dt { + margin-bottom: .35rem; + font-size: .68rem; + } + .monthly-lab-fact dd { + gap: .45rem; + font-size: .79rem; + line-height: 1.4; + } + .monthly-lab-fact-access dd { + flex-wrap: nowrap; + } + .monthly-lab-fact-access code { + min-width: 0; + flex: 1; + font-size: .8rem; + } + .monthly-lab-fact-access .monthly-copy-button { + min-width: 42px; + min-height: 38px; + padding: 0 .7rem; + } + .monthly-lab-detail-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: .65rem; + margin-top: .9rem; + } + .monthly-lab-detail-actions :is(.content-primary-action,.monthly-lab-source) { + width: 100%; + min-width: 0; + min-height: 42px; + padding: .6rem .7rem; + font-size: .78rem; + line-height: 1.2; + text-align: center; + } +} + diff --git a/components/arcade/facilitator-panel-gate.tsx b/components/arcade/facilitator-panel-gate.tsx index 4fccdf0f8..a47df5a56 100644 --- a/components/arcade/facilitator-panel-gate.tsx +++ b/components/arcade/facilitator-panel-gate.tsx @@ -6,9 +6,13 @@ import FacilitatorPanel from "./facilitator-panel" import { FACILITATOR_PANEL_OPEN_EVENT, FACILITATOR_PARTICIPATION_EVENT, + FACILITATOR_PROGRAM_STATE_EVENT, normalizeFacilitatorProfileUrl, readFacilitatorParticipation, + readFacilitatorProgramState, type FacilitatorParticipationDetail, + type FacilitatorProgramState, + type FacilitatorProgramStateDetail, } from "./facilitator-participation" import { readActiveDashboard, observeDashboardMode } from "./dashboard-state" @@ -21,6 +25,9 @@ function readStoredProfileUrl(): string { export default function FacilitatorPanelGate() { const [profileUrl, setProfileUrl] = useState("") const [participating, setParticipating] = useState(false) + const [programState, setProgramState] = useState( + () => readFacilitatorProgramState(), + ) const returnFocusRef = useRef(null) const drawerWasOpenRef = useRef(false) @@ -79,14 +86,37 @@ export default function FacilitatorPanelGate() { } }, [profileUrl]) + useEffect(() => { + const syncProgramState = () => setProgramState(readFacilitatorProgramState()) + const onProgramStateChange = (event: Event) => { + const detail = (event as CustomEvent).detail + setProgramState(detail?.state ?? readFacilitatorProgramState()) + } + + syncProgramState() + window.addEventListener(FACILITATOR_PROGRAM_STATE_EVENT, onProgramStateChange) + return () => + window.removeEventListener( + FACILITATOR_PROGRAM_STATE_EVENT, + onProgramStateChange, + ) + }, []) + useEffect(() => { const updateLauncherVisibility = () => { const launcher = document.querySelector(".facilitator-launcher") if (!launcher) return + const programAllowsLauncher = + programState === "active" || programState === "unconfigured" launcher.hidden = - !participating || !launcher.classList.contains("is-participating") + !participating || + !programAllowsLauncher || + !launcher.classList.contains("is-participating") + document.documentElement.dataset.facilitatorLauncherVisible = launcher.hidden + ? "false" + : "true" } updateLauncherVisibility() @@ -98,8 +128,11 @@ export default function FacilitatorPanelGate() { attributeFilter: ["class"], }) - return () => observer.disconnect() - }, [participating]) + return () => { + observer.disconnect() + delete document.documentElement.dataset.facilitatorLauncherVisible + } + }, [participating, programState]) useEffect(() => { const restoreFocusWhenDrawerCloses = () => { diff --git a/components/arcade/facilitator-participation.ts b/components/arcade/facilitator-participation.ts index ee284354d..2be2feed9 100644 --- a/components/arcade/facilitator-participation.ts +++ b/components/arcade/facilitator-participation.ts @@ -1,6 +1,19 @@ export const FACILITATOR_PARTICIPATION_EVENT = "arcade-facilitator-participation-change" export const FACILITATOR_PANEL_OPEN_EVENT = "arcade-facilitator-panel-open" +export const FACILITATOR_PROGRAM_STATE_EVENT = + "arcade-facilitator-program-state-change" + +export type FacilitatorProgramState = + | "unknown" + | "active" + | "ended" + | "unconfigured" + | "disabled" + +export type FacilitatorProgramStateDetail = { + state: FacilitatorProgramState +} const PARTICIPATION_STORAGE_PREFIX = "arcade-facilitator-participation-v1" @@ -61,3 +74,35 @@ export function writeFacilitatorParticipation( ), ) } + +const FACILITATOR_PROGRAM_STATES = new Set([ + "unknown", + "active", + "ended", + "unconfigured", + "disabled", +]) + +export function readFacilitatorProgramState(): FacilitatorProgramState { + if (typeof document === "undefined") return "unknown" + + const state = document.documentElement.dataset.facilitatorProgramState + return FACILITATOR_PROGRAM_STATES.has(state as FacilitatorProgramState) + ? (state as FacilitatorProgramState) + : "unknown" +} + +export function writeFacilitatorProgramState( + state: FacilitatorProgramState, +): void { + if (typeof document === "undefined" || typeof window === "undefined") return + if (document.documentElement.dataset.facilitatorProgramState === state) return + + document.documentElement.dataset.facilitatorProgramState = state + window.dispatchEvent( + new CustomEvent( + FACILITATOR_PROGRAM_STATE_EVENT, + { detail: { state } }, + ), + ) +} diff --git a/components/arcade/monthly-games-panel.tsx b/components/arcade/monthly-games-panel.tsx index 7cf4599ad..7fe920ff3 100644 --- a/components/arcade/monthly-games-panel.tsx +++ b/components/arcade/monthly-games-panel.tsx @@ -553,11 +553,19 @@ export default function MonthlyGamesPanel({ badges, hasProfile }: MonthlyGamesPa - {detailPath && {catalog.messages.labDetails}} - {game.joinUrl && ( - - {text("openGame")} - + {(detailPath || game.joinUrl) && ( +
+ {detailPath && ( + + {catalog.messages.labDetails} + + )} + {game.joinUrl && ( + + {text("openGame")} + + )} +
)} diff --git a/components/arcade/program-countdown.tsx b/components/arcade/program-countdown.tsx index 738f2ff88..6f1964026 100644 --- a/components/arcade/program-countdown.tsx +++ b/components/arcade/program-countdown.tsx @@ -9,10 +9,14 @@ import { type DeadlineSource, type ResolvedDeadline, } from "./countdown-deadline" +import { + FACILITATOR_PANEL_OPEN_EVENT, + writeFacilitatorProgramState, + type FacilitatorProgramState, +} from "./facilitator-participation" const HOST_SELECTOR = ".program-countdown-host" const HERO_SELECTOR = ".arcade-hero" -const FACILITATOR_LAUNCHER_SELECTOR = ".facilitator-launcher" const DEFAULT_TIME_ZONE_OFFSET = "+05:30" const FIREBASE_CDN_VERSION = "12.18.0" const DEFAULT_FETCH_INTERVAL_MS = 900_000 @@ -339,10 +343,18 @@ function formatPart(value: number): string { return String(value).padStart(2, "0") } +function facilitatorProgramState( + config: ProgramCountdownConfig | undefined, + nowMs: number, +): FacilitatorProgramState { + if (!config) return "unknown" + if (!config.enabled) return "disabled" + if (!config.deadline) return "unconfigured" + return countdownParts(config.deadline, nowMs).ended ? "ended" : "active" +} + function openFacilitatorDetails() { - document - .querySelector(FACILITATOR_LAUNCHER_SELECTOR) - ?.click() + window.dispatchEvent(new Event(FACILITATOR_PANEL_OPEN_EVENT)) } function ProgramCard({ @@ -389,8 +401,12 @@ function ProgramCard({ ) : remaining.ended ? (
- Unavailable - Program tracker + {config.id === "facilitator" ? "Season ended" : "Unavailable"} + + {config.id === "facilitator" + ? "Facilitator Program 2026 has ended" + : "Program tracker"} +
{config.id === "facilitator" ? ( {copyStatus === "failed" ? m.labCopyFailed : ""} : m.notAvailable}
-
{text("deadline")}
{lab.deadline && Number.isFinite(Date.parse(lab.deadline)) ? : deadline}
- {lab.points !== null &&
{m.arcadePoints}
{text(lab.points === 1 ? "arcadePoint" : "arcadePoints", { count: formatNumber(lab.points) })}
} - {lab.spotsRemaining !== null &&
{m.labHistoricalSlots}
{formatNumber(lab.spotsRemaining)}
} - -
- {lab.joinUrl && labState(lab, now) !== "labArchived" && {text("openGame")}} - {lab.sourceUrl && {m.labSource}} - {lab.joinUrl && labState(lab, now) === "labArchived" && {m.labDetails}} -
+ + const breadcrumbs = ( + + ) + + return ( +
+ +
+ {months && ( +
+ {months.map((item) => ( + + + + + {monthLabel(item.month, intlLocale)} + {m.labCount.replace("{count}", formatNumber(item.count))} + +
- -

{m.labSourceDescription}

{lab.description ?? m.labMissingDescription}

- } - {(month || lab) &&

{m.labHistoricalNotice}

} -
-
-
+ )} + + {labs && ( + <> +
+

{m.labCount.replace("{count}", formatNumber(labs.length))}

+ +
+ +
+ {labs.map((item) => { + const state = labState(item, now) + return ( + + + {item.imageUrl ? ( + + ) : ( + + + + + {m[state]} + {item.points !== null && ( + + + )} + + + {item.title} + + + {m.labDetails} + + + + ) + })} +
+ + )} + + {lab && detailState && ( + <> + +
+ {lab.imageUrl ? ( + + ) : ( +
+ +
+
+ {m[detailState]} +
+ +
+
+
+
+ {lab.accessCode ? ( + <> + {lab.accessCode} + + {copyStatus === "failed" ? m.labCopyFailed : ""} + + ) : m.notAvailable} +
+
+ +
+
+
+ {lab.deadline && Number.isFinite(Date.parse(lab.deadline)) + ? + : deadline} +
+
+ + {lab.points !== null && ( +
+
+
{text(lab.points === 1 ? "arcadePoint" : "arcadePoints", { count: formatNumber(lab.points) })}
+
+ )} + + {lab.spotsRemaining !== null && ( +
+
+
{formatNumber(lab.spotsRemaining)}
+
+ )} +
+ +
+ {lab.joinUrl && detailState !== "labArchived" && ( + + {text("openGame")} + + )} + {lab.sourceUrl && ( + + {m.labSource} + + )} + {lab.joinUrl && detailState === "labArchived" && ( + + {m.labDetails} + + )} +
+
+
+ + +
+
+

{lab.description ?? m.labMissingDescription}

+
+ + )} + + {(month || lab) && ( + + )} + + + + ) } diff --git a/scripts/verify-monthly-labs.mjs b/scripts/verify-monthly-labs.mjs index 472f575c6..d367c7593 100644 --- a/scripts/verify-monthly-labs.mjs +++ b/scripts/verify-monthly-labs.mjs @@ -30,10 +30,10 @@ try { assert.equal(await page.locator("h1").count(), 1) if (path === "/monthly-labs/") { await page.locator("h1").filter({ hasText: catalog.messages.monthlyLabs }).waitFor() - assert.equal(await page.locator(".content-card").count(), Object.keys(snapshot.months).length) + assert.equal(await page.locator(".monthly-lab-month-card").count(), Object.keys(snapshot.months).length) } else if (path.endsWith("/09/")) { - assert.equal(await page.locator(".content-card").count(), september.length) - await page.locator(".content-primary-action").filter({ hasText: catalog.messages.labDetails }).first().waitFor() + assert.equal(await page.locator(".monthly-lab-game-card").count(), september.length) + await page.locator(".monthly-lab-game-card").filter({ hasText: catalog.messages.labDetails }).first().waitFor() } else { assert.equal(await page.locator("h1").innerText(), example.title) await page.locator(".monthly-lab-status").filter({ hasText: catalog.messages.labArchived }).waitFor() diff --git a/tests/facilitator-launcher-state.test.mjs b/tests/facilitator-launcher-state.test.mjs new file mode 100644 index 000000000..a14b0a567 --- /dev/null +++ b/tests/facilitator-launcher-state.test.mjs @@ -0,0 +1,43 @@ +import assert from "node:assert/strict" +import { readFileSync } from "node:fs" +import test from "node:test" + +const participation = readFileSync( + new URL("../components/arcade/facilitator-participation.ts", import.meta.url), + "utf8", +) +const countdown = readFileSync( + new URL("../components/arcade/program-countdown.tsx", import.meta.url), + "utf8", +) +const gate = readFileSync( + new URL("../components/arcade/facilitator-panel-gate.tsx", import.meta.url), + "utf8", +) +const backToTopStyles = readFileSync( + new URL("../app/styles/back-to-top.css", import.meta.url), + "utf8", +) + +test("Facilitator season state is shared with the launcher gate", () => { + assert.match(participation, /FACILITATOR_PROGRAM_STATE_EVENT/) + assert.match(participation, /writeFacilitatorProgramState/) + assert.match(countdown, /facilitatorProgramState\(facilitatorProgram, nowMs\)/) + assert.match(gate, /programState === "active" \|\| programState === "unconfigured"/) + assert.match(gate, /facilitatorLauncherVisible/) +}) + +test("ended Facilitator uses the existing detail event instead of a visible launcher", () => { + assert.match(countdown, /Season ended/) + assert.match(countdown, /FACILITATOR_PANEL_OPEN_EVENT/) + assert.doesNotMatch(countdown, /FACILITATOR_LAUNCHER_SELECTOR/) + assert.match(gate, /FACILITATOR_PANEL_OPEN_EVENT/) +}) + +test("mobile back-to-top moves above a visible Facilitator launcher", () => { + assert.match( + backToTopStyles, + /html\[data-facilitator-launcher-visible="true"\] \.back-to-top/, + ) + assert.match(backToTopStyles, /bottom: calc\(148px \+ env\(safe-area-inset-bottom\)\)/) +}) diff --git a/tests/monthly-labs.test.mjs b/tests/monthly-labs.test.mjs index 8c73cc3ff..89ce9afbd 100644 --- a/tests/monthly-labs.test.mjs +++ b/tests/monthly-labs.test.mjs @@ -82,6 +82,30 @@ test("all monthly page copy has translated catalogs with matching placeholders a assert.deepEqual(leaks, []) assert.match(source, /useSiteCatalog/) assert.match(source, /lab\.deadlineTimeZone \?\? "UTC"/) - assert.match(source, /labState\(lab, now\) !== "labArchived"/) + assert.match(source, /detailState !== "labArchived"/) assert.match(readRepoFile("app/sitemap.ts"), /monthlyLabPath/) }) + +test("monthly labs use one responsive UI hierarchy across archive, month and detail pages", () => { + const source = readRepoFile("components/monthly-labs/pages.tsx") + const styles = readRepoFile("app/styles/site-ui.css") + + for (const className of [ + "monthly-lab-archive-grid", + "monthly-lab-month-card", + "monthly-lab-game-grid", + "monthly-lab-game-card", + "monthly-lab-detail-card", + "monthly-lab-fact-grid", + "monthly-lab-history-note", + ]) { + assert.ok(source.includes(className), `monthly lab JSX missing ${className}`) + assert.ok(styles.includes(`.${className}`), `monthly lab CSS missing ${className}`) + } + + assert.match(source, /href="\/monthly-labs\/" className="monthly-lab-back-link"/) + assert.match(source, /className="monthly-lab-detail-actions"/) + assert.match(styles, /@media \(max-width: 640px\)[\s\S]*monthly-lab-game-grid/) + assert.doesNotMatch(source, /className="grid gap-4 md:grid-cols-2"/) +}) + diff --git a/tests/program-countdown.test.mjs b/tests/program-countdown.test.mjs index a95bd79d0..dd85b8bee 100644 --- a/tests/program-countdown.test.mjs +++ b/tests/program-countdown.test.mjs @@ -95,8 +95,8 @@ test("ended programs switch from zero countdown boxes to an archive-style state" assert.match(countdown, /data-program-state=\{!remaining \? "unconfigured" : remaining\.ended \? "ended" : "active"\}/) assert.match(countdown, /remaining\.ended \? \(/) assert.match(countdown, /program-countdown-ended/) - assert.match(countdown, />UnavailableProgram tracker