mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
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:
1 parent
07ccf45f32
commit
1cf17f7301
49 files changed
+123
-535
No files matched your search
@@ -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">·</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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -119,7 +119,7 @@ const jsonLd = faqSchema ? [blogPostSchema, faqSchema] : blogPostSchema;
|
||||
<>
|
||||
<span class="text-surface-700">·</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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user