style(website): solid hairlines, Bricolage Grotesque display face, drop TV effects (#1539)

* style(website): solid hairlines, Bricolage Grotesque display face, drop TV effects

First of three landing redesign PRs. This one only touches tokens and
surface treatment so every page (home, blog, /features, /compare,
/download) benefits without any structural change:

- Replace every dashed border with a solid hairline; `.card-dotted`
  becomes `.card-panel` (bg step + hairline, lighter hairline on hover)
  and the section divider is solid `surface-800`.
- Swap the display face from Playfair Display to Bricolage Grotesque
  (DM Sans body and IBM Plex Mono unchanged). The italic accent word
  that every heading repeated is gone; only page-level h1s keep the
  accent, and as colour rather than italic.
- Remove the CRT/TV effects: the `.btn-tv` block and its nine keyframes,
  the scanline overlay on the hero screenshot, the CRT hover overlay and
  teal glow shadows on feature cards, the pulsing mascot, and the unused
  scan/tv-* Tailwind animations.
- The hero download button and the header Download link are now solid
  accent buttons; the self-host link is a plain text link.
- Blog tag chips use neutral surface borders so teal stays reserved for
  actions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* style(website): finish the dashed sweep in prose and package rows

The blanket `border-dashed ` removal ate the utility but not the
variant prefix in the blog prose classes, leaving
`prose-pre:prose-pre:...` and `prose-hr:prose-hr:...`; and the
Linux package rows still used `divide-dashed`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 authored and GitHub committed 2026-09-05 12:11:56 +02:00
1 parent 07ccf45f32
commit 1cf17f7301
49 files changed
+123 -535

No files matched your search

+2 -2
View File
@@ -48,11 +48,11 @@ const schemaPayload = jsonLd
<meta name="format-detection" content="telephone=no" />
{keywords.length > 0 && <meta name="keywords" content={keywords.join(', ')} />}
<!-- Fonts: Playfair Display (display), DM Sans (body), IBM Plex Mono (code) -->
<!-- Fonts: Bricolage Grotesque (display), DM Sans (body), IBM Plex Mono (code) -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400;1,500;1,600;1,700&family=DM+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono:wght@400;500&display=swap"
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700;12..96,800&family=DM+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
@@ -19,7 +19,7 @@ const formattedDate = pubDate.toLocaleDateString('en-US', {
<a
href={`/iptvnator/blog/${slug}/`}
class="card-dotted group block"
class="card-panel group block"
>
{heroImage && (
<div class="overflow-hidden rounded-t-lg -mx-6 -mt-6 mb-5">
@@ -40,7 +40,7 @@ const formattedDate = pubDate.toLocaleDateString('en-US', {
<>
<span class="text-surface-700">&middot;</span>
{tags.map((tag) => (
<span class="rounded-full border border-dashed border-accent-500/30 px-2 py-0.5 text-xs text-accent-400">
<span class="rounded-md border border-surface-800 bg-surface-900/60 px-2 py-0.5 text-xs text-surface-300">
{tag}
</span>
))}
@@ -28,7 +28,7 @@ const packageManagers = [
Get started
</span>
<h2 class="mt-4 font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl lg:text-5xl">
Get <em class="font-medium text-accent-400" style="font-style: italic;">IPTVnator</em>
Get IPTVnator
</h2>
<p class="mt-4 text-lg text-surface-400">
Download for your platform or use it directly in the browser.
@@ -41,7 +41,7 @@ const packageManagers = [
platforms.map((platform) => (
<a
href={platform.href}
class="card-dotted group flex items-center gap-4 px-8 py-5"
class="card-panel group flex items-center gap-4 px-8 py-5"
>
<svg class="h-7 w-7 text-surface-500 transition-colors duration-200 group-hover:text-accent-400" fill="currentColor" viewBox="0 0 24 24">
<path d={platform.icon} />
@@ -59,7 +59,7 @@ const packageManagers = [
<div class="mx-auto mt-10 max-w-xl space-y-3">
{
packageManagers.map((pm) => (
<div class="flex items-center gap-4 rounded-lg border border-dashed border-surface-700/60 bg-surface-900/50 px-5 py-3.5">
<div class="flex items-center gap-4 rounded-lg border border-surface-700/60 bg-surface-900/50 px-5 py-3.5">
<span class="w-20 shrink-0 font-mono text-[10px] font-medium uppercase tracking-[0.15em] text-surface-500">
{pm.label}
</span>
+8 -11
View File
@@ -84,7 +84,7 @@ const carouselFeatures: FeatureCard[] = [
Features
</span>
<h2 class="mt-4 font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl lg:text-5xl">
Everything <em class="font-medium text-accent-400" style="font-style: italic;">you need</em>
Everything you need
</h2>
<p class="mt-4 text-lg text-surface-400">
A full-featured IPTV player packed with the tools for the best viewing experience.
@@ -95,7 +95,7 @@ const carouselFeatures: FeatureCard[] = [
<div class="stagger-children mt-16 grid grid-cols-1 gap-5 sm:grid-cols-2" data-stagger>
{
heroFeatures.map((feature, i) => (
<a href={feature.href} class="feature-card group relative block overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-gradient-to-br from-surface-900/80 via-surface-950/60 to-surface-900/40 p-7 transition-all duration-500 hover:border-accent-500/40 hover:shadow-[0_0_30px_rgba(32,168,168,0.08)]">
<a href={feature.href} class="feature-card group relative block overflow-hidden rounded-lg border border-surface-600/60 bg-gradient-to-br from-surface-900/80 via-surface-950/60 to-surface-900/40 p-7 transition-all duration-500 hover:border-accent-500/40">
<!-- Background watermark icon — large, rotated, partially clipped -->
<div class={`feature-watermark pointer-events-none absolute -bottom-6 -right-4 opacity-[0.04] transition-all duration-700 ease-out group-hover:opacity-[0.1] ${i % 2 === 0 ? 'watermark-left' : 'watermark-right'}`}>
@@ -106,9 +106,6 @@ const carouselFeatures: FeatureCard[] = [
<!-- Watermark gradient mask — fades icon into card bg from top-left -->
<div class="pointer-events-none absolute inset-0 bg-gradient-to-br from-surface-900/95 via-surface-950/40 to-transparent" />
<!-- CRT scanline overlay on hover -->
<div class="pointer-events-none absolute inset-0 z-10 opacity-0 transition-opacity duration-500 group-hover:opacity-100" style="background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(32,168,168,0.015) 2px, rgba(32,168,168,0.015) 4px);" />
<!-- Card content -->
<div class="relative z-[1]">
<!-- Mono label + accent line -->
@@ -148,10 +145,10 @@ const carouselFeatures: FeatureCard[] = [
<!-- First set of cards -->
{
carouselFeatures.map((feature, i) => (
<a href={feature.href ?? '/iptvnator/features/'} class="group relative block w-64 shrink-0 overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-gradient-to-b from-surface-900/60 to-surface-950/40 p-5 transition-all duration-500 hover:border-accent-500/40 hover:shadow-[0_0_24px_rgba(32,168,168,0.06)]">
<a href={feature.href ?? '/iptvnator/features/'} class="group relative block w-64 shrink-0 overflow-hidden rounded-lg border border-surface-600/60 bg-gradient-to-b from-surface-900/60 to-surface-950/40 p-5 transition-all duration-500 hover:border-accent-500/40">
<div class="relative mb-3.5 inline-block">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-500 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35 group-hover:shadow-[0_0_14px_rgba(32,168,168,0.12)]">
<svg class="h-5 w-5 text-accent-400 transition-all duration-500 group-hover:text-accent-300 group-hover:drop-shadow-[0_0_4px_rgba(32,168,168,0.4)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-500 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35">
<svg class="h-5 w-5 text-accent-400 transition-all duration-500 group-hover:text-accent-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<Fragment set:html={feature.icon} />
</svg>
</div>
@@ -167,10 +164,10 @@ const carouselFeatures: FeatureCard[] = [
<!-- Duplicate set for seamless loop -->
{
carouselFeatures.map((feature, i) => (
<div class="group relative w-64 shrink-0 overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-gradient-to-b from-surface-900/60 to-surface-950/40 p-5 transition-all duration-500 hover:border-accent-500/40 hover:shadow-[0_0_24px_rgba(32,168,168,0.06)]" aria-hidden="true">
<div class="group relative w-64 shrink-0 overflow-hidden rounded-lg border border-surface-600/60 bg-gradient-to-b from-surface-900/60 to-surface-950/40 p-5 transition-all duration-500 hover:border-accent-500/40" aria-hidden="true">
<div class="relative mb-3.5 inline-block">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-500 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35 group-hover:shadow-[0_0_14px_rgba(32,168,168,0.12)]">
<svg class="h-5 w-5 text-accent-400 transition-all duration-500 group-hover:text-accent-300 group-hover:drop-shadow-[0_0_4px_rgba(32,168,168,0.4)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-500 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35">
<svg class="h-5 w-5 text-accent-400 transition-all duration-500 group-hover:text-accent-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<Fragment set:html={feature.icon} />
</svg>
</div>
@@ -19,7 +19,7 @@ const formattedDate = pubDate.toLocaleDateString('en-US', {
<a
href={`/iptvnator/blog/${slug}/`}
class="card-dotted group block overflow-hidden"
class="card-panel group block overflow-hidden"
>
<div class="grid items-center gap-8 md:grid-cols-2">
<!-- Thumbnail -->
@@ -55,7 +55,7 @@ const formattedDate = pubDate.toLocaleDateString('en-US', {
{tags.length > 0 && (
<div class="mt-2 flex flex-wrap gap-1.5">
{tags.map((tag) => (
<span class="rounded-full border border-dashed border-accent-500/30 px-2 py-0.5 text-xs text-accent-400">
<span class="rounded-md border border-surface-800 bg-surface-900/60 px-2 py-0.5 text-xs text-surface-300">
{tag}
</span>
))}
+2 -2
View File
@@ -25,7 +25,7 @@ const socialLinks = [
];
---
<footer class="relative overflow-hidden border-t border-dashed border-surface-700/60">
<footer class="relative overflow-hidden border-t border-surface-700/60">
<div class="pointer-events-none absolute inset-0">
<div class="absolute -bottom-32 -right-16 h-[28rem] w-[28rem] rounded-full bg-accent-500/[0.07] blur-3xl"></div>
<div class="absolute -bottom-24 right-2 h-[23rem] w-[23rem]">
@@ -76,7 +76,7 @@ const socialLinks = [
</div>
</div>
<div class="mt-10 flex flex-col items-center justify-between gap-4 border-t border-dashed border-surface-700/60 pt-8 md:flex-row">
<div class="mt-10 flex flex-col items-center justify-between gap-4 border-t border-surface-700/60 pt-8 md:flex-row">
<p class="font-mono text-[11px] text-surface-600">
MIT License. Built with <a href="https://astro.build" class="text-surface-500 transition-colors hover:text-accent-400">Astro</a>.
</p>
@@ -1,4 +1,4 @@
<section class="mt-16 border-t border-dashed border-surface-700/60 pt-10" aria-labelledby="blog-comments-heading">
<section class="mt-16 border-t border-surface-700/60 pt-10" aria-labelledby="blog-comments-heading">
<div class="mb-6">
<h2 id="blog-comments-heading" class="font-display text-2xl font-semibold tracking-tight text-surface-100">
Comments
+3 -3
View File
@@ -56,7 +56,7 @@ const navLinks = [
</a>
<a
href="https://github.com/4gray/iptvnator/releases/latest"
class="rounded-lg border border-accent-500/30 bg-accent-500/10 px-4 py-2 text-[13px] font-medium text-accent-400 transition-all duration-200 hover:border-accent-400/50 hover:bg-accent-500/20 hover:text-accent-300"
class="rounded-lg bg-accent-500 px-4 py-2 text-[13px] font-semibold text-surface-950 transition-colors duration-200 hover:bg-accent-400"
>
Download
</a>
@@ -80,7 +80,7 @@ const navLinks = [
<!-- Mobile menu -->
<div
id="mobile-menu"
class="hidden border-t border-dashed border-surface-700/60 bg-surface-950/98 backdrop-blur-2xl md:hidden"
class="hidden border-t border-surface-700/60 bg-surface-950/98 backdrop-blur-2xl md:hidden"
>
<div class="space-y-1 px-6 py-5">
{
@@ -107,7 +107,7 @@ const navLinks = [
</a>
<a
href="https://github.com/4gray/iptvnator/releases/latest"
class="flex-1 rounded-lg border border-accent-500/30 bg-accent-500/10 px-3 py-2.5 text-center text-[13px] font-medium text-accent-400 transition-all hover:border-accent-400/50 hover:bg-accent-500/20"
class="flex-1 rounded-lg bg-accent-500 px-3 py-2.5 text-center text-[13px] font-semibold text-surface-950 transition-colors hover:bg-accent-400"
>
Download
</a>
+15 -36
View File
@@ -40,14 +40,14 @@ const heroSocialLinks = [
</div>
<div class="relative mx-auto max-w-6xl px-6">
<div class="pointer-events-none absolute -top-4 -right-12 z-0 hidden w-[18rem] opacity-75 blur-[0.2px] md:block lg:w-[22rem] xl:w-[25rem]">
<div class="pointer-events-none absolute -top-4 -right-12 z-0 hidden w-[18rem] opacity-75 md:block lg:w-[22rem] xl:w-[25rem]">
<div class="absolute inset-0 rounded-full bg-accent-500/[0.16] blur-3xl"></div>
<Image
src={mascotImg}
alt=""
aria-hidden="true"
width={520}
class="relative w-full object-contain saturate-110 animate-glow-pulse"
class="relative w-full object-contain saturate-110"
/>
</div>
@@ -60,9 +60,9 @@ const heroSocialLinks = [
<span class="h-px w-8 bg-accent-500/50"></span>
</div>
<!-- Headline with serif italic -->
<!-- Headline: the one place the accent colour is allowed inside a heading -->
<h1 class="font-display text-5xl font-bold tracking-tight text-surface-50 sm:text-7xl lg:text-8xl">
Watch, <em class="font-medium not-italic text-accent-400" style="font-style: italic;">your way.</em>
Watch, <span class="text-accent-400">your way.</span>
</h1>
<p class="mx-auto mt-8 max-w-xl text-lg leading-relaxed text-surface-400">
@@ -70,42 +70,25 @@ const heroSocialLinks = [
Stalker portals. Desktop app and Progressive Web App.
</p>
<!-- CTA: Big TV-effect download button -->
<div class="mt-12 flex flex-col items-center gap-6">
<!-- CTA -->
<div class="mt-12 flex flex-col items-center gap-5">
<a
href="https://github.com/4gray/iptvnator/releases/latest"
class="btn-tv group rounded-2xl px-10 py-6 sm:px-14 sm:py-7"
class="group inline-flex items-center gap-3 rounded-xl bg-accent-500 px-8 py-4 text-base font-semibold text-surface-950 transition-colors duration-200 hover:bg-accent-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-300 sm:px-10 sm:py-5 sm:text-lg"
>
<!-- TV effect layers -->
<span class="tv-scanline"></span>
<span class="tv-noise"></span>
<span class="tv-vignette"></span>
<!-- Button content -->
<span class="relative z-10 flex items-center gap-4">
<svg class="h-6 w-6 text-accent-400 transition-transform duration-300 group-hover:translate-y-0.5 sm:h-7 sm:w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
<span class="flex flex-col items-start">
<span class="btn-tv-text font-display text-2xl font-bold tracking-tight text-surface-50 sm:text-3xl">
Download <em class="font-medium text-accent-400" style="font-style: italic;">for Desktop</em>
</span>
<span class="font-mono text-[10px] uppercase tracking-[0.2em] text-surface-500">
macOS · Windows · Linux
</span>
</span>
</span>
<svg class="h-5 w-5 transition-transform duration-300 group-hover:translate-y-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-5-5m5 5l5-5M4 20h16" />
</svg>
Download for desktop
<span class="hidden font-mono text-[11px] font-medium uppercase tracking-[0.15em] text-surface-950/60 sm:inline">macOS · Windows · Linux</span>
</a>
<!-- Secondary: self-hosted web app link -->
<a
href="https://github.com/4gray/iptvnator/blob/master/docker/README.md"
class="group inline-flex items-center gap-2.5 rounded-lg border border-dashed border-surface-600/60 px-7 py-3.5 text-sm font-medium text-surface-400 transition-all duration-300 hover:border-accent-500/30 hover:text-accent-400"
class="group inline-flex items-center gap-2 text-sm font-medium text-surface-400 transition-colors duration-200 hover:text-surface-100"
>
<svg class="h-4 w-4 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
Or self-host the Web App
Or self-host the web app
<svg class="h-3.5 w-3.5 transition-transform duration-200 group-hover:translate-x-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
@@ -140,7 +123,7 @@ const heroSocialLinks = [
href={link.href}
target="_blank"
rel="noopener noreferrer"
class="group inline-flex items-center gap-2 rounded-full border border-dashed border-surface-700/70 bg-surface-900/40 px-3 py-1.5 text-[11px] font-mono uppercase tracking-[0.08em] text-surface-400 transition-all duration-300 hover:border-accent-500/40 hover:bg-accent-500/[0.06] hover:text-accent-300"
class="group inline-flex items-center gap-2 rounded-full border border-surface-700/70 bg-surface-900/40 px-3 py-1.5 text-[11px] font-mono uppercase tracking-[0.08em] text-surface-400 transition-all duration-300 hover:border-accent-500/40 hover:bg-accent-500/[0.06] hover:text-accent-300"
aria-label={link.label}
title={link.label}
>
@@ -160,10 +143,6 @@ const heroSocialLinks = [
<!-- Framed screenshot -->
<div class="relative overflow-hidden rounded-xl border border-surface-700/60 bg-surface-900 shadow-2xl shadow-black/40">
<!-- Scanline overlay -->
<div class="pointer-events-none absolute inset-0 z-10 overflow-hidden opacity-[0.03]">
<div class="h-full w-full animate-scan" style="background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.5) 2px, rgba(255,255,255,0.5) 4px);"></div>
</div>
<img
src="/iptvnator/screenshots/screenshot-player.webp"
alt="IPTVnator desktop app: channel list, live player, and EPG guide in the dark theme"
@@ -53,7 +53,7 @@ const tabs = [
Interface
</span>
<h2 class="mt-4 font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl lg:text-5xl">
See it <em class="font-medium text-accent-400" style="font-style: italic;">in action</em>
See it in action
</h2>
<p class="mt-4 text-lg text-surface-400">
A clean, intuitive interface designed for the best viewing experience.
@@ -62,7 +62,7 @@ const tabs = [
<div class="mt-14">
<!-- Tab buttons with underline style -->
<div class="flex flex-wrap justify-center gap-1 rounded-lg border border-dashed border-surface-700/60 bg-surface-900/50 p-1.5" role="tablist">
<div class="flex flex-wrap justify-center gap-1 rounded-lg border border-surface-700/60 bg-surface-900/50 p-1.5" role="tablist">
{
tabs.map((tab, i) => (
<button
@@ -99,7 +99,7 @@ const tabs = [
<!-- Window chrome -->
<div class="relative overflow-hidden rounded-xl border border-surface-700/60 bg-surface-900 shadow-2xl shadow-black/30">
<div class="flex items-center gap-2 border-b border-dashed border-surface-700/60 px-4 py-2.5">
<div class="flex items-center gap-2 border-b border-surface-700/60 px-4 py-2.5">
<div class="flex gap-1.5">
<span class="h-2.5 w-2.5 rounded-full bg-red-500/70" />
<span class="h-2.5 w-2.5 rounded-full bg-yellow-500/70" />
@@ -28,7 +28,7 @@ const supportLinks = [
<div class="relative mx-auto max-w-6xl px-6">
<div class="grid gap-6 lg:grid-cols-[1.15fr_0.85fr]">
<div class="card-dotted relative overflow-hidden rounded-2xl bg-surface-900/35">
<div class="card-panel relative overflow-hidden rounded-2xl bg-surface-900/35">
<div class="pointer-events-none absolute -top-12 -right-14 hidden h-52 w-52 opacity-35 mix-blend-screen md:block">
<img
src="/iptvnator/illustrations/support-open-source-signal.svg"
@@ -41,7 +41,7 @@ const supportLinks = [
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Support</span>
<h2 class="mt-4 font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl">
IPTVnator stays <em class="font-medium text-accent-400" style="font-style: italic;">open source</em>
IPTVnator stays open source
</h2>
<p class="mt-5 max-w-2xl text-base leading-relaxed text-surface-300 md:pr-24">
IPTVnator was built as an open-source project and will remain one. If it helps you, your support keeps the
@@ -54,7 +54,7 @@ const supportLinks = [
href={item.href}
target="_blank"
rel="noopener noreferrer"
class="group flex h-full flex-col rounded-xl border border-dashed border-surface-700/70 bg-surface-950/50 p-4 transition-all duration-300 hover:border-accent-500/40 hover:bg-accent-500/[0.06]"
class="group flex h-full flex-col rounded-xl border border-surface-700/70 bg-surface-950/50 p-4 transition-all duration-300 hover:border-accent-500/40 hover:bg-accent-500/[0.06]"
>
<div class="flex min-h-[3rem] items-center gap-3 text-surface-200">
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-accent-500/25 bg-accent-500/10">
@@ -70,7 +70,7 @@ const supportLinks = [
</div>
</div>
<aside class="card-dotted rounded-2xl bg-surface-900/25">
<aside class="card-panel rounded-2xl bg-surface-900/25">
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Disclaimer</span>
<p class="mt-4 text-sm leading-relaxed text-surface-300">
IPTVnator is a media player application. It does not provide, host, or sell any digital media content,
+1 -1
View File
@@ -27,7 +27,7 @@ const styles = {
const variant = styles[type];
---
<div class={`not-prose my-6 rounded-xl border border-dashed p-4 sm:p-5 ${variant.wrapper}`}>
<div class={`not-prose my-6 rounded-xl border p-4 sm:p-5 ${variant.wrapper}`}>
<div class="flex items-start gap-3">
<span class={`mt-0.5 ${variant.icon}`} aria-hidden="true">
<svg class="h-4.5 w-4.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
@@ -21,7 +21,7 @@ const sliderId = `slider-${Math.random().toString(36).slice(2, 10)}`;
data-blog-slider={sliderId}
tabindex="0"
>
<div class="group relative overflow-hidden rounded-xl border border-dashed border-surface-700/70 bg-surface-900/50">
<div class="group relative overflow-hidden rounded-xl border border-surface-700/70 bg-surface-900/50">
<img
src={images[0].src}
alt={images[0].alt}
@@ -12,10 +12,10 @@ interface Props {
const { rows = [] } = Astro.props;
---
<div class="not-prose my-6 overflow-x-auto rounded-xl border border-dashed border-surface-700/70 bg-surface-900/50">
<div class="not-prose my-6 overflow-x-auto rounded-xl border border-surface-700/70 bg-surface-900/50">
<table class="w-full min-w-[560px] border-collapse text-left">
<thead>
<tr class="border-b border-dashed border-surface-700/60">
<tr class="border-b border-surface-700/60">
<th class="px-4 py-3 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">Area</th>
<th class="px-4 py-3 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">Change</th>
<th class="px-4 py-3 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">Impact</th>
@@ -23,7 +23,7 @@ const { rows = [] } = Astro.props;
</thead>
<tbody>
{rows.map((row) => (
<tr class="border-b border-dashed border-surface-700/40 last:border-b-0">
<tr class="border-b border-surface-700/40 last:border-b-0">
<td class="px-4 py-3 text-sm font-semibold text-surface-200">{row.area}</td>
<td class="px-4 py-3 text-sm text-surface-300">{row.change}</td>
<td class="px-4 py-3 text-sm text-surface-400">{row.impact ?? '—'}</td>
@@ -7,10 +7,10 @@ interface Props {
const { command, title = 'Command' } = Astro.props;
---
<div class="not-prose my-6 rounded-xl border border-dashed border-surface-700/70 bg-surface-900/55 p-3">
<div class="not-prose my-6 rounded-xl border border-surface-700/70 bg-surface-900/55 p-3">
<div class="mb-2 flex items-center justify-between">
<span class="font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">{title}</span>
<button class="copy-command-btn rounded-md border border-dashed border-surface-700/70 px-2 py-1 font-mono text-[10px] uppercase tracking-[0.08em] text-surface-400 transition-colors hover:text-accent-300" data-command={command}>
<button class="copy-command-btn rounded-md border border-surface-700/70 px-2 py-1 font-mono text-[10px] uppercase tracking-[0.08em] text-surface-400 transition-colors hover:text-accent-300" data-command={command}>
Copy
</button>
</div>
@@ -13,14 +13,14 @@ const { items = [] } = Astro.props;
<div class="not-prose my-6 space-y-2">
{items.map((item) => (
<details class="group rounded-xl border border-dashed border-surface-700/70 bg-surface-900/45 px-4 py-3">
<details class="group rounded-xl border border-surface-700/70 bg-surface-900/45 px-4 py-3">
<summary class="cursor-pointer list-none">
<span class="flex items-center justify-between gap-3 text-sm font-semibold text-surface-100">
<span>{item.q}</span>
<span class="text-surface-500 transition-transform duration-200 group-open:rotate-45">+</span>
</span>
</summary>
<p class="mt-2 border-t border-dashed border-surface-700/60 pt-2 text-sm leading-relaxed text-surface-400">{item.a}</p>
<p class="mt-2 border-t border-surface-700/60 pt-2 text-sm leading-relaxed text-surface-400">{item.a}</p>
</details>
))}
</div>
@@ -22,7 +22,7 @@ const iconPaths = {
<div class="not-prose my-6 grid gap-3 sm:grid-cols-2">
{items.map((item) => (
<div class="rounded-xl border border-dashed border-surface-700/70 bg-surface-900/45 p-4">
<div class="rounded-xl border border-surface-700/70 bg-surface-900/45 p-4">
<div class="mb-2.5 flex items-center gap-2.5">
<span class="flex h-7 w-7 items-center justify-center rounded-md border border-accent-500/25 bg-accent-500/[0.1]">
<svg class="h-3.5 w-3.5 text-accent-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.7">
@@ -22,7 +22,7 @@ const colClass = {
<div class="not-prose my-6">
<div class={`grid gap-3 ${colClass[columns]}`}>
{images.map((img, idx) => (
<figure class="group overflow-hidden rounded-xl border border-dashed border-surface-700/70 bg-surface-900/45">
<figure class="group overflow-hidden rounded-xl border border-surface-700/70 bg-surface-900/45">
<button
type="button"
class="gallery-open block w-full"
@@ -31,7 +31,7 @@ const iconPaths = {
href={item.href}
target={item.href.startsWith('/') ? undefined : '_blank'}
rel={item.href.startsWith('/') ? undefined : 'noopener noreferrer'}
class="group rounded-xl border border-dashed border-surface-700/70 bg-surface-900/50 p-4 transition-all duration-300 hover:border-accent-500/40 hover:bg-accent-500/[0.06]"
class="group rounded-xl border border-surface-700/70 bg-surface-900/50 p-4 transition-all duration-300 hover:border-accent-500/40 hover:bg-accent-500/[0.06]"
>
<div class="flex items-center gap-2.5">
<span class="flex h-7 w-7 items-center justify-center rounded-md border border-accent-500/25 bg-accent-500/[0.09]">
@@ -12,7 +12,7 @@ const { href, label, external = true } = Astro.props;
href={href}
target={external ? '_blank' : undefined}
rel={external ? 'noopener noreferrer' : undefined}
class="not-prose group my-6 inline-flex items-center gap-2 rounded-lg border border-dashed border-accent-500/35 bg-accent-500/[0.08] px-4 py-2.5 text-sm font-medium text-accent-200 transition-all duration-200 hover:border-accent-400/50 hover:bg-accent-500/[0.14] hover:text-accent-100"
class="not-prose group my-6 inline-flex items-center gap-2 rounded-lg border border-accent-500/35 bg-accent-500/[0.08] px-4 py-2.5 text-sm font-medium text-accent-200 transition-all duration-200 hover:border-accent-400/50 hover:bg-accent-500/[0.14] hover:text-accent-100"
>
<span>{label ?? 'Open link'}</span>
<svg class="h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
@@ -8,7 +8,7 @@ interface Props {
const { version, releaseDate, channels = [] } = Astro.props;
---
<div class="not-prose my-6 rounded-xl border border-dashed border-surface-700/70 bg-surface-900/50 p-4 sm:p-5">
<div class="not-prose my-6 rounded-xl border border-surface-700/70 bg-surface-900/50 p-4 sm:p-5">
<div class="grid gap-3 sm:grid-cols-3">
<div>
<p class="m-0 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-500">Version</p>
@@ -7,7 +7,7 @@ interface Props {
const { title = 'Show spoiler', open = false } = Astro.props;
---
<details class="not-prose my-5 rounded-xl border border-dashed border-surface-700/70 bg-surface-900/50 px-4 py-3" open={open}>
<details class="not-prose my-5 rounded-xl border border-surface-700/70 bg-surface-900/50 px-4 py-3" open={open}>
<summary class="cursor-pointer list-none font-mono text-xs uppercase tracking-[0.1em] text-accent-300">
<span class="inline-flex items-center gap-2">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
@@ -16,7 +16,7 @@ const { title = 'Show spoiler', open = false } = Astro.props;
{title}
</span>
</summary>
<div class="mt-3 border-t border-dashed border-surface-700/60 pt-3 text-sm leading-relaxed text-surface-300 [&_p]:my-2 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 [&_li]:my-1.5 [&_li]:marker:text-surface-600 [&_a]:text-accent-400 [&_a:hover]:underline [&_strong]:font-semibold [&_strong]:text-surface-100 [&_code]:rounded [&_code]:bg-surface-800/50 [&_code]:px-1 [&_code]:font-mono [&_code]:text-accent-300">
<div class="mt-3 border-t border-surface-700/60 pt-3 text-sm leading-relaxed text-surface-300 [&_p]:my-2 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 [&_li]:my-1.5 [&_li]:marker:text-surface-600 [&_a]:text-accent-400 [&_a:hover]:underline [&_strong]:font-semibold [&_strong]:text-surface-100 [&_code]:rounded [&_code]:bg-surface-800/50 [&_code]:px-1 [&_code]:font-mono [&_code]:text-accent-300">
<slot />
</div>
</details>
@@ -15,6 +15,6 @@ const styles = {
} as const;
---
<span class={`not-prose inline-flex items-center rounded-full border border-dashed px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.08em] ${styles[type]}`}>
<span class={`not-prose inline-flex items-center rounded-full border px-2.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.08em] ${styles[type]}`}>
{label ?? type}
</span>
@@ -7,12 +7,12 @@ interface Props {
const { steps = [], title } = Astro.props;
---
<div class="not-prose my-6 rounded-xl border border-dashed border-surface-700/70 bg-surface-900/45 p-4 sm:p-5">
<div class="not-prose my-6 rounded-xl border border-surface-700/70 bg-surface-900/45 p-4 sm:p-5">
{title && <p class="mb-3 font-mono text-[11px] uppercase tracking-[0.12em] text-accent-400">{title}</p>}
<ol class="m-0 list-none space-y-3 p-0">
{steps.map((step, index) => (
<li class="flex items-start gap-3">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-dashed border-accent-500/35 bg-accent-500/[0.1] font-mono text-[11px] font-semibold text-accent-200">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-accent-500/35 bg-accent-500/[0.1] font-mono text-[11px] font-semibold text-accent-200">
{index + 1}
</span>
<p class="m-0 pt-0.5 text-sm leading-relaxed text-surface-200">{step}</p>
@@ -6,8 +6,8 @@ interface Props {
const { title = 'Terminal' } = Astro.props;
---
<div class="not-prose my-6 overflow-hidden rounded-xl border border-dashed border-surface-700/70 bg-surface-900/60">
<div class="flex items-center justify-between border-b border-dashed border-surface-700/60 px-4 py-2.5">
<div class="not-prose my-6 overflow-hidden rounded-xl border border-surface-700/70 bg-surface-900/60">
<div class="flex items-center justify-between border-b border-surface-700/60 px-4 py-2.5">
<div class="flex items-center gap-1.5" aria-hidden="true">
<span class="h-2.5 w-2.5 rounded-full bg-red-500/70"></span>
<span class="h-2.5 w-2.5 rounded-full bg-yellow-500/70"></span>
@@ -32,12 +32,12 @@ const { comparison, titleLead, titleAccent, lede, verdict } = Astro.props;
<h1 class="mt-4 font-display text-4xl font-bold tracking-tight text-surface-50 sm:text-5xl lg:text-6xl">
{titleLead}{' '}
<em class="font-medium text-accent-400" style="font-style: italic;">{titleAccent}</em>
<span class="text-accent-400">{titleAccent}</span>
</h1>
<p class="mt-6 text-lg leading-relaxed text-surface-400">{lede}</p>
<div class="mt-10 rounded-xl border border-dashed border-accent-500/35 bg-accent-500/[0.06] p-6">
<div class="mt-10 rounded-xl border border-accent-500/35 bg-accent-500/[0.06] p-6">
<span class="font-mono text-[10px] uppercase tracking-[0.15em] text-accent-400">Short answer</span>
<p class="mt-2 text-base leading-relaxed text-surface-200">{verdict}</p>
</div>
@@ -19,7 +19,7 @@ const others = COMPARISONS.filter((comparison) => comparison.slug !== current);
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Other comparisons</span>
<h2 class="mt-3 font-display text-2xl font-bold tracking-tight text-surface-50 sm:text-3xl">
Still{' '}
<em class="font-medium text-accent-400" style="font-style: italic;">deciding?</em>
deciding?
</h2>
</div>
<a href={COMPARE_HUB_HREF} class="font-mono text-xs uppercase tracking-[0.1em] text-accent-500 transition-colors hover:text-accent-400">All comparisons</a>
@@ -27,7 +27,7 @@ const others = COMPARISONS.filter((comparison) => comparison.slug !== current);
<div class="mt-10 grid gap-4 sm:grid-cols-2">
{others.map((comparison) => (
<a href={comparison.href} class="card-dotted group flex flex-col gap-3 p-6">
<a href={comparison.href} class="card-panel group flex flex-col gap-3 p-6">
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-300 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35">
<svg class="h-4.5 w-4.5 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d={comparison.icon} />
@@ -24,10 +24,10 @@ const { columns, rowHeader = 'Capability', rows, caption } = Astro.props;
---
<figure class="not-prose">
<div class="overflow-x-auto rounded-lg border border-dashed border-surface-600/60 bg-surface-900/40">
<div class="overflow-x-auto rounded-lg border border-surface-600/60 bg-surface-900/40">
<table class="w-full min-w-[720px] border-collapse text-left">
<thead>
<tr class="border-b border-dashed border-surface-700/60">
<tr class="border-b border-surface-700/60">
<th scope="col" class="px-5 py-4 font-mono text-[10px] uppercase tracking-[0.15em] text-surface-500">{rowHeader}</th>
{columns.map((column) => (
<th scope="col" class="px-5 py-4 font-mono text-[10px] uppercase tracking-[0.15em] text-accent-400">{column}</th>
@@ -36,7 +36,7 @@ const { columns, rowHeader = 'Capability', rows, caption } = Astro.props;
</thead>
<tbody>
{rows.map((row) => (
<tr class="border-b border-dashed border-surface-700/40 last:border-b-0 transition-colors hover:bg-surface-900/60">
<tr class="border-b border-surface-700/40 last:border-b-0 transition-colors hover:bg-surface-900/60">
<th scope="row" class="px-5 py-4 text-left align-top text-sm font-semibold text-surface-100">
{row.label}
{row.note && <span class="mt-1 block text-xs font-normal leading-relaxed text-surface-500">{row.note}</span>}
@@ -14,7 +14,7 @@ const detail = size ? `${download.detail} · ${size}` : download.detail;
const baseClass = 'group inline-flex items-center gap-4 rounded-xl px-6 py-4 transition-all duration-300';
const variantClass = primary
? 'bg-accent-500 text-surface-950 shadow-[0_0_30px_rgba(32,168,168,0.25)] hover:-translate-y-0.5 hover:bg-accent-400 hover:shadow-[0_0_44px_rgba(32,168,168,0.4)]'
: 'border border-dashed border-surface-600/60 bg-surface-900/50 text-surface-100 hover:border-accent-500/40 hover:bg-accent-500/[0.05]';
: 'border border-surface-600/60 bg-surface-900/50 text-surface-100 hover:border-accent-500/40 hover:bg-accent-500/[0.05]';
const detailClass = primary ? 'text-surface-950/70' : 'text-surface-500';
---
@@ -55,7 +55,7 @@ const releaseDate = formatReleaseDate(release.publishedAt);
<h1 class="mt-6 font-display text-4xl font-bold tracking-tight text-surface-50 sm:text-5xl lg:text-6xl">
{titleLead}{' '}
<em class="font-medium text-accent-400" style="font-style: italic;">{titleAccent}</em>
<span class="text-accent-400">{titleAccent}</span>
</h1>
<p class="mt-6 max-w-xl text-lg leading-relaxed text-surface-400">{lede}</p>
@@ -82,14 +82,14 @@ const releaseDate = formatReleaseDate(release.publishedAt);
<ul class="mt-8 flex flex-wrap gap-2" aria-label="Key facts">
{facts.map((fact) => (
<li class="rounded-full border border-dashed border-surface-700/70 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-400">{fact}</li>
<li class="rounded-full border border-surface-700/70 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-400">{fact}</li>
))}
</ul>
</div>
<figure class="relative">
<div class="pointer-events-none absolute -inset-6 rounded-3xl bg-accent-500/[0.05] blur-2xl"></div>
<div class="relative overflow-hidden rounded-xl border border-dashed border-surface-600/60 bg-surface-900/60 p-2 shadow-[0_30px_80px_rgba(0,0,0,0.45)]">
<div class="relative overflow-hidden rounded-xl border border-surface-600/60 bg-surface-900/60 p-2 shadow-[0_30px_80px_rgba(0,0,0,0.45)]">
<img
src={screenshot.src}
alt={screenshot.alt}
@@ -21,7 +21,7 @@ const { id, eyebrow, title, accent, lead } = Astro.props;
{accent && (
<Fragment>
{' '}
<em class="font-medium text-accent-400" style="font-style: italic;">{accent}</em>
{accent}
</Fragment>
)}
</h2>
@@ -13,7 +13,7 @@ const { items } = Astro.props;
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{items.map((item, index) => (
<div class="group relative overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-gradient-to-b from-surface-900/60 to-surface-950/40 p-6 transition-all duration-500 hover:border-accent-500/40 hover:shadow-[0_0_24px_rgba(32,168,168,0.06)]">
<div class="group relative overflow-hidden rounded-lg border border-surface-600/60 bg-gradient-to-b from-surface-900/60 to-surface-950/40 p-6 transition-all duration-500 hover:border-accent-500/40 hover:shadow-[0_0_24px_rgba(32,168,168,0.06)]">
<div class="mb-4 flex items-center gap-3">
<span class="font-mono text-[10px] font-medium uppercase tracking-[0.2em] text-accent-500/70 transition-colors duration-300 group-hover:text-accent-400">
{String(index + 1).padStart(2, '0')}
@@ -2,7 +2,7 @@
import { LATEST_RELEASE_URL } from '../../lib/downloads';
---
<div class="rounded-xl border border-dashed border-amber-500/35 bg-amber-500/[0.07] p-5 sm:p-6">
<div class="rounded-xl border border-amber-500/35 bg-amber-500/[0.07] p-5 sm:p-6">
<div class="flex items-start gap-3">
<span class="mt-0.5 text-amber-300" aria-hidden="true">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
@@ -17,11 +17,11 @@ interface Props {
const { title, rows } = Astro.props;
---
<div class="overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-surface-900/40">
<div class="border-b border-dashed border-surface-700/60 px-5 py-3">
<div class="overflow-hidden rounded-lg border border-surface-600/60 bg-surface-900/40">
<div class="border-b border-surface-700/60 px-5 py-3">
<span class="font-mono text-[11px] uppercase tracking-[0.15em] text-accent-400">{title}</span>
</div>
<ul class="divide-y divide-dashed divide-surface-700/60">
<ul class="divide-y divide-surface-700/60">
{rows.map(({ download, command, note }) => {
const size = formatAssetSize(download.size);
return (
@@ -19,20 +19,20 @@ const others = DOWNLOAD_PLATFORMS.filter((platform) => platform !== current);
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Other platforms</span>
<h2 class="mt-3 font-display text-2xl font-bold tracking-tight text-surface-50 sm:text-3xl">
Also available for{' '}
<em class="font-medium text-accent-400" style="font-style: italic;">{others.map((platform) => PLATFORM_LABELS[platform]).join(' and ')}</em>
{others.map((platform) => PLATFORM_LABELS[platform]).join(' and ')}
</h2>
</div>
<div class="flex flex-wrap gap-3">
{others.map((platform) => (
<a href={PLATFORM_PAGES[platform]} class="card-dotted group flex items-center gap-3 px-5 py-3">
<a href={PLATFORM_PAGES[platform]} class="card-panel group flex items-center gap-3 px-5 py-3">
<svg class="h-5 w-5 text-surface-500 transition-colors duration-200 group-hover:text-accent-400" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d={PLATFORM_ICON_PATHS[platform]} />
</svg>
<span class="text-sm font-semibold text-surface-100">IPTVnator for {PLATFORM_LABELS[platform]}</span>
</a>
))}
<a href="/iptvnator/download/" class="card-dotted flex items-center px-5 py-3 text-sm font-semibold text-surface-300 hover:text-accent-400">
<a href="/iptvnator/download/" class="card-panel flex items-center px-5 py-3 text-sm font-semibold text-surface-300 hover:text-accent-400">
All downloads
</a>
</div>
@@ -11,7 +11,7 @@ interface Props {
const { items } = Astro.props;
---
<dl class="grid gap-px overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-surface-700/40 sm:grid-cols-2">
<dl class="grid gap-px overflow-hidden rounded-lg border border-surface-600/60 bg-surface-700/40 sm:grid-cols-2">
{items.map((item) => (
<div class="bg-surface-950/95 px-5 py-4">
<dt class="font-mono text-[10px] uppercase tracking-[0.15em] text-surface-500">{item.term}</dt>
@@ -48,7 +48,7 @@ const { feature, titleLead, titleAccent, lede, facts, screenshot } = Astro.props
<h1 class="mt-6 font-display text-4xl font-bold tracking-tight text-surface-50 sm:text-5xl lg:text-6xl">
{titleLead}{' '}
<em class="font-medium text-accent-400" style="font-style: italic;">{titleAccent}</em>
<span class="text-accent-400">{titleAccent}</span>
</h1>
<p class="mt-6 max-w-xl text-lg leading-relaxed text-surface-400">{lede}</p>
@@ -65,7 +65,7 @@ const { feature, titleLead, titleAccent, lede, facts, screenshot } = Astro.props
</a>
<a
href={feature.guide.href}
class="inline-flex items-center gap-3 rounded-xl border border-dashed border-surface-600/60 bg-surface-900/50 px-6 py-4 text-base font-semibold text-surface-100 transition-all duration-300 hover:border-accent-500/40 hover:bg-accent-500/[0.05]"
class="inline-flex items-center gap-3 rounded-xl border border-surface-600/60 bg-surface-900/50 px-6 py-4 text-base font-semibold text-surface-100 transition-all duration-300 hover:border-accent-500/40 hover:bg-accent-500/[0.05]"
>
<svg class="h-5 w-5 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.253v13m0-13C10.832 5.483 9.246 5 7.5 5 5.205 5 3.182 5.836 2 7.117v11.41C3.182 17.247 5.205 16.41 7.5 16.41c1.746 0 3.332.484 4.5 1.254m0-11.41C13.168 5.483 14.754 5 16.5 5c2.295 0 4.318.836 5.5 2.117v11.41C20.818 17.247 18.795 16.41 16.5 16.41c-1.746 0-3.332.484-4.5 1.254" />
@@ -76,14 +76,14 @@ const { feature, titleLead, titleAccent, lede, facts, screenshot } = Astro.props
<ul class="mt-8 flex flex-wrap gap-2" aria-label="Key facts">
{facts.map((fact) => (
<li class="rounded-full border border-dashed border-surface-700/70 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-400">{fact}</li>
<li class="rounded-full border border-surface-700/70 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.12em] text-surface-400">{fact}</li>
))}
</ul>
</div>
<figure class="relative">
<div class="pointer-events-none absolute -inset-6 rounded-3xl bg-accent-500/[0.05] blur-2xl"></div>
<div class="relative overflow-hidden rounded-xl border border-dashed border-surface-600/60 bg-surface-900/60 p-2 shadow-[0_30px_80px_rgba(0,0,0,0.45)]">
<div class="relative overflow-hidden rounded-xl border border-surface-600/60 bg-surface-900/60 p-2 shadow-[0_30px_80px_rgba(0,0,0,0.45)]">
<img
src={screenshot.src}
alt={screenshot.alt}
@@ -19,7 +19,7 @@ const others = FEATURES.filter((feature) => feature.slug !== current);
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">More features</span>
<h2 class="mt-3 font-display text-2xl font-bold tracking-tight text-surface-50 sm:text-3xl">
The rest of{' '}
<em class="font-medium text-accent-400" style="font-style: italic;">IPTVnator</em>
IPTVnator
</h2>
</div>
<a href={FEATURES_HUB_HREF} class="font-mono text-xs uppercase tracking-[0.1em] text-accent-500 transition-colors hover:text-accent-400">All features</a>
@@ -27,7 +27,7 @@ const others = FEATURES.filter((feature) => feature.slug !== current);
<div class="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{others.map((feature) => (
<a href={feature.href} class="card-dotted group flex flex-col gap-3 p-5">
<a href={feature.href} class="card-panel group flex flex-col gap-3 p-5">
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-300 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35">
<svg class="h-4.5 w-4.5 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d={feature.icon} />
+3 -3
View File
@@ -119,7 +119,7 @@ const jsonLd = faqSchema ? [blogPostSchema, faqSchema] : blogPostSchema;
<>
<span class="text-surface-700">&middot;</span>
{tags.map((tag) => (
<span class="rounded-full border border-dashed border-accent-500/30 px-2.5 py-0.5 text-xs font-medium text-accent-400">
<span class="rounded-md border border-surface-800 bg-surface-900/60 px-2.5 py-0.5 text-xs font-medium text-surface-300">
{tag}
</span>
))}
@@ -138,7 +138,7 @@ const jsonLd = faqSchema ? [blogPostSchema, faqSchema] : blogPostSchema;
<img
src={AUTHOR_AVATAR_URL}
alt={`${author} avatar`}
class="h-8 w-8 rounded-full border border-dashed border-accent-500/30 object-cover"
class="h-8 w-8 rounded-full border border-surface-700 object-cover"
loading="lazy"
/>
<div>
@@ -168,7 +168,7 @@ const jsonLd = faqSchema ? [blogPostSchema, faqSchema] : blogPostSchema;
<!-- Content -->
<div
class="prose prose-invert mt-12 max-w-none prose-headings:font-display prose-headings:font-semibold prose-headings:tracking-tight prose-headings:text-surface-100 prose-p:text-surface-300 prose-a:text-accent-400 prose-a:no-underline prose-a:decoration-accent-500/30 hover:prose-a:underline prose-strong:text-surface-100 prose-code:rounded prose-code:bg-surface-800/50 prose-code:px-1.5 prose-code:py-0.5 prose-code:font-mono prose-code:text-accent-300 prose-code:before:content-[''] prose-code:after:content-[''] prose-pre:border prose-pre:border-dashed prose-pre:border-surface-700/60 prose-pre:bg-surface-900/50 prose-blockquote:border-accent-500/30 prose-blockquote:text-surface-400 prose-hr:border-dashed prose-hr:border-surface-700/60"
class="prose prose-invert mt-12 max-w-none prose-headings:font-display prose-headings:font-semibold prose-headings:tracking-tight prose-headings:text-surface-100 prose-p:text-surface-300 prose-a:text-accent-400 prose-a:no-underline prose-a:decoration-accent-500/30 hover:prose-a:underline prose-strong:text-surface-100 prose-code:rounded prose-code:bg-surface-800/50 prose-code:px-1.5 prose-code:py-0.5 prose-code:font-mono prose-code:text-accent-300 prose-code:before:content-[''] prose-code:after:content-[''] prose-pre:border prose-pre:border-surface-700/60 prose-pre:bg-surface-900/50 prose-blockquote:border-accent-500/30 prose-blockquote:text-surface-400 prose-hr:border-surface-700/60"
>
<slot />
</div>
+4 -4
View File
@@ -48,7 +48,7 @@ const jsonLd = [
<div class="mx-auto max-w-2xl text-center">
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Compare</span>
<h1 class="mt-4 font-display text-4xl font-bold tracking-tight text-surface-50 sm:text-5xl lg:text-6xl">
Which option is <em class="font-medium text-accent-400" style="font-style: italic;">right for you</em>
Which option is <span class="text-accent-400">right for you</span>
</h1>
<p class="mt-6 text-lg leading-relaxed text-surface-400">
IPTVnator asks you to choose a few times: how to connect your provider, which engine plays a stream, and which
@@ -58,7 +58,7 @@ const jsonLd = [
<div class="mt-14 grid gap-5 md:grid-cols-3">
{COMPARISONS.map((comparison) => (
<a href={comparison.href} class="card-dotted group flex flex-col p-7">
<a href={comparison.href} class="card-panel group flex flex-col p-7">
<span class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-300 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35">
<svg class="h-5 w-5 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d={comparison.icon} />
@@ -84,13 +84,13 @@ const jsonLd = [
<div class="max-w-2xl">
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Already decided?</span>
<h2 class="mt-4 font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl">
Read about a <em class="font-medium text-accent-400" style="font-style: italic;">single feature</em>
Read about a single feature
</h2>
<p class="mt-4 text-lg text-surface-400">Each feature page goes into detail and links to its setup guide.</p>
</div>
<div class="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{FEATURES.map((feature) => (
<a href={feature.href} class="card-dotted group flex items-center gap-3 p-5">
<a href={feature.href} class="card-panel group flex items-center gap-3 p-5">
<span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20">
<svg class="h-4.5 w-4.5 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d={feature.icon} />
+3 -3
View File
@@ -74,7 +74,7 @@ const jsonLd = [
<div class="mx-auto max-w-2xl text-center">
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Download</span>
<h1 class="mt-4 font-display text-4xl font-bold tracking-tight text-surface-50 sm:text-5xl lg:text-6xl">
Get <em class="font-medium text-accent-400" style="font-style: italic;">IPTVnator</em>
Get <span class="text-accent-400">IPTVnator</span>
</h1>
<p class="mt-6 text-lg leading-relaxed text-surface-400">
Pick your operating system for a direct download, install steps and the answers to the usual questions.
@@ -90,7 +90,7 @@ const jsonLd = [
<div class="mt-14 grid gap-5 md:grid-cols-3">
{cards.map((card) => (
<a href={card.href} class="card-dotted group flex flex-col p-7">
<a href={card.href} class="card-panel group flex flex-col p-7">
<div class="flex items-center gap-3">
<span class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-300 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35">
<svg class="h-5 w-5 text-accent-400" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
@@ -125,7 +125,7 @@ const jsonLd = [
<div>
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Package managers</span>
<h2 class="mt-4 font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl">
One command <em class="font-medium text-accent-400" style="font-style: italic;">instead</em>
One command instead
</h2>
<p class="mt-4 text-lg text-surface-400">
Homebrew, the Snap Store and the AUR track every release. The self-hosted browser version runs from a
@@ -205,7 +205,7 @@ const jsonLd = buildDownloadPageSchema({
It is opt-in and can be switched off again at any time.
</p>
</div>
<figure class="overflow-hidden rounded-xl border border-dashed border-surface-600/60 bg-surface-900/60 p-2">
<figure class="overflow-hidden rounded-xl border border-surface-600/60 bg-surface-900/60 p-2">
<img
src="/iptvnator/screenshots/external-player-support-mpv.webp"
alt="Playback settings with MPV selected as the external player"
+1 -1
View File
@@ -132,7 +132,7 @@ const jsonLd = buildFeaturePageSchema({
<DownloadSection id="sources" eyebrow="Sources" title="Where the guide" accent="comes from">
<div class="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] lg:items-start">
<SpecList items={sources} />
<figure class="overflow-hidden rounded-xl border border-dashed border-surface-600/60 bg-surface-900/60 p-2">
<figure class="overflow-hidden rounded-xl border border-surface-600/60 bg-surface-900/60 p-2">
<img
src="/iptvnator/blog/guides/screenshots/guide-epg-settings-dark.png"
alt="Settings with the EPG section open, a source URL entered and its status chip"
+5 -5
View File
@@ -61,7 +61,7 @@ const jsonLd = [
<div class="mx-auto max-w-2xl text-center">
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Features</span>
<h1 class="mt-4 font-display text-4xl font-bold tracking-tight text-surface-50 sm:text-5xl lg:text-6xl">
Everything <em class="font-medium text-accent-400" style="font-style: italic;">IPTVnator</em> does
Everything <span class="text-accent-400">IPTVnator</span> does
</h1>
<p class="mt-6 text-lg leading-relaxed text-surface-400">
One player for the three ways providers hand out access, with a program guide, a phone remote and the
@@ -77,7 +77,7 @@ const jsonLd = [
<div class="mt-14 grid gap-5 md:grid-cols-2 lg:grid-cols-3">
{FEATURES.map((feature) => (
<a href={feature.href} class="card-dotted group flex flex-col p-7">
<a href={feature.href} class="card-panel group flex flex-col p-7">
<span class="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-500/[0.08] ring-1 ring-accent-500/20 transition-all duration-300 group-hover:bg-accent-500/[0.14] group-hover:ring-accent-500/35">
<svg class="h-5 w-5 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d={feature.icon} />
@@ -93,7 +93,7 @@ const jsonLd = [
</span>
</a>
))}
<a href="/iptvnator/download/" class="group flex flex-col justify-between rounded-lg border border-dashed border-accent-500/40 bg-accent-500/[0.06] p-7 transition-all duration-300 hover:bg-accent-500/[0.1]">
<a href="/iptvnator/download/" class="group flex flex-col justify-between rounded-lg border border-accent-500/40 bg-accent-500/[0.06] p-7 transition-all duration-300 hover:bg-accent-500/[0.1]">
<div>
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">Get started</span>
<h2 class="mt-4 font-display text-xl font-bold tracking-tight text-surface-50">Download for Windows, macOS or Linux</h2>
@@ -116,10 +116,10 @@ const jsonLd = [
<div class="max-w-2xl">
<span class="font-mono text-xs uppercase tracking-[0.2em] text-accent-500">And more</span>
<h2 class="mt-4 font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl">
The rest of the <em class="font-medium text-accent-400" style="font-style: italic;">desktop app</em>
The rest of the desktop app
</h2>
</div>
<dl class="mt-12 grid gap-px overflow-hidden rounded-lg border border-dashed border-surface-600/60 bg-surface-700/40 sm:grid-cols-2">
<dl class="mt-12 grid gap-px overflow-hidden rounded-lg border border-surface-600/60 bg-surface-700/40 sm:grid-cols-2">
{more.map((item) => (
<div class="bg-surface-950/95 px-5 py-4">
<dt class="font-display text-base font-semibold text-surface-100">{item.title}</dt>
@@ -157,7 +157,7 @@ const jsonLd = buildFeaturePageSchema({
repairs the stored connection.
</p>
</div>
<figure class="overflow-hidden rounded-xl border border-dashed border-surface-600/60 bg-surface-900/60 p-2">
<figure class="overflow-hidden rounded-xl border border-surface-600/60 bg-surface-900/60 p-2">
<img
src="/iptvnator/blog/guides/screenshots/guide-stalker-add-playlist-dark.png"
alt="Add playlist dialog with the Stalker portal method, a portal URL and a MAC address"
@@ -158,7 +158,7 @@ const jsonLd = buildFeaturePageSchema({
episode by episode for offline viewing.
</p>
</div>
<figure class="overflow-hidden rounded-xl border border-dashed border-surface-600/60 bg-surface-900/60 p-2">
<figure class="overflow-hidden rounded-xl border border-surface-600/60 bg-surface-900/60 p-2">
<img
src="/iptvnator/blog/v0-23/screenshots/xtream-series-season-open-dark.png"
alt="Series details with season tabs and the episode list of a season"
+1 -1
View File
@@ -81,7 +81,7 @@ const homeSchema = [
Blog
</span>
<h2 class="mt-4 font-display text-3xl font-bold tracking-tight text-surface-50 sm:text-4xl lg:text-5xl">
Latest <em class="font-medium text-accent-400" style="font-style: italic;">updates</em>
Latest updates
</h2>
<p class="mt-4 text-lg text-surface-400">
Release notes, tutorials, and project updates.
+7 -363
View File
@@ -34,7 +34,7 @@
@layer components {
.section-divider {
border: none;
border-top: 1px dashed theme('colors.surface.700');
border-top: 1px solid theme('colors.surface.800');
@apply mx-auto max-w-5xl;
}
@@ -58,358 +58,13 @@
.stagger-children.stagger-visible > *:nth-child(11) { animation-delay: 800ms; }
.stagger-children.stagger-visible > *:nth-child(12) { animation-delay: 880ms; }
.card-dotted {
@apply border border-dashed border-surface-600/60 rounded-lg p-6 transition-all duration-300;
/* Elevated panel: a bg step plus a hairline, never a dashed outline. */
.card-panel {
@apply rounded-xl border border-surface-800 bg-surface-900/40 p-6 transition-colors duration-300;
}
.card-dotted:hover {
@apply border-accent-500/40 bg-accent-500/[0.03];
}
/* ── TV-effect download button ── */
.btn-tv {
position: relative;
display: inline-flex;
overflow: hidden;
isolation: isolate;
border: 1px solid rgba(32, 168, 168, 0.3);
background: linear-gradient(135deg, rgba(32, 168, 168, 0.12) 0%, rgba(32, 168, 168, 0.06) 50%, rgba(32, 168, 168, 0.1) 100%);
background-size: 180% 180%;
background-position: 0% 50%;
box-shadow:
0 0 20px rgba(32, 168, 168, 0.15),
0 0 40px rgba(32, 168, 168, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.06),
inset 0 0 20px rgba(32, 168, 168, 0.03);
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
animation: tvIdleGlow 2.1s ease-in-out infinite, tvSurfaceShift 2.8s linear infinite, tvButtonJitter 1.2s steps(2, end) infinite;
will-change: transform, box-shadow, background-position;
}
.btn-tv::before {
content: '';
position: absolute;
inset: 0;
z-index: 1;
opacity: 0.34;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(32, 168, 168, 0.03) 2px,
rgba(32, 168, 168, 0.03) 4px
);
transition: opacity 0.3s ease;
pointer-events: none;
animation: tvHorizontalRoll 2.2s linear infinite;
}
.btn-tv::after {
content: '';
position: absolute;
inset: 0;
z-index: 1;
opacity: 0.22;
pointer-events: none;
mix-blend-mode: screen;
background-image:
linear-gradient(180deg, rgba(32, 168, 168, 0) 0%, rgba(32, 168, 168, 0.1) 48%, rgba(32, 168, 168, 0) 100%),
repeating-linear-gradient(
0deg,
transparent 0px,
transparent 5px,
rgba(180, 255, 255, 0.045) 6px,
transparent 7px
);
background-size: 100% 100%, 100% 12px;
background-position: 0 0, 0 0;
animation: tvInterferenceShift 0.45s steps(14) infinite, tvNoSignalBurst 3.6s steps(1, end) infinite;
}
/* TV scanline sweep overlay */
.btn-tv .tv-scanline {
position: absolute;
inset: 0;
z-index: 2;
opacity: 0.75;
overflow: hidden;
pointer-events: none;
transition: opacity 0.3s ease;
}
.btn-tv .tv-scanline::after {
content: '';
position: absolute;
left: 0;
right: 0;
height: 40%;
background: linear-gradient(
180deg,
transparent,
rgba(32, 168, 168, 0.08) 40%,
rgba(32, 168, 168, 0.12) 50%,
rgba(32, 168, 168, 0.08) 60%,
transparent
);
animation: tvScan 1.1s linear infinite;
}
/* Static noise overlay */
.btn-tv .tv-noise {
position: absolute;
inset: -50%;
z-index: 3;
opacity: 0.08;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.4'/%3E%3C/svg%3E");
background-size: 128px 128px;
background-position: 0 0;
pointer-events: none;
transition: opacity 0.3s ease;
mix-blend-mode: overlay;
animation: tvNoiseDrift 0.55s steps(24) infinite;
}
.btn-tv .tv-noise::after {
content: '';
position: absolute;
top: 48%;
left: -38%;
width: 34%;
height: 1px;
border-radius: 9999px;
background: linear-gradient(
90deg,
transparent 0%,
rgba(32, 168, 168, 0.5) 40%,
rgba(155, 255, 255, 0.8) 50%,
rgba(32, 168, 168, 0.5) 60%,
transparent 100%
);
box-shadow: 0 0 10px rgba(32, 168, 168, 0.35);
opacity: 0.95;
height: 2px;
animation: tvNoiseLineX 1.15s linear infinite;
}
/* CRT vignette corners */
.btn-tv .tv-vignette {
position: absolute;
inset: 0;
z-index: 4;
opacity: 0;
border-radius: inherit;
box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.3);
pointer-events: none;
transition: opacity 0.4s ease;
}
/* Hover state: TV turns on */
.btn-tv:hover {
border-color: rgba(32, 168, 168, 0.5);
background: linear-gradient(135deg, rgba(32, 168, 168, 0.18) 0%, rgba(32, 168, 168, 0.08) 50%, rgba(32, 168, 168, 0.15) 100%);
box-shadow:
0 0 44px rgba(32, 168, 168, 0.45),
0 0 100px rgba(32, 168, 168, 0.22),
0 0 130px rgba(32, 168, 168, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.08),
inset 0 0 30px rgba(32, 168, 168, 0.06);
transform: translateY(-2px) scale(1.055);
}
.btn-tv:hover::before {
opacity: 0.75;
}
.btn-tv:hover::after {
opacity: 0.45;
}
.btn-tv:hover .tv-scanline {
opacity: 1;
}
.btn-tv:hover .tv-noise {
opacity: 0.16;
animation: tvNoiseDrift 0.35s steps(30) infinite, tvFlicker 0.08s infinite;
}
.btn-tv:hover .tv-noise::after {
opacity: 1;
animation-duration: 0.65s;
}
.btn-tv:hover .tv-vignette {
opacity: 1;
}
/* Glitch text on hover */
.btn-tv:hover .btn-tv-text {
text-shadow:
2px 0 rgba(32, 168, 168, 0.3),
-2px 0 rgba(255, 100, 100, 0.15);
animation: tvTextGlitch 4s infinite;
}
.btn-tv .btn-tv-text {
animation: tvSubpixelDrift 8s ease-in-out infinite;
}
@keyframes tvTextGlitch {
0%, 93%, 100% {
text-shadow:
0 0 rgba(32, 168, 168, 0),
0 0 rgba(255, 100, 100, 0);
}
94% {
text-shadow:
2px 0 rgba(32, 168, 168, 0.4),
-2px 0 rgba(255, 100, 100, 0.2);
}
95% {
text-shadow:
-1px 0 rgba(32, 168, 168, 0.3),
1px 0 rgba(255, 100, 100, 0.15);
}
96% {
text-shadow:
3px 0 rgba(32, 168, 168, 0.5),
-3px 0 rgba(255, 100, 100, 0.25);
}
97% {
text-shadow:
0 0 rgba(32, 168, 168, 0),
0 0 rgba(255, 100, 100, 0);
}
}
@keyframes tvIdleGlow {
0%, 100% {
box-shadow:
0 0 18px rgba(32, 168, 168, 0.14),
0 0 36px rgba(32, 168, 168, 0.04),
inset 0 1px 0 rgba(255, 255, 255, 0.05),
inset 0 0 18px rgba(32, 168, 168, 0.02);
}
50% {
box-shadow:
0 0 24px rgba(32, 168, 168, 0.2),
0 0 48px rgba(32, 168, 168, 0.07),
inset 0 1px 0 rgba(255, 255, 255, 0.07),
inset 0 0 24px rgba(32, 168, 168, 0.04);
}
}
@keyframes tvSurfaceShift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@keyframes tvButtonJitter {
0%, 100% {
transform: translate3d(0, 0, 0);
}
20% {
transform: translate3d(0.4px, -0.3px, 0);
}
40% {
transform: translate3d(-0.5px, 0.2px, 0);
}
60% {
transform: translate3d(0.3px, 0.1px, 0);
}
80% {
transform: translate3d(-0.2px, -0.2px, 0);
}
}
@keyframes tvNoiseDrift {
0% {
transform: translate3d(-1%, -0.5%, 0) scale(1);
background-position: 0 0;
}
50% {
transform: translate3d(1%, 0.8%, 0) scale(1.015);
background-position: 22px -18px;
}
100% {
transform: translate3d(-0.5%, -0.2%, 0) scale(1);
background-position: 44px -8px;
}
}
@keyframes tvNoiseLineX {
0% {
transform: translateX(0);
}
100% {
transform: translateX(420%);
}
}
@keyframes tvHorizontalRoll {
0% {
transform: translateY(0);
}
100% {
transform: translateY(20px);
}
}
@keyframes tvInterferenceShift {
0% {
transform: translate3d(0, 0, 0);
background-position: 0 0, 0 0;
}
100% {
transform: translate3d(0, 0, 0);
background-position: 0 0, 0 12px;
}
}
@keyframes tvNoSignalBurst {
0%, 60%, 100% {
filter: contrast(1) brightness(1);
transform: translate3d(0, 0, 0);
}
61% {
filter: contrast(2.1) brightness(1.35) saturate(1.15);
transform: translate3d(1.3%, -0.5%, 0);
}
62% {
filter: contrast(0.72) brightness(0.8) saturate(0.9);
transform: translate3d(-1.4%, 0.4%, 0);
}
63% {
filter: contrast(1.6) brightness(1.2) saturate(1.1);
transform: translate3d(0.6%, -0.2%, 0);
}
}
@keyframes tvSubpixelDrift {
0%, 100% {
transform: translate3d(0, 0, 0);
text-shadow:
0 0 rgba(32, 168, 168, 0),
0 0 rgba(255, 110, 110, 0);
}
45% {
transform: translate3d(0.1px, 0, 0);
text-shadow:
0.5px 0 rgba(32, 168, 168, 0.2),
-0.5px 0 rgba(255, 110, 110, 0.08);
}
55% {
transform: translate3d(-0.1px, 0, 0);
text-shadow:
0.4px 0 rgba(32, 168, 168, 0.16),
-0.4px 0 rgba(255, 110, 110, 0.06);
}
a.card-panel:hover,
.card-panel:has(> a:hover) {
@apply border-surface-600 bg-surface-900/70;
}
/* ── Carousel infinite marquee ── */
@@ -432,16 +87,5 @@
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.btn-tv,
.btn-tv::before,
.btn-tv::after,
.btn-tv .tv-noise,
.btn-tv .tv-noise::after,
.btn-tv .tv-scanline::after,
.btn-tv .btn-tv-text,
.btn-tv:hover .btn-tv-text {
animation-duration: 16s !important;
}
}
}
+3 -35
View File
@@ -41,23 +41,18 @@ export default {
},
},
fontFamily: {
display: ['"Playfair Display"', 'Georgia', 'serif'],
display: ['"Bricolage Grotesque"', '"DM Sans"', 'system-ui', 'sans-serif'],
body: ['"DM Sans"', 'system-ui', 'sans-serif'],
mono: ['"IBM Plex Mono"', 'monospace'],
},
fontSize: {
'7xl': ['4.5rem', { lineHeight: '1.05', letterSpacing: '-0.03em' }],
'8xl': ['6rem', { lineHeight: '1', letterSpacing: '-0.04em' }],
'7xl': ['4.5rem', { lineHeight: '1.02', letterSpacing: '-0.03em' }],
'8xl': ['6rem', { lineHeight: '0.98', letterSpacing: '-0.035em' }],
'9xl': ['8rem', { lineHeight: '0.95', letterSpacing: '-0.04em' }],
},
animation: {
'fade-up': 'fadeUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards',
'fade-in': 'fadeIn 0.6s ease forwards',
'glow-pulse': 'glowPulse 4s ease-in-out infinite',
'scan': 'scan 8s linear infinite',
'tv-scan': 'tvScan 3s linear infinite',
'tv-flicker': 'tvFlicker 0.15s infinite',
'tv-glow': 'tvGlow 2s ease-in-out infinite',
},
keyframes: {
fadeUp: {
@@ -68,33 +63,6 @@ export default {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
glowPulse: {
'0%, 100%': { opacity: '0.4' },
'50%': { opacity: '0.8' },
},
scan: {
'0%': { transform: 'translateY(-100%)' },
'100%': { transform: 'translateY(100%)' },
},
tvScan: {
'0%': { transform: 'translateY(-100%)' },
'100%': { transform: 'translateY(100%)' },
},
tvFlicker: {
'0%': { opacity: '0.97' },
'5%': { opacity: '0.95' },
'10%': { opacity: '0.98' },
'15%': { opacity: '0.94' },
'20%': { opacity: '0.98' },
'50%': { opacity: '0.96' },
'80%': { opacity: '0.98' },
'90%': { opacity: '0.95' },
'100%': { opacity: '0.98' },
},
tvGlow: {
'0%, 100%': { boxShadow: '0 0 30px rgba(32, 168, 168, 0.3), 0 0 60px rgba(32, 168, 168, 0.1), inset 0 0 30px rgba(32, 168, 168, 0.05)' },
'50%': { boxShadow: '0 0 40px rgba(32, 168, 168, 0.4), 0 0 80px rgba(32, 168, 168, 0.15), inset 0 0 40px rgba(32, 168, 168, 0.08)' },
},
},
},
},