diff options
Diffstat (limited to 'startpage')
| -rw-r--r-- | startpage/.htaccess | 8 | ||||
| -rw-r--r-- | startpage/README.md | 49 | ||||
| -rw-r--r-- | startpage/amazon.png | bin | 0 -> 40952 bytes | |||
| -rw-r--r-- | startpage/basedlii.png | bin | 0 -> 42930 bytes | |||
| -rw-r--r-- | startpage/belleville-old.png | bin | 0 -> 99209 bytes | |||
| -rw-r--r-- | startpage/belleville.png | bin | 0 -> 62329 bytes | |||
| -rw-r--r-- | startpage/buyvm.png | bin | 0 -> 4693 bytes | |||
| -rw-r--r-- | startpage/cards.txt | 189 | ||||
| -rw-r--r-- | startpage/ctvnews.png | bin | 0 -> 9930 bytes | |||
| -rw-r--r-- | startpage/cytube.png | bin | 0 -> 9027 bytes | |||
| -rw-r--r-- | startpage/ebay.png | bin | 0 -> 68914 bytes | |||
| -rw-r--r-- | startpage/gitlogo.png | bin | 0 -> 91512 bytes | |||
| -rw-r--r-- | startpage/heyuri.png | bin | 0 -> 28769 bytes | |||
| -rw-r--r-- | startpage/index.php | 454 | ||||
| -rw-r--r-- | startpage/kodiaks.png | bin | 0 -> 40401 bytes | |||
| -rw-r--r-- | startpage/lain.png | bin | 0 -> 118394 bytes | |||
| -rw-r--r-- | startpage/liivid.png | bin | 0 -> 19502 bytes | |||
| -rw-r--r-- | startpage/nav_links.txt | 108 | ||||
| -rw-r--r-- | startpage/nav_links_xhtml.txt | 108 | ||||
| -rw-r--r-- | startpage/news.png | bin | 0 -> 13477 bytes | |||
| -rw-r--r-- | startpage/newspaper.png | bin | 0 -> 85234 bytes | |||
| -rw-r--r-- | startpage/noteback.gif | bin | 0 -> 89 bytes | |||
| -rw-r--r-- | startpage/old-youtube-icon-75.png | bin | 0 -> 78193 bytes | |||
| -rw-r--r-- | startpage/openbsd_version.txt | 1 | ||||
| -rw-r--r-- | startpage/render_cards.php | 76 | ||||
| -rw-r--r-- | startpage/roundcube.png | bin | 0 -> 21377 bytes | |||
| -rw-r--r-- | startpage/shoppingcart.png | bin | 0 -> 49558 bytes | |||
| -rw-r--r-- | startpage/sm_logo.png | bin | 0 -> 6879 bytes | |||
| -rw-r--r-- | startpage/somethingawful.png | bin | 0 -> 36511 bytes | |||
| -rw-r--r-- | startpage/sonemic.png | bin | 0 -> 26546 bytes | |||
| -rw-r--r-- | startpage/sonymp3.png | bin | 0 -> 24258 bytes | |||
| -rw-r--r-- | startpage/startpage-icon.png | bin | 0 -> 26234 bytes | |||
| -rw-r--r-- | startpage/startpage.css | 1892 | ||||
| -rw-r--r-- | startpage/startpage.js | 824 | ||||
| -rw-r--r-- | startpage/tf2.png | bin | 0 -> 35302 bytes | |||
| -rw-r--r-- | startpage/tilde.png | bin | 0 -> 1909 bytes | |||
| -rw-r--r-- | startpage/videogame.jpg | bin | 0 -> 52996 bytes | |||
| -rw-r--r-- | startpage/videogame.png | bin | 0 -> 26775 bytes | |||
| -rw-r--r-- | startpage/wallpaper.jpg | bin | 0 -> 215198 bytes |
39 files changed, 3709 insertions, 0 deletions
diff --git a/startpage/.htaccess b/startpage/.htaccess new file mode 100644 index 0000000..3f07952 --- /dev/null +++ b/startpage/.htaccess @@ -0,0 +1,8 @@ +<IfModule mod_headers.c> + <Files "index.html"> + Header set X-Content-Type-Options "nosniff" + Header set Referrer-Policy "strict-origin-when-cross-origin" + Header set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" + Header set Content-Security-Policy "default-src 'self'; base-uri 'self'; script-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' https://sirocco.accuweather.com; form-action 'self' https://www.google.com https://www.google.ca; object-src 'none'" + </Files> +</IfModule> diff --git a/startpage/README.md b/startpage/README.md new file mode 100644 index 0000000..7ab37c8 --- /dev/null +++ b/startpage/README.md @@ -0,0 +1,49 @@ +# startpage/ + +Retro terminal personal startpage for sillylaird.ca. + +## Usage +- Open https://www.sillylaird.ca/startpage/ +- Use the engine picker to switch search (persisted). +- Type in "Filter cards & links" to live-filter the grid. +- Keyboard: `/` focuses filter; `?` shows help overlay; `Esc` clears. +- Cards powered by `cards.txt` (edit via /admin.php?section=startpage or directly). +- Last.fm now playing pulls from site API. + +## Data format (cards.txt) +``` +# Category Name | icon.png | alt +Label | https://url +Label2 | https://url2 + +# Next Category | ... +``` + +- One header line starts with `# ` +- Following lines until blank or next header are links `Label | url` +- Supported schemes: http/https/gemini/gopher +- Icons must be local filenames in this dir (no ..) + +## Editing +Preferred: /admin.php (authenticated). + +## OpenBSD boot banner version +The boot overlay shows `OpenBSD X.Y`. Source of truth: + +1. `openbsd_version.txt` — written by `../tools/update_openbsd_version.sh` (daily cron scrapes https://www.openbsd.org/) +2. JS date formula fallback if the cache is missing (anchor 7.9 = May 2026) + +`index.php` injects `window.OPENBSD_RELEASE` from the cache file. + +## Improvements (current) +- Removed vestigial age modal. +- Added live card filter + keyboard (`/`, `?`, Esc). +- Persisted search engine choice. +- CSS grid cards. +- Subtle wallpaper texture. +- Global theme toggle supported. +- OpenBSD version auto-updated via cron. + +Legacy: nav_links.txt (unused flat list). + +See index.php, startpage.css, startpage.js, render_cards.php for implementation. diff --git a/startpage/amazon.png b/startpage/amazon.png Binary files differnew file mode 100644 index 0000000..9be01b5 --- /dev/null +++ b/startpage/amazon.png diff --git a/startpage/basedlii.png b/startpage/basedlii.png Binary files differnew file mode 100644 index 0000000..db0451b --- /dev/null +++ b/startpage/basedlii.png diff --git a/startpage/belleville-old.png b/startpage/belleville-old.png Binary files differnew file mode 100644 index 0000000..3d2eab0 --- /dev/null +++ b/startpage/belleville-old.png diff --git a/startpage/belleville.png b/startpage/belleville.png Binary files differnew file mode 100644 index 0000000..545f1c2 --- /dev/null +++ b/startpage/belleville.png diff --git a/startpage/buyvm.png b/startpage/buyvm.png Binary files differnew file mode 100644 index 0000000..3e18331 --- /dev/null +++ b/startpage/buyvm.png diff --git a/startpage/cards.txt b/startpage/cards.txt new file mode 100644 index 0000000..35d7d39 --- /dev/null +++ b/startpage/cards.txt @@ -0,0 +1,189 @@ +# Blog | newspaper.png | Writing +Blog | https://blog.sillylaird.ca/ +DJLain | https://djlain.com/ +RisingThumb | https://risingthumb.xyz/ +Stallman | https://stallman.org/ +Joel on Software | https://www.joelonsoftware.com/ +Coding Horror | https://blog.codinghorror.com/ +Simon Willison | https://simonwillison.net/ +Brendan Gregg | https://www.brendangregg.com/blog/ +LWN | https://lwn.net/ +Dan Luu | https://danluu.com/ +Julia Evans | https://jvns.ca/ +Rachel by the Bay | https://rachelbythebay.com/w/ +Drew DeVault | https://drewdevault.com/ +Schneier | https://www.schneier.com/ +Krebs on Security | https://krebsonsecurity.com/ +Phoronix | https://www.phoronix.com/ +Undeadly | https://undeadly.org/ + +# Multimedia | basedlii.png | Visual +YouTube | https://www.youtube.com/ +Rumble | https://rumble.com/ +BitChute | https://www.bitchute.com/ +NicoVideo | https://www.nicovideo.jp/ +BiliBili | https://www.bilibili.com/ +Youku | https://www.youku.com/ku/webhome +Basedlii.space | https://basedlii.space/ +PeerTube | https://peertube.1312.media/videos/browse?sort=-trending&scope=local&c=true&s=3 +TikTok | https://www.tiktok.com/ +Douyin | https://www.douyin.com/ +Instagram | https://www.instagram.com/ +Kick | https://kick.com/ +WCO | https://www.wco.tv/ +AnikotoTV | https://anikototv.to/home +Nyaa Torrents | https://nyaa.si/ +PrimeWire | https://primewire.mov/home +Sync | https://www.sync.com/ + +# Music | sonemic.png | Visual +NTS | https://www.nts.live/ +KWSX Radio | https://radio.kwsx.online/ +Radio | https://radio.sillylaird.ca/ +M3U Player | https://junguler.github.io/m3u-player-web/# +Apple Music | https://music.apple.com/ca/browse +Youtube Music | https://music.youtube.com/ +SoundCloud | https://soundcloud.com/discover +BandCamp | https://bandcamp.com/ +RateYourMusic | https://rateyourmusic.com/ +Discogs | https://www.discogs.com/ +Last.FM | https://www.last.fm/user/SillyLaird +Libre.FM | https://libre.fm/ +Playlistor | https://playlistor.io/ +/mu/ | https://boards.4chan.org/mu/ +Head-Fi | https://www.head-fi.org/ +PitchFork | https://pitchfork.com/ +theneedledrop | https://www.youtube.com/@theneedledrop + +# Shopping | shoppingcart.png | Visual +Amazon Canada | https://www.amazon.ca/ +Amazon USA | https://www.amazon.com/ +Ebay Canada | https://www.ebay.ca/ +Ebay USA | https://www.ebay.com/ +Yahoo Auctions | https://www.jauce.com/ +NewEgg | https://www.newegg.ca/ +Canada Computers | https://www.canadacomputers.com/en/ +Long & McQuade | https://www.long-mcquade.com/ +Temu | https://www.temu.com/ +DHGate | https://www.dhgate.com/ +Ottawa Black Bears | https://ottawablackbears.com/ +Ottawa Senators | https://ottawateamshop.ca/ +Ottawa Redblacks | https://shop.ottawaredblacks.com/ +Belleville Sens | https://shop.bellevillesens.com/ +Ottawa 67's | https://chl.ca/ohl-67s/ +Baltimore Ravens | https://shop.baltimoreravens.com/ +Baltimore Orioles | https://www.mlbshop.com/baltimore-orioles/ + +# Repository | gitlogo.png | Source +Git Hub | https://github.com/ +9front | http://9front.org/ +GitWeb Gentoo | https://gitweb.gentoo.org/ +GitHub Ubuntu | https://github.com/ubuntu +GitHub Arch Linux | https://github.com/archlinux +GitHub Void Linux | https://github.com/void-linux +GitHub Babi | https://github.com/asottile/babi +CodeBerg | https://codeberg.org/ +Git Lain.La | https://gitlab.lain.la/ +Git SDF | https://git.sdf.org/sillylaird +Git Termer | https://git.termer.net/sillylaird +SourceHut | https://sr.ht/~sillylaird/ +Bisqwit | https://bisqwit.iki.fi/src/ +Gitee | https://gitee.com/ +ESR GitLab | https://gitlab.com/esr +Git.Sillylaird.ca | https://git.sillylaird.ca +CVS.Sillylaird.ca | https://cvs.sillylaird.ca + +# BBS | heyuri.png | BBS +Heyuri | https://heyuri.net/ +PokemonBBS | https://pokemonbbs.com/post/ +Heyaoi | https://www.heyaoi.net/ +Heyaoi Online | https://micro.heyaoi.online/ +/s4s/ | https://boards.4chan.org/s4s/ +futabachannel | https://futabachannel.neocities.org/ +Cooking | https://dat.2chan.net/t/futaba.htm +Ramen | https://dat.2chan.net/21/futaba.htm +Train | https://dat.2chan.net/r/futaba.htm +BaseBall | https://zip.2chan.net/1/futaba.htm +Soccer | https://zip.2chan.net/12/futaba.htm +Mahjong | https://may.2chan.net/25/futaba.htm +Politics | https://nov.2chan.net/35/futaba.htm +2d Img | https://dat.2chan.net/img2/futaba.htm +HomeBrew Pc | https://zip.2chan.net/3/futaba.htm +Consumer Tech | https://dat.2chan.net/48/futaba.htm +ptt BBS | https://www.ptt.cc/bbs/index.html + +# Local News/Weather | https://sirocco.accuweather.com/nx_mosaic_640x480_public/sir/inmasirne.gif | Env +CP24 | https://www.cp24.com/ +CTV News | https://www.ctvnews.ca/weather +CBC News | https://www.cbc.ca/news/canada/ottawa +Global News | https://globalnews.ca/ +JapanToday | https://japantoday.com/ +CGTN | https://www.cgtn.com/ +新华网 (Xinhuanet) | https://www.xinhuanet.com/ +TheGlobeAndMail | https://www.theglobeandmail.com/ +Reuters | https://www.reuters.com/ +Fox News | https://www.foxnews.com/ +Drudge Report | https://www.drudgereport.com/ +JoshWho News | https://news.joshwho.net/ +Current News | https://www.livenowfox.com/live +Quinte News | https://www.quintenews.com/ +Local Weather | https://weather.com/ +Japan Time | https://www.nict.go.jp/JST/JST5_E.html +Local Traffic | https://511on.ca/ + +# Sports | belleville.png | Env +live.qq | https://live.qq.com/directory/all +SportsMediaWatch | https://www.sportsmediawatch.com/sports-on-tv-today-games-time-channel/ +Sofascore | https://www.sofascore.com/ +TheAHLInsider | https://theahl.com/ +ESPN | https://www.espn.com/ +ESPN WATCH | https://www.espn.com/watch/ +SportsNet | https://www.sportsnet.ca/ +TSN | https://www.tsn.ca/ +TSN (WATCH) | https://www.tsn.ca/live/ +FloSports | https://www.flosports.tv/ +NLL | https://www.tsn.ca/lacrosse/article/nll-lacrosse-on-tsn-broadcast-schedule-2/ +Plus Nll | https://plus.nll.com/ +TGL Golf | https://tglgolf.com/ +Hanshin Tigers | https://hanshintigers.jp/en/ +Carolina Chaos | https://premierlacrosseleague.com/teams/carolina-chaos +BellevilleKodiaks | https://bellevillekodiaks.lacrosseshift.com/ +BellevilleSens | https://bellevillesens.com/ + +# Forums | somethingawful.png | Env +Something Awful | https://www.somethingawful.com/ +Agora Road | https://forum.agoraroad.com/index.php +Hacker News | https://news.ycombinator.com/ +lobste.rs | https://lobste.rs/ +Slashdot | https://slashdot.org/ +Ars Tech Forum | https://arstechnica.com/civis/ +Quora | https://www.quora.com/?prevent_redirect=1 +XDA-Developers | https://www.xda-developers.com/ +level1techs | https://forum.level1techs.com/ +DaemonForums | https://daemonforums.org/forumdisplay.php?f=11 +OSDev | https://forum.osdev.org/ +ID Forums | https://idforums.net/ +HellsGamers Forum | https://hellsgamers.com/forums/ +LTT Forum | https://linustechtips.com/ +LowEndTalk | https://lowendtalk.com/ +HardForum | https://hardforum.com/ +GeekHack | https://geekhack.org/index.php + +# Video Games | videogame.png | Env +Steam | https://store.steampowered.com/ +GoG | https://www.gog.com/ +OSRS RuneScape | https://oldschool.runescape.com/ +RSCEvolution | https://rscrevolution.com/ +2004Scape | https://2004.lostcity.rs/ +2005Scape | http://2005.rs/ +UO Outlands | https://uooutlands.com/ +DtxMania | https://dtxmania.net/ +ApprovedDTX | https://approvedtx.blogspot.com/?m=1 +FuruKon | https://dtx.furukon.com/ +DJLain Gitadora | https://djlain.com/gitadora/ +DJLain Pop'n | https://djlain.com/popn/ +IIDX Guide | https://iidx.org/ +Etterna | https://etternaonline.com/ +FFR | https://www.flashflashrevolution.com/ +Realm of Espionage | https://wiki.realmofespionage.xyz/start +NewGrounds | https://www.newgrounds.com/ diff --git a/startpage/ctvnews.png b/startpage/ctvnews.png Binary files differnew file mode 100644 index 0000000..87d7065 --- /dev/null +++ b/startpage/ctvnews.png diff --git a/startpage/cytube.png b/startpage/cytube.png Binary files differnew file mode 100644 index 0000000..6f47350 --- /dev/null +++ b/startpage/cytube.png diff --git a/startpage/ebay.png b/startpage/ebay.png Binary files differnew file mode 100644 index 0000000..9a75110 --- /dev/null +++ b/startpage/ebay.png diff --git a/startpage/gitlogo.png b/startpage/gitlogo.png Binary files differnew file mode 100644 index 0000000..34cd85c --- /dev/null +++ b/startpage/gitlogo.png diff --git a/startpage/heyuri.png b/startpage/heyuri.png Binary files differnew file mode 100644 index 0000000..46510d5 --- /dev/null +++ b/startpage/heyuri.png diff --git a/startpage/index.php b/startpage/index.php new file mode 100644 index 0000000..a3791f2 --- /dev/null +++ b/startpage/index.php @@ -0,0 +1,454 @@ +<?php +header('Cache-Control: no-cache, no-store, must-revalidate'); +require_once __DIR__ . '/../partials/i18n.php'; +$lang = 'en'; +?> +<!DOCTYPE html> +<html lang="en" dir="ltr"> + +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title><?= htmlspecialchars(t('startpage.title'), ENT_QUOTES, 'UTF-8') ?></title> + + <meta name="color-scheme" content="dark light" /> +<?php + // The card names this page, not the homepage: og/twitter title + og:url and + // the hreflang alternates all point at /startpage/, matching the canonical. + $meta = [ + 'lang' => 'en', + 'title' => t('startpage.title'), + 'desc' => t('startpage.desc'), + 'base' => 'index', + 'self_url' => 'https://www.sillylaird.ca/startpage/', + 'og_url' => 'https://www.sillylaird.ca/startpage/', + 'en_url' => 'https://www.sillylaird.ca/startpage/', + 'x_default' => 'https://www.sillylaird.ca/startpage/', + 'ja_url' => 'https://www.sillylaird.ca/startpage/', + 'zh_url' => 'https://www.sillylaird.ca/startpage/', + ]; + include __DIR__ . '/../partials/head_meta.php'; +?> + <link rel="stylesheet" href="/assets/css/fonts.css?v=<?= @filemtime(__DIR__ . '/../assets/css/fonts.css') ?: time() ?>" /> + <link rel="stylesheet" href="/assets/css/site.css?v=<?= @filemtime(__DIR__ . '/../assets/css/site.css') ?: time() ?>" /> + <link rel="stylesheet" href="/assets/css/skeleton.css?v=<?= @filemtime(__DIR__ . '/../assets/css/skeleton.css') ?: time() ?>" /> + + <link rel="stylesheet" href="startpage.css?v=<?= @filemtime(__DIR__ . '/startpage.css') ?: time() ?>" /> +<?php + // Boot banner OpenBSD version: tools/update_openbsd_version.sh (cron) writes + // openbsd_version.txt from openbsd.org; JS falls back to a date formula if missing. + $openbsd_release = ''; + $openbsd_ver_file = __DIR__ . '/openbsd_version.txt'; + if (is_readable($openbsd_ver_file)) { + $v = trim((string) @file_get_contents($openbsd_ver_file)); + if (preg_match('/^\d+\.\d+$/', $v)) { + $openbsd_release = $v; + } + } +?> + <script>window.OPENBSD_RELEASE=<?= json_encode($openbsd_release, JSON_UNESCAPED_SLASHES) ?>;</script> + <script defer src="startpage.js?v=<?= @filemtime(__DIR__ . '/startpage.js') ?: time() ?>"></script> + <noscript><style>.boot-overlay{display:none!important}</style></noscript> +</head> + +<body class="startpage-booting startpage-comfort comfort-spacing"> + <a class="skip-link" href="#main">Skip to content</a> + <div id="boot-overlay" class="boot-overlay" aria-hidden="true"> + <div class="boot-log" id="boot-log"></div> + </div> + <div class="crt-overlay" aria-hidden="true"></div> + <?php include '../partials/header.php'; ?> + + <?php + $lastfm_initial = ''; + $lastfm_cache_file = sys_get_temp_dir() . '/sillylaird_lastfm/recent.json'; + if (is_file($lastfm_cache_file)) { + $lastfm_raw = @file_get_contents($lastfm_cache_file); + $lastfm_tracks = $lastfm_raw !== false ? json_decode($lastfm_raw, true) : null; + if (is_array($lastfm_tracks)) { + $pick = null; + foreach ($lastfm_tracks as $t) { + if (!empty($t['nowplaying'])) { $pick = $t; break; } + } + if (!$pick) { + foreach ($lastfm_tracks as $t) { + if (!empty($t['name'])) { $pick = $t; break; } + } + } + if ($pick && !empty($pick['name']) && !empty($pick['artist'])) { + $prefix = !empty($pick['nowplaying']) ? 'Now playing: ' : 'Last played: '; + $lastfm_initial = $prefix . $pick['name'] . ' — ' . $pick['artist']; + } + } + } + ?> + <div id="lastfm-widget" class="lastfm-widget"> + <span class="lastfm-label">♫</span> + <span id="lastfm-status"><?= htmlspecialchars($lastfm_initial, ENT_QUOTES, 'UTF-8') ?></span> + <a href="https://www.last.fm/user/SillyLaird" class="lastfm-profile" target="_blank" rel="noopener noreferrer">Last.fm Profile</a> + </div> + + <section id="konaste-status" class="konaste-status" aria-label="Konaste / BEMANI PC maintenance status"> + <div class="konaste-head"> + <h3 class="konaste-title">Konaste / BEMANI PC Status</h3> + <small class="konaste-clock">Computer time: + <span id="konaste-clock" class="konaste-clock-time"><span data-clock="h">--</span><span class="konaste-clock-colon">:</span><span data-clock="m">--</span><span class="konaste-clock-colon">:</span><span data-clock="s">--</span> <span data-clock="ap" class="konaste-clock-ap"></span></span> + <span data-clock="tz" class="konaste-clock-tz"></span> + </small> + </div> + + <div class="konaste-game" data-game="both"> + <span class="konaste-dot" data-role="dot"></span> + <span class="konaste-names"> + <a class="konaste-name" href="https://p.eagate.573.jp/game/eacpopn/lively/" target="_blank" rel="noopener noreferrer">pop'n music Lively</a> + <span class="konaste-name-sep">•</span> + <a class="konaste-name" href="https://p.eagate.573.jp/game/eacgitadora/index.html" target="_blank" rel="noopener noreferrer">GITADORA Konaste</a> + <span class="konaste-name-sep">•</span> + <a class="konaste-name" href="https://p.eagate.573.jp/game/infinitas/2/index.html" target="_blank" rel="noopener noreferrer">beatmania IIDX INFINITAS</a> + </span> + <span class="konaste-text" data-role="text">Checking…</span> + <span class="konaste-countdown" data-role="countdown"></span> + </div> + + <small class="konaste-note">Maintenance times are converted from the server schedule to the connected computer's local time. Unexpected outages can still happen. Schedule via <a href="https://iidx.org/eamuse_maint" target="_blank" rel="noopener noreferrer">iidx.org/eamuse_maint</a>.</small> + </section> + + + <main id="main"> + <section class="search-section"> + <div class="top-left-links"> + <a href="https://grok.com/" target="_blank" rel="noopener noreferrer" class="search-link chatllm-link">Grok</a> + <a href="https://www.heyaoi.net/" target="_blank" rel="noopener noreferrer" class="search-link">Heyaoi</a> + <a href="https://micro.heyaoi.online/" target="_blank" rel="noopener noreferrer" class="search-link">Heyaoi Online</a> + <a href="https://www.sportsmediawatch.com/sports-on-tv-today-games-time-channel/" target="_blank" rel="noopener noreferrer" class="search-link">SportsMediaWatch</a> + <nav class="ctx-menu"> + <ul class="ctx-root"> + <li class="ctx-parent"> + <span class="search-link ctx-trigger">Sports <span class="ctx-arrow"></span></span> + <ul class="ctx-flyout"> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Watch</span> + <ul class="ctx-flyout"> + <li><a href="https://www.sportsmediawatch.com/sports-on-tv-today-games-time-channel/" target="_blank" rel="noopener noreferrer" class="ctx-item">SportsMediaWatch</a></li> + <li><a href="https://www.espn.com/watch/" target="_blank" rel="noopener noreferrer" class="ctx-item">ESPN WATCH</a></li> + <li><a href="https://www.sportsnet.ca/" target="_blank" rel="noopener noreferrer" class="ctx-item">SportsNet</a></li> + <li><a href="https://www.tsn.ca/live/" target="_blank" rel="noopener noreferrer" class="ctx-item">TSN</a></li> + <li><a href="https://www.flosports.tv/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloSports</a></li> + <li><a href="https://www.hometeamlive.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">HomeTeamLive</a></li> + <li><a href="https://www.dazn.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">DAZN</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Lacrosse</span> + <ul class="ctx-flyout"> + <li><a href="https://www.tsn.ca/lacrosse/article/nll-lacrosse-on-tsn-broadcast-schedule-2/" target="_blank" rel="noopener noreferrer" class="ctx-item">TSN NLL Schedule</a></li> + <li><a href="https://premierlacrosseleague.com/schedule" target="_blank" rel="noopener noreferrer" class="ctx-item">PLL Schedule</a></li> + <li><a href="https://www.ncaa.com/scoreboard/lacrosse-men/d1" target="_blank" rel="noopener noreferrer" class="ctx-item">NCAA Lacrosse (M)</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Football</span> + <ul class="ctx-flyout"> + <li><a href="https://www.cfl.ca/plus" target="_blank" rel="noopener noreferrer" class="ctx-item">CFL+</a></li> + <li><a href="https://www.nfl.com/schedules/" target="_blank" rel="noopener noreferrer" class="ctx-item">NFL Schedule</a></li> + <li><a href="https://www.ncaa.com/scoreboard/football/fbs" target="_blank" rel="noopener noreferrer" class="ctx-item">NCAA Football</a></li> + <li><a href="https://www.flofootball.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloFootball</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Basketball</span> + <ul class="ctx-flyout"> + <li><a href="https://www.nba.com/schedule" target="_blank" rel="noopener noreferrer" class="ctx-item">NBA Schedule</a></li> + <li><a href="https://www.cebl.ca/games" target="_blank" rel="noopener noreferrer" class="ctx-item">CEBL Schedule</a></li> + <li><a href="https://www.ncaa.com/scoreboard/basketball-men/d1" target="_blank" rel="noopener noreferrer" class="ctx-item">NCAA Basketball (M)</a></li> + <li><a href="https://www.ncaa.com/scoreboard/basketball-women/d1" target="_blank" rel="noopener noreferrer" class="ctx-item">NCAA Basketball (W)</a></li> + <li><a href="https://www.flohoops.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloHoops</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Rugby</span> + <ul class="ctx-flyout"> + <li><a href="https://www.nrl.com/watch/live/" target="_blank" rel="noopener noreferrer" class="ctx-item">NRL Schedule</a></li> + <li><a href="https://www.sixnationsrugby.com/en/m6n/fixtures/" target="_blank" rel="noopener noreferrer" class="ctx-item">Six Nations</a></li> + <li><a href="https://www.florugby.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloRugby</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Hockey</span> + <ul class="ctx-flyout"> + <li><a href="https://www.nhl.com/schedule" target="_blank" rel="noopener noreferrer" class="ctx-item">NHL Schedule</a></li> + <li><a href="https://theahl.com/schedule" target="_blank" rel="noopener noreferrer" class="ctx-item">AHL Schedule</a></li> + <li><a href="https://echl.com/schedule" target="_blank" rel="noopener noreferrer" class="ctx-item">ECHL Schedule</a></li> + <li><a href="https://chl.ca/" target="_blank" rel="noopener noreferrer" class="ctx-item">CHL Schedule</a></li> + <li><a href="https://chl.ca/ohl/schedule/" target="_blank" rel="noopener noreferrer" class="ctx-item">OHL Schedule</a></li> + <li><a href="https://chl.ca/whl/schedule/" target="_blank" rel="noopener noreferrer" class="ctx-item">WHL Schedule</a></li> + <li><a href="https://chl.ca/lhjmq/schedule/" target="_blank" rel="noopener noreferrer" class="ctx-item">QMJHL Schedule</a></li> + <li><a href="https://www.ncaa.com/scoreboard/icehockey-men/d1" target="_blank" rel="noopener noreferrer" class="ctx-item">NCAA Hockey (M)</a></li> + <li><a href="https://www.ncaa.com/scoreboard/icehockey-women/d1" target="_blank" rel="noopener noreferrer" class="ctx-item">NCAA Hockey (W)</a></li> + <li><a href="https://www.flohockey.tv/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloHockey</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Wrestling</span> + <ul class="ctx-flyout"> + <li><a href="https://www.ncaa.com/sports/wrestling-men/d1" target="_blank" rel="noopener noreferrer" class="ctx-item">NCAA Wrestling</a></li> + <li><a href="https://www.flowrestling.org/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloWrestling</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Grappling</span> + <ul class="ctx-flyout"> + <li><a href="https://www.flograppling.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloGrappling</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Combat</span> + <ul class="ctx-flyout"> + <li><a href="https://www.flocombat.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloCombat</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Racing</span> + <ul class="ctx-flyout"> + <li><a href="https://www.formula1.com/en/racing/2026" target="_blank" rel="noopener noreferrer" class="ctx-item">F1 Calendar</a></li> + <li><a href="https://www.floracing.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloRacing</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Track</span> + <ul class="ctx-flyout"> + <li><a href="https://www.flotrack.org/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloTrack</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Soccer</span> + <ul class="ctx-flyout"> + <li><a href="https://www.ncaa.com/scoreboard/soccer-men/d1" target="_blank" rel="noopener noreferrer" class="ctx-item">NCAA Soccer</a></li> + <li><a href="https://www.premierleague.com/fixtures" target="_blank" rel="noopener noreferrer" class="ctx-item">EPL Fixtures</a></li> + <li><a href="https://www.uefa.com/uefachampionsleague/fixtures-results/" target="_blank" rel="noopener noreferrer" class="ctx-item">UEFA Champions League</a></li> + <li><a href="https://www.mlssoccer.com/schedule/" target="_blank" rel="noopener noreferrer" class="ctx-item">MLS Schedule</a></li> + <li><a href="https://www.flofc.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloFC</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Baseball</span> + <ul class="ctx-flyout"> + <li><a href="https://www.mlb.com/schedule" target="_blank" rel="noopener noreferrer" class="ctx-item">MLB Schedule</a></li> + <li><a href="https://www.ncaa.com/scoreboard/baseball/d1" target="_blank" rel="noopener noreferrer" class="ctx-item">NCAA Baseball</a></li> + <li><a href="https://www.flobaseball.tv/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloBaseball</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Softball</span> + <ul class="ctx-flyout"> + <li><a href="https://www.ncaa.com/scoreboard/softball/d1" target="_blank" rel="noopener noreferrer" class="ctx-item">NCAA Softball</a></li> + <li><a href="https://www.flosoftball.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloSoftball</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Volleyball</span> + <ul class="ctx-flyout"> + <li><a href="https://www.ncaa.com/scoreboard/volleyball-women/d1" target="_blank" rel="noopener noreferrer" class="ctx-item">NCAA Volleyball (W)</a></li> + <li><a href="https://www.flovolleyball.tv/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloVolleyball</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Swimming</span> + <ul class="ctx-flyout"> + <li><a href="https://www.floswimming.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloSwimming</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Gymnastics</span> + <ul class="ctx-flyout"> + <li><a href="https://www.flogymnastics.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloGymnastics</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Cheer</span> + <ul class="ctx-flyout"> + <li><a href="https://www.flocheer.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloCheer</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Dance</span> + <ul class="ctx-flyout"> + <li><a href="https://www.flodance.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloDance</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Bowling</span> + <ul class="ctx-flyout"> + <li><a href="https://www.flobowling.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloBowling</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Rodeo</span> + <ul class="ctx-flyout"> + <li><a href="https://www.florodeo.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloRodeo</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Marching</span> + <ul class="ctx-flyout"> + <li><a href="https://www.flomarching.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloMarching</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Cycling</span> + <ul class="ctx-flyout"> + <li><a href="https://www.flobikes.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloBikes</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Elite</span> + <ul class="ctx-flyout"> + <li><a href="https://www.floelite.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">FloElite</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Cricket</span> + <ul class="ctx-flyout"> + <li><a href="https://www.icc-cricket.com/fixtures-results" target="_blank" rel="noopener noreferrer" class="ctx-item">ICC Fixtures</a></li> + <li><a href="https://www.iplt20.com/matches/schedule" target="_blank" rel="noopener noreferrer" class="ctx-item">IPL Schedule</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Tennis</span> + <ul class="ctx-flyout"> + <li><a href="https://www.atptour.com/en/tournaments" target="_blank" rel="noopener noreferrer" class="ctx-item">ATP Tour</a></li> + <li><a href="https://www.wtatennis.com/tournaments" target="_blank" rel="noopener noreferrer" class="ctx-item">WTA Tour</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Golf</span> + <ul class="ctx-flyout"> + <li><a href="https://www.pgatour.com/schedule" target="_blank" rel="noopener noreferrer" class="ctx-item">PGA Tour Schedule</a></li> + </ul> + </li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Aussie Rules</span> + <ul class="ctx-flyout"> + <li><a href="https://www.afl.com.au/fixture" target="_blank" rel="noopener noreferrer" class="ctx-item">AFL Fixture</a></li> + </ul> + </li> + </ul> + </li> + </ul> + </nav> + <a href="https://downloads.khinsider.com/" target="_blank" rel="noopener noreferrer" class="search-link khinsider-link">KHInsider</a> + <a href="https://animethemes.moe/" target="_blank" rel="noopener noreferrer" class="search-link">AnimeThemes</a> + <a href="https://squid.wtf/" target="_blank" rel="noopener noreferrer" class="search-link squid-link">Squid</a> + <a href="https://strmfree.link/" target="_blank" rel="noopener noreferrer" class="search-link">StrmFree</a> + <a href="https://anikototv.to/home" target="_blank" rel="noopener noreferrer" class="search-link">AnikotoTV</a> + <a href="https://www.wco.tv/" target="_blank" rel="noopener noreferrer" class="search-link">WCO</a> + <a href="https://fmhy.net/" target="_blank" rel="noopener noreferrer" class="search-link">FMHY</a> + </div> + <div class="top-right-links"> + <a href="https://mail.sillylaird.ca/" target="_blank" rel="noopener noreferrer" class="search-link">Mail</a> + <a href="https://mail.google.com/" target="_blank" rel="noopener noreferrer" class="search-link">Gmail</a> + <nav class="ctx-menu"> + <ul class="ctx-root"> + <li class="ctx-parent"> + <span class="search-link ctx-trigger"><span class="ctx-arrow"></span> Links</span> + <ul class="ctx-flyout"> + <li><a href="https://www.google.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">Google</a></li> + <li><a href="https://mail.sillylaird.ca/" target="_blank" rel="noopener noreferrer" class="ctx-item">Mail</a></li> + <li><a href="https://mail.google.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">Gmail</a></li> + <li><a href="https://blog.sillylaird.ca/" target="_blank" rel="noopener noreferrer" class="ctx-item">Blog</a></li> + <li><a href="https://kick.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">Kick</a></li> + <li><a href="https://www.nicovideo.jp/" target="_blank" rel="noopener noreferrer" class="ctx-item">Nicovideo</a></li> + <li><a href="https://www.bilibili.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">Bilibili</a></li> + <li><a href="https://www.youtube.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">YouTube</a></li> + <li class="ctx-parent"> + <span class="ctx-item ctx-has-sub">Radio's</span> + <ul class="ctx-flyout"> + <li><a href="https://radio.garden/listen/gotanno-89-2/9sBfGBXL" target="_blank" rel="noopener noreferrer" class="ctx-item">Radio.Garden</a></li> + <li><a href="https://radio.sillylaird.ca/" target="_blank" rel="noopener noreferrer" class="ctx-item">Radio</a></li> + </ul> + </li> + </ul> + </li> + </ul> + </nav> + <nav class="ctx-menu"> + <ul class="ctx-root"> + <li class="ctx-parent"> + <span class="search-link ctx-trigger"><span class="ctx-arrow"></span> Private Servers</span> + <ul class="ctx-flyout"> + <li><a href="https://2004.lostcity.rs/title" target="_blank" rel="noopener noreferrer" class="ctx-item">2004Scape</a></li> + <li><a href="https://2005.rs/" target="_blank" rel="noopener noreferrer" class="ctx-item">2005Scape</a></li> + </ul> + </li> + </ul> + </nav> + <nav class="ctx-menu"> + <ul class="ctx-root"> + <li class="ctx-parent"> + <span class="search-link ctx-trigger"><span class="ctx-arrow"></span> Rhythm Games</span> + <ul class="ctx-flyout"> + <li><a href="https://p.eagate.573.jp/game/eacpopn/lively/" target="_blank" rel="noopener noreferrer" class="ctx-item">Pop'n Music Lively</a></li> + <li><a href="https://p.eagate.573.jp/game/eacgitadora/index.html" target="_blank" rel="noopener noreferrer" class="ctx-item">Gitadora</a></li> + </ul> + </li> + </ul> + </nav> + <nav class="ctx-menu"> + <ul class="ctx-root"> + <li class="ctx-parent"> + <span class="search-link ctx-trigger"><span class="ctx-arrow"></span> Shopping</span> + <ul class="ctx-flyout"> + <li><a href="https://www.ebay.ca/" target="_blank" rel="noopener noreferrer" class="ctx-item">eBay</a></li> + <li><a href="https://www.dhgate.com/" target="_blank" rel="noopener noreferrer" class="ctx-item">DHgate</a></li> + </ul> + </li> + </ul> + </nav> + <a href="http://9p.sillylaird.ca" target="_blank" rel="noopener noreferrer" class="search-link">9P/9F</a> + <a href="http://xp.sillylaird.ca" target="_blank" rel="noopener noreferrer" class="search-link">XP</a> + <a href="https://os.sillylaird.ca/" target="_blank" rel="noopener noreferrer" class="search-link">LainOS</a> + <a href="https://www.openbsd.org/" target="_blank" rel="noopener noreferrer" class="search-link">OpenBSD</a> + <a href="https://9p.io/plan9/" target="_blank" rel="noopener noreferrer" class="search-link">Plan 9</a> + <a href="http://9front.org/" target="_blank" rel="noopener noreferrer" class="search-link">9front</a> + </div> + <h2 class="quick-search-title"><a href="https://4get.sillylaird.ca" target="_blank" rel="noopener noreferrer">4get..</a></h2> + <h2 class="quick-search-title"><a href="https://www.youtube.com/" target="_blank" rel="noopener noreferrer">Youtube..</a></h2> + <h2 class="quick-search-title"><a href="https://kick.com/" target="_blank" rel="noopener noreferrer">Kick..</a></h2> + <h2 class="quick-search-title"><a href="https://rumble.com/" target="_blank" rel="noopener noreferrer">Rumble..</a></h2> + <h2 id="unified-search-title"><a href="https://4get.sillylaird.ca" target="_blank" rel="noopener noreferrer">4get Search..</a></h2> + <form id="unified-search-form" action="https://4get.sillylaird.ca/web" method="get" target="_blank" rel="noopener noreferrer"> + <div class="unified-search"> + <div class="search-engine-picker"> + <button type="button" id="search-engine-toggle" class="search-engine-toggle" title="Click to switch search engine" aria-label="Switch search engine" aria-haspopup="listbox" aria-expanded="false">4G <span class="search-engine-caret">▾</span></button> + <ul id="search-engine-menu" class="search-engine-menu" role="listbox" hidden></ul> + </div> + <input type="search" id="unified-search-input" name="s" class="search-box unified-search-input" placeholder="Search 4get..." autocomplete="off" /> + <div class="search-engine-spacer"> + <button type="button" id="unified-search-clear" class="search-clear" aria-label="Clear search" title="Clear search" hidden><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="#ff5555" stroke-width="3" stroke-linecap="round" aria-hidden="true"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg></button> + </div> + </div> + </form> + <div class="card-filter-wrap"> + <input type="search" id="card-filter" class="search-box card-filter" placeholder="Filter cards & links (type to search)..." autocomplete="off" /> + <span id="card-filter-count" class="filter-count" aria-live="polite"></span> + </div> + + <div class="clock-container"> + <div id="realtime-date"></div> + <div id="realtime-clock" class="clock-time"></div> + </div> + </section> + + <section class="grid" id="cards-grid"> + <?php include __DIR__ . "/render_cards.php"; ?> + </section> + </main> + + <?php + // Shared site footer (colophon, counter, badges). Comfort spacing lives in the header. + include __DIR__ . '/../partials/footer.php'; + ?> + +</body> + +</html> diff --git a/startpage/kodiaks.png b/startpage/kodiaks.png Binary files differnew file mode 100644 index 0000000..52bae9a --- /dev/null +++ b/startpage/kodiaks.png diff --git a/startpage/lain.png b/startpage/lain.png Binary files differnew file mode 100644 index 0000000..792f486 --- /dev/null +++ b/startpage/lain.png diff --git a/startpage/liivid.png b/startpage/liivid.png Binary files differnew file mode 100644 index 0000000..6fdb36b --- /dev/null +++ b/startpage/liivid.png diff --git a/startpage/nav_links.txt b/startpage/nav_links.txt new file mode 100644 index 0000000..acb37f5 --- /dev/null +++ b/startpage/nav_links.txt @@ -0,0 +1,108 @@ +https://blog.sillylaird.ca/
+https://guestbook.sillylaird.ca
+https://www.sillylaird.ca/changelog/
+/journal/
+https://diary.sillylaird.ca/bbs.php
+https://live.sillylaird.ca/
+/gaming/
+http://9p.sillylaird.ca/
+http://localhost:3000/
+https://sillylaird.myportfolio.com/
+https://sillylaird.srht.site/
+https://www.sillylaird.ca/startpage/
+https://sr.ht/~sillylaird/
+https://www.heyaoi.net/
+https://micro.heyaoi.online/
+https://up.heyuri.net/user/boards/sillylaird/
+https://www.basedlii.space/
+https://trello.com/
+https://csgoani.me/
+https://ytmnd.com/
+https://buyvm.net/
+https://www.1984hosting.com/
+https://digdeeper.club/
+https://djlain.com/
+https://freezepeach.social/
+https://polymarket.com/
+https://www.livenowfox.com/live
+https://mail.sillylaird.ca/
+https://mail.google.com/mail/u/0/
+https://ecitizenemail.sealand.io/
+https://anonhosting.io/
+https://tuta.com/
+https://app.secria.me/
+https://proton.me/mail
+https://cock.li/
+https://pissmail.com/
+https://x.com
+https://www.instagram.com/
+https://truthsocial.com/
+https://gab.com/home
+https://www.minds.com/
+https://freesoftwareextremist.com/users/sillylaird
+https://detroitriotcity.com/
+https://noauthority.social/
+https://lemmy.zip/
+https://grok.com/
+https://micro.heyaoi.online/
+https://claude.ai/new
+https://www.testsprite.com/
+https://www.phind.com/
+https://chat.deepseek.com/
+https://chat.qwen.ai/
+https://elephas.app/
+https://www.diabrowser.com/
+https://ollama.com/search
+https://discord.com/
+https://www.remove.bg/
+https://waifu2x.io/
+https://tineye.com/
+https://www.ilovepdf.com/
+https://drive.google.com/
+https://www.sync.com/en/
+https://www.photopea.com/
+https://www.adobe.com/
+https://lobste.rs/
+https://news.ycombinator.com/
+https://www.reuters.com/
+https://www.drudgereport.com/
+https://www.foxnews.com/
+https://www.ctvnews.ca/
+https://www.cbc.ca/
+https://weather.com/
+https://511on.ca/
+https://radio.garden/
+https://rateyourmusic.com/
+https://pitchfork.com/
+http://www.last.fm/
+https://www.discogs.com/
+https://anonradio.net/
+https://r-a-d.io/
+https://91x.fm/
+https://chuo.fm/
+https://www.youtube.com/
+https://www.nicovideo.jp/
+https://www.bilibili.com/
+https://www.tiktok.com/explore
+https://fmhy.net/
+https://deepwebnest.com/
+https://theindex.moe/
+https://theporndude.com/
+https://www.heyuri.net/
+https://endchan.net/
+https://8chan.moe/
+https://doushio.com/moe/
+https://futabachannel.neocities.org/
+https://momizou.neocities.org/
+https://momicha.neocities.org/
+https://5ch.net/
+https://bbs.animanch.com/
+https://www.pornhub.com/
+https://www.xvideos.com/
+https://www.redtube.com/
+https://www.xnxx.com/
+https://www.youporn.com/
+https://www.xhamster.com/
+https://nhentai.net/
+https://gelbooru.com/
+https://jav.vercel.app
diff --git a/startpage/nav_links_xhtml.txt b/startpage/nav_links_xhtml.txt new file mode 100644 index 0000000..3cc8674 --- /dev/null +++ b/startpage/nav_links_xhtml.txt @@ -0,0 +1,108 @@ +<a href="https://blog.sillylaird.ca/">Blog</a><br />
+<a href="/guestbook.php">GuestBook</a><br />
+<a href="https://www.sillylaird.ca/changelog/">ChangeLog</a><br />
+<a href="/journal/">Journal</a><br />
+<a href="https://diary.sillylaird.ca/bbs.php">Diary</a><br />
+<a href="https://live.sillylaird.ca/">Live</a><br />
+<a href="/gaming/">Gaming</a><br />
+<a href="http://9p.sillylaird.ca/">9p.sillylaird.ca</a><br />
+<a href="http://localhost:3000/">Local.Dev</a><br />
+<a href="https://sillylaird.myportfolio.com/">MyPortfolio</a><br />
+<a href="https://sillylaird.srht.site/">(srht.site) SH</a><br />
+<a href="https://www.sillylaird.ca/startpage/">StartPage</a><br />
+<a href="https://sr.ht/~sillylaird/">SourceHut</a><br />
+<a href="https://www.heyaoi.net/">Heyaoi</a><br />
+<a href="https://micro.heyaoi.online/">Heyaoi Online</a><br />
+<a href="https://up.heyuri.net/user/boards/sillylaird/">up@heyuri</a><br />
+<a href="https://www.basedlii.space/">Basedlii.space</a><br />
+<a href="https://trello.com/">Trello</a><br />
+<a href="https://csgoani.me/">csgoani.me</a><br />
+<a href="https://ytmnd.com/">ytmnd</a><br />
+<a href="https://buyvm.net/">BuyVM</a><br />
+<a href="https://www.1984hosting.com/">1984Hosting</a><br />
+<a href="https://digdeeper.club/">DigDeeper</a><br />
+<a href="https://djlain.com/">DJLain</a><br />
+<a href="https://freezepeach.social/">FreezePeach</a><br />
+<a href="https://polymarket.com/">PolyMarket</a><br />
+<a href="https://www.livenowfox.com/live">Current News</a><br />
+<a href="https://mail.sillylaird.ca/">mail@sillylaird.ca</a><br />
+<a href="https://mail.google.com/mail/u/0/">Gmail</a><br />
+<a href="https://ecitizenemail.sealand.io/">SeaLand</a><br />
+<a href="https://anonhosting.io/">Anonhosting</a><br />
+<a href="https://tuta.com/">Tuta</a><br />
+<a href="https://app.secria.me/">Secria</a><br />
+<a href="https://proton.me/mail">Proton</a><br />
+<a href="https://cock.li/">Cockli</a><br />
+<a href="https://pissmail.com/">PissMail</a><br />
+<a href="https://x.com">X/Twitter</a><br />
+<a href="https://www.instagram.com/">Instagram</a><br />
+<a href="https://truthsocial.com/">TruthSocial</a><br />
+<a href="https://gab.com/home">Gab</a><br />
+<a href="https://www.minds.com/">Minds</a><br />
+<a href="https://freesoftwareextremist.com/users/sillylaird">FreeSoftwareEx</a><br />
+<a href="https://detroitriotcity.com/">DetroitRiotCity</a><br />
+<a href="https://noauthority.social/">NoAuthority</a><br />
+<a href="https://lemmy.zip/">Lemmy.zip</a><br />
+<a href="https://grok.com/">Grok</a><br />
+<a href="https://micro.heyaoi.online/">Heyaoi Online</a><br />
+<a href="https://claude.ai/new">Claude</a><br />
+<a href="https://www.testsprite.com/">TestSprite</a><br />
+<a href="https://www.phind.com/">phind</a><br />
+<a href="https://chat.deepseek.com/">DeepSeek</a><br />
+<a href="https://chat.qwen.ai/">Qwen</a><br />
+<a href="https://elephas.app/">Elephas</a><br />
+<a href="https://www.diabrowser.com/">DiaBrowser</a><br />
+<a href="https://ollama.com/search">Ollama</a><br />
+<a href="https://discord.com/">Discord</a><br />
+<a href="https://www.remove.bg/">RemoveBG</a><br />
+<a href="https://waifu2x.io/">Waifu2x</a><br />
+<a href="https://tineye.com/">TinEye</a><br />
+<a href="https://www.ilovepdf.com/">iLovePDF</a><br />
+<a href="https://drive.google.com/">Google Drive</a><br />
+<a href="https://www.sync.com/en/">Sync</a><br />
+<a href="https://www.photopea.com/">Photopea</a><br />
+<a href="https://www.adobe.com/">Adobe</a><br />
+<a href="https://lobste.rs/">Lobsters</a><br />
+<a href="https://news.ycombinator.com/">HackerNews</a><br />
+<a href="https://www.reuters.com/">Reuters</a><br />
+<a href="https://www.drudgereport.com/">Drudge Report</a><br />
+<a href="https://www.foxnews.com/">Fox News</a><br />
+<a href="https://www.ctvnews.ca/">CTV News</a><br />
+<a href="https://www.cbc.ca/">CBC News</a><br />
+<a href="https://weather.com/">Local Weather</a><br />
+<a href="https://511on.ca/">Local Traffic</a><br />
+<a href="https://radio.garden/">Radio.Garden</a><br />
+<a href="https://rateyourmusic.com/">RateYourMusic</a><br />
+<a href="https://pitchfork.com/">PitchFork</a><br />
+<a href="http://www.last.fm/">LAST.FM</a><br />
+<a href="https://www.discogs.com/">Discogs</a><br />
+<a href="https://anonradio.net/">Anonradio</a><br />
+<a href="https://r-a-d.io/">R/a/dio</a><br />
+<a href="https://91x.fm/">Loyalist Radio</a><br />
+<a href="https://chuo.fm/">CHUO Radio</a><br />
+<a href="https://www.youtube.com/">YouTube</a><br />
+<a href="https://www.nicovideo.jp/">NicoVideo</a><br />
+<a href="https://www.bilibili.com/">BiliBili</a><br />
+<a href="https://www.tiktok.com/explore">TikTok</a><br />
+<a href="https://fmhy.net/">FMHY</a><br />
+<a href="https://deepwebnest.com/">DeepWebNest</a><br />
+<a href="https://theindex.moe/">The index (Anime)</a><br />
+<a href="https://theporndude.com/">ThePornDude (XXX)</a><br />
+<a href="https://www.heyuri.net/">Heyuri</a><br />
+<a href="https://endchan.net/">EndChan</a><br />
+<a href="https://8chan.moe/">8chan.moe</a><br />
+<a href="https://doushio.com/moe/">/moe/</a><br />
+<a href="https://futabachannel.neocities.org/">FutabaChannel</a><br />
+<a href="https://momizou.neocities.org/">Momizou</a><br />
+<a href="https://momicha.neocities.org/">Momicha</a><br />
+<a href="https://5ch.net/">5ch</a><br />
+<a href="https://bbs.animanch.com/">Animan BBS</a><br />
+<a href="https://www.pornhub.com/">PornHub</a><br />
+<a href="https://www.xvideos.com/">XVideos</a><br />
+<a href="https://www.redtube.com/">RedTube</a><br />
+<a href="https://www.xnxx.com/">XNXX</a><br />
+<a href="https://www.youporn.com/">YouPorn</a><br />
+<a href="https://www.xhamster.com/">xHamster</a><br />
+<a href="https://nhentai.net/">nhentai</a><br />
+<a href="https://gelbooru.com/">Gelbooru</a><br />
+<a href="https://jav.vercel.app">jav.vercel.app</a><br />
diff --git a/startpage/news.png b/startpage/news.png Binary files differnew file mode 100644 index 0000000..32d2b8c --- /dev/null +++ b/startpage/news.png diff --git a/startpage/newspaper.png b/startpage/newspaper.png Binary files differnew file mode 100644 index 0000000..0e9122e --- /dev/null +++ b/startpage/newspaper.png diff --git a/startpage/noteback.gif b/startpage/noteback.gif Binary files differnew file mode 100644 index 0000000..2630a60 --- /dev/null +++ b/startpage/noteback.gif diff --git a/startpage/old-youtube-icon-75.png b/startpage/old-youtube-icon-75.png Binary files differnew file mode 100644 index 0000000..b16d3ae --- /dev/null +++ b/startpage/old-youtube-icon-75.png diff --git a/startpage/openbsd_version.txt b/startpage/openbsd_version.txt new file mode 100644 index 0000000..fbf1779 --- /dev/null +++ b/startpage/openbsd_version.txt @@ -0,0 +1 @@ +7.9 diff --git a/startpage/render_cards.php b/startpage/render_cards.php new file mode 100644 index 0000000..369a5fd --- /dev/null +++ b/startpage/render_cards.php @@ -0,0 +1,76 @@ +<?php +if (realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME'] ?? '')) { http_response_code(404); exit; } + +function startpage_valid_card_url(string $url): bool { + if ($url === '') return false; + if ($url[0] === '/') return !str_starts_with($url, '//') && !str_contains($url, "\0"); + $scheme = strtolower(parse_url($url, PHP_URL_SCHEME) ?? ''); + return in_array($scheme, ['http', 'https', 'gemini', 'gopher'], true) && filter_var($url, FILTER_VALIDATE_URL) !== false; +} + +function startpage_valid_card_asset(string $ref): bool { + if ($ref === '' || str_contains($ref, "\0")) return false; + if (startpage_valid_card_url($ref)) return true; + return !str_contains($ref, '..') && preg_match('/^[A-Za-z0-9][A-Za-z0-9._\/-]*$/', $ref); +} + +function startpage_card_data(): array { + $file = __DIR__ . '/cards.txt'; + $raw = is_file($file) ? file_get_contents($file) : ''; + $cards = []; + $cur = null; + foreach (explode("\n", str_replace("\r\n", "\n", (string)$raw)) as $line) { + $line = trim($line); + if ($line === '') continue; + if ($line[0] === '#') { + if ($cur !== null) $cards[] = $cur; + $parts = array_map('trim', explode('|', ltrim($line, '#'), 3)); + $title = $parts[0] ?? ''; + $image = $parts[1] ?? ''; + $alt = $parts[2] ?? 'Visual'; + if ($title === '' || !startpage_valid_card_asset($image)) { $cur = null; continue; } + $cur = ['title' => $title, 'image' => $image, 'alt' => $alt !== '' ? $alt : 'Visual', 'links' => []]; + continue; + } + if ($cur === null) continue; + $parts = array_map('trim', explode('|', $line, 2)); + $label = $parts[0] ?? ''; + $url = $parts[1] ?? ''; + if ($label !== '' && startpage_valid_card_url($url)) $cur['links'][] = ['label' => $label, 'url' => $url]; + } + if ($cur !== null) $cards[] = $cur; + $all = array_values(array_filter($cards, fn($card) => $card['links'] !== [])); + return $all; +} + +function startpage_seasonal_sports_image(string $image): string { + // Sports card logo follows the local season: Belleville Senators (AHL) + // October–April, Belleville Kodiaks (OSL Sr B lacrosse) May–September. + if ($image !== 'belleville.png') return $image; + $month = (int)date('n'); + return ($month >= 5 && $month <= 9) ? 'kodiaks.png' : 'belleville.png'; +} + +$allCardData = startpage_card_data(); +foreach ($allCardData as &$card) { + $card['image'] = startpage_seasonal_sports_image($card['image']); +} +unset($card); +?> +<?php foreach ($allCardData as $card): ?> + <article class="card" data-title="<?= htmlspecialchars($card['title'], ENT_QUOTES, 'UTF-8') ?>" data-labels="<?= htmlspecialchars(implode(' ', array_column($card['links'], 'label')), ENT_QUOTES, 'UTF-8') ?>"> + <div class="card-titlebar"> + <span class="card-dots" aria-hidden="true"><span></span><span></span><span></span></span> + <span class="card-title"><?= htmlspecialchars($card['title'], ENT_QUOTES, 'UTF-8') ?></span> + </div> + <div class="card-body"> + <div class="thumb"><img src="<?= htmlspecialchars($card['image'], ENT_QUOTES, 'UTF-8') ?>" alt="<?= htmlspecialchars($card['alt'], ENT_QUOTES, 'UTF-8') ?>" loading="lazy" decoding="async" /></div> + <div class="card-links"> +<?php foreach ($card['links'] as $link): ?> + <a href="<?= htmlspecialchars($link['url'], ENT_QUOTES, 'UTF-8') ?>" title="<?= htmlspecialchars($link['label'] . ' — ' . $link['url'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($link['label'], ENT_QUOTES, 'UTF-8') ?></a> +<?php endforeach; ?> + </div> + </div> + </article> + +<?php endforeach; ?> diff --git a/startpage/roundcube.png b/startpage/roundcube.png Binary files differnew file mode 100644 index 0000000..d972dbe --- /dev/null +++ b/startpage/roundcube.png diff --git a/startpage/shoppingcart.png b/startpage/shoppingcart.png Binary files differnew file mode 100644 index 0000000..fe3f85e --- /dev/null +++ b/startpage/shoppingcart.png diff --git a/startpage/sm_logo.png b/startpage/sm_logo.png Binary files differnew file mode 100644 index 0000000..f17bc70 --- /dev/null +++ b/startpage/sm_logo.png diff --git a/startpage/somethingawful.png b/startpage/somethingawful.png Binary files differnew file mode 100644 index 0000000..e462ec7 --- /dev/null +++ b/startpage/somethingawful.png diff --git a/startpage/sonemic.png b/startpage/sonemic.png Binary files differnew file mode 100644 index 0000000..f266a34 --- /dev/null +++ b/startpage/sonemic.png diff --git a/startpage/sonymp3.png b/startpage/sonymp3.png Binary files differnew file mode 100644 index 0000000..b627d5a --- /dev/null +++ b/startpage/sonymp3.png diff --git a/startpage/startpage-icon.png b/startpage/startpage-icon.png Binary files differnew file mode 100644 index 0000000..0a6ef90 --- /dev/null +++ b/startpage/startpage-icon.png diff --git a/startpage/startpage.css b/startpage/startpage.css new file mode 100644 index 0000000..307e9cc --- /dev/null +++ b/startpage/startpage.css @@ -0,0 +1,1892 @@ +:root { + --bg: #000000; + --text: #8b0000; + --accent: #ff0000; + --gold: #8b0000; + --border: #2a1515; + --neon-green: #39ff14; + --neon-cyan: #ff1a1a; + --accent-cyan: #8b0000; + --accent-pink: #a00000; + --accent-magenta: #8b0000; + --font-mono: "JetBrains Mono", "IBM Plex Mono", "Fira Code", "Source Code Pro", ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, Consolas, monospace; + --lh-body: 1.55; + --lh-comfort: 1.85; + --link-glow: 255, 0, 0; + --btn-glow: 139, 0, 0; + --radius: 4px; + /* Sharper edges for a retro feel */ + --logo-size: clamp(80px, 10vw, 100px); + --fs-base: clamp(14px, 1.1vw, 17px); +} + +* { + box-sizing: border-box; +} + +/* site.css sets html,body{height:100%}, which clamps the page to one viewport + and makes the overflowing body scroll inside that box — a second scrollbar + next to the browser's. Let the page grow naturally; min-height keeps the + footer pinned to the bottom. */ +html, +body { + height: auto; + min-height: 100vh; + min-height: 100dvh; /* dynamic vh: excludes mobile address bar so content isn't cut off */ +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font: 400 var(--fs-base) var(--font-mono); + line-height: var(--lh-body); + display: flex; + flex-direction: column; + min-height: 100vh; + min-height: 100dvh; + position: relative; + /* Cyber grid + retro wallpaper */ + background-image: + linear-gradient(rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0.72)), + linear-gradient(rgba(255, 0, 0, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 0, 0, 0.03) 1px, transparent 1px), + url('wallpaper.jpg'); + background-size: cover, 28px 28px, 28px 28px, cover; + background-position: center, center, center, center; + background-attachment: fixed, fixed, fixed, fixed; + background-blend-mode: normal; +} + +.crt-overlay { + pointer-events: none; + position: fixed; + inset: 0; + z-index: 9998; + background: + repeating-linear-gradient( + 0deg, + rgba(0, 0, 0, 0.12) 0px, + rgba(0, 0, 0, 0.12) 1px, + transparent 1px, + transparent 3px + ); + opacity: 0.35; + mix-blend-mode: multiply; +} + +body::after { + content: ''; + pointer-events: none; + position: fixed; + inset: 0; + z-index: 9997; + background: radial-gradient(ellipse at center, transparent 55%, rgba(0, 0, 0, 0.45) 100%); +} + +@media (prefers-reduced-motion: reduce) { + .crt-overlay { display: none; } + body.startpage-booting .boot-overlay { display: none !important; } + body.startpage-ready { animation: none; } + .boot-log .boot-cursor { animation: none; } +} + +/* No scrollbars at any resolution: the overlay never scrolls, and the page + behind it is locked while booting. .boot-log is scaled down by JS + (fitBootLog) whenever it would exceed the viewport, so nothing is clipped. */ +html:has(> body.startpage-booting), +body.startpage-booting { + overflow: hidden; +} + +.boot-overlay { + position: fixed; + inset: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + overscroll-behavior: none; + background: #000; + padding: 2rem; + transition: opacity 0.35s ease; + /* Failsafe: if JS errors, don't trap users on a black screen */ + animation: boot-dismiss-failsafe 0s 3s forwards; +} + +/* Short/narrow viewports: give the log the padding back as usable room so the + auto-fit scale stays readable instead of shrinking to fit 2rem gutters. */ +@media (max-height: 600px), (max-width: 600px) { + .boot-overlay { padding: 0.75rem; } +} + +.boot-overlay.boot-fade { + opacity: 0; + pointer-events: none; +} + +.boot-overlay[hidden] { + display: none !important; + animation: none; +} + +@keyframes boot-dismiss-failsafe { + to { + opacity: 0; + visibility: hidden; + pointer-events: none; + } +} + +.boot-log { + margin: auto; + max-width: 42rem; + width: 100%; + color: #f2ca30; + font: inherit; + line-height: 1.6; + /* Scale set by JS. Origin is the top edge: an over-tall flex item with + margin:auto is start-aligned, so scaling from the centre would leave the + shrunk box hanging past the bottom. Below scale 1 the log is centered by + flex anyway, so the origin only matters while shrinking. */ + transform-origin: center top; +} + +.boot-title { + color: #f2ca30; + margin-bottom: 0.9rem; +} + +.boot-line { + white-space: pre-wrap; +} + +.boot-login { + margin-top: 0.9rem; +} + +.boot-log .boot-cursor { + color: #f2ca30; + animation: cursor-blink 0.9s step-end infinite; +} + +body.startpage-ready { + animation: boot-reveal 0.45s ease; +} + +@keyframes boot-reveal { + from { opacity: 0.55; } +} + +body.startpage-ready .search-section h2 { + animation: cursor-blink 1.1s step-end infinite; +} + +@keyframes cursor-blink { + 50% { opacity: 0.55; } +} + +a { + color: var(--accent); + text-decoration: none; + transition: 0.1s; +} + +a:hover { + background: var(--accent); + color: #000 !important; +} + +/* Layout Overrides for Unified Header */ +.header-inner { + max-width: none !important; + margin: 0 !important; + padding-left: 20px !important; + padding-right: 20px !important; +} + +main, +footer { + max-width: none !important; + margin: 0 !important; + padding-left: 20px; + padding-right: 20px; +} + +/* Search Section: Left Aligned */ +.search-section { + padding: 10px 0 40px; + text-align: center; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.top-left-links { + position: absolute; + top: 10px; + left: 20px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 5px; + text-align: left; + z-index: 1; + width: max-content; +} + +/* Windows-style cascading right-click menu for the Sports links */ +.ctx-menu, +.ctx-menu ul { + list-style: none; + margin: 0; + padding: 0; +} + +.ctx-parent { + position: relative; +} + +.ctx-trigger { + cursor: pointer; + user-select: none; +} + +/* Flyout panels: hidden until parent opened */ +.ctx-flyout { + min-width: 170px; + display: none; + flex-direction: column; + padding: 4px; + background: #0a0a0a; + border: 1px solid var(--accent); + border-radius: 4px; + box-shadow: 0 0 12px rgba(255, 0, 0, 0.4); + z-index: 60; + max-height: 80vh; + overflow-y: auto; + /* scroll works, scrollbar hidden */ + scrollbar-width: none; + -ms-overflow-style: none; +} + +.ctx-flyout::-webkit-scrollbar { + display: none; +} + +/* Root dropdown (the category list under "Sports") drops straight down */ +.ctx-root > .ctx-parent > .ctx-flyout { + position: absolute; + top: calc(100% + 4px); + left: 0; +} + +/* Top-left dropdown (Sports) flies out to the RIGHT of the trigger */ +.top-left-links .ctx-root > .ctx-parent > .ctx-flyout { + top: 0; + left: calc(100% + 6px); +} + +/* Top-right dropdown (Links) flies out to the LEFT of the trigger. */ +.top-right-links .ctx-root > .ctx-parent > .ctx-flyout { + top: 0; + left: auto; + right: calc(100% + 6px); +} + +/* Category link lists float free (fixed) so the scrolling root menu can't clip them; + exact left/top set by JS so they cascade to the right and flip near screen edges */ +.ctx-root .ctx-flyout .ctx-flyout { + position: fixed; + top: 0; + left: 0; +} + +.ctx-parent.open > .ctx-flyout { + display: flex; +} + +/* Individual menu rows */ +.ctx-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 4px 10px; + font-size: 0.95rem; + font-weight: bold; + white-space: nowrap; + color: var(--accent); + text-decoration: none; + border-radius: 2px; + cursor: pointer; +} + +.ctx-item:hover, +.ctx-parent.open > .ctx-item { + background: var(--accent); + color: #000 !important; +} + +/* Submenu arrow indicator */ +.ctx-has-sub::after { + content: '▶'; + font-size: 0.8em; + opacity: 0.7; + display: inline-block; + transition: transform 0.2s ease; +} + +/* Open submenu: right-pointing arrow rotates 90deg to point down */ +.ctx-parent.open > .ctx-has-sub::after { + transform: rotate(90deg); +} + +/* Both menus use a crisp CSS border triangle instead of the ◀ / ▶ glyphs + (the fallback font renders those diagonally). Rotates 90deg down on open. */ +.top-left-links .ctx-arrow, +.top-left-links .ctx-has-sub::after, +.top-right-links .ctx-arrow, +.top-right-links .ctx-has-sub::after { + content: ''; + display: inline-block; + width: 0; + height: 0; + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + opacity: 0.85; + transform: none; + transition: transform 0.2s ease; + vertical-align: middle; +} + +/* Top-right triangles point left */ +.top-right-links .ctx-arrow, +.top-right-links .ctx-has-sub::after { + border-right: 11px solid currentColor; + border-left: 0; +} + +/* Top-left triangles point right */ +.top-left-links .ctx-arrow, +.top-left-links .ctx-has-sub::after { + border-left: 11px solid currentColor; + border-right: 0; +} + +/* Trigger arrows (Sports / Google Services / Radio's) — bigger, matching the old glyph */ +.top-left-links .ctx-arrow, +.top-right-links .ctx-arrow { + border-top-width: 9px; + border-bottom-width: 9px; +} +.top-right-links .ctx-arrow { margin-right: 0.4em; border-right-width: 12px; } +.top-left-links .ctx-arrow { margin-left: 0.4em; border-left-width: 12px; } +.top-left-links .ctx-has-sub::after { margin-left: 0.4em; } + +/* Top-right submenus cascade LEFT, so their arrow sits to the LEFT of the + label (before it), with the whole row packed to the left edge. */ +.top-right-links .ctx-has-sub { + flex-direction: row-reverse; + justify-content: flex-end; +} +.top-right-links .ctx-has-sub::after { + margin-left: 0; + margin-right: 0.4em; +} + +/* On open the left-pointing (top-right) triangle rotates down to the bottom */ +.top-right-links .ctx-parent.open > .ctx-has-sub::after { + transform: rotate(-90deg); +} + +/* Trigger arrows rotate to point down on open; direction depends on which way they point */ +.ctx-arrow { + display: inline-block; + transition: transform 0.2s ease; +} + +/* Top-left trigger arrow points right (▶), rotates +90 -> down on open */ +.top-left-links .ctx-parent.open > .ctx-trigger .ctx-arrow { + transform: rotate(90deg); +} + +/* Top-right trigger arrow rotates 90deg down (left -> bottom) on open */ +.top-right-links .ctx-parent.open > .ctx-trigger .ctx-arrow { + transform: rotate(-90deg); +} + +.search-link { + font-size: 1.1rem; + font-weight: bold; + text-decoration: none; + transition: 0.1s; + color: var(--accent); +} + +.top-right-links { + position: absolute; + top: 10px; + right: 20px; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 5px; + z-index: 1; + width: max-content; +} + +.search-link:hover { + background: var(--accent); + color: #000 !important; +} + +body a:hover, +body a:focus-visible { + color: #ffd0d0; +} + +.search-section h2 { + color: var(--accent); + margin-bottom: 15px; + font-size: 1.2rem; + font-weight: bold; +} + +.search-section h2 a { + color: inherit; + text-decoration: none; + transition: color 0.1s; +} + +.search-section h2 a:hover, +.search-section h2 a:focus-visible { + color: var(--accent) !important; + background: transparent !important; + text-shadow: + 0 0 4px rgba(255, 0, 0, .95), + 0 0 12px rgba(139, 0, 0, .75), + 0 0 24px rgba(139, 0, 0, .45); + box-shadow: + 0 0 0 1px rgba(139, 0, 0, .55), + 0 0 10px rgba(255, 0, 0, .45), + 0 0 24px -4px rgba(139, 0, 0, .4); +} + +/* Plain destination links stacked above the engine title; same look as it. */ +.search-section h2.quick-search-title { + margin-bottom: 6px; +} + +/* First quick link sits on the section's top line; the rest stack under it. */ +.search-section h2.quick-search-title:first-of-type { + margin-top: 0; +} + +.search-box { + width: 100%; + max-width: 584px; + background: #000; + border: 1px solid var(--accent); + padding: 12px; + color: var(--accent); + font-family: inherit; + outline: none; + border-radius: var(--radius); + transition: box-shadow 0.2s; + margin: 0 auto; + display: block; +} + +.search-box:focus { + box-shadow: 0 0 10px var(--accent); +} + +/* Skeleton sets input[type=...]{font-size:var(--fs-base)} (specificity 0,1,1) and + loads after this file, so it wins over plain .search-box. On mobile --fs-base + clamps to 14px, which makes iOS Safari auto-zoom (and reflow) on focus. Force + >=16px on the search inputs to stop that. !important needed to beat Skeleton. */ +input.search-box { font-size: 16px !important; } + +/* Unified search with engine toggle */ +.unified-search { + display: flex; + align-items: stretch; + width: 100%; + max-width: 584px; + margin: 0 auto; + border: 1px solid var(--accent); + border-radius: var(--radius); + background: #000; + position: relative; +} + +.unified-search:focus-within { + box-shadow: 0 0 10px var(--accent); +} + +.search-engine-picker { + position: relative; + display: flex; +} + +.search-engine-toggle { + background: #000; + color: var(--accent); + border: none; + border-right: 1px solid var(--accent); + font-family: inherit; + font-weight: bold; + font-size: 1rem; + padding: 0 14px; + cursor: pointer; + min-width: 64px; + transition: 0.15s; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.search-engine-toggle:hover { + background: var(--accent); + color: #000; +} + +.search-engine-caret { + font-size: 0.7rem; + opacity: 0.8; +} + +.search-engine-menu { + position: absolute; + top: calc(100% + 2px); + left: 0; + min-width: 180px; + margin: 0; + padding: 0; + list-style: none; + background: #000; + border: 1px solid var(--accent); + border-radius: var(--radius); + z-index: 50; + text-align: left; + box-shadow: 0 0 10px rgba(255, 0, 0, 0.4); +} + +.search-engine-menu[hidden] { + display: none; +} + +.search-engine-menu li { + padding: 8px 12px; + color: var(--accent); + cursor: pointer; + font-family: inherit; + font-size: 0.9rem; + display: flex; + align-items: center; + gap: 10px; + border-bottom: 1px solid var(--border); +} + +.search-engine-menu li:last-child { + border-bottom: none; +} + +.search-engine-menu li:hover, +.search-engine-menu li[aria-selected="true"] { + background: var(--accent); + color: #000; +} + +.search-engine-menu .engine-icon { + font-weight: bold; + min-width: 28px; + text-align: center; +} + +.unified-search .unified-search-input { + border: none; + border-radius: 0; + max-width: none; + flex: 1; + width: auto; + margin: 0; + text-align: center; +} + +.unified-search .unified-search-input:focus { + box-shadow: none; +} + +/* kill ugly native search clear (webkit) + edge clear, use custom .search-clear */ +.unified-search .unified-search-input::-webkit-search-cancel-button, +.unified-search .unified-search-input::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; + display: none; +} +.unified-search .unified-search-input::-ms-clear, +.unified-search .unified-search-input::-ms-reveal { + display: none; + width: 0; + height: 0; +} + +.search-engine-spacer { + min-width: 64px; + display: flex; + align-items: center; + justify-content: center; +} + +.search-clear { + background: #8b0000; + border: 1px solid #8b0000; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + opacity: 1; + transition: background 0.15s, border-color 0.15s, transform 0.15s; +} + +.search-clear svg { + display: block; + stroke: #ff5555 !important; +} + +.search-clear svg line { + stroke: #ff5555 !important; +} + +.search-clear:hover { + background: #ff0000; + border-color: #ff0000; + transform: scale(1.05); +} + +.search-clear:hover svg, +.search-clear:hover svg line { + stroke: #8b0000 !important; +} + +.search-clear:active { + transform: scale(0.92); +} + +.search-clear:focus-visible { + outline: 1px solid var(--accent); + outline-offset: 1px; + opacity: 1; +} + +.search-clear[hidden] { + display: none; +} + +.clock-container { + margin-top: 15px; + color: var(--text); + font-size: 0.9rem; + opacity: 0.8; + text-align: center; +} + +.clock-time { + color: var(--accent); + letter-spacing: 1px; +} + +/* Grid Layout: Left Aligned */ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 24px; + padding: 0; +} + +.card { + border: 1px solid var(--border); + background: #050505; + padding: 0; + display: flex; + flex-direction: column; + align-items: stretch; + transition: transform .2s ease, border-color .2s ease, box-shadow .25s ease; + min-width: 0; + overflow: hidden; + border-radius: var(--radius); + box-shadow: + 0 0 0 1px rgba(255, 0, 0, 0.08), + 0 4px 18px rgba(0, 0, 0, 0.45); +} + +.card:hover { + border-color: var(--accent); + transform: translateY(-2px) scale(1.01); + box-shadow: + 0 0 0 1px rgba(255, 0, 0, 0.45), + 0 0 14px rgba(255, 0, 0, 0.25), + 0 8px 24px rgba(0, 0, 0, 0.55); +} + +.card-titlebar { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + background: linear-gradient(180deg, #1a0000, #0a0000); + border-bottom: 1px solid var(--border); + font-weight: 700; + font-size: 0.85rem; + letter-spacing: 0.3px; + text-transform: uppercase; +} + +.card-dots { + display: inline-flex; + gap: 5px; +} + +.card-dots span { + width: 8px; + height: 8px; + border-radius: 50%; + background: #333; + border: 1px solid #555; +} + +.card-dots span:nth-child(1) { background: #ff5f57; border-color: #cc4a44; } +.card-dots span:nth-child(2) { background: #febc2e; border-color: #c99524; } +.card-dots span:nth-child(3) { background: #28c840; border-color: #1f9a32; } + +.card-title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--accent); + text-shadow: 0 0 6px rgba(255, 0, 0, 0.35); +} + +.card-body { + padding: 15px 15px 20px; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.card.hidden { + display: none !important; +} + +.card-filter-wrap { + width: 100%; + max-width: 584px; + margin: 10px auto 0; + position: relative; +} + +.card-filter { + margin: 0; + text-align: center; + font-size: 0.95rem; + padding: 10px 12px; + padding-right: 55px; + border-radius: var(--radius); + /* always-on active glow, matches focused search look even when not focused */ + box-shadow: 0 0 10px var(--accent); +} + +/* kill native search clear for the card filter too */ +.card-filter::-webkit-search-cancel-button, +.card-filter::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; + display: none; +} +.card-filter::-ms-clear { + display: none; +} + +.filter-count { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + font-size: 0.75rem; + color: #666; + white-space: nowrap; + pointer-events: none; + opacity: 0.7; +} + +body.light-mode .filter-count { + color: #8b0000; +} + +.card .thumb { + width: 100%; + height: 70px; + display: flex; + align-items: center; + justify-content: flex-start; + margin-bottom: 15px; + opacity: 0.85; + transition: opacity .2s ease, transform .2s ease; +} + +.card:hover .thumb { + opacity: 1; + transform: scale(1.03); +} + +.card img { + max-width: 80px; + max-height: 100%; + object-fit: contain; +} + +.card-links { + font-size: 0.95rem; + line-height: 1.6; + border-top: 1px solid var(--border); + width: 100%; + padding-top: 16px; + text-align: left; + display: grid; + grid-template-columns: 1fr; + align-items: flex-start; + gap: 12px; +} + +.card-links a { + display: inline-block; + width: auto; + max-width: 100%; + /* Dotted labels ("wiki.realmofespionage") are one unbreakable token: their + min-content width sets the grid track and pushes the whole link column + past the card's right edge on narrow columns. Let them wrap. */ + min-width: 0; + overflow-wrap: anywhere; + padding: 2px 6px 4px; + line-height: 1.45; + border-radius: 2px; + background: #0a0a0a; + text-shadow: + 0 0 4px rgba(255, 0, 0, .9), + 0 0 10px rgba(255, 0, 0, .5); + box-shadow: + 0 0 0 1px rgba(255, 0, 0, .9), + 0 0 6px rgba(255, 0, 0, .35); + transition: transform .2s ease, box-shadow .25s ease, background .2s ease; +} + +.card-links a:hover, +.card-links a:focus-visible { + transform: translateY(-1px) scale(1.02); + background: color-mix(in srgb, var(--accent) 12%, #0a0a0a); + color: #ffd0d0; + box-shadow: + 0 0 0 1px rgba(var(--link-glow), .65), + 0 0 12px rgba(var(--link-glow), .4); +} + +footer { + border-top: 1px solid var(--border); + margin-top: auto; + color: #555; +} + +/* Last.fm now-playing bar */ +.lastfm-widget { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 20px; + margin-top: 12px; + border-bottom: 1px solid var(--border); + background: #050505; + font-size: 0.8rem; + color: #888; + flex-wrap: wrap; +} + + +.lastfm-label { + color: #d51007; +} + +#lastfm-status { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.lastfm-profile { + color: #d51007; + white-space: nowrap; + flex-shrink: 0; +} + +.lastfm-profile:hover { + background: #d51007; + color: #000 !important; +} + +/* Phones / touch devices: bigger text, easier tap target, stacked layout */ +@media (max-width: 600px), (hover: none) and (pointer: coarse) and (max-width: 820px) { + + .lastfm-widget { + flex-direction: column; + align-items: stretch; + gap: 8px; + padding: 12px 16px; + font-size: 0.95rem; + text-align: center; + } + + .lastfm-label { + font-size: 1.2rem; + } + + #lastfm-status { + white-space: normal; + overflow: visible; + text-overflow: clip; + flex: none; + line-height: 1.4; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + word-break: break-word; + overflow-wrap: anywhere; + } + + #lastfm-status .lastfm-prefix, + #lastfm-status .lastfm-track-name, + #lastfm-status .lastfm-track-artist { + display: block; + max-width: 100%; + } + + #lastfm-status .lastfm-track-name { + font-weight: bold; + } + + #lastfm-status .lastfm-sep { + display: none; + } + + #lastfm-status .lastfm-track-artist::before { + content: '— '; + opacity: 0.6; + } + + .lastfm-profile { + align-self: center; + padding: 10px 18px; + min-height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid #d51007; + border-radius: 4px; + font-weight: bold; + } +} + + + +/* Persistent red glow halo on the link groups (visible on desktop and touch) */ +.top-left-links .search-link, +.top-right-links .search-link { + display: inline-block; + padding: 1px 4px; + border-radius: 2px; + text-shadow: + 0 0 4px rgba(255, 0, 0, .9), + 0 0 12px rgba(139, 0, 0, .6), + 0 0 24px rgba(139, 0, 0, .35); + box-shadow: + 0 0 0 1px rgba(255, 0, 0, .5), + 0 0 10px rgba(139, 0, 0, .55), + 0 0 24px -4px rgba(139, 0, 0, .4); +} + +/* Phone-only: stack the link groups vertically so they don't overlap the search */ +@media (max-width: 600px), (hover: none) and (pointer: coarse) and (max-width: 820px) { + .search-section { + padding-top: 20px; + } + + .top-left-links, + .top-right-links { + position: static; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 0 10px; + margin-bottom: 20px; + text-align: center; + z-index: auto; + width: 100%; + } + + .top-left-links .search-link, + .top-right-links .search-link { + font-size: 0.95rem; + } +} + +@media (max-width: 850px) { + + header, + main, + footer { + padding: 20px; + } + + header { + flex-direction: column; + } + + .nav-columns { + width: 100%; + } + + .age-modal-content { + margin: 20px; + padding: 20px; + } + + .search-section { + padding: 20px 0 20px 0; + } + + .search-box { + padding: 8px 10px; + font-size: 16px; /* >=16px: prevent iOS Safari auto-zoom on focus */ + } + + .unified-search { + max-width: 100%; + } + + .card-filter-wrap { + max-width: 100%; + } + + .search-engine-toggle { + min-width: 48px; + padding: 0 8px; + font-size: 0.9rem; + } + + .unified-search .unified-search-input { + min-width: 0; + } +} + + + +/* Light mode: snow/ice startpage theme */ +body.light-mode { + --bg: #fff8f1; + --surface: #fffdf9; + --text: #1a2a33; + --ink: #1a2a33; + --muted: #5c6b73; + --accent: #8b0000; + --gold: #8b0000; + --border: #d9c9b8; + --shadow: rgba(92, 72, 52, .12); + --link-glow: 139, 0, 0; + background: #fff8f1 !important; + background-image: none !important; + color: var(--text); +} + +body.light-mode main, +body.light-mode footer, +body.light-mode .search-section, +body.light-mode .grid { + background: #fff8f1; +} + +body.light-mode .card, +body.light-mode .lastfm-widget, +body.light-mode .ctx-flyout, +body.light-mode .unified-search, +body.light-mode .search-box, +body.light-mode .search-engine-toggle, +body.light-mode .search-engine-menu, +body.light-mode .unified-search .unified-search-input, +body.light-mode .age-modal-content, +body.light-mode .age-modal-btn, +body.light-mode .modal-search-input, +body.light-mode .modal-search-button { + background: #ffffff !important; + color: var(--text) !important; + border-color: var(--border) !important; +} + +body.light-mode .card-links, +body.light-mode .search-engine-toggle, +body.light-mode .ctx-flyout, +body.light-mode .search-engine-menu li, +body.light-mode footer, +body.light-mode .lastfm-widget, +body.light-mode .modal-search { + border-color: var(--border) !important; +} + +body.light-mode .top-left-links .search-link, +body.light-mode .top-right-links .search-link { + color: var(--accent); + text-shadow: none; + box-shadow: none; +} + +body.light-mode .card { + box-shadow: + 0 0 0 1px rgba(255, 255, 255, .95), + 0 0 16px rgba(139, 0, 0, .18), + 0 0 34px rgba(255, 255, 255, .88); +} + +body.light-mode .card-links a { + background: #fff4f4; + color: var(--accent); + text-shadow: + 0 0 3px rgba(255, 255, 255, 1), + 0 0 10px rgba(255, 255, 255, .9), + 0 0 18px rgba(139, 0, 0, .28); + box-shadow: + 0 0 0 1px rgba(255, 255, 255, .95), + 0 0 10px rgba(255, 255, 255, .9), + 0 0 20px -4px rgba(139, 0, 0, .36); +} + +body.light-mode .ctx-flyout, +body.light-mode .search-engine-menu { + box-shadow: 0 8px 22px var(--shadow); +} + +body.light-mode .ctx-item, +body.light-mode .search-engine-menu li { + color: var(--text); + text-shadow: none; +} + +body.light-mode .ctx-parent.open > .ctx-item { + background: var(--accent); + color: #ffffff !important; +} + +body.light-mode .search-section h2, +body.light-mode .search-section h2 a, +body.light-mode .clock-time, +body.light-mode .lastfm-label, +body.light-mode .lastfm-profile, +body.light-mode .age-modal-content h2 { + color: var(--accent); +} + +body.light-mode .clock-container, +body.light-mode .lastfm-widget, +body.light-mode footer { + color: var(--muted); +} + +body.light-mode .card:hover, +body.light-mode .unified-search:focus-within, +body.light-mode .search-box:focus { + border-color: var(--accent); +} + +body.light-mode .card:hover { + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 1), + 0 0 18px rgba(139, 0, 0, .32), + 0 0 42px rgba(255, 255, 255, .95); +} + +body.light-mode .search-engine-menu { + box-shadow: 0 8px 22px var(--shadow); +} + +body.light-mode button.search-clear { + background: #f8e8e8 !important; + border-color: var(--border) !important; +} + +body.light-mode .search-clear svg, +body.light-mode .search-clear svg line { + stroke: var(--accent) !important; +} + +body.light-mode button.search-clear:hover { + background: var(--accent) !important; + border-color: var(--accent) !important; +} + +body.light-mode .search-clear:hover svg, +body.light-mode .search-clear:hover svg line { + stroke: #ffffff !important; +} + +body.light-mode a:hover, +body.light-mode .search-link:hover, +body.light-mode .ctx-item:hover, +body.light-mode .search-engine-toggle:hover, +body.light-mode .search-engine-menu li:hover, +body.light-mode .search-engine-menu li[aria-selected="true"], +body.light-mode .lastfm-profile:hover, +body.light-mode .card-links a:hover, +body.light-mode .age-modal-btn:hover, +body.light-mode .age-modal-content form button:hover { + background: var(--accent) !important; + color: #ffffff !important; + border-color: var(--accent) !important; + text-shadow: none; +} + +body.light-mode .age-modal { + background: rgba(255, 248, 241, 0.92); +} + +body.light-mode .age-modal-btn.deny:hover { + background: #555 !important; + border-color: #555 !important; + color: #ffffff !important; +} + +body .site-header .brand-text strong, +body .site-header .brand-text span, +body .site-header .header-button, +body .site-header #menu-toggle, +body .site-header nav.site-nav a, +body .site-header .lang-menu a { + color: var(--accent); +} + +/* --------------------------------------------------------------------------- + Mobile / touch: the desktop context menus cascade as absolute/fixed flyouts + that run off the right edge and below the viewport. On small / touch screens + render every flyout in-flow instead: full-width, stacked under its trigger, + nested levels indented (accordion). Kills all overflow. !important is needed + to override the inline left/top the JS (positionFlyout) sets and the fixed + positioning from the base rules. + --------------------------------------------------------------------------- */ +@media (max-width: 600px), (hover: none) and (pointer: coarse) and (max-width: 820px) { + .ctx-menu .ctx-flyout { + position: static !important; + inset: auto !important; + left: auto !important; + top: auto !important; + right: auto !important; + width: 100% !important; + min-width: 0 !important; + max-width: 100% !important; + max-height: none !important; + box-sizing: border-box; + margin: 4px 0; + box-shadow: none; + overflow: visible !important; + } + + /* indent + accent rule on nested submenus so the hierarchy reads clearly */ + .ctx-menu .ctx-flyout .ctx-flyout { + margin: 4px 0 4px 14px; + border-left: 2px solid var(--accent); + } + + /* left-align rows and give finger-friendly tap targets */ + .ctx-menu .ctx-item { + justify-content: flex-start; + padding: 10px 12px; + } + + /* Mobile uses a distinct chevron (❯) that rotates down on open, instead of + the desktop filled triangles. Applies to both submenu arrows and the + top-level trigger arrows (Sports / Google Services / Radio's). */ + .ctx-menu .ctx-has-sub::after { + content: '❯'; + transform: rotate(0deg); + transition: transform 0.2s ease; + } + .ctx-menu .ctx-parent.open > .ctx-has-sub::after { + transform: rotate(90deg); + } + + /* Trigger arrows: hide the desktop glyph (◀ / ▶) and show the same chevron, + rotating down on open. */ + .ctx-menu .ctx-arrow { font-size: 0; } + .ctx-menu .ctx-arrow::after { + content: '❯'; + font-size: 1rem; + display: inline-block; + transition: transform 0.2s ease; + } + .ctx-menu .ctx-parent.open > .ctx-trigger .ctx-arrow { + transform: none; + } + .ctx-menu .ctx-parent.open > .ctx-trigger .ctx-arrow::after { + transform: rotate(90deg); + } + + /* Drop the desktop triangles on mobile; the chevron (❯) is used instead */ + .top-left-links .ctx-arrow, + .top-left-links .ctx-has-sub::after, + .top-right-links .ctx-arrow, + .top-right-links .ctx-has-sub::after { + border: 0; + width: auto; + height: auto; + margin: 0; + } + .top-left-links .ctx-has-sub::after, + .top-right-links .ctx-has-sub::after { content: '❯'; } + /* Mobile accordion: keep chevron on the right (undo desktop left-flip) */ + .top-right-links .ctx-has-sub { + flex-direction: row; + justify-content: space-between; + } +} + +/* Comfortable line spacing toggle for long link lists */ +.comfort-toggle { + margin-top: 12px; + padding: 6px 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #111; + color: var(--accent); + font: inherit; + font-size: 0.82rem; + cursor: pointer; + transition: background .15s, color .15s, border-color .15s, box-shadow .2s; +} + +.comfort-toggle:hover, +.comfort-toggle:focus-visible { + background: var(--accent); + color: #000; + outline: none; +} + +.comfort-toggle[aria-pressed="true"] { + border-color: var(--neon-green); + color: var(--neon-green); + box-shadow: 0 0 10px rgba(57, 255, 20, .25); +} + +body.startpage-comfort, +body.comfort-spacing { + line-height: var(--lh-comfort); +} + +body.startpage-comfort .card-body, +body.startpage-comfort .clock-container, +body.startpage-comfort .lastfm-widget { + line-height: var(--lh-comfort); +} + +body.startpage-comfort .card-links { + line-height: var(--lh-comfort); + gap: 22px; +} + +body.startpage-comfort .card-links a { + line-height: 1.7; + padding: 4px 8px 6px; +} + +.search-section h2 { + font-weight: 800; + position: relative; +} + +.search-section h2::after { + content: ''; + display: block; + width: 2.5rem; + height: 2px; + margin: 6px auto 0; + border-radius: 2px; + background: linear-gradient(90deg, #8b0000, var(--accent)); + box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 45%, transparent); + animation: sp-neon-pulse 3.5s ease-in-out infinite; +} + +@keyframes sp-neon-pulse { + 50% { + box-shadow: + 0 0 10px color-mix(in srgb, var(--accent) 50%, transparent), + 0 0 18px color-mix(in srgb, #8b0000 25%, transparent); + } +} + +body.light-mode .comfort-toggle { + background: #fff8f1; + color: #8b0000; + border-color: #e8c8c8; +} + +body.light-mode .comfort-toggle:hover { + background: #8b0000; + color: #fff; +} + +body.light-mode .comfort-toggle[aria-pressed="true"] { + color: #5a0000; + border-color: #8b0000; + box-shadow: 0 0 10px rgba(139, 0, 0, .2); +} + +/* Konaste / BEMANI PC maintenance status card (under the Last.fm widget) */ +.konaste-status { + margin: 12px 20px 0; + padding: 12px 16px; + border: 1px solid var(--border); + border-radius: 4px; + background: #050505; + font-size: 0.8rem; + color: #888; +} + +.konaste-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 8px; + border-bottom: 1px solid var(--border); + padding-bottom: 6px; +} + +.konaste-title { + margin: 0; + font-size: 0.85rem; + color: var(--accent); + letter-spacing: 0.03em; +} + +.konaste-updated { color: #666; } + +.konaste-clock { color: #666; } + +.konaste-clock-time { + color: var(--neon-cyan); + font-variant-numeric: tabular-nums; + letter-spacing: 0.02em; +} + +.konaste-clock-colon { opacity: 0.6; margin: 0 1px; } + +.konaste-clock-ap { color: var(--neon-cyan); font-size: 0.9em; } + +.konaste-clock-tz { color: #666; margin-left: 4px; } + +.konaste-names { + display: inline-flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.konaste-name-sep { color: #555; flex-shrink: 0; } + +.konaste-game { + display: grid; + grid-template-columns: 12px auto minmax(0, 1fr); + align-items: start; + column-gap: 10px; + row-gap: 5px; + padding: 5px 0; +} + +.konaste-dot { grid-column: 1; grid-row: 1; margin-top: 3px; } + +.konaste-names { grid-column: 2; grid-row: 1; } + +.konaste-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; + background: #555; + box-shadow: 0 0 6px rgba(255, 255, 255, 0.15); +} + +.konaste-dot.is-online { + background: var(--neon-green); + box-shadow: 0 0 8px var(--neon-green); +} + +.konaste-dot.is-maintenance { + background: #febc2e; + box-shadow: 0 0 8px #febc2e; +} + +.konaste-name { + color: #d51007; + white-space: nowrap; + flex-shrink: 0; + font-weight: 600; +} + +.konaste-name:hover { + background: #d51007; + color: #000 !important; +} + +.konaste-text { + grid-column: 3; + grid-row: 1; + color: #aaa; + line-height: 1.55; + text-align: right; + white-space: pre-line; +} + +.konaste-countdown { + grid-column: 3; + text-align: right; + color: var(--neon-cyan); + font-variant-numeric: tabular-nums; +} + +.konaste-note { + display: block; + margin-top: 8px; + padding-top: 6px; + border-top: 1px solid var(--border); + color: #666; + line-height: 1.4; +} + +.konaste-note a { color: #8b0000; } + +@media (max-width: 600px), (hover: none) and (pointer: coarse) and (max-width: 820px) { + .konaste-status { font-size: 0.9rem; } + .konaste-game { padding: 7px 0; } + .konaste-dot { width: 12px; height: 12px; } +} + +/* --------------------------------------------------------------------------- + Mid-size viewports (portrait monitors, half-screen windows, small laptops): + the absolutely-positioned link columns overlap the centered search box + between the phone breakpoint (600px) and ~1100px. Pull them into the flow + as centered wrapping rows above the search instead. Root dropdowns drop + straight down under their trigger; nested flyouts keep the JS fixed + positioning, which already clamps to the viewport. + On touch screens <=820px the accordion rules above still win (!important). + --------------------------------------------------------------------------- */ +@media (min-width: 601px) and (max-width: 1100px) { + .search-section { + padding-top: 16px; + } + + .top-left-links, + .top-right-links { + position: static; + width: 100%; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 8px 16px; + margin-bottom: 12px; + text-align: center; + z-index: auto; + } + + /* Root dropdowns open downward under the trigger instead of sideways */ + .top-left-links .ctx-root > .ctx-parent > .ctx-flyout, + .top-right-links .ctx-root > .ctx-parent > .ctx-flyout { + top: calc(100% + 4px); + left: 0; + right: auto; + max-width: 92vw; + } +} + +/* Narrow viewports: the 3-column konaste row (dot | names | right-aligned text) + squeezes the status text into a sliver. Stack text/countdown under the names. */ +@media (max-width: 850px) { + .konaste-game { + grid-template-columns: 12px minmax(0, 1fr); + } + + .konaste-text { + grid-column: 2; + grid-row: 2; + text-align: left; + } + + .konaste-countdown { + grid-column: 2; + text-align: left; + } + + .grid { + gap: 16px; + } +} + +/* Light mode */ +body.light-mode .konaste-status { + background: #f4f4f4; + border-color: #d9c2c2; + color: #555; +} +body.light-mode .konaste-title { color: #b00020; } +body.light-mode .konaste-text { color: #444; } +body.light-mode .konaste-updated, +body.light-mode .konaste-note { color: #888; } +body.light-mode .konaste-countdown { color: #8b0000; } +body.light-mode .konaste-name, +body.light-mode .konaste-note a { color: #b00020; } + +/* ===== OpenBSD-style boot screen additions ===== */ +.boot-log { + text-align: center; +} +/* Whole boot log is centered (ASCII blocks keep their own left-aligned + internal alignment via .boot-ascii-wrap's column flex). */ +.boot-log .boot-line, +.boot-log .boot-login, +.boot-log .boot-title { + text-align: center; +} +.boot-avatar { + width: clamp(72px, 10vw, 96px); + height: clamp(72px, 10vw, 96px); + border-radius: 50%; + object-fit: cover; + border: 3px solid #f2ca30; + box-shadow: 0 0 14px rgba(242, 202, 48, 0.55); + display: block; + margin: 0 auto 10px; +} +/* Lain hero card pinned above Puffy's bubble column. The wrap reserves the + avatar's height as padding so the absolute placement never clips the ASCII + or the lines above it. */ +.boot-puffy-wrap { + position: relative; + display: inline-block; + padding-top: calc(clamp(72px, 10vw, 96px) + 10px); + /* Right padding just under half an avatar: lands the avatar centre on the + top bubble without escaping the wrap, which .boot-ascii-wrap clips + horizontally. Scales with the avatar so the alignment holds at every + clamped ASCII size. */ + padding-right: calc(clamp(72px, 10vw, 96px) * 0.385); +} +.boot-puffy-wrap .boot-avatar { + position: absolute; + top: 0; + right: 0; + margin: 0; +} +/* Puffy stacked over the OpenBSD wordmark. */ +.boot-ascii-wrap { + /* overflow-x:hidden alone forces overflow-y to auto, which put a real + scrollbar inside the boot screen. `clip` has no such side effect: the + wide ASCII is still clipped horizontally, y stays visible, nothing + scrolls. `overflow: hidden` is the fallback for pre-Chrome-90 UAs. */ + overflow: hidden; + overflow-x: clip; + overflow-y: visible; + margin-bottom: 12px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} +.boot-openbsd-wordmark { + display: inline-block; + margin: 0; + color: #f2ca30; + text-shadow: + 0 0 8px rgba(242, 202, 48, 0.65), + 0 0 18px rgba(242, 202, 48, 0.3); + font-family: var(--font-mono); + font-size: clamp(4px, min(0.85vw, 0.95vh), 10px); + line-height: 1.15; + text-align: left; + white-space: pre; +} +/* Puffy is 23 lines tall (Gentoo's g was 9) — smaller type keeps the whole + boot log on screen at 1080p and under. */ +.boot-puffy-ascii { + display: inline-block; + margin: 0; + color: #f2ca30; + text-shadow: 0 0 6px rgba(242, 202, 48, 0.4); + font-family: var(--font-mono); + font-size: clamp(4px, min(0.9vw, 1.0vh), 11px); + line-height: 1.15; + text-align: left; + white-space: pre; +} +.boot-log .boot-dim { color: #f2ca30; } +.boot-log .boot-user { + color: #f2ca30; + font-weight: 600; + text-shadow: 0 0 6px rgba(242, 202, 48, 0.55); +} +.boot-log .boot-path { color: #f2ca30; font-weight: 600; } +.boot-log .boot-line, +.boot-log .boot-svc, +.boot-log .boot-login { + color: #f2ca30; +} + +/* --------------------------------------------------------------------------- + Responsive pass: portrait / vertical monitors + denser desktop grid. + The hero used to eat ~440px of height on 1080p, pushing every card below + the fold; portrait viewports over 1100px kept the absolute side link + columns and squeezed the search box into the middle third. + --------------------------------------------------------------------------- */ + +/* Tighter quick-link stack (same type, less air between the rules). */ +.search-section h2.quick-search-title { + line-height: 1.25; + margin-bottom: 2px; +} + +.search-section h2.quick-search-title::after { + margin-top: 4px; +} + +/* Desktop hero compaction: cards start above the fold on 1080p. */ +@media (min-width: 851px) { + .search-section { + padding: 10px 0 22px; + } + + .clock-container { + margin-top: 10px; + } + + /* More cards per row: 200px tracks + tighter gutters. */ + .grid { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 18px; + } +} + +/* Wide screens have spare horizontal room: let the search inputs use it. */ +@media (min-width: 1400px) { + .unified-search, + .card-filter-wrap, + .search-box { + max-width: 680px; + } +} + +@media (min-width: 1600px) { + .grid { + grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); + gap: 20px; + } +} + +/* Portrait / vertical monitors and tablets: drop the absolute side columns so + the search block gets the full width instead of the middle third. */ +@media (orientation: portrait) and (min-width: 601px) and (max-width: 1500px) { + .search-section { + padding-top: 12px; + } + + .top-left-links, + .top-right-links { + position: static; + width: 100%; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 8px 16px; + margin-bottom: 10px; + text-align: center; + z-index: auto; + } + + /* Root dropdowns open downward under the trigger instead of sideways */ + .top-left-links .ctx-root > .ctx-parent > .ctx-flyout, + .top-right-links .ctx-root > .ctx-parent > .ctx-flyout { + top: calc(100% + 4px); + left: 0; + right: auto; + max-width: 92vw; + } +} + +/* Tall-and-narrow portrait (vertical 1080p panel): keep card text legible but + pack more columns than the default 220px track would allow. */ +@media (orientation: portrait) and (min-width: 900px) { + .grid { + grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); + gap: 16px; + } +} + +/* The search form is a flex item, so it shrink-wrapped to ~384px and the + 584/680px max-width on .unified-search never applied. Let it fill the row. */ +#unified-search-form { + width: 100%; + display: flex; + justify-content: center; +} + +/* Short desktop viewports (720p, laptop panels): shave the hero further so the + first card row clears the fold. */ +@media (min-width: 851px) and (max-height: 820px) { + .search-section { + padding: 6px 0 14px; + } + + .search-section h2.quick-search-title { + margin-bottom: 0; + } + + .clock-container { + margin-top: 6px; + } +} + +/* Red/dark-red terminal cursor on startpage controls (overrides cursor:pointer). */ +@media (hover: hover) and (pointer: fine) { + a, + button, + summary, + .search-link, + .ctx-item, + .ctx-trigger, + .comfort-toggle, + .search-engine-menu li, + .card, + label[for], + input[type="checkbox"], + input[type="radio"], + select { + cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='20' viewBox='0 0 16 20'%3E%3Crect x='6' y='0' width='4' height='14' fill='%23ff1a1a'/%3E%3Crect x='4' y='14' width='8' height='4' fill='%238b0000'/%3E%3C/svg%3E") 8 10, pointer; + } +} diff --git a/startpage/startpage.js b/startpage/startpage.js new file mode 100644 index 0000000..f9040c9 --- /dev/null +++ b/startpage/startpage.js @@ -0,0 +1,824 @@ +(function () { + function byId(id) { + return document.getElementById(id); + } + + function updateClock() { + const dateEl = byId('realtime-date'); + const timeEl = byId('realtime-clock'); + if (!dateEl || !timeEl) return; + + const now = new Date(); + const options = { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }; + dateEl.textContent = now.toLocaleDateString(undefined, options).toUpperCase(); + timeEl.textContent = now.toLocaleTimeString(); + } + + setInterval(updateClock, 1000); + updateClock(); + + // Last.fm now-playing widget + const LASTFM_CACHE_KEY = 'lastfmLastGood'; + const LASTFM_BASE_INTERVAL = 10000; + const LASTFM_MAX_INTERVAL = 60000; + let lastfmFailures = 0; + let lastfmTimer = null; + + function renderLastFmTrack(track) { + const statusEl = byId('lastfm-status'); + const widgetEl = byId('lastfm-widget'); + if (!statusEl || !track || !track.name || !track.artist) return; + const prefix = track.nowplaying ? 'Now playing: ' : 'Last played: '; + statusEl.innerHTML = ''; + const prefixEl = document.createElement('span'); + prefixEl.className = 'lastfm-prefix'; + prefixEl.textContent = prefix; + const nameEl = document.createElement('span'); + nameEl.className = 'lastfm-track-name'; + nameEl.textContent = track.name; + const sepEl = document.createElement('span'); + sepEl.className = 'lastfm-sep'; + sepEl.textContent = ' — '; + const artistEl = document.createElement('span'); + artistEl.className = 'lastfm-track-artist'; + artistEl.textContent = track.artist; + statusEl.appendChild(prefixEl); + statusEl.appendChild(nameEl); + statusEl.appendChild(sepEl); + statusEl.appendChild(artistEl); + if (widgetEl) widgetEl.style.display = ''; + } + + + (function primeFromCache() { + try { + const raw = localStorage.getItem(LASTFM_CACHE_KEY); + if (raw) renderLastFmTrack(JSON.parse(raw)); + } catch (_) {} + })(); + + function scheduleLastFm() { + const backoff = Math.min( + LASTFM_MAX_INTERVAL, + LASTFM_BASE_INTERVAL * Math.pow(2, lastfmFailures) + ); + clearTimeout(lastfmTimer); + lastfmTimer = setTimeout(updateLastFm, backoff); + } + + function updateLastFm() { + const statusEl = byId('lastfm-status'); + if (!statusEl) return; + + fetch('/api/lastfm.php') + .then(function (r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }) + .then(function (data) { + if (data && data.error) throw new Error(data.message || 'api error'); + const tracks = Array.isArray(data) ? data : []; + // Prefer a currently-playing track; otherwise fall back to the most + // recent scrobble so the widget always has something to show. + const track = tracks.find(function (t) { return t && t.nowplaying; }) + || tracks.find(function (t) { return t && t.name; }) + || null; + if (track) { + renderLastFmTrack(track); + try { localStorage.setItem(LASTFM_CACHE_KEY, JSON.stringify(track)); } catch (_) {} + } + lastfmFailures = 0; + }) + .catch(function () { + // Never show an error message. Keep whatever is currently rendered + // (primed from localStorage on load), or leave the slot blank. + lastfmFailures++; + }) + .finally(scheduleLastFm); + } + + updateLastFm(); + + document.addEventListener('visibilitychange', function () { + if (document.visibilityState === 'visible') { + lastfmFailures = 0; + updateLastFm(); + } + }); + + // Unified search engine switcher + const SEARCH_ENGINES = [ + { key: 'google', label: 'G', title: 'Google Search..', placeholder: 'Search Google...', action: 'https://www.google.ca/search', param: 'q', site: 'https://www.google.ca' }, + { key: 'brave', label: 'B', title: 'Brave Search..', placeholder: 'Search Brave...', action: 'https://search.brave.com/search', param: 'q', site: 'https://search.brave.com' }, + { key: 'startpage', label: 'SP', title: 'Startpage Search..', placeholder: 'Search Startpage...', action: 'https://www.startpage.com/sp/search', param: 'query', site: 'https://www.startpage.com' }, + { key: 'youtube', label: 'YT', title: 'YouTube Search..', placeholder: 'Search YouTube...', action: 'https://www.youtube.com/results', param: 'search_query', site: 'https://www.youtube.com' }, + { key: 'sillylaird',label: 'SL', title: 'SillyLaird Search..', placeholder: 'Search SillyLaird...', action: 'https://search.sillylaird.ca/search', param: 'q', site: 'https://search.sillylaird.ca' }, + { key: '4get', label: '4G', title: '4get Search..', placeholder: 'Search 4get...', action: 'https://4get.sillylaird.ca/web', param: 's', site: 'https://4get.sillylaird.ca' } + ]; + const SEARCH_ENGINE_KEY = 'startpageSearchEngine_v2'; + + (function initUnifiedSearch() { + const form = byId('unified-search-form'); + const input = byId('unified-search-input'); + const toggle = byId('search-engine-toggle'); + const menu = byId('search-engine-menu'); + const title = byId('unified-search-title'); + const clearBtn = byId('unified-search-clear'); + if (!form || !input || !toggle || !menu) return; + + function syncClear() { + if (!clearBtn) return; + clearBtn.hidden = input.value.length === 0; + } + if (clearBtn) { + input.addEventListener('input', syncClear); + clearBtn.addEventListener('click', function () { + input.value = ''; + syncClear(); + input.focus(); + }); + syncClear(); + } + + let idx = 0; + try { + const saved = localStorage.getItem(SEARCH_ENGINE_KEY); + if (saved) { + const savedIdx = SEARCH_ENGINES.findIndex(function (e) { return e.key === saved; }); + if (savedIdx >= 0) idx = savedIdx; + } else { + idx = SEARCH_ENGINES.findIndex(function (e) { return e.key === 'google'; }); + } + } catch (_) { + idx = SEARCH_ENGINES.findIndex(function (e) { return e.key === 'google'; }); + } + if (idx < 0) idx = 0; + + SEARCH_ENGINES.forEach(function (e, i) { + const li = document.createElement('li'); + li.setAttribute('role', 'option'); + li.dataset.index = String(i); + li.innerHTML = '<span class="engine-icon"></span><span class="engine-name"></span>'; + li.querySelector('.engine-icon').textContent = e.label; + li.querySelector('.engine-name').textContent = e.title.replace(/\.\.$/, ''); + li.addEventListener('click', function () { + idx = i; + apply(idx); + closeMenu(); + input.focus(); + }); + menu.appendChild(li); + }); + + function apply(i) { + const e = SEARCH_ENGINES[i]; + form.setAttribute('action', e.action); + input.setAttribute('name', e.param); + input.setAttribute('placeholder', e.placeholder); + toggle.innerHTML = ''; + toggle.appendChild(document.createTextNode(e.label + ' ')); + const caret = document.createElement('span'); + caret.className = 'search-engine-caret'; + caret.textContent = '▾'; + toggle.appendChild(caret); + toggle.setAttribute('title', 'Search engine: ' + e.title); + if (title) { + title.innerHTML = '<a href="' + e.site + '" target="_blank" rel="noopener noreferrer">' + e.title + '</a>'; + } + Array.prototype.forEach.call(menu.children, function (li, li_i) { + li.setAttribute('aria-selected', li_i === i ? 'true' : 'false'); + }); + try { localStorage.setItem(SEARCH_ENGINE_KEY, e.key); } catch (_) {} + } + + function openMenu() { + menu.hidden = false; + toggle.setAttribute('aria-expanded', 'true'); + } + function closeMenu() { + menu.hidden = true; + toggle.setAttribute('aria-expanded', 'false'); + } + + toggle.addEventListener('click', function (ev) { + ev.stopPropagation(); + if (menu.hidden) openMenu(); else closeMenu(); + }); + + document.addEventListener('click', function (ev) { + if (menu.hidden) return; + if (ev.target === toggle || toggle.contains(ev.target)) return; + if (menu.contains(ev.target)) return; + closeMenu(); + }); + + document.addEventListener('keydown', function (ev) { + if (ev.key === 'Escape' && !menu.hidden) closeMenu(); + }); + + apply(idx); + })(); + +})(); // close the main outer IIFE that wraps clock/lastfm/unified/etc. + + // Live card filter (works on data-attrs emitted by render_cards.php) + (function initCardFilter() { + const byId = (id) => document.getElementById(id); + const filterInput = byId('card-filter'); + const grid = byId('cards-grid'); + const countEl = byId('card-filter-count'); + if (!filterInput || !grid) return; + + const cards = Array.from(grid.querySelectorAll('.card')); + if (!cards.length) return; + + // Ensure clean state (in case of previous inline or classes) + cards.forEach(function (c) { c.classList.remove('hidden'); }); + + // Debug (open console if needed): console.log('[startpage filter] initialized with', cards.length, 'cards'); + + function normalize(s) { + return (s || '').toLowerCase().trim(); + } + + function applyFilter() { + const q = normalize(filterInput.value); + let visible = 0; + cards.forEach(function (card) { + const title = normalize(card.dataset.title); + const labels = normalize(card.dataset.labels); + const hay = title + ' ' + labels; + const match = !q || hay.indexOf(q) !== -1; + card.classList.toggle('hidden', !match); + if (match) visible++; + }); + if (countEl) { + countEl.textContent = q ? (visible + ' / ' + cards.length) : ''; + } + } + + filterInput.addEventListener('input', applyFilter); + + // Keyboard: '/' focuses the card filter (when not in input already) + document.addEventListener('keydown', function (e) { + const ae = document.activeElement; + const isTyping = ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.isContentEditable); + if (e.key === '/' && !isTyping) { + e.preventDefault(); + filterInput.focus(); + filterInput.select(); + } + if (e.key === 'Escape' && ae === filterInput) { + filterInput.value = ''; + applyFilter(); + filterInput.blur(); + } + }); + + // Initial + applyFilter(); + })(); + + // Keyboard help overlay (?) + (function initKeyboardHelp() { + document.addEventListener('keydown', function (e) { + if (e.key === '?' && !e.shiftKey) { + const ae = document.activeElement; + if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.isContentEditable)) return; + e.preventDefault(); + let help = document.getElementById('kbd-help'); + if (!help) { + help = document.createElement('div'); + help.id = 'kbd-help'; + help.style.cssText = 'position:fixed;bottom:12px;right:12px;background:#111;border:1px solid #8b0000;padding:10px 14px;font-size:0.8rem;z-index:999;color:#8b0000;max-width:280px;line-height:1.5;'; + help.innerHTML = '<strong>Keyboard</strong><br>/ — focus card filter<br>Esc — clear / close<br>? — toggle this<br>Click engine picker or cards<br>Theme toggle in header'; + document.body.appendChild(help); + const remover = function (ev) { + if (ev.key === '?' || ev.key === 'Escape') { + help.remove(); + document.removeEventListener('keydown', remover); + } + }; + document.addEventListener('keydown', remover); + } else { + help.remove(); + } + } + }); + })(); + +/* Click-to-open cascading Sports menu (left click toggles, like a context menu) */ +document.querySelectorAll('.ctx-menu').forEach(function (menu) { + function closeAll() { + menu.querySelectorAll('.ctx-parent.open').forEach(function (p) { + p.classList.remove('open'); + }); + } + + // A "trigger" is any element that owns a flyout: the Sports root or a category label + menu.querySelectorAll('.ctx-trigger, .ctx-has-sub').forEach(function (trigger) { + trigger.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + const parent = trigger.closest('.ctx-parent'); + const wasOpen = parent.classList.contains('open'); + // close siblings at the same level + const siblings = parent.parentElement.querySelectorAll(':scope > .ctx-parent.open'); + siblings.forEach(function (s) { if (s !== parent) s.classList.remove('open'); }); + // toggle this one (also closes its descendants when collapsing) + if (wasOpen) { + parent.classList.remove('open'); + parent.querySelectorAll('.ctx-parent.open').forEach(function (d) { d.classList.remove('open'); }); + } else { + parent.classList.add('open'); + positionFlyout(parent); + } + }); + }); + + // Place a category's (fixed-position) link list beside its label, cascading right, + // flipping left / clamping when it would run off a screen edge. + // Skips the root dropdown (direct child of .ctx-root), which CSS drops straight down. + function positionFlyout(parent) { + const flyout = parent.querySelector(':scope > .ctx-flyout'); + if (!flyout || parent.parentElement.classList.contains('ctx-root')) return; + + const label = parent.getBoundingClientRect(); + const box = parent.parentElement.getBoundingClientRect(); // the whole menu box (incl. scrollbar) + const fw = flyout.offsetWidth; + const fh = flyout.offsetHeight; + const margin = 4; + const gap = 2; + + // Top-right menus cascade LEFT (toward screen centre); everything else cascades right. + // Flip to the other side only when the preferred side runs off the viewport edge. + const openLeft = parent.closest('.top-right-links') !== null; + let left; + if (openLeft) { + left = box.left - fw - gap; // open to the left of the whole box + if (left < margin) { + left = box.right + gap; // no room left: flip to the right + } + } else { + left = box.right + gap; // top-left menus only ever cascade right (match the ▶ arrow) + } + if (left < margin) left = margin; + if (left + fw > window.innerWidth - margin) left = window.innerWidth - fw - margin; + + // align top with the label, clamp into the viewport + let top = label.top; + if (top + fh > window.innerHeight - margin) { + top = Math.max(margin, window.innerHeight - fh - margin); + } + + flyout.style.left = left + 'px'; + flyout.style.top = top + 'px'; + } + + // Clicking a real link closes the whole menu + menu.querySelectorAll('a.ctx-item').forEach(function (a) { + a.addEventListener('click', closeAll); + }); + + // Scrolling the category list closes any open (fixed) submenu so it can't float detached + const rootFlyout = menu.querySelector('.ctx-root > .ctx-parent > .ctx-flyout'); + if (rootFlyout) { + rootFlyout.addEventListener('scroll', function () { + rootFlyout.querySelectorAll(':scope > .ctx-parent.open').forEach(function (p) { + p.classList.remove('open'); + }); + }); + } + + // Click outside closes everything + document.addEventListener('click', function (e) { + if (!menu.contains(e.target)) closeAll(); + }); +}); + +/* Auto-extend the red section box so it always contains the absolutely-positioned link columns */ +(function () { + function fitSectionToLinks() { + const section = document.querySelector('.search-section'); + if (!section) return; + const groups = section.querySelectorAll('.top-left-links, .top-right-links'); + let needed = 0; + groups.forEach(function (g) { + // offsetTop (relative to section) + full height of the link column + needed = Math.max(needed, g.offsetTop + g.offsetHeight); + }); + // small breathing room below the last link + section.style.minHeight = needed > 0 ? (needed + 20) + 'px' : ''; + } + + window.addEventListener('load', fitSectionToLinks); + window.addEventListener('resize', fitSectionToLinks); + if (document.readyState === 'complete') fitSectionToLinks(); +})(); + +/* Subtle terminal boot sequence on first visit per session */ +(function () { + var overlay = document.getElementById('boot-overlay'); + var logEl = document.getElementById('boot-log'); + var body = document.body; + if (!overlay || !logEl) return; + + var skipBoot = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (skipBoot || sessionStorage.getItem('startpageBooted') === '1') { + overlay.hidden = true; + body.classList.remove('startpage-booting'); + body.classList.add('startpage-ready'); + return; + } + + /* OpenBSD /etc/rc prints plain lines, no OpenRC stars or [ ok ] brackets. */ + var steps = [ + 'savecore: no core dump', + 'checking quotas: done.', + 'mounting /startpage/cards.txt', + 'starting network daemons: sshd httpd sndiod.', + 'starting local daemons: cron scrobbler.', + 'bringing up the Wired.' + ]; + var idx = 0; + + function el(tag, cls, text) { + var n = document.createElement(tag); + if (cls) n.className = cls; + if (text) n.textContent = text; + return n; + } + + /* The overlay never scrolls (CSS overflow:hidden), so the log has to fit. + Measure the untransformed layout size (scrollWidth/Height ignore the + transform) against the overlay's content box and scale down to fit. + Runs after every append and on resize/orientation change. */ + /* body{overflow:hidden} only reaches the viewport if the UA propagates it, + and here it does not — the document stayed scrollable behind the overlay. + Lock the root directly; CSS does the same via :has for pre-JS paint. */ + var rootStyle = document.documentElement.style; + var prevRootOverflow = rootStyle.overflow; + rootStyle.overflow = 'hidden'; + + var fitRaf = 0; + function fitBootLog() { + fitRaf = 0; + var cs = window.getComputedStyle(overlay); + var availW = overlay.clientWidth + - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight); + var availH = overlay.clientHeight + - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom); + if (availW <= 0 || availH <= 0) return; + + logEl.style.transform = 'none'; + var w = logEl.scrollWidth; + var h = logEl.scrollHeight; + if (!w || !h) return; + + var scale = Math.min(1, availW / w, availH / h); + logEl.style.transform = scale < 1 ? 'scale(' + scale + ')' : 'none'; + } + + function scheduleFit() { + if (fitRaf) return; + fitRaf = window.requestAnimationFrame(fitBootLog); + } + + window.addEventListener('resize', scheduleFit); + window.addEventListener('orientationchange', scheduleFit); + + var OPENBSD_WORDMARK = + ' ____ ____ _____ ____' + '\n' + + ' / __ \\____ ___ ____ / __ ) ___// __ \\' + '\n' + + ' / / / / __ \\/ _ \\/ __ \\/ __ \\__ \\/ / / /' + '\n' + + '/ /_/ / /_/ / __/ / / / /_/ /__/ / /_/ /' + '\n' + + '\\____/ .___/\\___/_/ /_/_____/____/_____/' + '\n' + + ' /_/'; + + var PUFFY_ASCII = + ' _' + '\n' + + ' (_)' + '\n' + + ' | .' + '\n' + + ' . |L /| . _' + '\n' + + ' _ . |\\ _| \\--+._/| . (_)' + '\n' + + ' / ||\\| Y J ) / |/| ./' + '\n' + + ' J |)\'( | ` F`.\'/ _' + '\n' + + ' -<| F __ .-< (_)' + '\n' + + ' | / .-\'. `. /-. L___' + '\n' + + ' J \\ < \\ | | O\\|.-\' _' + '\n' + + ' _J \\ .- \\/ O | | \\ |F (_)' + '\n' + + ' \'-F -<_. \\ .-\' `-\' L__' + '\n' + + '__J _ _. >-\' )._. |-\'' + '\n' + + ' `-|.\' /_. \\_| F' + '\n' + + ' /.- . _.<' + '\n' + + ' /\' /.\' .\' `\\' + '\n' + + ' /L /\' |/ _.-\'-\\' + '\n' + + ' /\'J ___.---\'\\|' + '\n' + + ' |\\ .--\' V | `. `' + '\n' + + ' |/`. `-. `._)' + '\n' + + ' / .-.\\' + '\n' + + ' \\ ( `\\' + '\n' + + ' `.\\'; + + var avatar = el('img', 'boot-avatar'); + avatar.src = '/assets/img/lain.png'; + avatar.alt = ''; + avatar.width = 96; + avatar.height = 96; + + /* Lain sits directly above Puffy's bubble column (the `(_)` marks on the + right of the ASCII), so the bubbles read as rising toward her. */ + var puffyWrap = el('div', 'boot-puffy-wrap'); + puffyWrap.appendChild(avatar); + puffyWrap.appendChild(el('pre', 'boot-puffy-ascii', PUFFY_ASCII)); + + var asciiWrap = el('div', 'boot-ascii-wrap'); + asciiWrap.appendChild(puffyWrap); + asciiWrap.appendChild(el('pre', 'boot-openbsd-wordmark', OPENBSD_WORDMARK)); + logEl.appendChild(asciiWrap); + fitBootLog(); + /* lain.png may land after first layout and change the log's height. */ + avatar.addEventListener('load', scheduleFit); + + /* Prefer the version cached by tools/update_openbsd_version.sh (cron pulls + openbsd.org). Fallback: OpenBSD ships ~1 May and ~1 Nov, rolling + x.9 -> (x+1).0; anchor 7.9 = May 2026 so the banner never goes stale. */ + function openbsdRelease() { + var cached = (typeof window.OPENBSD_RELEASE === 'string') + ? window.OPENBSD_RELEASE.trim() : ''; + if (/^\d+\.\d+$/.test(cached)) return cached; + + var now = new Date(); + var m = now.getMonth(); + var halves = (now.getFullYear() - 2026) * 2 + (m >= 10 ? 1 : (m >= 4 ? 0 : -1)); + var minor = 9 + halves; + return (7 + Math.floor(minor / 10)) + '.' + (minor % 10); + } + + logEl.appendChild(el('div', 'boot-title', + 'OpenBSD ' + openbsdRelease() + ' (GENERIC.MP) #1: sillylaird.ca — amd64')); + fitBootLog(); + + function finishBoot() { + if (fitRaf) window.cancelAnimationFrame(fitRaf); + rootStyle.overflow = prevRootOverflow; + window.removeEventListener('resize', scheduleFit); + window.removeEventListener('orientationchange', scheduleFit); + sessionStorage.setItem('startpageBooted', '1'); + overlay.hidden = true; + body.classList.remove('startpage-booting'); + body.classList.add('startpage-ready'); + } + + function fadeOut() { + overlay.classList.add('boot-fade'); + window.setTimeout(finishBoot, 380); + } + + function showPrompt() { + var prompt = el('div', 'boot-login'); + prompt.appendChild(el('span', 'boot-user', 'sillylaird@Navi')); + prompt.appendChild(el('span', null, ':')); + prompt.appendChild(el('span', 'boot-path', '~')); + prompt.appendChild(el('span', null, '$ ')); + prompt.appendChild(el('span', 'boot-cursor', '█')); + logEl.appendChild(prompt); + fitBootLog(); + window.setTimeout(fadeOut, 420); + } + + function typeLogin() { + var login = el('div', 'boot-login'); + var label = el('span', 'boot-dim', 'Navi login: '); + var typed = el('span', null, ''); + var cursor = el('span', 'boot-cursor', '█'); + login.appendChild(label); + login.appendChild(typed); + login.appendChild(cursor); + logEl.appendChild(login); + fitBootLog(); + + var name = 'sillylaird'; + var i = 0; + function typeChar() { + if (i < name.length) { + typed.textContent += name.charAt(i); + fitBootLog(); + i += 1; + window.setTimeout(typeChar, 70); + return; + } + cursor.remove(); + window.setTimeout(function () { + logEl.appendChild(el('div', 'boot-login boot-dim', + 'Password: ')); + logEl.appendChild(el('div', 'boot-dim', + 'Welcome to OpenBSD: The proactively secure Unix-like operating system.')); + fitBootLog(); + window.setTimeout(showPrompt, 240); + }, 180); + } + window.setTimeout(typeChar, 150); + } + + function nextLine() { + if (idx >= steps.length) { + typeLogin(); + return; + } + var line = el('div', 'boot-line'); + var left = el('span', 'boot-svc', steps[idx]); + line.appendChild(left); + logEl.appendChild(line); + fitBootLog(); + idx += 1; + window.setTimeout(nextLine, idx === 1 ? 110 : 130); + } + + window.setTimeout(nextLine, 140); +})(); + +/* Comfortable line spacing — synced with site-wide comfort toggle (header ↕) */ +(function () { + var KEY = 'siteComfortSpacing_v1'; + var btn = document.getElementById('comfort-toggle'); + var body = document.body; + if (!btn) return; + + function apply(on) { + body.classList.toggle('startpage-comfort', on); + body.classList.toggle('comfort-spacing', on); + // Keep the header icon button compact (do not replace ↕ with long text). + btn.setAttribute('aria-pressed', on ? 'true' : 'false'); + btn.setAttribute('title', on ? 'Comfort spacing: ON' : 'Comfort spacing: OFF'); + btn.setAttribute('aria-label', on ? 'Comfort spacing on' : 'Comfort spacing off'); + } + + // Use comfortable spacing unless this browser has explicitly opted out. + apply(localStorage.getItem(KEY) !== '0'); + + btn.addEventListener('click', function () { + var next = !body.classList.contains('startpage-comfort'); + localStorage.setItem(KEY, next ? '1' : '0'); + apply(next); + }); +})(); + +/* Konaste / BEMANI PC maintenance status — computed client-side from the fixed + e-amusement schedule (shared backend, so both games share windows): + - Daily maintenance 05:00–07:00 JST + - Extended maintenance 02:00–07:00 JST on the 3rd Tuesday of the month + JST is a fixed UTC+9 offset (no DST). Display times use the timezone + configured on the viewer's computer/browser. */ +(function () { + var root = document.getElementById('konaste-status'); + if (!root) return; + + var JST_OFFSET_MS = 9 * 3600 * 1000; + var DISPLAY_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // JST wall-clock fields for a real Date. + function jstFields(date) { + var j = new Date(date.getTime() + JST_OFFSET_MS); + return { + y: j.getUTCFullYear(), + mo: j.getUTCMonth(), + d: j.getUTCDate(), + dow: j.getUTCDay() + }; + } + + // Real epoch ms for a JST wall-clock instant. + function jstToEpoch(y, mo, d, h, m) { + return Date.UTC(y, mo, d, h, m) - JST_OFFSET_MS; + } + + // 3rd Tuesday => Tuesday (dow 2) with day-of-month 15..21. + function isThirdTuesday(f) { + return f.dow === 2 && f.d >= 15 && f.d <= 21; + } + + // Maintenance window [start, end) in real epoch ms for the JST calendar day + // that a given JST-field set falls on. offsetDays shifts that day. + function windowFor(baseFields, offsetDays) { + var day = new Date(Date.UTC(baseFields.y, baseFields.mo, baseFields.d + offsetDays)); + var f = { y: day.getUTCFullYear(), mo: day.getUTCMonth(), d: day.getUTCDate(), dow: day.getUTCDay() }; + var startHour = isThirdTuesday(f) ? 2 : 5; + return { + start: jstToEpoch(f.y, f.mo, f.d, startHour, 0), + end: jstToEpoch(f.y, f.mo, f.d, 7, 0), + extended: isThirdTuesday(f) + }; + } + + function pad2(n) { return n < 10 ? '0' + n : '' + n; } + + function fmtLocal(epoch) { + var d = new Date(epoch); + try { + var formatted = new Intl.DateTimeFormat(navigator.language || undefined, { + weekday: 'short', hour: '2-digit', minute: '2-digit', + timeZone: DISPLAY_TIME_ZONE + }).format(d); + return formatted; + } catch (e) { + return d.toLocaleString(); + } + } + + function fmtCountdown(ms) { + var total = Math.max(0, Math.floor(ms / 60000)); + var h = Math.floor(total / 60); + var m = total % 60; + if (h > 0) return h + 'h ' + m + 'm'; + return m + 'm'; + } + + // Determine status at the current moment. + function compute(now) { + var f = jstFields(now); + var t = now.getTime(); + // Today's window plus the previous day's (guards a window that we might + // still be inside just after JST midnight — none currently cross it, but + // this keeps the logic correct if hours ever change). + var candidates = [windowFor(f, -1), windowFor(f, 0)]; + for (var i = 0; i < candidates.length; i++) { + var w = candidates[i]; + if (t >= w.start && t < w.end) { + return { online: false, window: w, until: w.end }; + } + } + // Online: find the next upcoming window start. + var next = null; + for (var d = 0; d <= 40; d++) { + var w2 = windowFor(f, d); + if (w2.start > t) { next = w2; break; } + } + return { online: true, next: next }; + } + + // Live local clock — hour / minute / second kept in separate fields, ticks in + // real time using the computer's connected timezone. + function renderClock(now) { + var el = document.getElementById('konaste-clock'); + if (!el) return; + var h = el.querySelector('[data-clock="h"]'); + var m = el.querySelector('[data-clock="m"]'); + var s = el.querySelector('[data-clock="s"]'); + var ap = el.querySelector('[data-clock="ap"]'); + var parts = new Intl.DateTimeFormat('en-US', { + timeZone: DISPLAY_TIME_ZONE, + hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true + }).formatToParts(now); + var clockParts = {}; + for (var i = 0; i < parts.length; i++) clockParts[parts[i].type] = parts[i].value; + if (h) h.textContent = clockParts.hour || '--'; + if (m) m.textContent = clockParts.minute || '--'; + if (s) s.textContent = clockParts.second || '--'; + if (ap) ap.textContent = clockParts.dayPeriod || ''; + var tz = root.querySelector('[data-clock="tz"]'); + if (tz) tz.textContent = ''; + } + + // Single shared online/offline line — pop'n Lively and GITADORA use the same + // e-amusement maintenance window, so one status covers both. + function renderStatus(now) { + var state = compute(now); + var row = root.querySelector('.konaste-game'); + if (!row) return; + var dot = row.querySelector('[data-role="dot"]'); + var text = row.querySelector('[data-role="text"]'); + var cd = row.querySelector('[data-role="countdown"]'); + + dot.className = 'konaste-dot'; + cd.textContent = ''; + + if (!state.online) { + dot.classList.add('is-maintenance'); + text.textContent = (state.window.extended ? 'Extended maintenance' : 'Under maintenance') + + '\nStarted: ' + fmtLocal(state.window.start) + + '\nEnds: ' + fmtLocal(state.window.end); + cd.textContent = 'Remaining: ' + fmtCountdown(state.until - now.getTime()); + } else { + dot.classList.add('is-online'); + text.textContent = 'Online • play now'; + if (state.next) { + text.textContent += '\nNext maintenance: ' + fmtLocal(state.next.start) + + ' to ' + fmtLocal(state.next.end); + cd.textContent = 'Starts in: ' + fmtCountdown(state.next.start - now.getTime()); + } + } + } + + function tick() { + var now = new Date(); + renderClock(now); + renderStatus(now); + } + + tick(); + setInterval(tick, 1000); + document.addEventListener('visibilitychange', function () { + if (!document.hidden) tick(); + }); +})(); diff --git a/startpage/tf2.png b/startpage/tf2.png Binary files differnew file mode 100644 index 0000000..bd3794a --- /dev/null +++ b/startpage/tf2.png diff --git a/startpage/tilde.png b/startpage/tilde.png Binary files differnew file mode 100644 index 0000000..20e1184 --- /dev/null +++ b/startpage/tilde.png diff --git a/startpage/videogame.jpg b/startpage/videogame.jpg Binary files differnew file mode 100644 index 0000000..1d9b1d2 --- /dev/null +++ b/startpage/videogame.jpg diff --git a/startpage/videogame.png b/startpage/videogame.png Binary files differnew file mode 100644 index 0000000..c8e3020 --- /dev/null +++ b/startpage/videogame.png diff --git a/startpage/wallpaper.jpg b/startpage/wallpaper.jpg Binary files differnew file mode 100644 index 0000000..d67ac74 --- /dev/null +++ b/startpage/wallpaper.jpg |
