This commit is contained in:
2026-10-11 11:06:58 -07:00
parent ad9a41036f
commit f28898089e
25 changed files with 3320 additions and 453 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ body.is-404 .error-container {
body.is-404 .error-code { body.is-404 .error-code {
margin: 0 0 16px; margin: 0 0 16px;
color: var(--text-heading); color: var(--text);
font-size: clamp(74px, 14vw, 112px); font-size: clamp(74px, 14vw, 112px);
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
@@ -42,7 +42,7 @@ body.is-404 .error-code {
body.is-404 .error-title { body.is-404 .error-title {
margin: 0 0 24px; margin: 0 0 24px;
color: var(--text-heading); color: var(--text);
font-size: 18px; font-size: 18px;
font-weight: 800; font-weight: 800;
letter-spacing: 0.12em; letter-spacing: 0.12em;

View File

@@ -29,145 +29,6 @@
color: inherit; color: inherit;
} }
#search-modal {
position: fixed;
inset: 0;
z-index: 100;
display: none;
}
#search-modal.open {
display: block;
}
.search-panel {
position: relative;
width: min(640px, calc(100vw - 24px));
margin: 52px auto 0;
border-radius: 20px;
overflow: hidden;
background: var(--search-panel-bg) !important;
border: 1px solid var(--border);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.search-input-row {
display: flex;
align-items: center;
gap: 12px;
padding: 18px 20px;
}
.search-input-row .ico {
width: 18px;
height: 18px;
color: var(--muted);
}
#search-input {
flex: 1;
border: none;
outline: none;
background: transparent;
color: var(--text-heading);
font: inherit;
font-size: 18px;
}
#search-results {
max-height: 40vh;
overflow-y: auto;
padding: 0 6px 8px;
}
.search-label {
padding: 8px 14px;
font-size: 12px;
color: var(--muted);
}
.sr {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 12px;
border-radius: 12px;
}
.sr>.ico {
margin-top: 3px;
color: var(--muted);
}
.sr.on {
background: var(--hover-bg);
}
.sr-body {
min-width: 0;
}
.sr .t {
font-size: 14px;
font-weight: 600;
line-height: 1.35;
color: var(--text-heading);
}
.sr .s {
display: -webkit-box;
margin-top: 2px;
font-size: 12.5px;
line-height: 1.4;
color: var(--muted);
overflow: hidden;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.sr .p {
margin-top: 2px;
font-size: 12px;
line-height: 1.35;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sr .sep {
margin: 0 6px;
opacity: 0.7;
}
.sr .hit {
font-weight: 700;
color: var(--text-heading);
}
.search-footer {
display: flex;
gap: 18px;
padding: 12px 20px;
font-size: 12px;
color: var(--muted);
border-top: 1px solid var(--border-subtle);
}
.search-footer .close-hint {
margin-left: auto;
}
.search-footer kbd {
margin-right: 4px;
padding: 1px 5px;
border-radius: 4px;
font: 11px var(--font);
}
.code-wrap { .code-wrap {
position: relative; position: relative;
} }
@@ -254,7 +115,7 @@
} }
.cg-copy-btn:hover { .cg-copy-btn:hover {
color: var(--text-heading); color: var(--text);
} }
.code-group-panel { .code-group-panel {
@@ -456,13 +317,11 @@ main.prose blockquote p {
#search-trigger kbd { #search-trigger kbd {
display: none; display: none;
} }
}
.search-panel { main.prose h1,
margin-top: 50px; main.prose h2,
border-radius: 30px; main.prose h3,
} main.prose h4 {
cursor: pointer;
.search-footer {
display: none;
}
} }

262
assets/styling/features.css Normal file
View File

@@ -0,0 +1,262 @@
.bm-btn {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
padding: 0;
border: none;
border-radius: 50%;
background: none;
color: var(--muted);
cursor: pointer;
transition: color 0.15s ease, background 0.15s ease;
}
.bm-btn .ico {
width: 18px;
height: 18px;
}
.bm-btn:hover {
background: var(--hover-bg);
color: var(--text);
}
.bm-btn.on {
color: var(--text);
}
.bm-btn.on .ico {
fill: currentColor;
}
.personal-card {
margin: 18px 0 8px;
padding: 16px 18px;
border-radius: 22px;
background: var(--callout-note-bg);
}
.personal-card label {
display: block;
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: var(--text);
}
.pc-row {
display: flex;
gap: 8px;
}
.pc-row input {
flex: 1;
min-width: 0;
height: 36px;
padding: 0 14px;
border: 1px solid var(--border);
border-radius: 100px;
background: var(--search-bg);
color: var(--text);
font: inherit;
font-size: 13.5px;
outline: none;
}
.pc-row input.bad {
border-color: #ef4444;
}
.pc-btn {
height: 36px;
padding: 0 18px;
border: none;
border-radius: 100px;
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
font: inherit;
font-size: 13.5px;
font-weight: 600;
cursor: pointer;
}
main.prose .pc-hint {
margin: 8px 0 0;
font-size: 12.5px;
line-height: 1.5;
color: var(--muted);
}
.rec-badge {
display: inline-flex;
align-items: center;
margin-left: 8px;
padding: 2px 10px;
border-radius: 100px;
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
font-size: 11.5px;
font-weight: 700;
letter-spacing: 0.02em;
vertical-align: middle;
white-space: nowrap;
}
tbody tr.rec-row td {
background: var(--hover-bg);
}
tbody tr.rec-row td:first-child {
padding-left: 12px;
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
}
tbody tr.rec-row td:last-child {
padding-right: 12px;
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
}
#footer-note {
margin: 0 0 16px;
font-size: 13.5px;
color: var(--muted);
}
.changelog,
.contributors {
margin: 20px 0;
}
.contributors {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
main.prose .muted-line {
margin: 0;
color: var(--muted);
}
.cl-item {
padding: 16px 0;
border-bottom: 1px solid var(--border-subtle);
}
.cl-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
font-size: 12.5px;
color: var(--muted);
}
.cl-tag {
padding: 2px 10px;
border-radius: 100px;
background: var(--hover-bg);
color: var(--text);
font-weight: 600;
}
.cl-title {
margin-top: 8px;
font-size: 16px;
font-weight: 700;
color: var(--text);
}
main.prose .cl-notes {
margin: 6px 0 0;
font-size: 14px;
}
.cl-link {
display: inline-block;
margin-top: 8px;
font-size: 13px;
}
.ct-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0px 14px 0px 5px;
border-radius: 100px;
background: var(--hover-bg);
color: var(--text);
;
font-size: 13.5px;
margin-right: 10px;
height: 40px;
align-items: center;
}
.ct-chip em {
font-style: normal;
font-size: 12px;
color: var(--muted);
}
.ct-avatar {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
font-size: 12px;
font-weight: 700;
}
.ct-avatar-img {
width: 26px;
height: 26px;
}
.confetti-canvas {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 3000;
}
body:not(.is-home) main.prose {
zoom: var(--text-scale, 1);
}
body.no-deco .cobweb,
body.no-deco .spider-container,
body.no-deco .snowfall,
body.no-deco .leaf-fall,
body.no-deco .light-string {
display: none !important;
}
body.no-video .video-bg,
body.no-video .body-blurred-video,
body.no-video .header-video {
display: none !important;
}
body.no-motion *,
body.no-motion *::before,
body.no-motion *::after {
animation: none !important;
transition: none !important;
scroll-behavior: auto !important;
}
@media (max-width: 900px) {
.rec-badge {
margin-left: 0 !important;
margin-top: 10px !important;
}
}

View File

@@ -6,6 +6,7 @@
z-index: 41; z-index: 41;
backdrop-filter: blur(10px) saturate(1.2); backdrop-filter: blur(10px) saturate(1.2);
-webkit-backdrop-filter: blur(10px) saturate(1.2); -webkit-backdrop-filter: blur(10px) saturate(1.2);
background: var(--bg-card);
width: 50%; width: 50%;
border-radius: 30px; border-radius: 30px;
} }
@@ -99,11 +100,11 @@
} }
#navbar .tabs a:hover { #navbar .tabs a:hover {
color: var(--text-heading); color: var(--text);
} }
#navbar .tabs a.active { #navbar .tabs a.active {
color: var(--text-heading); color: var(--text);
font-weight: 600; font-weight: 600;
} }
@@ -155,19 +156,19 @@
} }
#sidebar li a.active { #sidebar li a.active {
color: var(--text-heading); color: var(--text);
} }
#sidebar li a:hover { #sidebar li a:hover {
background: var(--hover-bg); background: var(--hover-bg);
color: var(--text-heading); color: var(--text);
} }
#content-col { #content-col {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
display: flex; display: flex;
padding: 16px 20px 20px; padding: 16px max(20px, env(safe-area-inset-right)) 20px max(20px, env(safe-area-inset-left));
gap: 92px; gap: 92px;
max-width: 100%; max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
@@ -178,7 +179,7 @@
flex: 1; flex: 1;
min-width: 0; min-width: 0;
max-width: 720px; max-width: 720px;
margin-top: 130px; margin-top: 120px;
border-radius: 12px; border-radius: 12px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
@@ -213,7 +214,7 @@ footer#site-footer {
gap: 8px; gap: 8px;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--text-heading); color: var(--text);
transition: opacity 0.15s ease; transition: opacity 0.15s ease;
} }
@@ -248,7 +249,7 @@ footer#site-footer {
} }
.footer-socials a:hover { .footer-socials a:hover {
color: var(--text-heading); color: var(--text);
} }
.footer-icon { .footer-icon {
@@ -361,6 +362,7 @@ footer#site-footer {
border-radius: 10px; border-radius: 10px;
background: none; background: none;
color: var(--text); color: var(--text);
;
font: inherit; font: inherit;
font-size: 13.5px; font-size: 13.5px;
cursor: pointer; cursor: pointer;
@@ -397,11 +399,11 @@ footer#site-footer {
#theme-menu button.on { #theme-menu button.on {
font-weight: 600; font-weight: 600;
color: var(--text-heading); color: var(--text);
} }
#theme-menu button.on .ico { #theme-menu button.on .ico {
color: var(--text-heading); color: var(--text);
} }
#theme-menu button.on .check { #theme-menu button.on .check {
@@ -479,6 +481,7 @@ footer#site-footer {
border-radius: 8px; border-radius: 8px;
background: none; background: none;
color: var(--text); color: var(--text);
;
font: inherit; font: inherit;
font-size: 13.5px; font-size: 13.5px;
cursor: pointer; cursor: pointer;
@@ -505,15 +508,16 @@ footer#site-footer {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
color: var(--text); color: var(--text);
;
} }
#theme-seg-menu button.on span { #theme-seg-menu button.on span {
color: var(--text-heading); color: var(--text);
font-weight: 600; font-weight: 600;
} }
#theme-seg-menu button.on .ico { #theme-seg-menu button.on .ico {
color: var(--text-heading); color: var(--text);
} }
#theme-seg-menu button .check { #theme-seg-menu button .check {
@@ -526,7 +530,7 @@ footer#site-footer {
#theme-seg-menu button.on .check { #theme-seg-menu button.on .check {
visibility: visible; visibility: visible;
color: var(--text-heading); color: var(--text);
} }
#theme-seg-menu button:hover { #theme-seg-menu button:hover {
@@ -535,7 +539,7 @@ footer#site-footer {
#theme-seg-menu button.on { #theme-seg-menu button.on {
font-weight: 600; font-weight: 600;
color: var(--text-heading); color: var(--text);
} }
#theme-seg-menu .check { #theme-seg-menu .check {
@@ -564,7 +568,7 @@ footer#site-footer {
#theme-seg>button.on { #theme-seg>button.on {
background: var(--hover-bg); background: var(--hover-bg);
color: var(--text-heading); color: var(--text);
} }
#mobile-bar { #mobile-bar {
@@ -645,7 +649,7 @@ footer#site-footer {
} }
.mobile-crumbs .crumb-heading { .mobile-crumbs .crumb-heading {
color: var(--text-heading); color: var(--text);
font-weight: 600; font-weight: 600;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -667,7 +671,7 @@ footer#site-footer {
border: none; border: none;
background: var(--hover-bg); background: var(--hover-bg);
border-radius: 100px; border-radius: 100px;
color: var(--text-heading); color: var(--text);
font: inherit; font: inherit;
font-size: 15px; font-size: 15px;
cursor: pointer; cursor: pointer;
@@ -728,11 +732,11 @@ footer#site-footer {
.mt-menu .mt-item:hover { .mt-menu .mt-item:hover {
background: var(--hover-bg); background: var(--hover-bg);
color: var(--text-heading); color: var(--text);
} }
.mt-menu .mt-item.on { .mt-menu .mt-item.on {
color: var(--text-heading); color: var(--text);
font-weight: 600; font-weight: 600;
} }
@@ -754,12 +758,6 @@ footer#site-footer {
display: none; display: none;
} }
body.is-home #sidebar,
body.is-home #toc,
body.is-home #mobile-bar {
display: none !important;
}
body.is-home #layout { body.is-home #layout {
display: block; display: block;
width: 100%; width: 100%;
@@ -805,14 +803,23 @@ body.is-home main.prose {
align-items: center; align-items: center;
} }
body.is-home #toc,
body.is-home #sidebar,
body.is-home #mobile-bar {
display: none !important;
}
@media (max-width: 900px) { @media (max-width: 900px) {
:root { :root {
--header-height: 60px; --header-height: 60px;
} }
#header-container { #header-container {
width: 91%; width: 90%;
border-radius: 25px; border-radius: 30px;
backdrop-filter: blur(15px) saturate(1.2);
-webkit-backdrop-filter: blur(15px) saturate(1.2);
top: max(6px, env(safe-area-inset-top));
} }
#navbar .tabs, #navbar .tabs,
@@ -865,7 +872,7 @@ body.is-home main.prose {
} }
#sidebar #theme-seg>button.on { #sidebar #theme-seg>button.on {
color: var(--text-heading); color: var(--text);
font-weight: 900; font-weight: 900;
} }
@@ -939,6 +946,7 @@ body.is-home main.prose {
height: 60px; height: 60px;
margin: 0 -16px 12px; margin: 0 -16px 12px;
padding: 0 16px; padding: 0 16px;
margin-top: max(6px, env(safe-area-inset-top));
} }
#sidebar .side-head .brand { #sidebar .side-head .brand {
@@ -958,4 +966,44 @@ body.is-home main.prose {
align-items: center; align-items: center;
margin-right: 4px; margin-right: 4px;
} }
}
@media (orientation: landscape) and (max-height: 550px) {
:root {
--header-height: 52px;
}
#header-container {
top: max(6px, env(safe-area-inset-top));
width: min(600px, 88vw);
border-radius: 20px;
}
#navbar .nav-top {
height: 46px;
}
#navbar .tabs {
height: 38px;
}
#navbar .tabs a {
padding: 4px 10px;
font-size: 13px;
}
#main-wrap {
margin-top: 100px;
}
#sidebar {
top: calc(var(--header-height) + 16px);
max-height: calc(100vh - var(--header-height) - 16px);
margin-top: 10px;
}
#mobile-bar {
padding: 6px 14px;
font-size: 13px;
}
} }

858
assets/styling/overlay.css Normal file
View File

@@ -0,0 +1,858 @@
#search-modal {
position: fixed;
inset: 0;
z-index: 100;
display: none;
}
#search-modal.open {
display: block;
}
.search-panel {
position: relative;
width: min(640px, calc(100vw - 24px));
margin: 52px auto 0;
border-radius: 20px;
overflow: hidden;
background: var(--search-panel-bg) !important;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.search-input-row {
display: flex;
align-items: center;
gap: 12px;
padding: 18px 20px;
}
.search-input-row .ico {
width: 18px;
height: 18px;
color: var(--muted);
}
#search-input {
flex: 1;
border: none;
outline: none;
background: transparent;
color: var(--text);
font: inherit;
font-size: 18px;
}
#search-results {
max-height: 40vh;
overflow-y: auto;
padding: 0 6px 8px;
}
.search-label {
padding: 8px 14px;
font-size: 12px;
color: var(--muted);
}
.sr {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 12px;
border-radius: 12px;
}
.sr>.ico {
margin-top: 3px;
color: var(--muted);
}
.sr.on {
background: var(--hover-bg);
}
.sr-body {
min-width: 0;
}
.sr .t {
font-size: 14px;
font-weight: 600;
line-height: 1.35;
color: var(--text);
}
.sr .s {
display: -webkit-box;
margin-top: 2px;
font-size: 12.5px;
line-height: 1.4;
color: var(--muted);
overflow: hidden;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.sr .p {
margin-top: 2px;
font-size: 12px;
line-height: 1.35;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sr .sep {
margin: 0 6px;
opacity: 0.7;
}
.sr .hit {
font-weight: 700;
color: var(--text);
}
.search-footer {
display: flex;
gap: 18px;
padding: 12px 20px;
font-size: 12px;
color: var(--muted);
border-top: 1px solid var(--border-subtle);
}
.search-footer .close-hint {
margin-left: auto;
}
.search-footer kbd {
margin-right: 4px;
padding: 1px 5px;
border-radius: 4px;
font: 11px var(--font);
}
@media (max-width: 900px) {
.search-panel {
margin-top: 50px;
border-radius: 30px;
}
.search-footer {
display: none;
}
}
.sheet-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}
.sheet-overlay.active {
opacity: 1;
pointer-events: auto;
}
.sheet-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.sheet-panel {
position: relative;
width: min(500px, calc(100vw - 32px));
background: var(--bg-modal);
border-radius: 28px;
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
padding: 32px 28px 28px;
box-sizing: border-box;
transform: scale(0.92) translateY(16px);
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
display: flex;
flex-direction: column;
}
.sheet-overlay.active .sheet-panel {
transform: scale(1) translateY(0);
}
.sheet-handle-bar {
display: none;
}
.sheet-title {
font-size: 22px;
font-weight: 700;
color: var(--text);
margin: 0 0 10px;
line-height: 1.3;
text-align: center;
}
.sheet-desc {
font-size: 14.5px;
color: var(--muted);
line-height: 1.6;
margin: 0 0 24px;
text-align: center;
}
.sheet-actions {
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
}
.sheet-btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 22px;
border-radius: 100px;
background: var(--btn-primary-bg);
color: var(--btn-primary-text) !important;
font-size: 14px;
font-weight: 600;
text-decoration: none !important;
transition: transform 0.15s ease, opacity 0.15s ease;
}
.sheet-btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.sheet-btn-never {
padding: 10px 18px;
border: none;
background: none;
color: var(--muted);
font: inherit;
font-size: 13.5px;
font-weight: 500;
cursor: pointer;
border-radius: 100px;
transition: color 0.15s ease, background 0.15s ease;
}
.sheet-btn-never:hover {
color: var(--text);
background: var(--hover-bg);
}
@media (max-width: 900px) {
.sheet-overlay {
align-items: flex-end;
}
.sheet-panel {
width: 100%;
max-width: 100%;
border-radius: 28px 28px 0 0;
border-bottom: none;
padding: 14px 22px 32px;
transform: translateY(100%);
transition: transform 0.45s cubic-bezier(0.32, 0.72, 0, 1);
touch-action: none;
max-height: 90vh;
}
.sheet-panel.dragging {
transition: none !important;
}
.sheet-overlay.active .sheet-panel {
transform: translateY(0);
}
.sheet-handle-bar {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 6px 0 16px;
cursor: grab;
}
.sheet-handle {
width: 40px;
height: 5px;
border-radius: 999px;
background: var(--muted);
opacity: 0.45;
}
.sheet-actions {
flex-direction: column;
width: 100%;
gap: 8px;
}
.sheet-btn-primary {
width: 100%;
}
.sheet-btn-never {
width: 100%;
text-align: center;
}
}
@media (orientation: landscape) and (max-height: 550px) {
.sheet-panel {
max-height: 94vh;
overflow-y: auto;
padding: 10px max(24px, env(safe-area-inset-right)) 16px max(24px, env(safe-area-inset-left));
}
.sheet-title {
font-size: 18px;
margin-bottom: 6px;
}
.sheet-desc {
font-size: 13px;
margin-bottom: 12px;
line-height: 1.45;
}
.sheet-actions {
flex-direction: row;
gap: 10px;
}
.sheet-btn-primary,
.sheet-btn-never {
width: auto;
padding: 8px 18px;
font-size: 13px;
}
}
.image-lightbox {
position: fixed;
inset: 0;
z-index: 2000;
background: transparent;
backdrop-filter: blur(0px);
-webkit-backdrop-filter: blur(0px);
opacity: 0;
pointer-events: none;
transition: background 0.42s cubic-bezier(0.2, 0.9, 0.3, 1),
backdrop-filter 0.42s cubic-bezier(0.2, 0.9, 0.3, 1),
opacity 0.25s ease;
}
.image-lightbox.active {
background: var(--bg-modal);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
opacity: 1;
pointer-events: auto;
}
.lightbox-controls {
position: fixed;
top: max(16px, env(safe-area-inset-top));
right: max(16px, env(safe-area-inset-right));
display: flex;
align-items: center;
gap: 3px;
z-index: 2003;
background: var(--bg-menu);
border-radius: 100px;
padding: 3px 6px;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
opacity: 0;
transform: translateY(-8px) scale(0.96);
transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1) 0.08s,
transform 0.3s cubic-bezier(0.16, 1, 0.3, 1) 0.08s;
}
.image-lightbox.active .lightbox-controls {
opacity: 1;
transform: translateY(0) scale(1);
}
.lightbox-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: transparent;
color: var(--muted);
font-family: var(--font);
font-size: 16px;
line-height: 1;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.lightbox-btn:hover {
background: var(--hover-bg);
color: var(--text);
}
.lightbox-btn svg {
display: block;
width: 16px;
height: 16px;
fill: currentColor;
flex-shrink: 0;
}
.lightbox-img {
position: fixed;
object-fit: contain;
border-radius: 16px;
transition: top 0.44s cubic-bezier(0.2, 0.9, 0.3, 1),
left 0.44s cubic-bezier(0.2, 0.9, 0.3, 1),
width 0.44s cubic-bezier(0.2, 0.9, 0.3, 1),
height 0.44s cubic-bezier(0.2, 0.9, 0.3, 1),
border-radius 0.44s cubic-bezier(0.2, 0.9, 0.3, 1),
transform 0.25s ease;
z-index: 2002;
transform-origin: center center;
will-change: top, left, width, height, transform;
}
main.prose img {
cursor: zoom-in;
transition: opacity 0.2s ease, transform 0.2s ease;
}
main.prose img:hover {
opacity: 0.95;
}
#settings-trigger,
.side-settings {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
border: none;
border-radius: 10px;
background: none;
color: var(--muted);
cursor: pointer;
}
#settings-trigger .ico,
.side-settings .ico {
width: 18px;
height: 18px;
}
#settings-trigger:hover,
.side-settings:hover {
background: var(--hover-bg);
color: var(--text);
}
.settings-panel {
position: relative;
display: flex;
flex-direction: column;
padding: 0;
overflow: hidden;
}
.st-top,
.st-bottom {
position: absolute;
left: 0;
right: 0;
z-index: 3;
isolation: isolate;
}
.st-top::before,
.st-bottom::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
backdrop-filter: blur(18px) saturate(1.2);
-webkit-backdrop-filter: blur(18px) saturate(1.2);
}
.st-top {
top: 0;
padding: 26px 28px 30px;
}
.st-top::before {
background: linear-gradient(to bottom, var(--bg-menu) 40%, transparent);
background: linear-gradient(to bottom, color-mix(in srgb, var(--bg-menu) 94%, transparent), color-mix(in srgb, var(--bg-menu) 80%, transparent) 60%, transparent);
-webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent);
mask-image: linear-gradient(to bottom, #000 55%, transparent);
}
.st-top .sheet-title {
margin: 0 0 6px;
font-size: 20px;
}
.st-top .sheet-desc {
margin: 0;
font-size: 13.5px;
line-height: 1.5;
}
.st-bottom {
bottom: 0;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
padding: 36px 28px calc(20px + env(safe-area-inset-bottom));
}
.st-bottom::before {
background: linear-gradient(to top, var(--bg-menu) 40%, transparent);
background: linear-gradient(to top, color-mix(in srgb, var(--bg-menu) 94%, transparent), color-mix(in srgb, var(--bg-menu) 80%, transparent) 60%, transparent);
-webkit-mask-image: linear-gradient(to top, #000 55%, transparent);
mask-image: linear-gradient(to top, #000 55%, transparent);
}
.st-bottom .sheet-btn-primary {
border: none;
font-family: inherit;
cursor: pointer;
}
.st-bottom .sheet-btn-primary.done {
opacity: 0.85;
}
.st-scroll {
flex: 1;
min-height: 0;
padding: var(--st-top, 110px) 28px var(--st-bottom, 84px);
overflow-y: auto;
overscroll-behavior: contain;
touch-action: pan-y;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.st-group {
margin-bottom: 18px;
}
.st-heading {
margin: 0 0 8px 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.st-card {
border-radius: 20px;
background: var(--hover-bg);
overflow: hidden;
}
.st-row,
.st-field {
display: flex;
gap: 14px;
padding: 14px 16px;
border-top: 1px solid var(--border-subtle);
cursor: default;
}
.st-card>:first-child {
border-top: none;
}
.st-row {
align-items: center;
justify-content: space-between;
}
.st-row.stack {
flex-direction: column;
align-items: stretch;
}
label.st-row {
cursor: pointer;
}
.st-field {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.st-text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.st-label {
font-size: 14px;
font-weight: 600;
line-height: 1.35;
color: var(--text);
}
.st-hint {
font-size: 12.5px;
line-height: 1.45;
color: var(--muted);
}
.st-hint.err {
color: #ef4444;
}
.st-input {
width: 100%;
height: 40px;
padding: 0 16px;
border: none;
border-radius: 100px;
background: var(--search-bg);
color: var(--text);
font: inherit;
font-size: 14px;
outline: none;
transition: border-color 0.15s ease;
}
.st-input::placeholder {
color: var(--muted);
opacity: 0.8;
}
.st-input:focus {
border-color: var(--text);
}
.st-input.bad {
border-color: #ef4444;
}
.st-swatches {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.st-swatch {
display: block;
position: relative;
flex-shrink: 0;
width: 26px;
height: 26px;
padding: 0;
border: none;
border-radius: 50%;
background: var(--sw, var(--hover-bg));
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.st-swatch:hover {
transform: scale(1.12);
}
.st-swatch.on {
box-shadow: 0 0 0 2px var(--bg-menu), 0 0 0 4px var(--text);
;
}
.st-swatch.reset {
background: linear-gradient(135deg, transparent 46%, var(--muted) 46%, var(--muted) 54%, transparent 54%), var(--search-bg);
box-shadow: inset 0 0 0 1.5px var(--muted);
}
.st-swatch.reset.on {
box-shadow: inset 0 0 0 1.5px var(--muted), 0 0 0 2px var(--bg-menu), 0 0 0 4px var(--text);
;
}
.st-swatch.custom {
background: conic-gradient(from 0deg, #ff5c5c, #f4b942, #5fd68f, #8ad1ff, #c084fc, #ff5c5c);
}
.st-swatch.custom input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
padding: 0;
border: none;
opacity: 0;
cursor: pointer;
}
.st-seg {
display: flex;
gap: 2px;
padding: 3px;
border-radius: 100px;
background: var(--search-bg);
}
.st-seg button {
flex: 1;
height: 32px;
padding: 0 10px;
border: none;
border-radius: 100px;
background: none;
color: var(--muted);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.st-seg button:hover {
color: var(--text);
}
.st-seg button.on {
background: var(--btn-primary-bg);
color: var(--btn-primary-text);
}
.st-switch {
position: relative;
flex-shrink: 0;
width: 44px;
height: 26px;
}
.st-switch input {
position: absolute;
inset: 0;
z-index: 1;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.st-track {
position: absolute;
inset: 0;
border: 1px solid var(--border);
border-radius: 100px;
background: var(--search-bg);
transition: background 0.2s ease, border-color 0.2s ease;
}
.st-track::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--muted);
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), background 0.2s ease;
}
.st-switch input:checked+.st-track {
border-color: transparent;
background: var(--btn-primary-bg);
}
.st-switch input:checked+.st-track::after {
transform: translateX(18px);
background: var(--btn-primary-text);
}
.st-switch input:focus-visible+.st-track {
outline: 2px solid var(--text);
outline-offset: 2px;
}
@media (min-width: 901px) {
.settings-panel {
width: min(540px, calc(100vw - 32px));
max-height: min(88vh, 780px);
}
}
@media (max-width: 900px) {
.st-top {
padding: 8px 22px 28px;
}
.st-scroll {
padding-left: 22px;
padding-right: 22px;
}
.st-bottom {
padding: 36px 22px calc(18px + env(safe-area-inset-bottom));
}
.st-bottom .sheet-btn-primary {
flex: 1;
width: auto;
}
.st-bottom .sheet-btn-never {
flex: 0 0 auto;
width: auto;
}
}
@media (orientation: landscape) and (max-height: 550px) {
.settings-panel {
max-height: 94vh;
}
.st-top {
padding: 14px 24px 22px;
}
.st-top .sheet-title {
font-size: 17px;
margin-bottom: 0;
}
.st-top .sheet-desc {
display: none;
}
}

View File

@@ -11,6 +11,10 @@ main.prose .eyebrow {
font-weight: 600; font-weight: 600;
margin: 0 0 8px; margin: 0 0 8px;
color: var(--text); color: var(--text);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
} }
main.prose h1 { main.prose h1 {
@@ -18,7 +22,7 @@ main.prose h1 {
font-weight: 700; font-weight: 700;
letter-spacing: 0; letter-spacing: 0;
margin: 0 0 6px; margin: 0 0 6px;
color: var(--text-heading); color: var(--text);
} }
main.prose .page-description { main.prose .page-description {
@@ -48,21 +52,22 @@ main.prose h3 {
main.prose h2, main.prose h2,
main.prose h3 { main.prose h3 {
scroll-margin-top: 130px; scroll-margin-top: 130px;
color: var(--text-heading); color: var(--text);
} }
main.prose p, main.prose p,
main.prose li { main.prose li {
color: var(--text); color: var(--muted);
line-height: 1.75; line-height: 1.75;
} }
main.prose strong { main.prose strong {
color: var(--text-heading); color: var(--text);
} }
main.prose a { main.prose a {
color: var(--text); color: var(--text);
;
font-weight: 600; font-weight: 600;
text-decoration: underline; text-decoration: underline;
} }
@@ -93,7 +98,7 @@ main.prose hr {
main.prose code { main.prose code {
background: var(--code-inline-bg); background: var(--code-inline-bg);
color: var(--code-inline-text); color: var(--text);
font-family: var(--mono); font-family: var(--mono);
font-size: 13px; font-size: 13px;
padding: 2px 6px; padding: 2px 6px;
@@ -162,6 +167,7 @@ td {
vertical-align: top; vertical-align: top;
border-bottom: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle);
color: var(--text); color: var(--text);
;
} }
tbody tr:last-child td { tbody tr:last-child td {
@@ -246,7 +252,7 @@ aside#toc .toc-title {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: 20px; line-height: 20px;
color: var(--text-heading); color: var(--text);
margin-bottom: 2px; margin-bottom: 2px;
} }
@@ -274,11 +280,11 @@ aside#toc li a {
} }
aside#toc li a:hover { aside#toc li a:hover {
color: var(--text-heading); color: var(--text);
} }
aside#toc li a.active { aside#toc li a.active {
color: var(--text-heading); color: var(--text);
font-weight: 600; font-weight: 600;
} }
@@ -286,4 +292,10 @@ aside#toc li a.active {
aside#toc { aside#toc {
display: none; display: none;
} }
}
@supports (-webkit-touch-callout: none) {
#main-wrap {
margin-top: max(180px, env(safe-area-inset-top)) !important;
}
} }

View File

@@ -4,7 +4,7 @@
:root[data-theme="dark"] { :root[data-theme="dark"] {
--bg: #000000; --bg: #000000;
--text: #f3f3f3; --text: #f3f3f3;
--text-heading: #ffffff; --text: #ffffff;
--muted: #a3a3a3; --muted: #a3a3a3;
--border: #2b2b2b98; --border: #2b2b2b98;
--bg-elevated: rgba(255, 255, 255, 0.08); --bg-elevated: rgba(255, 255, 255, 0.08);
@@ -38,6 +38,9 @@
--spider-display: none; --spider-display: none;
--cobweb-display: none; --cobweb-display: none;
--spider-thread: rgba(255, 255, 255, 0.3); --spider-thread: rgba(255, 255, 255, 0.3);
--snow-display: none;
--leaf-display: none;
--lights-display: none;
--hl-comment: #5c6370; --hl-comment: #5c6370;
--hl-punct: #abb2bf; --hl-punct: #abb2bf;
@@ -71,7 +74,7 @@
:root[data-theme="light"] { :root[data-theme="light"] {
--bg: #ffffff; --bg: #ffffff;
--text: #0b0b0c; --text: #0b0b0c;
--text-heading: #111827; --text: #111827;
--muted: #6b7280; --muted: #6b7280;
--border: #e5e7eb; --border: #e5e7eb;
--bg-elevated: rgba(0, 0, 0, 0.05); --bg-elevated: rgba(0, 0, 0, 0.05);
@@ -82,7 +85,7 @@
--code-inline-text: #0b0b0c; --code-inline-text: #0b0b0c;
--code-text: #24292e; --code-text: #24292e;
--bg-menu: #ffffff; --bg-menu: #ffffff;
--bg-modal: rgba(240, 240, 240, 0.7); --bg-modal: rgba(240, 240, 240);
--hover-bg: rgba(0, 0, 0, 0.05); --hover-bg: rgba(0, 0, 0, 0.05);
--search-bg: rgba(0, 0, 0, 0.05); --search-bg: rgba(0, 0, 0, 0.05);
--search-panel-bg: rgba(255, 255, 255, 0.85); --search-panel-bg: rgba(255, 255, 255, 0.85);
@@ -126,7 +129,7 @@
:root[data-theme="midnight"] { :root[data-theme="midnight"] {
--bg: #070913; --bg: #070913;
--text: #e2e8f0; --text: #e2e8f0;
--text-heading: #f8fafc; --text: #f8fafc;
--muted: #7f8ea3; --muted: #7f8ea3;
--border: #1a2234; --border: #1a2234;
--bg-elevated: rgba(30, 41, 59, 0.5); --bg-elevated: rgba(30, 41, 59, 0.5);
@@ -181,7 +184,7 @@
:root[data-theme="nord"] { :root[data-theme="nord"] {
--bg: #2e3440; --bg: #2e3440;
--text: #eceff4; --text: #eceff4;
--text-heading: #88c0d0; --text: #88c0d0;
--muted: #d8dee9; --muted: #d8dee9;
--border: #3b4252; --border: #3b4252;
--bg-elevated: rgba(67, 76, 94, 0.5); --bg-elevated: rgba(67, 76, 94, 0.5);
@@ -236,7 +239,7 @@
:root[data-theme="dracula"] { :root[data-theme="dracula"] {
--bg: #282a36; --bg: #282a36;
--text: #f8f8f2; --text: #f8f8f2;
--text-heading: #bd93f9; --text: #bd93f9;
--muted: #6272a4; --muted: #6272a4;
--border: #44475a; --border: #44475a;
--bg-elevated: rgba(68, 71, 90, 0.5); --bg-elevated: rgba(68, 71, 90, 0.5);
@@ -291,7 +294,7 @@
:root[data-theme="forest"] { :root[data-theme="forest"] {
--bg: #0d1612; --bg: #0d1612;
--text: #e2ede5; --text: #e2ede5;
--text-heading: #86efac; --text: #86efac;
--muted: #739481; --muted: #739481;
--border: #1a2f24; --border: #1a2f24;
--bg-elevated: rgba(34, 76, 56, 0.35); --bg-elevated: rgba(34, 76, 56, 0.35);
@@ -346,7 +349,7 @@
:root[data-theme="sunset"] { :root[data-theme="sunset"] {
--bg: #180d18; --bg: #180d18;
--text: #fce7f3; --text: #fce7f3;
--text-heading: #fb7185; --text: #fb7185;
--muted: #a77c98; --muted: #a77c98;
--border: #3b1d36; --border: #3b1d36;
--bg-elevated: rgba(80, 30, 70, 0.35); --bg-elevated: rgba(80, 30, 70, 0.35);
@@ -401,7 +404,7 @@
:root[data-theme="cyberpunk"] { :root[data-theme="cyberpunk"] {
--bg: #050508; --bg: #050508;
--text: #00ffcc; --text: #00ffcc;
--text-heading: #ffe600; --text: #ffe600;
--muted: #5e7a75; --muted: #5e7a75;
--border: #1a2a26; --border: #1a2a26;
--bg-elevated: rgba(0, 255, 204, 0.08); --bg-elevated: rgba(0, 255, 204, 0.08);
@@ -453,61 +456,6 @@
--hl-inserted-bg: rgba(0, 255, 204, 0.2); --hl-inserted-bg: rgba(0, 255, 204, 0.2);
} }
:root[data-theme="halloween"] {
--bg: #0a0604;
--text: #f5ebe6;
--text-heading: #ff7518;
--muted: #a37559;
--border: #3d1c0c;
--bg-elevated: rgba(255, 117, 24, 0.12);
--bg-card: rgba(28, 14, 8, 0.6);
--bg-card-hover: rgba(46, 21, 10, 0.85);
--bg-code: #050201;
--code-inline-bg: rgba(255, 117, 24, 0.15);
--code-inline-text: #ff944d;
--code-text: #fed7aa;
--bg-menu: #0e0704;
--bg-modal: rgba(10, 6, 4, 0.92);
--hover-bg: rgba(255, 117, 24, 0.12);
--search-bg: rgba(255, 117, 24, 0.08);
--search-panel-bg: rgba(14, 7, 4, 0.95);
--code-group-bg: #1c0e07;
--code-group-panel-bg: #0a0604;
--code-tab-active: #ff7518;
--footer-border: #3d1c0c;
--border-subtle: rgba(255, 117, 24, 0.15);
--btn-primary-bg: #ff7518;
--btn-primary-text: #000000;
--btn-home-bg: #ff7518;
--btn-home-text: #000000;
--callout-note-bg: rgba(255, 117, 24, 0.14);
--callout-info-bg: rgba(168, 85, 247, 0.15);
--callout-warning-bg: rgba(255, 154, 77, 0.18);
--callout-warning-text: #ffaa5e;
--body-radial-tint: rgba(10, 6, 4, 0.85);
--skeleton-pulse-1: rgba(255, 117, 24, 0.06);
--skeleton-pulse-2: rgba(255, 117, 24, 0.18);
--spider-display: block;
--cobweb-display: block;
--spider-thread: rgba(255, 154, 77, 0.4);
--hl-comment: #7c4f34;
--hl-punct: #fed7aa;
--hl-func: #ff944d;
--hl-keyword: #a855f7;
--hl-string: #84cc16;
--hl-var: #ff7518;
--hl-op: #a855f7;
--hl-num: #f59e0b;
--hl-param: #ffedd5;
--hl-class: #c084fc;
--hl-tag: #ff7518;
--hl-deleted-color: #ef4444;
--hl-deleted-bg: rgba(239, 68, 68, 0.2);
--hl-inserted-color: #84cc16;
--hl-inserted-bg: rgba(132, 204, 22, 0.2);
}
body.banner-dismissed { body.banner-dismissed {
--header-height: 109px; --header-height: 109px;
} }
@@ -526,14 +474,18 @@ html {
body { body {
min-height: 100%; min-height: 100%;
min-height: -webkit-fill-available;
margin: 0; margin: 0;
font-family: var(--font); font-family: var(--font);
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
;
line-height: 1.65; line-height: 1.65;
overflow-x: hidden; overflow-x: hidden;
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
} }
html::-webkit-scrollbar, html::-webkit-scrollbar,
@@ -564,101 +516,4 @@ body::before {
a { a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
}
.cobweb {
display: var(--cobweb-display);
position: fixed;
width: 160px;
height: 160px;
pointer-events: none;
z-index: 35;
opacity: 0.8;
}
.cobweb-top-left {
top: 0;
left: 0;
transform: scaleX(-1);
}
.cobweb-top-right {
top: 0;
right: 0;
}
.cobweb-media {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.spider-container {
display: var(--spider-display);
position: fixed;
top: 0;
right: 12%;
pointer-events: none;
z-index: 36;
animation: spiderDescend 10s ease-in-out infinite;
}
.spider-thread {
width: 1.5px;
height: 100%;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
background: var(--spider-thread);
}
.spider-body {
position: relative;
width: 48px;
height: 48px;
margin-top: 100px;
animation: spiderSway 3s ease-in-out infinite alternate;
}
.spider-media {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
scale: 2;
padding-top: 20px;
}
@keyframes spiderDescend {
0% {
transform: translateY(-80px);
}
45% {
transform: translateY(240px);
}
55% {
transform: translateY(240px);
}
90% {
transform: translateY(10px);
}
100% {
transform: translateY(-80px);
}
}
@keyframes spiderSway {
0% {
transform: rotate(-10deg);
}
100% {
transform: rotate(10deg);
}
} }

574
assets/styling/themes.css Normal file
View File

@@ -0,0 +1,574 @@
:root[data-theme="halloween"] {
--bg: #0a0604;
--text: #f5ebe6;
--text: #ff7518;
--muted: #a37559;
--border: #3d1c0c;
--bg-elevated: rgba(255, 117, 24, 0.12);
--bg-card: rgba(28, 14, 8, 0.6);
--bg-card-hover: rgba(46, 21, 10, 0.85);
--bg-code: #050201;
--code-inline-bg: rgba(255, 117, 24, 0.15);
--code-inline-text: #ff944d;
--code-text: #fed7aa;
--bg-menu: #0e0704;
--bg-modal: rgba(10, 6, 4, 0.92);
--hover-bg: rgba(255, 117, 24, 0.12);
--search-bg: rgba(255, 117, 24, 0.08);
--search-panel-bg: rgba(14, 7, 4, 0.95);
--code-group-bg: #1c0e07;
--code-group-panel-bg: #0a0604;
--code-tab-active: #ff7518;
--footer-border: #3d1c0c;
--border-subtle: rgba(255, 117, 24, 0.15);
--btn-primary-bg: #ff7518;
--btn-primary-text: #000000;
--btn-home-bg: #ff7518;
--btn-home-text: #000000;
--callout-note-bg: rgba(255, 117, 24, 0.14);
--callout-info-bg: rgba(168, 85, 247, 0.15);
--callout-warning-bg: rgba(255, 154, 77, 0.18);
--callout-warning-text: #ffaa5e;
--body-radial-tint: rgba(10, 6, 4, 0.85);
--skeleton-pulse-1: rgba(255, 117, 24, 0.06);
--skeleton-pulse-2: rgba(255, 117, 24, 0.18);
--spider-display: block;
--cobweb-display: block;
--spider-thread: rgba(255, 154, 77, 0.4);
--hl-comment: #7c4f34;
--hl-punct: #fed7aa;
--hl-func: #ff944d;
--hl-keyword: #a855f7;
--hl-string: #84cc16;
--hl-var: #ff7518;
--hl-op: #a855f7;
--hl-num: #f59e0b;
--hl-param: #ffedd5;
--hl-class: #c084fc;
--hl-tag: #ff7518;
--hl-deleted-color: #ef4444;
--hl-deleted-bg: rgba(239, 68, 68, 0.2);
--hl-inserted-color: #84cc16;
--hl-inserted-bg: rgba(132, 204, 22, 0.2);
}
:root[data-theme="christmas"] {
--bg: #07110c;
--text: #f4efe6;
--text: #ff5c5c;
--muted: #8fa89a;
--border: #1f3a2b;
--bg-elevated: rgba(255, 92, 92, 0.1);
--bg-card: rgba(10, 26, 18, 0.6);
--bg-card-hover: rgba(18, 44, 30, 0.85);
--bg-code: #040a07;
--code-inline-bg: rgba(245, 196, 81, 0.14);
--code-inline-text: #f5c451;
--code-text: #e8f5e9;
--bg-menu: #09150e;
--bg-modal: rgba(7, 17, 12, 0.9);
--hover-bg: rgba(255, 92, 92, 0.12);
--search-bg: rgba(245, 196, 81, 0.08);
--search-panel-bg: rgba(9, 21, 14, 0.95);
--code-group-bg: #11261a;
--code-group-panel-bg: #07110c;
--code-tab-active: #ff5c5c;
--footer-border: #1f3a2b;
--border-subtle: rgba(245, 196, 81, 0.14);
--btn-primary-bg: #d92d3a;
--btn-primary-text: #ffffff;
--btn-home-bg: #d92d3a;
--btn-home-text: #ffffff;
--callout-note-bg: rgba(47, 191, 113, 0.14);
--callout-info-bg: rgba(245, 196, 81, 0.14);
--callout-warning-bg: rgba(255, 92, 92, 0.18);
--callout-warning-text: #ff9a9a;
--body-radial-tint: rgba(7, 17, 12, 0.85);
--skeleton-pulse-1: rgba(245, 196, 81, 0.06);
--skeleton-pulse-2: rgba(245, 196, 81, 0.16);
--snow-display: block;
--lights-display: block;
--hl-comment: #5f8a72;
--hl-punct: #e8f5e9;
--hl-func: #f5c451;
--hl-keyword: #ff6b6b;
--hl-string: #5fd68f;
--hl-var: #ff9f68;
--hl-op: #ff6b6b;
--hl-num: #f5c451;
--hl-param: #fdf2d0;
--hl-class: #7fe3b0;
--hl-tag: #ff6b6b;
--hl-deleted-color: #ff6b6b;
--hl-deleted-bg: rgba(255, 107, 107, 0.18);
--hl-inserted-color: #5fd68f;
--hl-inserted-bg: rgba(95, 214, 143, 0.18);
}
:root[data-theme="autumn"] {
--bg: #1a0f08;
--text: #f7ead7;
--text: #f4b942;
--muted: #a68a6d;
--border: #3a2412;
--bg-elevated: rgba(244, 185, 66, 0.1);
--bg-card: rgba(40, 23, 11, 0.6);
--bg-card-hover: rgba(58, 36, 18, 0.85);
--bg-code: #0e0703;
--code-inline-bg: rgba(244, 185, 66, 0.14);
--code-inline-text: #f4b942;
--code-text: #fde7c0;
--bg-menu: #1a0f08;
--bg-modal: rgba(26, 15, 8, 0.9);
--hover-bg: rgba(244, 185, 66, 0.12);
--search-bg: rgba(244, 185, 66, 0.08);
--search-panel-bg: rgba(26, 15, 8, 0.95);
--code-group-bg: #28170b;
--code-group-panel-bg: #1a0f08;
--code-tab-active: #f4b942;
--footer-border: #3a2412;
--border-subtle: rgba(244, 185, 66, 0.14);
--btn-primary-bg: #f4b942;
--btn-primary-text: #1a0f08;
--btn-home-bg: #f4b942;
--btn-home-text: #1a0f08;
--callout-note-bg: rgba(200, 69, 44, 0.16);
--callout-info-bg: rgba(132, 160, 60, 0.16);
--callout-warning-bg: rgba(244, 185, 66, 0.18);
--callout-warning-text: #f8cf7a;
--body-radial-tint: rgba(26, 15, 8, 0.85);
--skeleton-pulse-1: rgba(244, 185, 66, 0.06);
--skeleton-pulse-2: rgba(244, 185, 66, 0.17);
--leaf-display: block;
--hl-comment: #8a6a4a;
--hl-punct: #fde7c0;
--hl-func: #f4b942;
--hl-keyword: #e0643f;
--hl-string: #a3b94a;
--hl-var: #e8913a;
--hl-op: #e0643f;
--hl-num: #f4b942;
--hl-param: #fdf0d8;
--hl-class: #d9a35b;
--hl-tag: #e0643f;
--hl-deleted-color: #e0643f;
--hl-deleted-bg: rgba(224, 100, 63, 0.18);
--hl-inserted-color: #a3b94a;
--hl-inserted-bg: rgba(163, 185, 74, 0.18);
}
:root[data-theme="winter"] {
--bg: #0d1b2a;
--text: #e6f0fa;
--text: #d6ecff;
--muted: #7f98b3;
--border: #1b2c44;
--bg-elevated: rgba(138, 209, 255, 0.1);
--bg-card: rgba(16, 32, 52, 0.6);
--bg-card-hover: rgba(26, 48, 76, 0.85);
--bg-code: #08121e;
--code-inline-bg: rgba(138, 209, 255, 0.14);
--code-inline-text: #8ad1ff;
--code-text: #cfe4f7;
--bg-menu: #0d1b2a;
--bg-modal: rgba(13, 27, 42, 0.88);
--hover-bg: rgba(138, 209, 255, 0.1);
--search-bg: rgba(138, 209, 255, 0.08);
--search-panel-bg: rgba(13, 27, 42, 0.95);
--code-group-bg: #132640;
--code-group-panel-bg: #0d1b2a;
--code-tab-active: #8ad1ff;
--footer-border: #1b2c44;
--border-subtle: rgba(138, 209, 255, 0.14);
--btn-primary-bg: #8ad1ff;
--btn-primary-text: #06101c;
--btn-home-bg: #8ad1ff;
--btn-home-text: #06101c;
--callout-note-bg: rgba(138, 209, 255, 0.14);
--callout-info-bg: rgba(185, 166, 255, 0.14);
--callout-warning-bg: rgba(255, 214, 138, 0.16);
--callout-warning-text: #ffd68a;
--body-radial-tint: rgba(13, 27, 42, 0.85);
--skeleton-pulse-1: rgba(138, 209, 255, 0.06);
--skeleton-pulse-2: rgba(138, 209, 255, 0.16);
--snow-display: block;
--hl-comment: #5b7491;
--hl-punct: #cfe4f7;
--hl-func: #8ad1ff;
--hl-keyword: #b9a6ff;
--hl-string: #9be7d4;
--hl-var: #ffb4c7;
--hl-op: #8ad1ff;
--hl-num: #ffd68a;
--hl-param: #e6f0fa;
--hl-class: #a9e4ff;
--hl-tag: #b9a6ff;
--hl-deleted-color: #ff8fa3;
--hl-deleted-bg: rgba(255, 143, 163, 0.18);
--hl-inserted-color: #9be7d4;
--hl-inserted-bg: rgba(155, 231, 212, 0.18);
}
.cobweb {
display: var(--cobweb-display);
position: fixed;
width: 160px;
height: 160px;
pointer-events: none;
z-index: 35;
opacity: 0.8;
}
.cobweb-top-left {
top: 0;
left: 0;
transform: scaleX(-1);
}
.cobweb-top-right {
top: 0;
right: 0;
}
.cobweb-media {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.spider-container {
display: var(--spider-display);
position: fixed;
top: 0;
right: 12%;
pointer-events: none;
z-index: 100;
animation: spiderDescend 10s ease-in-out infinite;
}
.spider-thread {
width: 1.5px;
height: 100vh;
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--spider-thread);
}
.spider-body {
position: relative;
width: 48px;
height: 48px;
margin-top: 100px;
pointer-events: auto;
cursor: pointer;
animation: spiderSway 3s ease-in-out infinite alternate;
}
.spider-media {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
scale: 2;
padding-top: 20px;
}
@keyframes spiderDescend {
0% {
transform: translateY(-80px);
}
45% {
transform: translateY(240px);
}
55% {
transform: translateY(240px);
}
90% {
transform: translateY(10px);
}
100% {
transform: translateY(-80px);
}
}
@keyframes spiderSway {
0% {
transform: rotate(-10deg);
}
100% {
transform: rotate(10deg);
}
}
@keyframes spiderDrop {
from {
transform: translateY(240px);
}
to {
transform: translateY(130vh);
}
}
:root {
--snow-tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cg fill='white'%3E%3Ccircle cx='22' cy='38' r='2.4' fill-opacity='.9'/%3E%3Ccircle cx='96' cy='126' r='1.8' fill-opacity='.7'/%3E%3Ccircle cx='168' cy='28' r='3' fill-opacity='.85'/%3E%3Ccircle cx='236' cy='204' r='2' fill-opacity='.75'/%3E%3Ccircle cx='272' cy='92' r='2.8' fill-opacity='.9'/%3E%3Ccircle cx='58' cy='252' r='2.2' fill-opacity='.8'/%3E%3Ccircle cx='196' cy='282' r='1.6' fill-opacity='.7'/%3E%3Ccircle cx='140' cy='190' r='2.4' fill-opacity='.85'/%3E%3C/g%3E%3Cg stroke='white' stroke-opacity='.9' stroke-width='1.3' stroke-linecap='round' fill='none'%3E%3Cpath transform='translate(126 72)' d='M0 -8V8M-6.9 -4L6.9 4M-6.9 4L6.9 -4'/%3E%3Cpath transform='translate(246 150)' d='M0 -6V6M-5.2 -3L5.2 3M-5.2 3L5.2 -3'/%3E%3Cpath transform='translate(40 160)' d='M0 -7V7M-6 -3.5L6 3.5M-6 3.5L6 -3.5'/%3E%3Cpath transform='translate(210 250)' d='M0 -6V6M-5.2 -3L5.2 3M-5.2 3L5.2 -3'/%3E%3C/g%3E%3C/svg%3E");
--leaf-tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='360' height='360'%3E%3Cg transform='translate(50 60) rotate(25) scale(1.2)' fill='%23e8710a'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3Cg transform='translate(190 40) rotate(-40)' fill='%23c2410c'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3Cg transform='translate(300 130) rotate(70) scale(1.3)' fill='%23eab308'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3Cg transform='translate(120 200) rotate(-15) scale(1.1)' fill='%23b45309'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3Cg transform='translate(250 280) rotate(140) scale(1.2)' fill='%23d97706'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3Cg transform='translate(40 310) rotate(-80)' fill='%23ca8a04'%3E%3Cpath d='M0 -14C10 -8 12 6 0 16C-12 6 -10 -8 0 -14Z'/%3E%3Cpath d='M0 -12V14' stroke='%23000' stroke-opacity='.25' stroke-width='1'/%3E%3C/g%3E%3C/svg%3E");
--lights-tile: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='56'%3E%3Cpath d='M0 4Q40 32 80 4Q120 32 160 4' fill='none' stroke='%23245c3a' stroke-width='1.6'/%3E%3Cg fill='%23222'%3E%3Crect x='18' y='13' width='4' height='4'/%3E%3Crect x='38' y='17' width='4' height='4'/%3E%3Crect x='58' y='13' width='4' height='4'/%3E%3Crect x='98' y='13' width='4' height='4'/%3E%3Crect x='118' y='17' width='4' height='4'/%3E%3Crect x='138' y='13' width='4' height='4'/%3E%3C/g%3E%3Cellipse cx='20' cy='22' rx='4' ry='5.5' fill='%23ff4d4d'/%3E%3Cellipse cx='40' cy='26' rx='4' ry='5.5' fill='%23ffd24d'/%3E%3Cellipse cx='60' cy='22' rx='4' ry='5.5' fill='%234dff88'/%3E%3Cellipse cx='100' cy='22' rx='4' ry='5.5' fill='%234da6ff'/%3E%3Cellipse cx='120' cy='26' rx='4' ry='5.5' fill='%23ff4d4d'/%3E%3Cellipse cx='140' cy='22' rx='4' ry='5.5' fill='%23ffd24d'/%3E%3C/svg%3E");
}
.snowfall,
.leaf-fall {
position: fixed;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 30;
}
.snowfall {
display: var(--snow-display);
}
.leaf-fall {
display: var(--leaf-display);
}
.snowfall::before,
.snowfall::after,
.leaf-fall::before,
.leaf-fall::after {
content: "";
position: absolute;
background-repeat: repeat;
will-change: transform;
}
.snowfall::before {
top: -600px;
left: 0;
right: 0;
bottom: 0;
background-image: var(--snow-tile);
background-size: 300px 300px;
opacity: 0.9;
animation: snowFallNear 18s linear infinite;
}
.snowfall::after {
top: -500px;
left: 0;
right: 0;
bottom: 0;
background-image: var(--snow-tile);
background-size: 500px 500px;
opacity: 0.6;
animation: snowFallFar 30s linear infinite;
}
.leaf-fall::before {
top: -720px;
left: 0;
right: -360px;
bottom: 0;
background-image: var(--leaf-tile);
background-size: 360px 360px;
opacity: 0.9;
animation: leafFallNear 26s linear infinite;
}
.leaf-fall::after {
top: -540px;
left: -540px;
right: 0;
bottom: 0;
background-image: var(--leaf-tile);
background-size: 540px 540px;
opacity: 0.7;
animation: leafFallFar 40s linear infinite;
}
.light-string {
display: var(--lights-display);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 56px;
pointer-events: none;
z-index: 100;
background-image: var(--lights-tile);
background-repeat: repeat-x;
background-size: 160px 56px;
animation: lightTwinkle 2.6s ease-in-out infinite alternate;
}
@keyframes snowFallNear {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(0, 600px, 0);
}
}
@keyframes snowFallFar {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(0, 500px, 0);
}
}
@keyframes leafFallNear {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(-360px, 720px, 0);
}
}
@keyframes leafFallFar {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(540px, 540px, 0);
}
}
@keyframes lightTwinkle {
from {
opacity: 0.65;
filter: drop-shadow(0 0 3px rgba(255, 214, 120, 0.5));
}
to {
opacity: 1;
filter: drop-shadow(0 0 8px rgba(255, 214, 120, 0.9));
}
}
@media (prefers-reduced-motion: reduce) {
.snowfall::before,
.snowfall::after,
.leaf-fall::before,
.leaf-fall::after,
.light-string {
animation: none;
}
}
#navbar .brand {
position: relative;
}
#navbar .brand::after {
content: "";
position: absolute;
top: -9px;
right: -9px;
font-size: 14px;
line-height: 1;
pointer-events: none;
}
:root[data-theme="halloween"] #navbar .brand::after {
content: "🎃";
}
:root[data-theme="autumn"] #navbar .brand::after {
content: "🍂";
}
:root[data-theme="winter"] #navbar .brand::after {
content: "❄️";
}
:root[data-theme="christmas"] #navbar .brand::after {
content: "🎄";
}
.st-themes {
padding: 14px 16px 16px;
}
.st-sub {
margin: 0 0 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.st-theme-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-bottom: 16px;
}
.st-theme-grid:last-child {
margin-bottom: 0;
}
.st-theme {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
height: 40px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 100px;
background: var(--search-bg);
color: var(--muted);
font: inherit;
font-size: 13.5px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.st-theme .ico {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.st-theme span {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.st-theme:hover {
color: var(--text);
}
.st-theme.on {
border-color: var(--text);
background: var(--hover-bg);
color: var(--text);
font-weight: 600;
}

View File

@@ -12,14 +12,14 @@ Provider availability and performance can vary based on your network environment
## How Providers Are Used ## How Providers Are Used
When your app calls `getStream()`—directly or through Stream API: When your app calls `getStream()`-directly or through Stream API:
1. All active (non-disabled) providers are queried **simultaneously** with per-source random jitter to stagger upstream requests 1. All active (non-disabled) providers are queried **simultaneously** with per-source random jitter to stagger upstream requests
2. Each provider has its own **timeout**, **retry count**, and **backoff delay** 2. Each provider has its own **timeout**, **retry count**, and **backoff delay**
3. Every returned URL is **verified** — first via a HEAD request to the raw upstream URL, then via a proxied HLS playability check 3. Every returned URL is **verified** - first via a HEAD request to the raw upstream URL, then via a proxied HLS playability check
4. Only URLs that pass both verification steps appear in the response as `source` events 4. Only URLs that pass both verification steps appear in the response as `source` events
Because the endpoint is SSE, **verified sources are emitted immediately as each provider resolves** — your player receives them progressively. The stream closes once every provider has either resolved or timed out. Because the endpoint is SSE, **verified sources are emitted immediately as each provider resolves** - your player receives them progressively. The stream closes once every provider has either resolved or timed out.
:::callout-info :::callout-info
Call `sdk.getSources(true)` to inspect enabled providers and `sdk.probeAllSources()` for a live availability signal. If you use Stream API, its `/health` endpoint exposes probes over HTTP. Call `sdk.getSources(true)` to inspect enabled providers and `sdk.probeAllSources()` for a live availability signal. If you use Stream API, its `/health` endpoint exposes probes over HTTP.
@@ -68,7 +68,7 @@ CACHE_TTL = 5 minutes
All provider stream URLs are wrapped in the server proxy (unless `skipProxy: true`): All provider stream URLs are wrapped in the server proxy (unless `skipProxy: true`):
``` ```
http://localhost:7860/api?url=<encoded_stream_url>&<proxyParam>=1 http://YOUR_API_URL/api?url=<encoded_stream_url>&<proxyParam>=1
``` ```
When your player fetches this path from your self-hosted instance, the server: When your player fetches this path from your self-hosted instance, the server:
@@ -83,5 +83,5 @@ When your player fetches this path from your self-hosted instance, the server:
Before a URL is emitted as a `source` event, it passes two verification steps: Before a URL is emitted as a `source` event, it passes two verification steps:
1. **`verifyStream`** — a HEAD request to the raw upstream URL. Must return HTTP `< 400`. 1. **`verifyStream`** - a HEAD request to the raw upstream URL. Must return HTTP `< 400`.
2. **`verifyPlayable`** — fetches the proxied URL, validates `#EXTM3U`, checks for `#EXTINF` or `#EXT-X-STREAM-INF`, then fetches the first segment. 2. **`verifyPlayable`** - fetches the proxied URL, validates `#EXTM3U`, checks for `#EXTINF` or `#EXT-X-STREAM-INF`, then fetches the first segment.

View File

@@ -93,19 +93,14 @@
gap: 6px; gap: 6px;
padding: 10px 22px; padding: 10px 22px;
border-radius: 100px; border-radius: 100px;
background: #000000; background: var(--btn-primary-bg);
color: #ffffff !important; color: var(--btn-primary-text) !important;
font-size: 13.5px; font-size: 13.5px;
font-weight: 600; font-weight: 600;
text-decoration: none !important; text-decoration: none !important;
transition: transform 0.15s ease; transition: transform 0.15s ease;
} }
html.dark .btn-primary {
background: #FFFFFF;
color: #000000 !important;
}
.btn-secondary { .btn-secondary {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -156,6 +151,11 @@
z-index: 1; z-index: 1;
} }
.header-desc {
font-size: 14.5px;
margin-top: 0 !important;
}
.section-head { .section-head {
margin-bottom: 22px; margin-bottom: 22px;
text-align: left; text-align: left;
@@ -166,13 +166,13 @@
font-size: 22px !important; font-size: 22px !important;
font-weight: 700 !important; font-weight: 700 !important;
margin: 0 0 6px !important; margin: 0 0 6px !important;
color: #ffffff !important; color: var(--text) !important;
} }
.section-head p { .section-head p {
font-size: 14.5px; font-size: 14.5px;
margin: 0; margin: 0;
color: #a1a1aa; color: var(--muted);
} }
.vyla-grid { .vyla-grid {
@@ -247,11 +247,71 @@
opacity: 0.75; opacity: 0.75;
} }
.hero-greeting {
margin: 0 0 14px;
padding: 6px 16px;
border-radius: 100px;
background: var(--hover-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
color: var(--text);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.01em;
}
.hero-resume {
max-width: min(90vw, 360px);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--text);
}
@media (max-width: 900px) { @media (max-width: 900px) {
#header-container { #header-container {
border-radius: 100px; border-radius: 100px;
} }
} }
@media (orientation: landscape) and (max-height: 550px) {
.home-header {
height: auto;
min-height: 280px;
padding-top: calc(var(--header-height) + 24px);
padding-bottom: 28px;
}
.header-logo-badge {
width: 44px;
height: 44px;
margin-bottom: 8px;
}
.header-title {
font-size: 26px;
margin-bottom: 6px;
}
.header-desc {
font-size: 13.5px;
margin-bottom: 14px;
max-width: 540px;
}
.content-section {
margin-top: 32px;
}
.vyla-grid {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
}
.feature-box {
padding: 18px;
}
}
</style> </style>
<div class="home-container"> <div class="home-container">

View File

@@ -3,8 +3,11 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Vyla Entertainment — Vyla</title> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<title>Vyla Entertainment - Vyla</title>
<meta name="description" <meta name="description"
content="Vyla Entertainment is a community-built, decentralized media platform and developer ecosystem. Featuring native client apps for Desktop, Mobile, and Android TV, a scraping SDK, and a self-hosted Stream API." /> content="Vyla Entertainment is a community-built, decentralized media platform and developer ecosystem. Featuring native client apps for Desktop, Mobile, and Android TV, a scraping SDK, and a self-hosted Stream API." />
<meta name="keywords" <meta name="keywords"
@@ -15,7 +18,7 @@
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="Vyla Entertainment — Free & Decentralized Media Platform" /> <meta property="og:title" content="Vyla Entertainment - Free & Decentralized Media Platform" />
<meta property="og:description" <meta property="og:description"
content="A community-built media ecosystem focused on freedom, privacy, and control. Featuring native Desktop, Android, and TV apps backed by modular developer tools." /> content="A community-built media ecosystem focused on freedom, privacy, and control. Featuring native Desktop, Android, and TV apps backed by modular developer tools." />
<meta property="og:url" content="https://vyla.cc/" /> <meta property="og:url" content="https://vyla.cc/" />
@@ -26,7 +29,7 @@
<meta property="og:image:alt" content="Vyla Entertainment Ecosystem" /> <meta property="og:image:alt" content="Vyla Entertainment Ecosystem" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Vyla Entertainment — Free & Decentralized Media Platform" /> <meta name="twitter:title" content="Vyla Entertainment - Free & Decentralized Media Platform" />
<meta name="twitter:description" <meta name="twitter:description"
content="Explore Vyla Home, Go, TV, and the developer SDK. Built for privacy, performance, and complete streaming control." /> content="Explore Vyla Home, Go, TV, and the developer SDK. Built for privacy, performance, and complete streaming control." />
<meta name="twitter:image" content="/assets/images/site_preview.png" /> <meta name="twitter:image" content="/assets/images/site_preview.png" />
@@ -77,6 +80,9 @@
<link rel="stylesheet" href="/assets/styling/layout.css" /> <link rel="stylesheet" href="/assets/styling/layout.css" />
<link rel="stylesheet" href="/assets/styling/prose.css" /> <link rel="stylesheet" href="/assets/styling/prose.css" />
<link rel="stylesheet" href="/assets/styling/components.css" /> <link rel="stylesheet" href="/assets/styling/components.css" />
<link rel="stylesheet" href="/assets/styling/themes.css" />
<link rel="stylesheet" href="/assets/styling/overlay.css" />
<link rel="stylesheet" href="/assets/styling/features.css" />
<link rel="stylesheet" href="/assets/styling/highlight.css" /> <link rel="stylesheet" href="/assets/styling/highlight.css" />
<link rel="stylesheet" href="/assets/styling/404.css" /> <link rel="stylesheet" href="/assets/styling/404.css" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -87,7 +93,12 @@
src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script> src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script> <script>
(function () { (function () {
var saved = localStorage.getItem('vyla-theme') || 'halloween'; var saved = localStorage.getItem('vyla-theme') || 'auto';
if (saved === 'auto') {
var d = new Date();
var m = d.getMonth();
saved = m === 9 ? 'halloween' : m === 10 ? 'autumn' : m === 11 ? 'christmas' : (m === 0 || m === 1) ? 'winter' : (m === 8 && d.getDate() >= 22) ? 'autumn' : 'dark';
}
document.documentElement.setAttribute('data-theme', saved); document.documentElement.setAttribute('data-theme', saved);
})(); })();
</script> </script>
@@ -106,10 +117,13 @@
<img class="spider-media" src="https://www.pngmart.com/files/1/Spider-PNG-File.png" alt="" /> <img class="spider-media" src="https://www.pngmart.com/files/1/Spider-PNG-File.png" alt="" />
</div> </div>
</div> </div>
<div class="snowfall" aria-hidden="true"></div>
<div class="leaf-fall" aria-hidden="true"></div>
<div class="light-string" aria-hidden="true"></div>
<video class="video-bg" autoplay muted loop playsinline></video> <video class="video-bg" autoplay muted loop playsinline></video>
<div id="banner"> <div id="banner">
<span> <span>
Rights holders can request source removal — <a href="/misc/project/dmca">DMCA policy</a>. Rights holders can request source removal - <a href="/misc/project/dmca">DMCA policy</a>.
</span> </span>
<button id="banner-close" onclick="dismissBanner()" aria-label="Dismiss">✕</button> <button id="banner-close" onclick="dismissBanner()" aria-label="Dismiss">✕</button>
</div> </div>
@@ -120,6 +134,8 @@
height="28" decoding="async" /></a> height="28" decoding="async" /></a>
<button id="search-trigger" type="button" onclick="openSearch()" aria-label="Search"></button> <button id="search-trigger" type="button" onclick="openSearch()" aria-label="Search"></button>
<div class="links"> <div class="links">
<button id="settings-trigger" type="button" onclick="openSettings()"
aria-label="Personalize"></button>
<button id="theme-toggle" type="button" onclick="toggleThemeMenu(event)" <button id="theme-toggle" type="button" onclick="toggleThemeMenu(event)"
aria-label="Theme"></button> aria-label="Theme"></button>
</div> </div>
@@ -139,6 +155,7 @@
<a id="footer-next" class="footer-link next" href="#"></a> <a id="footer-next" class="footer-link next" href="#"></a>
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p id="footer-note"></p>
<div class="footer-socials"> <div class="footer-socials">
<a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener" <a href="https://discord.gg/5eqRhKeywG" target="_blank" rel="noopener"
aria-label="Discord"> aria-label="Discord">

View File

@@ -20,7 +20,7 @@ At no point does Vyla store, transcode, or re-host any media file.
## Content Sources ## Content Sources
The following third-party sources are scraped or proxied by Vyla API. These are upstream providers — Vyla does not control, operate, or own any of them. The following third-party sources are scraped or proxied by Vyla API. These are upstream providers - Vyla does not control, operate, or own any of them.
You can find the full list of sources in the [stream-api sources directory](https://gitlab.com/vyla-entertainment/sdk/-/tree/main/src/sources). You can find the full list of sources in the [stream-api sources directory](https://gitlab.com/vyla-entertainment/sdk/-/tree/main/src/sources).

View File

@@ -0,0 +1,5 @@
# Changelog
<p class="page-description">Release notes for every Vyla project, pulled live from GitLab.</p>
<div id="changelog" class="changelog"></div>

View File

@@ -52,4 +52,12 @@ Massive thanks to:
- gavingogaming (Movieslay) - gavingogaming (Movieslay)
- The P-Stream contributors - The P-Stream contributors
- The CinePro contributors - The CinePro contributors
- The Movieslay contributors - The Movieslay contributors
---
## Contributors
People who have committed to Vyla projects, pulled live from GitLab.
<div id="contributors" class="contributors"></div>

20
misc/project/story.md Normal file
View File

@@ -0,0 +1,20 @@
# Our story
<p class="page-description">Why Vyla exists, what we care about, and how you can help.</p>
Vyla started as a small web project with a simple goal: a cleaner, more private way to discover and watch movies and shows without relying on a single company or centralized service. It has since grown into apps for desktop, Android and Android TV, plus the tools that power them.
Vyla is software, not a streaming service. We do not host or provide any media.
## What we care about
- **Control.** You decide how much of Vyla you use and where it runs.
- **Privacy.** Vyla stays local and self-hostable, with no unnecessary centralized dependencies.
- **Community.** Vyla is built in the open by the people who use it.
## Where to go next
- Pick an app: [Vyla Home](/products/home), [Vyla Go](/products/go) or [Vyla TV](/products/tv).
- Build with Vyla: start with the [SDK quickstart](/sdk/vyla-sdk/quickstart) or the [Stream API](/stream-api/overview).
- Help fix sources: read the [contributing guide](/sdk/vyla-sdk/contributing).
- See who made this possible on the [credits page](/misc/project/credit).

View File

@@ -12,6 +12,6 @@ Vyla Go is available for Android. Get the latest version below:
| Platform | Download | | Platform | Download |
| --- | --- | | --- | --- |
| Android | [Download .apk](https://gitlab.com/vyla-entertainment/go/-/releases) | | Android | [Download: v0.0.0 (.apk)](https://gitlab.com/vyla-entertainment/go/-/releases) |
Inside the releases page, you can find the latest version of Vyla Go for Android, find the `.apk` file, and install it on your device. Clicking the download button will retrieve the latest `.apk` package for your device.

View File

@@ -14,11 +14,11 @@ Vyla Home is available for Windows, macOS, and Linux. Get the latest version bel
| Platform | Download | | Platform | Download |
| --- | --- | | --- | --- |
| Windows | [Download .exe](https://gitlab.com/vyla-entertainment/home/-/releases) | | Windows | [Download: v0.0.0 (.exe)](https://gitlab.com/vyla-entertainment/home/-/releases) |
| macOS | [Download .dmg](https://gitlab.com/vyla-entertainment/home/-/releases) | | macOS | [Download: v0.0.0 (.dmg)](https://gitlab.com/vyla-entertainment/home/-/releases) |
| Linux | [Build from source](#linux-setup) | | Linux | [Build from source](#linux-setup) |
Inside the releases page, you can find the latest version of Vyla Home for each platform, find the `.exe` for windows, and `.dmg` for macOS. Clicking the download button will retrieve the latest installer for Windows or macOS.
## Linux Setup ## Linux Setup

View File

@@ -14,6 +14,6 @@ Vyla TV is available for Android TV. Get the latest version below:
| Platform | Download | | Platform | Download |
| --- | --- | | --- | --- |
| Android TV | [Download .apk](https://gitlab.com/vyla-entertainment/TV/-/releases) | | Android TV | [Download: v0.0.0 (.apk)](https://gitlab.com/vyla-entertainment/TV/-/releases) |
Inside the releases page, you can find the latest version of Vyla TV for Android TV, find the `.apk` file, and install it on your device. Clicking the download button will retrieve the latest `.apk` package for your Android TV device.

View File

@@ -0,0 +1,170 @@
# Contributing to Sources
<p class="page-description">Help maintain, fix broken scrapers, and add new video or download providers to the Vyla SDK.</p>
Third-party streaming and scraping endpoints frequently change their DOM layout, API endpoints, tokens, and CDN protections. Community contributions are vital to keeping providers operational.
---
## Getting Started
### 1. Clone the repository
Clone the SDK repository to your local machine:
```bash
git clone https://gitlab.com/vyla-entertainment/vyla-sdk.git
cd vyla-sdk
```
### 2. Install dependencies
Ensure you are running Node.js 18 or newer:
```bash
npm install
```
### 3. Environment configuration
Copy the example environment file and add your TMDB API key:
```bash
cp .env.example .env
```
Open `.env` and set your key:
```ini
TMDB_API_KEY=your_actual_tmdb_api_key_here
```
---
## Project Structure
- `src/sources/`: Scraper implementations for each provider.
- `src/config.js`: Source registry containing timeouts, retries, headers, and proxy flags.
- `src/downloads/downloads.js`: Downloader resolver extracting direct MP4 streams.
- `src/subtitles.js`: Subtitle mirror aggregation endpoints.
- `test/index.js`: Test runner for testing streams, subtitles, and downloads.
---
## Testing Sources
Testing is driven by `test/index.js`. Open the file and update `CONFIG` to target the source you are fixing or adding:
```javascript
const CONFIG = {
DEBUG: true,
MOVIE_ID: "936075",
TV_ID: "37854",
TV_SEASON: "1",
TV_EPISODE: "1",
STREAM_SOURCE: "vidrock",
TEST_SUBTITLE: false,
TEST_DOWNLOAD: false,
TEST_MOVIE: true,
TEST_TV: true,
EXTRA_SDK_PARAMS: {}
};
```
Run the test suite:
```bash
npm test
```
When `DEBUG: true` is enabled, all outbound fetch calls, headers, request payloads, and raw responses will be logged to your terminal and saved to `test/debug-output.json`.
---
## Fixing or Adding a Source
### 1. Source Resolver
All source files are located in `src/sources/`. Each module must export an asynchronous `getStream` function:
```javascript
export async function getStream({ id, s, e, clientIP, audio, sdk, config }) {
const isTv = Boolean(s && e);
const mediaId = id;
const streamUrl = "https://example-cdn.com/stream/playlist.m3u8";
return {
url: streamUrl,
quality: "auto",
isM3U8: true,
headers: {
Referer: "https://example-provider.to/",
Origin: "https://example-provider.to"
},
allUrls: [
{
url: streamUrl,
quality: "1080p",
isM3U8: true
}
]
};
}
```
### 2. Registering in `src/config.js`
Add or update the source entry inside the `SOURCES` array in `src/config.js`:
```javascript
{
key: 'mysource',
label: 'MySource',
sourceFile: 'mysource',
proxyParam: 'ms',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
cdnHeaders: [{
pattern: /cdn\.mysource\.to/i,
headers: {
Referer: 'https://mysource.to/',
Origin: 'https://mysource.to',
},
}],
}
```
- `skipProxy`: Set to `true` if the stream plays directly in client players without server-side proxying.
- `multiUrl`: Set to `true` if the scraper provides multiple resolutions or fallback mirrors.
- `cdnHeaders`: Include custom headers required by the upstream CDN to avoid HTTP 403 Forbidden responses.
---
## Download Providers
Downloads are handled automatically by `src/downloads/downloads.js`. The downloader queries active sources that have `skipProxy: true` enabled and inspects returned streams:
- URLs ending in `.mp4` or containing `.mp4` queries are automatically extracted.
- `.m3u8` streams are filtered out because they are HLS segment playlists rather than direct file downloads.
- If you are implementing a provider that supplies raw MP4 video files, return them in `allUrls` or `url` with `skipProxy: true` in `src/config.js`.
To verify download resolution in `test/index.js`, toggle:
```javascript
TEST_DOWNLOAD: true
```
---
## Submitting Pull Requests
1. Verify that your updated source passes tests for both movies (`TEST_MOVIE: true`) and TV shows (`TEST_TV: true`).
2. Ensure you have not committed your `.env` file or personal TMDB API keys.
3. Push your branch and open a Merge Request on GitLab.

View File

@@ -19,7 +19,7 @@ The SDK can be used independently of the self-hosted Stream API. If you prefer t
- **Automated Anime Sub/Dub Routing**: Automatically configures audio track requirements (`sub` vs `dub`) based on provider naming conventions. - **Automated Anime Sub/Dub Routing**: Automatically configures audio track requirements (`sub` vs `dub`) based on provider naming conventions.
- **Multi-Source Subtitle Aggregation**: Merges subtitle tracks from multiple redundant subtitle mirrors into a unified VTT/SRT format. - **Multi-Source Subtitle Aggregation**: Merges subtitle tracks from multiple redundant subtitle mirrors into a unified VTT/SRT format.
- **Metadata Fallbacks**: Automatically maps TMDB IDs to external IDs (such as IMDB and AniList) for smooth stream lookups. - **Metadata Fallbacks**: Automatically maps TMDB IDs to external IDs (such as IMDB and AniList) for smooth stream lookups.
- **Self-Contained**: No need to deploy a separate API server — all streaming functionality runs within your existing Node.js application. - **Self-Contained**: No need to deploy a separate API server - all streaming functionality runs within your existing Node.js application.
--- ---
@@ -82,8 +82,8 @@ const geoStream = await sdk.getStream("sourcename", "155", null, null, "203.0.11
| Parameter | Type | Required | Default | Description | | Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `key` | `string` | **Yes** | — | Unique key identifier for the provider (e.g. `"sourcename"`). | | `key` | `string` | **Yes** | - | Unique key identifier for the provider (e.g. `"sourcename"`). |
| `id` | `string` | **Yes** | — | TMDB media ID (e.g., `"155"`). | | `id` | `string` | **Yes** | - | TMDB media ID (e.g., `"155"`). |
| `s` | `string \| number` | **No** | `null` | Season number (required for TV shows). | | `s` | `string \| number` | **No** | `null` | Season number (required for TV shows). |
| `e` | `string \| number` | **No** | `null` | Episode number (required for TV shows). | | `e` | `string \| number` | **No** | `null` | Episode number (required for TV shows). |
| `clientIP` | `string` | **No** | `null` | End-user IP address forwarded for geo-restricted scrapers. | | `clientIP` | `string` | **No** | `null` | End-user IP address forwarded for geo-restricted scrapers. |
@@ -127,7 +127,7 @@ const tvSubs = await sdk.getSubtitles("1399", 1, 1);
| Parameter | Type | Required | Default | Description | | Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `id` | `string` | **Yes** | — | TMDB media ID. | | `id` | `string` | **Yes** | - | TMDB media ID. |
| `s` | `string \| number` | **No** | `null` | Season number (required for TV shows). | | `s` | `string \| number` | **No** | `null` | Season number (required for TV shows). |
| `e` | `string \| number` | **No** | `null` | Episode number (required for TV shows). | | `e` | `string \| number` | **No** | `null` | Episode number (required for TV shows). |

View File

@@ -41,7 +41,7 @@ GA_API_SECRET=
| `GA_MEASUREMENT_ID` / `GA_API_SECRET` | No | Enables Google Analytics event reporting. Leave blank to disable. | | `GA_MEASUREMENT_ID` / `GA_API_SECRET` | No | Enables Google Analytics event reporting. Leave blank to disable. |
| `PORT` | No | Port the server listens on. Defaults to `7860`. | | `PORT` | No | Port the server listens on. Defaults to `7860`. |
| `WORKER_COUNT` | No | Number of cluster workers to fork. Defaults to `1` locally. | | `WORKER_COUNT` | No | Number of cluster workers to fork. Defaults to `1` locally. |
| `ENABLE_DEBUG_ROUTE` | No | Set to `true` to enable `/api/debug/:id`. Keep this `false` in production — it exposes upstream request/response headers and raw source output. | | `ENABLE_DEBUG_ROUTE` | No | Set to `true` to enable `/api/debug/:id`. Keep this `false` in production - it exposes upstream request/response headers and raw source output. |
| `PROXY_STREAMS` | No | Set to `true` to route stream playback through your own server's `/api` proxy endpoint instead of direct URLs. | | `PROXY_STREAMS` | No | Set to `true` to route stream playback through your own server's `/api` proxy endpoint instead of direct URLs. |
| `PROXY_URL` | No | An external proxy base URL to use instead of the built-in `/api` proxy. Only used if `PROXY_STREAMS` is enabled. | | `PROXY_URL` | No | An external proxy base URL to use instead of the built-in `/api` proxy. Only used if `PROXY_STREAMS` is enabled. |

View File

@@ -4,7 +4,7 @@
## Use this only when HTTP is the right boundary ## Use this only when HTTP is the right boundary
Stream API packages Vyla SDK behind a small Node.js HTTP service. It is useful when a browser, mobile app, or another process needs progressive playback candidates over HTTP. It is not required to use Vyla, and it is not the primary integration for a Node.js application—use [Vyla SDK](/sdk) directly in that case. Stream API packages Vyla SDK behind a small Node.js HTTP service. It is useful when a browser, mobile app, or another process needs progressive playback candidates over HTTP. It is not required to use Vyla, and it is not the primary integration for a Node.js application-use [Vyla SDK](/sdk) directly in that case.
The service accepts TMDB IDs, asks configured SDK providers for candidates, verifies results, and emits usable sources as they arrive. A player can start with the first source while retaining later sources as fallbacks. The service accepts TMDB IDs, asks configured SDK providers for candidates, verifies results, and emits usable sources as they arrive. A player can start with the first source while retaining later sources as fallbacks.

View File

@@ -6,15 +6,15 @@
`GET /health` (or `/api/health`) probes active providers and returns a JSON summary. It includes a status, timestamp, TMDB-key presence, cache size, and a map of provider keys to `{ ok, ms }` results. `GET /health` (or `/api/health`) probes active providers and returns a JSON summary. It includes a status, timestamp, TMDB-key presence, cache size, and a map of provider keys to `{ ok, ms }` results.
Health is a signal, not a promise of playback. Provider behavior can differ by title, client network, and the moment of the request. Use it for observability and source selection—not as a guarantee that a viewer will receive a stream. Health is a signal, not a promise of playback. Provider behavior can differ by title, client network, and the moment of the request. Use it for observability and source selection-not as a guarantee that a viewer will receive a stream.
## Test one provider ## Test one provider
`GET /test/:id?source=:providerKey` resolves a single provider for a movie. Add `season` and `episode` (or `s` and `e`) for TV. `GET /test/:id?source=:providerKey` resolves a single provider for a movie. Add `season` and `episode` (or `s` and `e`) for TV.
```bash ```bash
curl "http://localhost:7860/test/155?source=provider-key" curl "YOUR_API_URL/test/155?source=provider-key"
curl "http://localhost:7860/test/1396?season=1&episode=1&source=provider-key" curl "YOUR_API_URL/test/1396?season=1&episode=1&source=provider-key"
``` ```
Use source keys reported by the running deployment. A test result reports whether resolution and verification succeeded; clients should inspect the result body rather than relying only on a transport success. Use source keys reported by the running deployment. A test result reports whether resolution and verification succeeded; clients should inspect the result body rather than relying only on a transport success.

View File

@@ -14,8 +14,8 @@ The API supports both the short paths below and `/api`-prefixed equivalents. A m
`id` is a TMDB ID. For example: `id` is a TMDB ID. For example:
```bash ```bash
curl -N "http://localhost:7860/movie?id=155" curl -N "YOUR_API_URL/movie?id=155"
curl -N "http://localhost:7860/tv?id=1396&season=1&episode=1" curl -N "YOUR_API_URL/tv?id=1396&season=1&episode=1"
``` ```
## Event contract ## Event contract
@@ -44,7 +44,7 @@ One event for each candidate that passes the service's verification pipeline.
"source": { "source": {
"source": "provider-key", "source": "provider-key",
"label": "Provider name", "label": "Provider name",
"url": "http://localhost:7860/api?url=…" "url": "YOUR_API_URL/api?url=…"
} }
} }
``` ```