feat(website): adopt expressive material design

This commit is contained in:
edde746
2026-07-14 07:19:46 +02:00
parent 991aee6c90
commit 0cb19a4e30
13 changed files with 1464 additions and 1166 deletions
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import AppleIcon from "~icons/simple-icons/apple"; import AppleIcon from "~icons/simple-icons/apple";
import GooglePlayIcon from "~icons/simple-icons/googleplay"; import GooglePlayIcon from "~icons/simple-icons/googleplay";
import LinuxIcon from "~icons/simple-icons/linux"; import LinuxIcon from "~icons/devicon-plain/linux";
import AmazonIcon from "~icons/cib/amazon"; import AmazonIcon from "~icons/cib/amazon";
import ChevronDownIcon from "~icons/heroicons/chevron-down-solid"; import ChevronDownIcon from "~icons/heroicons/chevron-down-solid";
import WindowsIcon from "./WindowsIcon.svelte"; import WindowsIcon from "./WindowsIcon.svelte";
@@ -133,80 +133,82 @@
.download-buttons { .download-buttons {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 0.75rem;
} }
.store-buttons, .store-buttons,
.desktop-buttons { .desktop-buttons {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} gap: 0.5rem;
.store-buttons {
gap: 0.75rem;
}
.desktop-buttons {
gap: 0.625rem;
} }
.store-button, .store-button,
.desktop-button { .desktop-button {
display: inline-flex; display: inline-flex;
min-height: 2.875rem;
align-items: center; align-items: center;
border-radius: 9999px; justify-content: center;
font-size: 0.875rem; gap: 0.625rem;
line-height: 1.25rem; border-radius: var(--radius-pill);
transition: background-color 150ms ease, color 150ms ease; padding-inline: 1rem;
font-size: 0.8125rem;
font-weight: 700;
line-height: 1;
transition:
border-radius var(--motion-expressive) var(--ease-standard),
color var(--motion-fast) var(--ease-standard),
background-color var(--motion-fast) var(--ease-standard);
} }
.store-button { .store-button {
gap: 0.625rem; color: var(--color-on-primary);
padding: 0.75rem 1.25rem; background: var(--color-text);
color: #111827;
background: #fff;
font-weight: 600;
} }
.store-button:hover { .store-button:hover,
background: #f3f4f6; .store-button:focus-visible {
border-radius: var(--radius-md);
background: #fff;
outline: none;
} }
.store-button :global(svg) { .store-button :global(svg) {
width: 1.25rem; width: 1.125rem;
height: 1.25rem; height: 1.125rem;
} }
.desktop-button { .desktop-button {
gap: 0.5rem; color: var(--color-text-muted);
padding: 0.5rem 1rem; background: var(--color-surface-highest);
border: 1px solid var(--color-border);
background: color-mix(in srgb, var(--color-surface) 80%, transparent);
} }
.desktop-button:hover, .desktop-button:hover,
.desktop-button:focus-visible,
.linux-button.active { .linux-button.active {
color: var(--color-text);
background: var(--color-surface-hover); background: var(--color-surface-hover);
outline: none;
}
.desktop-button:hover,
.desktop-button:focus-visible {
border-radius: var(--radius-md);
} }
.desktop-button :global(svg) { .desktop-button :global(svg) {
width: 0.875rem; width: 0.9375rem;
height: 0.875rem; height: 0.9375rem;
opacity: 0.7;
} }
.linux-control { .linux-control {
position: relative; position: relative;
} }
.linux-button {
cursor: default;
}
.chevron { .chevron {
width: 0.75rem; width: 0.75rem;
height: 0.75rem; height: 0.75rem;
transition: transform 300ms ease; transition: transform var(--motion-normal) var(--ease-standard);
} }
.chevron.open { .chevron.open {
@@ -216,55 +218,73 @@
.chevron :global(svg) { .chevron :global(svg) {
width: 0.75rem; width: 0.75rem;
height: 0.75rem; height: 0.75rem;
opacity: 1;
} }
.linux-menu { .linux-menu {
position: absolute; position: absolute;
bottom: 100%; bottom: calc(100% + 0.5rem);
left: 0; right: 0;
z-index: 10; z-index: 10;
width: 14rem; width: min(16rem, calc(100vw - 2rem));
margin-bottom: 0.5rem;
overflow: hidden; overflow: hidden;
border: 1px solid var(--color-border); border-radius: var(--radius-lg);
border-radius: 1rem; padding: 0.375rem;
background: var(--color-surface); background: var(--color-surface-highest);
box-shadow:
0 20px 25px -5px rgb(0 0 0 / 0.1),
0 8px 10px -6px rgb(0 0 0 / 0.1);
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 150ms ease, visibility 150ms ease; transform: translateY(0.5rem);
transition:
opacity var(--motion-normal) var(--ease-standard),
transform var(--motion-normal) var(--ease-standard),
visibility var(--motion-normal) var(--ease-standard);
} }
.linux-menu.open { .linux-menu.open {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transform: translateY(0);
} }
.linux-separator { .linux-separator {
border-top: 1px solid var(--color-border); height: var(--group-gap);
margin-block: 0.375rem;
background: var(--color-border);
} }
.linux-arch-label { .linux-arch-label {
padding: 0.625rem 1rem 0.25rem; padding: 0.625rem 0.75rem 0.375rem;
color: var(--color-text-muted); color: var(--color-text-subtle);
font-size: 0.75rem; font-family: var(--font-utility);
font-weight: 600; font-size: 0.6875rem;
letter-spacing: 0.025em; font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase; text-transform: uppercase;
} }
.linux-menu-item { .linux-menu-item {
display: block; display: block;
padding: 0.5rem 1rem; border-radius: var(--radius-md);
font-size: 0.875rem; padding: 0.625rem 0.75rem;
color: var(--color-text-muted);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.25rem; line-height: 1.25rem;
transition: background-color 150ms ease; transition:
color var(--motion-fast) var(--ease-standard),
background-color var(--motion-fast) var(--ease-standard);
} }
.linux-menu-item:hover { .linux-menu-item:hover,
background: var(--color-surface-hover); .linux-menu-item:focus-visible {
color: var(--color-text);
background: rgb(237 237 237 / 0.12);
outline: none;
}
@media (max-width: 460px) {
.store-button,
.desktop-button {
flex: 1 1 auto;
}
} }
</style> </style>
+89 -53
View File
@@ -33,8 +33,8 @@
<div class="faq-list"> <div class="faq-list">
{#each faqs as faq, i} {#each faqs as faq, i}
<ScrollReveal delay={i * 50}> <ScrollReveal delay={i * 50} class="faq-row">
<div id={faq.id} class="glass-card faq-card"> <div id={faq.id} class="flat-card faq-card">
<button <button
type="button" type="button"
class="faq-toggle" class="faq-toggle"
@@ -72,118 +72,154 @@
<style> <style>
.faq-section { .faq-section {
max-width: 64rem; width: min(100%, var(--page-width));
margin-inline: auto; margin-inline: auto;
padding: 4rem 1.5rem; padding: clamp(4rem, 9vw, 8rem) var(--page-gutter);
} }
.section-label { .section-label {
margin-bottom: 0.75rem; width: fit-content;
color: var(--color-accent); margin-bottom: 1rem;
font-size: 0.875rem; border-radius: var(--radius-full);
font-weight: 500; padding: 0.5rem 0.875rem;
letter-spacing: 0.025em; color: var(--color-text-muted);
line-height: 1.25rem; background: var(--color-surface);
text-transform: uppercase; font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.03em;
} }
.section-heading { .section-heading {
max-width: 12ch;
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 2.25rem; font-family: var(--font-display);
font-size: clamp(2.5rem, 7vw, 4.75rem);
font-weight: 700; font-weight: 700;
line-height: 2.5rem; letter-spacing: -0.045em;
line-height: 1;
text-wrap: balance;
} }
.section-description { .section-description {
max-width: 32rem; max-width: 34rem;
margin-bottom: 2.5rem; margin-bottom: clamp(2.5rem, 6vw, 4.5rem);
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 1.125rem; font-size: clamp(1rem, 2vw, 1.125rem);
line-height: 1.75rem; line-height: 1.7;
} }
.faq-list { .faq-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.625rem; gap: var(--group-gap);
} }
.faq-card { .faq-card {
border-radius: 1rem; overflow: hidden;
border-radius: var(--radius-xs);
background: var(--color-surface);
transition: background-color var(--motion-normal) var(--ease-standard);
}
.faq-card:has(.faq-toggle[aria-expanded="true"]) {
background: var(--color-surface-high);
}
:global(.faq-row:first-child) .faq-card {
border-radius: var(--radius-lg) var(--radius-lg) var(--radius-xs) var(--radius-xs);
}
:global(.faq-row:last-child) .faq-card {
border-radius: var(--radius-xs) var(--radius-xs) var(--radius-lg) var(--radius-lg);
} }
.faq-toggle { .faq-toggle {
display: flex; display: flex;
width: 100%; width: 100%;
min-height: 4.5rem;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
padding: 1rem 1.5rem; padding: 0.75rem 1rem 0.75rem clamp(1rem, 4vw, 1.5rem);
border-radius: inherit;
text-align: left; text-align: left;
transition: background-color var(--motion-fast) var(--ease-standard);
}
.faq-toggle:not([aria-expanded="true"]):hover {
background: rgb(237 237 237 / 0.08);
}
.faq-toggle:focus-visible {
background: rgb(237 237 237 / 0.14);
outline: none;
} }
.faq-question { .faq-question {
font-weight: 500; font-family: var(--font-display);
font-size: clamp(0.9375rem, 2vw, 1.0625rem);
font-weight: 700;
letter-spacing: -0.01em;
} }
.faq-icon { .faq-icon {
display: flex;
width: 2.5rem;
height: 2.5rem;
flex-shrink: 0; flex-shrink: 0;
width: 1.25rem; align-items: center;
height: 1.25rem; justify-content: center;
color: var(--color-accent); border-radius: var(--radius-full);
color: var(--color-text);
background: rgb(237 237 237 / 0.1);
transition:
border-radius var(--motion-normal) var(--ease-standard),
background-color var(--motion-fast) var(--ease-standard);
}
.faq-toggle[aria-expanded="true"] .faq-icon {
border-radius: var(--radius-md);
background: rgb(237 237 237 / 0.16);
} }
.faq-icon :global(svg) { .faq-icon :global(svg) {
width: 1.25rem; width: 1.125rem;
height: 1.25rem; height: 1.125rem;
} }
.faq-answer { .faq-answer {
display: grid; display: grid;
grid-template-rows: 0fr; grid-template-rows: 0fr;
transition: grid-template-rows 200ms ease; transition: grid-template-rows var(--motion-normal) var(--ease-standard);
} }
.faq-answer.open { .faq-answer.open {
grid-template-rows: 1fr; grid-template-rows: 1fr;
} }
.faq-answer > div { .faq-answer > div {
overflow: hidden;
min-height: 0; min-height: 0;
overflow: hidden;
} }
.faq-answer-content { .faq-answer-content {
padding: 0 1.5rem 1rem; max-width: 50rem;
padding: 0 clamp(1rem, 4vw, 1.5rem) 1.5rem;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.625; line-height: 1.7;
} }
.faq-answer-content :global(a) { .faq-answer-content :global(a) {
color: var(--color-accent); color: var(--color-text);
}
.faq-answer-content :global(a:hover) {
text-decoration: underline; text-decoration: underline;
text-decoration-color: var(--color-text-subtle);
text-underline-offset: 0.2em;
} }
@media (min-width: 640px) { .faq-answer-content :global(a:hover),
.faq-section { .faq-answer-content :global(a:focus-visible) {
padding-block: 6rem; text-decoration-color: var(--color-text);
} outline: none;
.section-description {
margin-bottom: 4rem;
}
}
@media (min-width: 768px) {
.faq-section {
padding-block: 8rem;
}
.section-heading {
font-size: 3rem;
line-height: 1;
}
} }
</style> </style>
File diff suppressed because it is too large Load Diff
+32 -46
View File
@@ -3,48 +3,33 @@
</script> </script>
<footer class="site-footer"> <footer class="site-footer">
<div class="footer-inner"> <div class="footer-content">
<!-- Gradient rule --> <div class="footer-brand">
<div class="gradient-rule"></div> <span class="footer-logo"><Logo /></span>
<span>Plezy</span>
<div class="footer-content">
<!-- Logo + name -->
<div class="footer-brand">
<span class="footer-logo"><Logo /></span>
<span>Plezy</span>
</div>
<!-- Links -->
<nav class="footer-nav">
<a href="https://github.com/edde746/plezy" target="_blank" rel="noopener noreferrer">GitHub</a>
<a href="/privacy">Privacy Policy</a>
</nav>
</div> </div>
<nav class="footer-nav" aria-label="Footer navigation">
<a href="https://github.com/edde746/plezy" target="_blank" rel="noopener noreferrer">GitHub</a>
<a href="/privacy">Privacy</a>
<a href="#home">Back to top</a>
</nav>
</div> </div>
</footer> </footer>
<style> <style>
.site-footer { .site-footer {
padding: 2.5rem 1.5rem; padding: clamp(3rem, 7vw, 5rem) var(--page-gutter);
}
.footer-inner {
max-width: 64rem;
margin-inline: auto;
}
.gradient-rule {
height: 1px;
margin-bottom: 2rem;
background: linear-gradient(to right, transparent, var(--color-border), transparent);
} }
.footer-content { .footer-content {
display: flex; display: flex;
width: min(100%, var(--page-width));
flex-direction: column; flex-direction: column;
align-items: center; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 2rem; gap: 1.5rem;
margin-inline: auto;
} }
.footer-brand { .footer-brand {
@@ -52,42 +37,43 @@
align-items: center; align-items: center;
gap: 0.625rem; gap: 0.625rem;
color: var(--color-text-muted); color: var(--color-text-muted);
font-weight: 500; font-weight: 700;
} }
.footer-logo :global(svg) { .footer-logo :global(svg) {
width: 1.75rem; width: 1.5rem;
height: 1.75rem; height: 1.5rem;
} }
.footer-nav { .footer-nav {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
gap: 2rem; gap: 0.25rem;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.875rem; font-size: 0.8125rem;
line-height: 1.25rem; font-weight: 600;
} }
.footer-nav a { .footer-nav a {
transition: color 150ms ease; border-radius: var(--radius-full);
padding: 0.625rem 0.875rem;
transition:
color var(--motion-fast) var(--ease-standard),
background-color var(--motion-fast) var(--ease-standard);
} }
.footer-nav a:hover { .footer-nav a:hover,
.footer-nav a:focus-visible {
color: var(--color-text); color: var(--color-text);
background: rgb(237 237 237 / 0.12);
outline: none;
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.site-footer {
padding-block: 4rem;
}
.gradient-rule {
margin-bottom: 3rem;
}
.footer-content { .footer-content {
flex-direction: row; flex-direction: row;
align-items: center;
} }
} }
</style> </style>
+158 -186
View File
@@ -6,65 +6,74 @@
</script> </script>
<section id="home" class="hero-section"> <section id="home" class="hero-section">
<!-- Giant background logo watermark --> <div class="hero-shell">
<div class="hero-watermark" aria-hidden="true"> <div class="hero-grid">
<Logo gradient={false} /> <div class="hero-copy">
</div> <div class="brand-row fade-in">
<span class="brand-logo"><Logo /></span>
<span>Plezy</span>
</div>
<!-- Ambient glow --> <h1 class="hero-title fade-in" style="animation-delay: 60ms;">
<div class="ambient-glow"></div> A beautiful client for <span>Plex &amp; Jellyfin</span>
</h1>
<p class="hero-description fade-in" style="animation-delay: 120ms;">
Stream your media library with HDR, Dolby Vision, offline downloads, and an experience built for every
screen.
</p>
<!-- Content: two-column on desktop --> <div id="download" class="download-area fade-in" style="animation-delay: 180ms;">
<div class="hero-content"> <DownloadButtons />
<!-- Left: text + buttons --> </div>
<div class="hero-copy">
<div class="brand-row fade-in" style="animation-delay: 0ms;">
<span class="brand-logo"><Logo /></span>
<span class="brand-name">Plezy</span>
</div> </div>
<h1 <div class="hero-visual fade-in" style="animation-delay: 240ms;">
class="hero-title fade-in" <div class="shape-field" aria-hidden="true">
style="animation-delay: 100ms;" <svg
> class="hero-shape arch-shape"
<span class="title-line">A beautiful client for</span> viewBox="0 0 1 1"
<span class="title-line gradient-text">Plex &amp; Jellyfin</span> preserveAspectRatio="none"
</h1> data-material-shape="Arch"
>
<p <path
class="hero-description fade-in" d="M0.146447 0.146447C0.236929 0.055964 0.361929 0 0.5 0C0.776142 0 1 0.223858 1 0.5C1 0.619526 1 0.739052 1 0.858579C1 0.936684 0.936683 1 0.858579 1C0.619526 1 0.380474 1 0.141421 1C0.063317 1 0 0.936683 0 0.858579C0 0.739052 0 0.619526 0 0.5C0 0.361929 0.055964 0.236929 0.146447 0.146447Z"
style="animation-delay: 200ms;" />
> </svg>
Stream your media library with HDR, Dolby Vision, offline downloads, and a gorgeous experience across all your <svg
devices. class="hero-shape cookie-shape"
</p> viewBox="0 0 1 1"
data-material-shape="Cookie6Sided"
<div class="fade-in" style="animation-delay: 300ms;"> >
<DownloadButtons /> <path
</div> d="M0.716525 0.8729C0.699648 0.882624 0.683956 0.894405 0.669829 0.908026C0.669329 0.908507 0.66883 0.908989 0.66833 0.90947C0.574432 1 0.425694 0.999869 0.331956 0.909174C0.303749 0.881883 0.269272 0.861938 0.231554 0.851089C0.230887 0.850897 0.23022 0.850705 0.229553 0.850514C0.104204 0.81446 0.029948 0.685584 0.061623 0.559056C0.071154 0.520984 0.071189 0.481153 0.061725 0.443063C0.061558 0.44239 0.06139 0.441717 0.061223 0.441043C0.029771 0.31446 0.104254 0.185715 0.229667 0.149882C0.267404 0.1391 0.301917 0.119215 0.330171 0.091974C0.330671 0.091493 0.33117 0.091011 0.331669 0.09053C0.425568 0 0.574306 0.000131 0.668044 0.090826C0.696251 0.118116 0.730728 0.138062 0.768446 0.148911C0.769113 0.149103 0.76978 0.149294 0.770446 0.149486C0.895796 0.18554 0.970052 0.314416 0.938377 0.440944C0.928846 0.479016 0.928811 0.518847 0.938275 0.556937C0.938442 0.55761 0.93861 0.558283 0.938777 0.558957C0.970229 0.685539 0.895746 0.814285 0.770333 0.850118C0.751464 0.855509 0.733402 0.863176 0.716525 0.8729Z"
</div> />
</svg>
<!-- Right: stacked phone mockup --> <svg
<div class="phone-stage fade-in" style="animation-delay: 400ms;"> class="hero-shape diamond-shape"
<div class="phone-stack"> viewBox="0 0 1 1"
<!-- Shadow phone behind --> data-material-shape="Diamond"
<div class="shadow-phone"> >
<path
d="M0.5 1C0.472713 1 0.445426 0.992561 0.421304 0.977684C0.398028 0.963329 0.371729 0.929255 0.319131 0.861106C0.252 0.774128 0.184869 0.68715 0.117738 0.600171C0.073881 0.543348 0.073881 0.46409 0.117738 0.407267C0.184869 0.320289 0.252 0.233311 0.319131 0.146333C0.371729 0.078184 0.398028 0.044109 0.421304 0.029754C0.469549 0 0.530451 0 0.578696 0.029754C0.601972 0.044109 0.628271 0.078184 0.680869 0.146333C0.748 0.233311 0.815131 0.320289 0.882262 0.407267C0.926119 0.46409 0.926119 0.543348 0.882262 0.600171C0.815131 0.68715 0.748 0.774128 0.680869 0.861106C0.628271 0.929255 0.601972 0.963329 0.578696 0.977684C0.554573 0.992561 0.527287 1 0.5 1Z"
/>
</svg>
</div>
<div class="phone secondary-phone">
<enhanced:img <enhanced:img
src={libraryImage} src={libraryImage}
alt="" alt="Plezy library screen on phone"
class="phone-image" class="phone-image"
loading="lazy" loading="eager"
sizes="(min-width: 768px) 300px, (min-width: 640px) 280px, 224px" sizes="(min-width: 1024px) 250px, (min-width: 640px) 34vw, 42vw"
/> />
</div> </div>
<!-- Main phone --> <div class="phone primary-phone">
<div class="main-phone">
<enhanced:img <enhanced:img
src={homeImage} src={homeImage}
alt="Plezy home screen" alt="Plezy home screen on phone"
class="phone-image" class="phone-image"
fetchpriority="high" fetchpriority="high"
sizes="(min-width: 768px) 300px, (min-width: 640px) 280px, 224px" sizes="(min-width: 1024px) 280px, (min-width: 640px) 38vw, 48vw"
/> />
</div> </div>
</div> </div>
@@ -74,196 +83,159 @@
<style> <style>
.hero-section { .hero-section {
position: relative; overflow-x: clip;
display: flex; padding-inline: var(--page-gutter);
min-height: 100dvh;
align-items: center;
overflow: hidden;
} }
.hero-watermark { .hero-shell {
position: absolute; width: min(100%, var(--page-width));
top: 50%;
right: -5rem;
width: 37.5rem;
transform: translateY(-50%);
opacity: 0.04;
pointer-events: none;
user-select: none;
}
.hero-watermark :global(svg) {
width: 100%;
height: auto;
}
.ambient-glow {
position: absolute;
top: 33.3333%;
right: 25%;
width: 31.25rem;
height: 31.25rem;
border-radius: 9999px;
background: color-mix(in srgb, var(--color-accent) 10%, transparent);
filter: blur(120px);
animation: pulse-glow 3s ease-in-out infinite;
pointer-events: none;
}
.hero-content {
position: relative;
display: grid;
width: 100%;
max-width: 80rem;
margin-inline: auto; margin-inline: auto;
padding: 4rem 1.5rem; padding-block: clamp(3rem, 8vw, 7rem) clamp(5rem, 10vw, 9rem);
gap: 3rem; }
.hero-grid {
display: grid;
align-items: center; align-items: center;
gap: clamp(4rem, 9vw, 8rem);
} }
.hero-copy { .hero-copy {
max-width: 36rem; min-width: 0;
}
.fade-in {
animation: fade-in-up 0.6s ease-out both;
} }
.brand-row { .brand-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
margin-bottom: 1.25rem; margin-bottom: clamp(2rem, 5vw, 3rem);
font-family: var(--font-display);
font-size: 1.125rem;
font-weight: 700;
} }
.brand-logo :global(svg) { .brand-logo :global(svg) {
width: 2.5rem; width: 2.25rem;
height: 2.5rem; height: 2.25rem;
}
.brand-name {
color: var(--color-text-muted);
font-size: 1.25rem;
font-weight: 600;
} }
.hero-title { .hero-title {
font-size: 2.25rem; max-width: 10ch;
line-height: 1.05; font-family: var(--font-display);
font-size: clamp(3rem, 10vw, 5.75rem);
font-weight: 700; font-weight: 700;
letter-spacing: -0.025em; letter-spacing: -0.055em;
line-height: 0.98;
text-wrap: balance;
} }
.title-line { .hero-title span {
display: block; color: var(--color-text-muted);
white-space: nowrap;
}
.gradient-text {
color: transparent;
background: linear-gradient(to right, var(--color-accent-dim), var(--color-accent));
-webkit-background-clip: text;
background-clip: text;
} }
.hero-description { .hero-description {
max-width: 28rem; max-width: 31rem;
margin-top: 1rem; margin-top: 1.5rem;
margin-bottom: 2rem;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 1rem; font-size: clamp(1rem, 2vw, 1.125rem);
line-height: 1.625; line-height: 1.65;
} }
.phone-stage { .download-area {
margin-top: 2rem;
scroll-margin-top: 2rem;
}
.hero-visual {
position: relative; position: relative;
isolation: isolate;
display: flex; display: flex;
width: min(100%, 35rem);
align-items: flex-start;
justify-content: center; justify-content: center;
gap: clamp(0.5rem, 2vw, 1.25rem);
margin-inline: auto;
padding: 0.5rem 0 2.5rem;
} }
.phone-stack { .shape-field {
position: relative;
width: 14rem;
}
.shadow-phone {
position: absolute; position: absolute;
top: 0.75rem; z-index: 0;
left: -2rem; inset: -1.5rem -1.5rem -1rem;
width: 100%; pointer-events: none;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--color-border) 30%, transparent);
border-radius: 2.5rem;
opacity: 0.4;
filter: blur(1px);
transform: rotate(-4deg);
} }
.main-phone { .hero-shape {
position: absolute;
display: block;
fill: currentColor;
opacity: 0.82;
}
.arch-shape {
top: -4%;
right: -12%;
width: 78%;
height: 96%;
color: var(--color-surface-high);
transform: rotate(4deg);
}
.cookie-shape {
top: 6%;
left: -10%;
width: clamp(11rem, 32vw, 17rem);
color: color-mix(in srgb, var(--color-brand) 10%, var(--color-surface-high));
transform: rotate(-10deg);
}
.diamond-shape {
bottom: -8%;
left: -10%;
width: clamp(10rem, 28vw, 15rem);
aspect-ratio: 1;
color: var(--color-surface-highest);
transform: rotate(-18deg);
}
.phone {
position: relative; position: relative;
z-index: 1;
min-width: 0;
overflow: hidden; overflow: hidden;
border: 2px solid color-mix(in srgb, var(--color-border) 60%, transparent); border-radius: clamp(1.75rem, 4vw, 2.75rem);
border-radius: 2.5rem; background: #000;
box-shadow: 0 0 80px -20px color-mix(in srgb, var(--color-accent) 20%, transparent);
} }
.secondary-phone {
width: 44%;
margin-top: clamp(3rem, 8vw, 6rem);
transform: rotate(-2.5deg);
}
.primary-phone {
width: 50%;
transform: rotate(2deg);
}
.phone :global(picture),
.phone-image { .phone-image {
display: block; display: block;
width: 100%; width: 100%;
height: auto; height: auto;
} }
@media (min-width: 640px) { .fade-in {
.hero-content { animation: fade-in-up var(--motion-expressive) var(--ease-standard) both;
padding-block: 6rem;
}
.brand-row {
margin-bottom: 2rem;
}
.hero-title {
font-size: 3rem;
}
.hero-description {
margin-top: 1.5rem;
margin-bottom: 2.5rem;
font-size: 1.125rem;
}
.phone-stack {
width: 17.5rem;
}
} }
@media (min-width: 768px) { @media (min-width: 960px) {
.hero-watermark { .hero-grid {
width: 50rem; grid-template-columns: minmax(28rem, 1fr) minmax(28rem, 0.9fr);
} }
.hero-title { .hero-visual {
font-size: 3.75rem; justify-self: end;
}
.phone-stack {
width: 18.75rem;
}
}
@media (min-width: 1024px) {
.hero-watermark {
width: 62.5rem;
}
.hero-content {
grid-template-columns: repeat(2, minmax(0, 1fr));
padding-inline: 3rem;
gap: 6rem;
}
.phone-stage {
justify-content: flex-end;
} }
} }
</style> </style>
+327 -241
View File
@@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import ScrollReveal from "./ScrollReveal.svelte"; import ScrollReveal from "./ScrollReveal.svelte";
import AppleIcon from "~icons/simple-icons/apple"; import AppleIcon from "~icons/simple-icons/apple";
import GooglePlayIcon from "~icons/simple-icons/googleplay"; import GooglePlayIcon from "~icons/simple-icons/googleplay";
import RedditIcon from "~icons/fa6-brands/reddit-alien"; import RedditIcon from "~icons/fa6-brands/reddit-alien";
import ArrowUpIcon from "~icons/heroicons/arrow-up-solid"; import ArrowUpIcon from "~icons/heroicons/arrow-up-solid";
import StarIcon from "~icons/heroicons/star-solid"; import StarIcon from "~icons/heroicons/star-solid";
import ChevronLeftIcon from "~icons/heroicons/chevron-left-solid";
import ChevronRightIcon from "~icons/heroicons/chevron-right-solid";
type SourceIcon = typeof AppleIcon | typeof GooglePlayIcon | typeof RedditIcon; type SourceIcon = typeof AppleIcon | typeof GooglePlayIcon | typeof RedditIcon;
@@ -78,8 +79,8 @@
upvotes: 50, upvotes: 50,
}, },
{ {
text: "If you're having troubles with the default plex app just ditch it and switch to this. Plezy is faster, and easier to use. Worth the $5", text: "Design and theme are much better. Love having the buffer size setting. Clean and intuitive. Single developer app that is consistently updated, thank you for your hard work.",
name: "boyknight53", name: "Corrykid",
source: "App Store", source: "App Store",
icon: AppleIcon, icon: AppleIcon,
}, },
@@ -96,10 +97,10 @@
icon: GooglePlayIcon, icon: GooglePlayIcon,
}, },
{ {
text: "College student here this is a godsend. Cannot recommend enough. For mediaheads on the go this is the best $4 you could ever spend.", text: "Perfect. I'm so excited for the development of this app. It's exactly what Plex should have been a decade ago. The client-side MPV player with full codec support fixes playback issues and removes the need for server-side lossy transcoding",
name: "djmeowmeow", name: "John Capasso",
source: "App Store", source: "Play Store",
icon: AppleIcon, icon: GooglePlayIcon,
}, },
{ {
text: "Much better than the official app! The sticking point for me is that downloads actually work reliably. The interface is great and snappy.", text: "Much better than the official app! The sticking point for me is that downloads actually work reliably. The interface is great and snappy.",
@@ -108,10 +109,10 @@
icon: GooglePlayIcon, icon: GooglePlayIcon,
}, },
{ {
text: "This has to be my new favorite plex client. Starts up and connects quickly. Quality is amazing. Well worth the $5.", text: "Just brilliant. Using on all my Android TV boxes. UI and UX are great. I was having intermittent audio drops with the latest build of the official Plex app. No issues with Plezy.",
name: "nasukaren", name: "Jon Evans",
source: "App Store", source: "Play Store",
icon: AppleIcon, icon: GooglePlayIcon,
}, },
{ {
text: "Fantastic and super lean looking! Must have if you have a Plex server.", text: "Fantastic and super lean looking! Must have if you have a Plex server.",
@@ -121,10 +122,58 @@
}, },
]; ];
let mounted = $state(false); const featuredReviews = [row1[3]!, row2[0]!, row2[4]!, row1[5]!];
const featuredReviewSet = new Set(featuredReviews);
const supportingReviews = [...row1, ...row2].filter((review) => !featuredReviewSet.has(review));
onMount(() => { let featuredIndex = $state(0);
mounted = true; let carouselHovered = $state(false);
let carouselFocused = $state(false);
let prefersReducedMotion = $state(false);
const featuredReview = $derived(featuredReviews[featuredIndex]!);
const carouselPaused = $derived(carouselHovered || carouselFocused || prefersReducedMotion);
function showReview(index: number) {
featuredIndex = (index + featuredReviews.length) % featuredReviews.length;
}
function previousReview() {
showReview(featuredIndex - 1);
}
function nextReview() {
showReview(featuredIndex + 1);
}
function handleCarouselFocusOut(event: FocusEvent) {
const currentTarget = event.currentTarget;
carouselFocused =
currentTarget instanceof HTMLElement &&
event.relatedTarget instanceof Node &&
currentTarget.contains(event.relatedTarget);
}
$effect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
const updatePreference = () => {
prefersReducedMotion = media.matches;
};
updatePreference();
media.addEventListener("change", updatePreference);
return () => media.removeEventListener("change", updatePreference);
});
$effect(() => {
const currentIndex = featuredIndex;
if (carouselPaused) return;
const timeout = window.setTimeout(() => {
showReview(currentIndex + 1);
}, 8000);
return () => window.clearTimeout(timeout);
}); });
</script> </script>
@@ -139,177 +188,292 @@
</ScrollReveal> </ScrollReveal>
</div> </div>
<!-- Row 1: scrolls left --> <div class="featured-wrap">
<div class="marquee-row row-gap"> <ScrollReveal>
<div class="marquee-track marquee-left"> <article
{#each row1 as review} class="featured-review"
{@const SourceIcon = review.icon} aria-label="Featured user reviews"
<div class="review-card glass-card"> aria-roledescription="carousel"
{#if review.source === "Reddit"} onpointerenter={() => carouselHovered = true}
onpointerleave={() => carouselHovered = false}
onfocusin={() => carouselFocused = true}
onfocusout={handleCarouselFocusOut}
>
<div class="featured-top">
{#if featuredReview.source === "Reddit"}
<div class="upvote"> <div class="upvote">
<ArrowUpIcon /> <ArrowUpIcon />
<span>{review.upvotes}</span> <span>{featuredReview.upvotes}</span>
</div> </div>
{:else} {:else}
<div class="stars"> <div class="stars" aria-label="5 out of 5 stars">
{#each Array(5) as _} {#each Array(5) as _}
<StarIcon /> <StarIcon aria-hidden="true" />
{/each} {/each}
</div> </div>
{/if} {/if}
<p class="review-text">"{review.text}"</p>
<div class="review-footer"> <div class="carousel-controls">
<span class="review-name">{review.name}</span> <button type="button" class="carousel-arrow" onclick={previousReview} aria-label="Previous featured review">
<div class="review-source"> <ChevronLeftIcon />
<SourceIcon /> </button>
<span>{review.source}</span> <div class="carousel-dots" role="group" aria-label="Choose featured review">
{#each featuredReviews as _, index}
<button
type="button"
class="carousel-dot"
class:active={featuredIndex === index}
onclick={() => showReview(index)}
aria-label={`Show featured review ${index + 1}`}
aria-current={featuredIndex === index ? "true" : undefined}
></button>
{/each}
</div> </div>
<button type="button" class="carousel-arrow" onclick={nextReview} aria-label="Next featured review">
<ChevronRightIcon />
</button>
</div> </div>
</div> </div>
{/each}
{#if mounted} <div class="featured-slide" aria-live={carouselPaused ? "polite" : "off"}>
<!-- Duplicate for seamless loop --> {#key featuredIndex}
<div aria-hidden="true" class="duplicate-cards"> {@const FeaturedSourceIcon = featuredReview.icon}
{#each row1 as review} <div class="featured-content">
{@const SourceIcon = review.icon} <blockquote>"{featuredReview.text}"</blockquote>
<div class="review-card glass-card">
{#if review.source === "Reddit"}
<div class="upvote">
<ArrowUpIcon />
<span>{review.upvotes}</span>
</div>
{:else}
<div class="stars">
{#each Array(5) as _}
<StarIcon />
{/each}
</div>
{/if}
<p class="review-text">"{review.text}"</p>
<div class="review-footer"> <div class="review-footer">
<span class="review-name">{review.name}</span> <span class="review-name">{featuredReview.name}</span>
<div class="review-source"> <div class="review-source">
<SourceIcon /> <FeaturedSourceIcon />
<span>{review.source}</span> <span>{featuredReview.source}</span>
</div> </div>
</div> </div>
</div> </div>
{/each} {/key}
</div> </div>
{/if} </article>
</div> </ScrollReveal>
</div> </div>
<!-- Row 2: scrolls right --> <div class="review-strip content-pad" role="list" aria-label="More user reviews">
<div class="marquee-row"> {#each supportingReviews as review}
<div class="marquee-track marquee-right"> {@const SourceIcon = review.icon}
{#each row2 as review} <article class="review-card" role="listitem">
{@const SourceIcon = review.icon} {#if review.source === "Reddit"}
<div class="review-card glass-card"> <div class="upvote">
{#if review.source === "Reddit"} <ArrowUpIcon />
<div class="upvote"> <span>{review.upvotes}</span>
<ArrowUpIcon /> </div>
<span>{review.upvotes}</span> {:else}
</div> <div class="stars" aria-label="5 out of 5 stars">
{:else} {#each Array(5) as _}
<div class="stars"> <StarIcon aria-hidden="true" />
{#each Array(5) as _} {/each}
<StarIcon /> </div>
{/each} {/if}
</div> <p class="review-text">"{review.text}"</p>
{/if} <div class="review-footer">
<p class="review-text">"{review.text}"</p> <span class="review-name">{review.name}</span>
<div class="review-footer"> <div class="review-source">
<span class="review-name">{review.name}</span> <SourceIcon />
<div class="review-source"> <span>{review.source}</span>
<SourceIcon />
<span>{review.source}</span>
</div>
</div> </div>
</div> </div>
{/each} </article>
{#if mounted} {/each}
<!-- Duplicate for seamless loop -->
<div aria-hidden="true" class="duplicate-cards">
{#each row2 as review}
{@const SourceIcon = review.icon}
<div class="review-card glass-card">
{#if review.source === "Reddit"}
<div class="upvote">
<ArrowUpIcon />
<span>{review.upvotes}</span>
</div>
{:else}
<div class="stars">
{#each Array(5) as _}
<StarIcon />
{/each}
</div>
{/if}
<p class="review-text">"{review.text}"</p>
<div class="review-footer">
<span class="review-name">{review.name}</span>
<div class="review-source">
<SourceIcon />
<span>{review.source}</span>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
</div> </div>
</section> </section>
<style> <style>
.reviews-section { .reviews-section {
overflow: hidden; overflow: hidden;
padding-block: 4rem; padding-block: clamp(4rem, 9vw, 8rem);
}
.reviews-header,
.featured-wrap {
width: min(100%, var(--page-width));
margin-inline: auto;
padding-inline: var(--page-gutter);
} }
.reviews-header { .reviews-header {
max-width: 64rem; margin-bottom: clamp(2rem, 5vw, 3.5rem);
margin-inline: auto;
margin-bottom: 2.5rem;
padding-inline: 1.5rem;
} }
.section-label { .section-label {
margin-bottom: 0.75rem; width: fit-content;
color: var(--color-accent); margin-bottom: 1rem;
font-size: 0.875rem; border-radius: var(--radius-full);
font-weight: 500; padding: 0.5rem 0.875rem;
letter-spacing: 0.025em; color: var(--color-text-muted);
line-height: 1.25rem; background: var(--color-surface);
text-transform: uppercase; font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.03em;
} }
.section-heading { .section-heading {
max-width: 12ch;
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 2.25rem; font-family: var(--font-display);
font-size: clamp(2.5rem, 7vw, 4.75rem);
font-weight: 700; font-weight: 700;
line-height: 2.5rem; letter-spacing: -0.045em;
line-height: 1;
text-wrap: balance;
} }
.section-description { .section-description {
max-width: 32rem; max-width: 34rem;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 1.125rem; font-size: clamp(1rem, 2vw, 1.125rem);
line-height: 1.75rem; line-height: 1.7;
} }
.row-gap { .featured-review {
margin-bottom: 1rem; display: flex;
min-height: clamp(22rem, 42vw, 31rem);
flex-direction: column;
justify-content: space-between;
border-radius: var(--radius-xl);
padding: clamp(1.5rem, 6vw, 4rem);
background: var(--color-surface);
}
.featured-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.featured-top .upvote {
margin-bottom: 0;
}
.carousel-controls,
.carousel-dots {
display: flex;
align-items: center;
}
.carousel-controls {
gap: 0.375rem;
}
.carousel-dots {
gap: 0.375rem;
padding-inline: 0.25rem;
}
.carousel-arrow,
.carousel-dot {
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-full);
color: var(--color-text-muted);
background: var(--color-surface-highest);
transition:
color var(--motion-fast) var(--ease-standard),
background-color var(--motion-fast) var(--ease-standard);
}
.carousel-arrow {
width: 2.25rem;
height: 2.25rem;
}
.carousel-arrow :global(svg) {
width: 1rem;
height: 1rem;
}
.carousel-dot {
width: 0.5rem;
height: 0.5rem;
padding: 0;
background: var(--color-text-subtle);
}
.carousel-dot.active {
background: var(--color-text);
}
.carousel-arrow:hover,
.carousel-arrow:focus-visible,
.carousel-dot:hover,
.carousel-dot:focus-visible {
color: var(--color-text);
background: rgb(237 237 237 / 0.18);
outline: none;
}
.carousel-dot:focus-visible {
box-shadow: 0 0 0 0.25rem rgb(237 237 237 / 0.12);
}
.featured-slide {
display: grid;
flex: 1;
}
.featured-content {
display: flex;
flex-direction: column;
justify-content: space-between;
animation: featured-review-enter var(--motion-normal) var(--ease-standard) both;
}
@keyframes featured-review-enter {
from {
opacity: 0;
transform: translateY(0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.featured-review blockquote {
max-width: 24ch;
margin: 2rem 0;
font-family: var(--font-display);
font-size: clamp(1.75rem, 4.5vw, 3.25rem);
font-weight: 700;
letter-spacing: -0.035em;
line-height: 1.15;
}
.review-strip {
display: flex;
gap: 1rem;
margin-top: 1rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none;
}
.review-strip::-webkit-scrollbar {
display: none;
} }
.review-card { .review-card {
display: flex; display: flex;
width: 300px; width: min(82vw, 20rem);
min-height: 14rem;
flex-shrink: 0; flex-shrink: 0;
flex-direction: column; flex-direction: column;
padding: 1.25rem; border-radius: var(--radius-lg);
border-radius: 1rem; padding: 1.5rem;
background: var(--color-surface-high);
scroll-snap-align: start;
}
.review-card:nth-child(3n) {
background: var(--color-surface);
} }
.upvote, .upvote,
@@ -321,23 +485,29 @@
} }
.upvote { .upvote {
width: fit-content;
gap: 0.375rem; gap: 0.375rem;
margin-bottom: 0.75rem; margin-bottom: 1.25rem;
color: var(--color-accent); border-radius: var(--radius-full);
font-size: 0.875rem; padding: 0.375rem 0.625rem;
font-weight: 500; color: #ff9a7d;
line-height: 1.25rem; background: rgb(255 118 82 / 0.14);
font-size: 0.75rem;
font-weight: 800;
} }
.upvote :global(svg) { .upvote :global(svg) {
width: 1rem; width: 0.875rem;
height: 1rem; height: 0.875rem;
} }
.stars { .stars {
gap: 0.125rem; gap: 0.125rem;
margin-bottom: 0.75rem; color: var(--color-brand);
color: var(--color-accent); }
.review-card .stars {
margin-bottom: 1.25rem;
} }
.stars :global(svg), .stars :global(svg),
@@ -348,116 +518,32 @@
.review-text { .review-text {
flex: 1; flex: 1;
margin-bottom: 1rem; margin-bottom: 1.5rem;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.875rem; font-size: 0.9375rem;
line-height: 1.625; line-height: 1.65;
} }
.review-footer { .review-footer {
justify-content: space-between; justify-content: space-between;
gap: 1rem;
} }
.review-name { .review-name {
font-size: 0.875rem; font-size: 0.8125rem;
font-weight: 600; font-weight: 700;
line-height: 1.25rem;
} }
.review-source { .review-source {
flex-shrink: 0;
gap: 0.375rem; gap: 0.375rem;
color: color-mix(in srgb, var(--color-text-muted) 60%, transparent); color: var(--color-text-subtle);
font-size: 0.75rem; font-size: 0.6875rem;
line-height: 1rem; font-weight: 600;
} }
.duplicate-cards { .content-pad {
display: contents; padding-left: max(var(--page-gutter), calc((100vw - var(--page-width)) / 2 + var(--page-gutter)));
} padding-right: var(--page-gutter);
.marquee-row {
overflow: hidden;
mask-image: linear-gradient(
to right,
transparent,
black 5%,
black 95%,
transparent
);
-webkit-mask-image: linear-gradient(
to right,
transparent,
black 5%,
black 95%,
transparent
);
}
.marquee-track {
display: flex;
gap: 1rem;
width: max-content;
will-change: transform;
}
.marquee-left {
animation: scroll-left 60s linear infinite;
}
.marquee-right {
animation: scroll-right 60s linear infinite;
}
.marquee-row:hover .marquee-track {
animation-play-state: paused;
}
@keyframes scroll-left {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}
@keyframes scroll-right {
from {
transform: translateX(-50%);
}
to {
transform: translateX(0);
}
}
@media (prefers-reduced-motion: reduce) {
.marquee-track {
animation-play-state: paused;
}
}
@media (min-width: 640px) {
.reviews-section {
padding-block: 6rem;
}
.reviews-header {
margin-bottom: 4rem;
}
.review-card {
width: 340px;
}
}
@media (min-width: 768px) {
.reviews-section {
padding-block: 8rem;
}
.section-heading {
font-size: 3rem;
line-height: 1;
}
} }
</style> </style>
+83 -75
View File
@@ -249,82 +249,110 @@
<style> <style>
.screenshots-section { .screenshots-section {
overflow: hidden; overflow: hidden;
padding-block: 4rem; padding-block: clamp(4rem, 9vw, 8rem);
} }
.screenshots-header { .screenshots-header {
max-width: 64rem; max-width: 64rem;
margin-inline: auto; margin-inline: auto;
margin-bottom: 2rem; margin-bottom: clamp(2rem, 5vw, 3.5rem);
padding-inline: 1.5rem; padding-inline: 1.5rem;
} }
.section-label { .section-label {
margin-bottom: 0.75rem; width: fit-content;
color: var(--color-accent); margin-bottom: 1rem;
font-size: 0.875rem; border-radius: var(--radius-full);
font-weight: 500; padding: 0.5rem 0.875rem;
letter-spacing: 0.025em; color: var(--color-text-muted);
line-height: 1.25rem; background: var(--color-surface);
text-transform: uppercase; font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.03em;
} }
.section-heading { .section-heading {
max-width: 12ch;
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 2.25rem; font-family: var(--font-display);
font-size: clamp(2.5rem, 7vw, 4.75rem);
font-weight: 700; font-weight: 700;
line-height: 2.5rem; letter-spacing: -0.045em;
line-height: 1;
text-wrap: balance;
} }
.section-description { .section-description {
max-width: 32rem; max-width: 34rem;
margin-bottom: 1.5rem; margin-bottom: 2rem;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 1.125rem; font-size: clamp(1rem, 2vw, 1.125rem);
line-height: 1.75rem; line-height: 1.7;
} }
.screenshot-controls { .screenshot-controls {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 0.75rem;
} }
.device-tabs { .device-tabs {
display: flex; display: flex;
width: fit-content; width: fit-content;
gap: 0.25rem; max-width: 100%;
border-radius: 0.75rem; gap: var(--group-gap);
background: rgb(255 255 255 / 0.04); overflow-x: auto;
border-radius: var(--radius-full);
padding: 0.25rem; padding: 0.25rem;
background: var(--color-surface);
scrollbar-width: none;
}
.device-tabs::-webkit-scrollbar {
display: none;
} }
.device-button { .device-button {
display: flex; display: flex;
min-height: 2.75rem;
flex-shrink: 0;
align-items: center; align-items: center;
justify-content: center;
gap: 0.5rem; gap: 0.5rem;
border-radius: 0.5rem; border-radius: var(--radius-pill);
padding: 0.5rem 1rem; padding-inline: 0.875rem;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.875rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 700;
line-height: 1.25rem; transition:
transition: background-color 150ms ease, color 150ms ease; border-radius var(--motion-expressive) var(--ease-standard),
color var(--motion-fast) var(--ease-standard),
background-color var(--motion-fast) var(--ease-standard);
} }
.device-button:not(.active):hover { .device-button:not(.active):hover,
color: color-mix(in srgb, var(--color-text) 80%, transparent); .device-button:not(.active):focus-visible {
color: var(--color-text);
background: rgb(237 237 237 / 0.12);
outline: none;
} }
.device-button.active { .device-button.active {
color: var(--color-text); color: var(--color-on-primary);
background: rgb(255 255 255 / 0.1); background: var(--color-text);
}
.device-button.active:hover,
.device-button.active:focus-visible {
border-radius: var(--radius-md);
background: #fff;
outline: none;
} }
.device-button :global(svg), .device-button :global(svg),
.scroll-arrow :global(svg) { .scroll-arrow :global(svg) {
width: 1rem; width: 1.125rem;
height: 1rem; height: 1.125rem;
} }
.device-label { .device-label {
@@ -334,28 +362,34 @@
.scroll-arrows { .scroll-arrows {
display: none; display: none;
align-items: center; align-items: center;
gap: 0.25rem; gap: var(--group-gap);
margin-left: auto; margin-left: auto;
} }
.scroll-arrow { .scroll-arrow {
display: flex; display: flex;
width: 2rem; width: 2.75rem;
height: 2rem; height: 2.75rem;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 0.5rem; border-radius: var(--radius-pill);
background: rgb(255 255 255 / 0.04); color: var(--color-text-subtle);
color: color-mix(in srgb, var(--color-text-muted) 30%, transparent); background: var(--color-surface);
transition: background-color 150ms ease, color 150ms ease; transition:
border-radius var(--motion-expressive) var(--ease-standard),
color var(--motion-fast) var(--ease-standard),
background-color var(--motion-fast) var(--ease-standard);
} }
.scroll-arrow.enabled { .scroll-arrow.enabled {
color: var(--color-text); color: var(--color-text);
} }
.scroll-arrow.enabled:hover { .scroll-arrow.enabled:hover,
background: rgb(255 255 255 / 0.1); .scroll-arrow.enabled:focus-visible {
border-radius: var(--radius-md);
background: var(--color-surface-highest);
outline: none;
} }
.screenshot-panels { .screenshot-panels {
@@ -371,10 +405,11 @@
width: 100%; width: 100%;
gap: 1.25rem; gap: 1.25rem;
overflow-x: auto; overflow-x: auto;
opacity: 0;
padding-bottom: 1rem; padding-bottom: 1rem;
opacity: 0;
pointer-events: none; pointer-events: none;
transition: opacity 220ms ease; scroll-snap-type: x mandatory;
transition: opacity var(--motion-normal) var(--ease-standard);
} }
.screenshot-strip.panel-active { .screenshot-strip.panel-active {
@@ -393,8 +428,6 @@
.screenshot-frame { .screenshot-frame {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
border: 1px solid color-mix(in srgb, var(--color-border) 40%, transparent);
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.5);
} }
.phone-frame { .phone-frame {
@@ -410,60 +443,35 @@
border-radius: 0.75rem; border-radius: 0.75rem;
} }
.tv-frame {
border-width: 2px;
border-color: color-mix(in srgb, var(--color-border) 30%, transparent);
background: #000;
}
.screenshot-image { .screenshot-image {
display: block; display: block;
width: auto; width: auto;
height: 100%; height: 100%;
object-fit: contain;
} }
.content-pad { .content-pad {
/* Left padding aligns with max-w-5xl (64rem) + px-6 */
padding-left: max(1.5rem, calc((100vw - 64rem) / 2 + 1.5rem)); padding-left: max(1.5rem, calc((100vw - 64rem) / 2 + 1.5rem));
padding-right: 1.5rem; padding-right: 1.5rem;
scroll-padding-left: max(1.5rem, calc((100vw - 64rem) / 2 + 1.5rem));
} }
.scrollbar-hide { .scrollbar-hide {
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
} }
.scrollbar-hide::-webkit-scrollbar { .scrollbar-hide::-webkit-scrollbar {
display: none; display: none;
} }
@media (min-width: 640px) { @media (min-width: 560px) {
.screenshots-section {
padding-block: 6rem;
}
.screenshots-header {
margin-bottom: 3rem;
}
.section-description {
margin-bottom: 2.5rem;
}
.device-label { .device-label {
display: inline; display: inline;
} }
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.screenshots-section {
padding-block: 8rem;
}
.section-heading {
font-size: 3rem;
line-height: 1;
}
.scroll-arrows { .scroll-arrows {
display: flex; display: flex;
} }
@@ -471,11 +479,11 @@
@media (min-width: 1024px) { @media (min-width: 1024px) {
.screenshot-panels { .screenshot-panels {
min-height: calc(480px + 1rem); min-height: calc(500px + 1rem);
} }
.screenshot-item { .screenshot-item {
height: 480px; height: 500px;
} }
} }
</style> </style>
+10 -4
View File
@@ -25,15 +25,21 @@
<div <div
bind:this={el} bind:this={el}
class="{className} scroll-reveal" class="{className} scroll-reveal"
style="opacity: {visible ? 1 : 0}; transform: translateY({visible ? 0 : 24}px); transition-delay: {delay}ms;" style="opacity: {visible ? 1 : 0}; transform: translateY({visible ? 0 : 16}px); transition-delay: {delay}ms;"
> >
{@render children()} {@render children()}
</div> </div>
<style> <style>
.scroll-reveal { .scroll-reveal {
transition-property: opacity, transform; transition:
transition-duration: 600ms; opacity var(--motion-expressive) var(--ease-standard),
transition-timing-function: ease-out; transform var(--motion-expressive) var(--ease-standard);
}
@media (prefers-reduced-motion: reduce) {
.scroll-reveal {
transform: none !important;
}
} }
</style> </style>
+34 -18
View File
@@ -27,33 +27,45 @@
<style> <style>
.error-page { .error-page {
display: flex; display: flex;
min-height: 100vh; min-height: 100dvh;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 4rem 1.5rem; padding: var(--page-gutter);
} }
.error-card { .error-card {
display: flex; display: flex;
max-width: 24rem; width: min(100%, 32rem);
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
border-radius: var(--radius-xl);
padding: clamp(2rem, 8vw, 4rem);
background: var(--color-surface);
text-align: center; text-align: center;
} }
.error-logo { .error-logo {
margin-bottom: 1.5rem; display: flex;
width: 5rem;
height: 5rem;
align-items: center;
justify-content: center;
margin-bottom: 2rem;
border-radius: var(--radius-lg);
background: var(--color-surface-highest);
} }
.error-logo :global(svg) { .error-logo :global(svg) {
width: 4rem; width: 2.5rem;
height: 4rem; height: 2.5rem;
} }
.error-code { .error-code {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-size: 3rem; font-family: var(--font-display);
font-size: clamp(3.5rem, 16vw, 6rem);
font-weight: 700; font-weight: 700;
letter-spacing: -0.06em;
line-height: 1; line-height: 1;
} }
@@ -64,19 +76,23 @@
.home-link { .home-link {
display: inline-flex; display: inline-flex;
min-height: 3rem;
align-items: center; align-items: center;
gap: 0.5rem; border-radius: var(--radius-pill);
padding: 0.75rem 1.25rem; padding-inline: 1.25rem;
border-radius: 9999px; color: var(--color-on-primary);
background: var(--color-accent); background: var(--color-text);
color: #fff; font-size: 0.8125rem;
font-size: 0.875rem; font-weight: 700;
font-weight: 600; transition:
line-height: 1.25rem; border-radius var(--motion-expressive) var(--ease-standard),
transition: background-color 150ms ease; background-color var(--motion-fast) var(--ease-standard);
} }
.home-link:hover { .home-link:hover,
background: color-mix(in srgb, var(--color-accent) 90%, transparent); .home-link:focus-visible {
border-radius: var(--radius-md);
background: #fff;
outline: none;
} }
</style> </style>
-2
View File
@@ -5,7 +5,6 @@
import Reviews from '$lib/components/Reviews.svelte'; import Reviews from '$lib/components/Reviews.svelte';
import FAQ from '$lib/components/FAQ.svelte'; import FAQ from '$lib/components/FAQ.svelte';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/Footer.svelte';
import NoiseOverlay from '$lib/components/NoiseOverlay.svelte';
import { faqSchemaMainEntity } from '$lib/content/faqs'; import { faqSchemaMainEntity } from '$lib/content/faqs';
const { data } = $props(); const { data } = $props();
@@ -95,7 +94,6 @@
{@html `<script type="application/ld+json">${JSON.stringify(faqSchema)}</script>`} {@html `<script type="application/ld+json">${JSON.stringify(faqSchema)}</script>`}
</svelte:head> </svelte:head>
<NoiseOverlay />
<Hero /> <Hero />
<Features /> <Features />
<Screenshots /> <Screenshots />
+67 -81
View File
@@ -1,26 +1,49 @@
:root { :root {
color-scheme: dark;
/* Mirrors the app's mono Material 3 theme. */
--color-bg: #0e0f12; --color-bg: #0e0f12;
--color-surface: #15171c; --color-surface: #15171c;
--color-surface-hover: #1a1c22; --color-surface-high: #1d1f25;
--color-border: #2a2d35; --color-surface-highest: #25272e;
--color-accent: #ff6b45; --color-surface-hover: #2b2e35;
--color-accent-glow: #ff6b4533; --color-border: rgb(237 237 237 / 0.12);
--color-accent-dim: #ab543a;
--color-text: #ededed; --color-text: #ededed;
--color-text-muted: #9ca3af; --color-text-muted: rgb(237 237 237 / 0.6);
--color-text-subtle: rgb(237 237 237 / 0.4);
--color-brand: #ff7652;
--color-accent: var(--color-brand);
--color-accent-dim: #b9573b;
--color-on-primary: #0e0f12;
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif; --radius-xs: 0.3125rem;
--radius-sm: 0.5rem;
--radius-md: 0.75rem;
--radius-lg: 1.25rem;
--radius-xl: 2.25rem;
--radius-full: 999px;
--radius-pill: 1.5rem;
--group-gap: 0.1875rem;
--animate-fade-in-up: fade-in-up 0.6s ease-out both; --space: 0.75rem;
--animate-pulse-glow: pulse-glow 3s ease-in-out infinite; --page-width: 76rem;
--animate-float: float 6s ease-in-out infinite; --page-gutter: clamp(1rem, 4vw, 2rem);
--motion-fast: 120ms;
--motion-normal: 200ms;
--motion-slow: 300ms;
--motion-expressive: 350ms;
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--font-display: "Avenir Next", "Segoe UI Variable Display", "SF Pro Display", ui-sans-serif, system-ui, sans-serif;
--font-body: "Inter", "Segoe UI Variable Text", "SF Pro Text", ui-sans-serif, system-ui, sans-serif;
--font-utility: ui-monospace, "SFMono-Regular", "Cascadia Code", monospace;
} }
*, *,
*::before, *::before,
*::after { *::after {
box-sizing: border-box; box-sizing: border-box;
border: 0 solid;
} }
html, html,
@@ -35,18 +58,26 @@ ul {
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
background-color: var(--color-bg); scroll-padding-top: 6rem;
background: var(--color-bg);
color: var(--color-text); color: var(--color-text);
} }
body { body {
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
font-family: var(--font-sans); overflow-x: hidden;
font-family: var(--font-body);
line-height: 1.5; line-height: 1.5;
background-color: var(--color-bg); background: var(--color-bg);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
::selection {
color: var(--color-on-primary);
background: var(--color-text);
} }
a { a {
@@ -54,6 +85,11 @@ a {
text-decoration: inherit; text-decoration: inherit;
} }
a,
button {
-webkit-tap-highlight-color: transparent;
}
button, button,
input, input,
textarea, textarea,
@@ -63,6 +99,7 @@ select {
} }
button { button {
border: 0;
padding: 0; padding: 0;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
@@ -97,84 +134,33 @@ ul {
list-style: none; list-style: none;
} }
.flat-card {
background: var(--color-surface);
}
@keyframes fade-in-up { @keyframes fade-in-up {
from { from {
opacity: 0; opacity: 0;
transform: translateY(24px); transform: translateY(1rem);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }
@keyframes pulse-glow { @media (prefers-reduced-motion: reduce) {
0%, 100% { html {
opacity: 0.6; scroll-behavior: auto;
filter: blur(60px);
} }
50% {
opacity: 1;
filter: blur(80px);
}
}
@keyframes float { *,
0%, 100% { *::before,
transform: translateY(0); *::after {
} scroll-behavior: auto !important;
50% { animation-duration: 0.01ms !important;
transform: translateY(-12px); animation-iteration-count: 1 !important;
} transition-duration: 0.01ms !important;
}
@keyframes gradient-shift {
0%, 100% {
transform: translate(0, 0) scale(1);
}
33% {
transform: translate(30px, -20px) scale(1.1);
}
66% {
transform: translate(-20px, 15px) scale(0.95);
}
}
/* Glassmorphism card utility */
.glass-card {
background: color-mix(in srgb, var(--color-surface) 60%, transparent);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--color-border);
transition: box-shadow 0.3s ease, border-color 0.3s ease;
}
.glass-card:hover {
border-color: var(--color-accent-glow);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
0 0 0 1px var(--color-accent-glow),
0 0 40px -12px var(--color-accent-glow);
}
/* Noise overlay texture */
.noise-overlay {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 50;
opacity: 0.035;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 256px 256px;
}
/* Scroll indicator */
@keyframes scroll-bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(8px);
} }
} }
+66 -50
View File
@@ -165,93 +165,109 @@
<style> <style>
.privacy-article { .privacy-article {
max-width: 64rem; width: min(100%, 58rem);
margin-inline: auto; margin-inline: auto;
padding: 4rem 1.5rem; padding: clamp(2rem, 7vw, 6rem) var(--page-gutter) clamp(5rem, 10vw, 9rem);
} }
.back-link { .back-link {
display: inline-flex; display: inline-flex;
min-height: 2.75rem;
align-items: center; align-items: center;
gap: 0.625rem; gap: 0.625rem;
margin-bottom: 3rem; margin-bottom: clamp(3rem, 8vw, 6rem);
border-radius: var(--radius-full);
padding-inline: 0.875rem;
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.875rem; background: var(--color-surface);
font-weight: 500; font-size: 0.8125rem;
line-height: 1.25rem; font-weight: 700;
transition: color 150ms ease; transition:
color var(--motion-fast) var(--ease-standard),
background-color var(--motion-fast) var(--ease-standard);
} }
.back-link:hover { .back-link:hover,
.back-link:focus-visible {
color: var(--color-text); color: var(--color-text);
background: var(--color-surface-highest);
outline: none;
} }
.back-logo :global(svg) { .back-logo :global(svg) {
width: 1.5rem; width: 1.375rem;
height: 1.5rem; height: 1.375rem;
} }
.privacy-heading { .privacy-heading {
margin-bottom: 0.5rem; margin-bottom: 0.75rem;
font-size: 1.875rem; font-family: var(--font-display);
font-size: clamp(2.75rem, 9vw, 5rem);
font-weight: 700; font-weight: 700;
line-height: 2.25rem; letter-spacing: -0.05em;
line-height: 1;
} }
.last-updated { .last-updated {
margin-bottom: 3rem; margin-bottom: clamp(3rem, 7vw, 5rem);
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 0.875rem; font-family: var(--font-utility);
line-height: 1.25rem; font-size: 0.75rem;
} }
.prose { .prose {
max-width: 48rem;
color: var(--color-text-muted); color: var(--color-text-muted);
line-height: 1.75;
max-width: none;
}
.prose h2 {
color: var(--color-text);
font-size: 1.25rem;
font-weight: 600;
margin-top: 2.5rem;
margin-bottom: 0.75rem;
}
.prose h3 {
color: var(--color-text);
font-size: 1rem; font-size: 1rem;
font-weight: 600; line-height: 1.8;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
} }
.prose h2 {
margin-top: 3.5rem;
margin-bottom: 1rem;
color: var(--color-text);
font-family: var(--font-display);
font-size: clamp(1.5rem, 4vw, 2rem);
font-weight: 700;
letter-spacing: -0.025em;
line-height: 1.2;
}
.prose h3 {
margin-top: 2rem;
margin-bottom: 0.625rem;
color: var(--color-text);
font-family: var(--font-display);
font-size: 1.0625rem;
font-weight: 700;
}
.prose p { .prose p {
margin-bottom: 1rem; margin-bottom: 1.125rem;
} }
.prose ul { .prose ul {
margin-bottom: 1.25rem;
border-radius: var(--radius-lg);
padding: 1.25rem 1.25rem 1.25rem 2.75rem;
background: var(--color-surface);
list-style: disc; list-style: disc;
padding-left: 1.5rem;
margin-bottom: 1rem;
} }
.prose li {
margin-bottom: 0.25rem; .prose li + li {
margin-top: 0.5rem;
} }
.prose a { .prose a {
color: var(--color-accent); color: var(--color-text);
text-decoration: underline; text-decoration: underline;
text-underline-offset: 2px; text-decoration-color: var(--color-text-subtle);
} text-underline-offset: 0.2em;
.prose a:hover {
opacity: 0.8;
} }
@media (min-width: 640px) { .prose a:hover,
.privacy-article { .prose a:focus-visible {
padding-block: 6rem; text-decoration-color: var(--color-text);
} outline: none;
.privacy-heading {
font-size: 2.25rem;
line-height: 2.5rem;
}
} }
</style> </style>
+41 -22
View File
@@ -75,68 +75,87 @@
<style> <style>
.scan-page { .scan-page {
display: flex; display: flex;
min-height: 100vh; min-height: 100dvh;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 4rem 1.5rem; padding: var(--page-gutter);
} }
.scan-card { .scan-card {
display: flex; display: flex;
max-width: 24rem; width: min(100%, 32rem);
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
border-radius: var(--radius-xl);
padding: clamp(2rem, 8vw, 4rem);
background: var(--color-surface);
text-align: center; text-align: center;
} }
.scan-logo { .scan-logo {
margin-bottom: 1.5rem; display: flex;
width: 5rem;
height: 5rem;
align-items: center;
justify-content: center;
margin-bottom: 2rem;
border-radius: var(--radius-lg);
background: var(--color-surface-highest);
} }
.scan-logo :global(svg) { .scan-logo :global(svg) {
width: 4rem; width: 2.5rem;
height: 4rem; height: 2.5rem;
} }
.scan-heading { .scan-heading {
margin-bottom: 0.5rem; margin-bottom: 0.75rem;
font-size: 1.5rem; font-family: var(--font-display);
font-size: clamp(2rem, 8vw, 3.25rem);
font-weight: 700; font-weight: 700;
line-height: 2rem; letter-spacing: -0.045em;
line-height: 1;
} }
.scan-description { .scan-description {
max-width: 24rem;
margin-bottom: 2rem; margin-bottom: 2rem;
color: var(--color-text-muted); color: var(--color-text-muted);
line-height: 1.65;
} }
.store-buttons { .store-buttons {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 0.75rem; gap: 0.5rem;
} }
.store-button { .store-button {
display: inline-flex; display: inline-flex;
min-height: 3rem;
align-items: center; align-items: center;
gap: 0.625rem; gap: 0.625rem;
padding: 0.75rem 1.25rem; border-radius: var(--radius-pill);
border-radius: 9999px; padding-inline: 1.25rem;
background: #fff; color: var(--color-on-primary);
color: #111827; background: var(--color-text);
font-size: 0.875rem; font-size: 0.8125rem;
font-weight: 600; font-weight: 700;
line-height: 1.25rem; transition:
transition: background-color 150ms ease; border-radius var(--motion-expressive) var(--ease-standard),
background-color var(--motion-fast) var(--ease-standard);
} }
.store-button:hover { .store-button:hover,
background: #f3f4f6; .store-button:focus-visible {
border-radius: var(--radius-md);
background: #fff;
outline: none;
} }
.store-button :global(svg) { .store-button :global(svg) {
width: 1.25rem; width: 1.125rem;
height: 1.25rem; height: 1.125rem;
} }
</style> </style>