From 9be86b36919b02618a0c99fbe91422ac25eb3630 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 10 Jul 2026 13:18:02 +0300 Subject: [PATCH 01/11] =?UTF-8?q?fix(core):=20=D1=80=D0=B5=D1=88=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20alpha=20=D0=B8=20point-glow=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D0=B5=D1=87=D0=BD=D0=BE=D0=B9=20sRGB8=20?= =?UTF-8?q?=D1=81=D0=B5=D1=82=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/labcolors-core/Cargo.toml | 4 + crates/labcolors-core/benches/glow_alpha.rs | 36 + crates/labcolors-core/src/alpha.rs | 711 ++++++++--- crates/labcolors-core/src/glow.rs | 1054 +++++++++++++++-- crates/labcolors-core/src/spaces/srgb.rs | 2 +- .../0004-finite-alpha-glow-reference.md | 131 ++ 6 files changed, 1673 insertions(+), 265 deletions(-) create mode 100644 crates/labcolors-core/benches/glow_alpha.rs create mode 100644 docs/decisions/0004-finite-alpha-glow-reference.md diff --git a/crates/labcolors-core/Cargo.toml b/crates/labcolors-core/Cargo.toml index 0c80c6f8..6bb3bcdb 100644 --- a/crates/labcolors-core/Cargo.toml +++ b/crates/labcolors-core/Cargo.toml @@ -39,3 +39,7 @@ harness = false [[bench]] name = "forward" harness = false + +[[bench]] +name = "glow_alpha" +harness = false diff --git a/crates/labcolors-core/benches/glow_alpha.rs b/crates/labcolors-core/benches/glow_alpha.rs new file mode 100644 index 00000000..dd103aa4 --- /dev/null +++ b/crates/labcolors-core/benches/glow_alpha.rs @@ -0,0 +1,36 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use labcolors_core::ViewingConditions; +use labcolors_core::glow::solve_screen_alpha_for_dj; +use std::hint::black_box; + +fn bench_glow_alpha(c: &mut Criterion) { + let vc = ViewingConditions::dim_surround(); + let mut group = c.benchmark_group("glow_alpha"); + for (name, tint, background, target) in [ + ("base", "#4A8FFF", "#101012", 2.3006), + ("bloom", "#FF3B30", "#101012", 13.3251), + ("unreachable", "#3E87FF", "#FFFFFF", 2.3006), + ] { + group.bench_with_input( + BenchmarkId::new("solve", name), + &(tint, background, target), + |b, &(tint, background, target)| { + b.iter(|| { + black_box( + solve_screen_alpha_for_dj( + black_box(tint), + black_box(background), + black_box(target), + black_box(&vc), + ) + .expect("benchmark inputs are valid"), + ) + }); + }, + ); + } + group.finish(); +} + +criterion_group!(benches, bench_glow_alpha); +criterion_main!(benches); diff --git a/crates/labcolors-core/src/alpha.rs b/crates/labcolors-core/src/alpha.rs index b7d864d9..b0114602 100644 --- a/crates/labcolors-core/src/alpha.rs +++ b/crates/labcolors-core/src/alpha.rs @@ -1,14 +1,14 @@ //! Альфа-аналог солидного цвета: прямой и обратный ход straight-alpha //! композита в ГАММА-КОДИРОВАННОМ sRGB. //! -//! Пространство закона — измеренный факт, не выбор: Figma композитит на -//! канвасе попарно по кодированным каналам `c = α·t + (1−α)·b`, и ровно этот -//! путь воспроизводит все 12 семантических dJ'-якорей движка +//! Кодированный домен заземлён измерением: модель `c = α·t + (1−α)·b` +//! воспроизводит все 12 семантических Figma-якорей движка //! (`reference/labui-figma-structure.md` §3–§4, воспроизводимо -//! `cargo run -p labcolors-core --example figma_anchor_provenance`); браузерный -//! композитинг CSS-альфы живёт в том же device-пространстве. Линейный свет -//! (внутренний `srgb_from_hex`) здесь не участвует — он для колориметрии, не -//! для наложения. +//! `cargo run -p labcolors-core --example figma_anchor_provenance`). Для +//! эмиссии контракт дополнительно фиксирует **encoded-sRGB8 source-over +//! reference**: арифметика идёт на байтах `0..255` с одним итоговым round. +//! Совпадение конкретного renderer зависит от его color-management профиля и +//! проверяется отдельно; линейный свет здесь только для последующей колориметрии. //! //! # Зачем обратный ход //! @@ -19,10 +19,12 @@ //! t = (c − (1−α)·b) / α (по каналам, кодированные значения) //! ``` //! -//! Композит инверсии равен солиду ПО ПОСТРОЕНИЮ, поэтому контраст, dJ' и -//! WCAG-статус альфа-аналога на каноническом фоне тождественны солиду — -//! AA-корректность наследуется алгеброй, а не проверяется заново (теорема -//! запинена тестом `inversion_identity_is_exact_on_continuous_values`). +//! В вещественной алгебре continuous encoded-sRGB композит инверсии равен +//! солиду тождественно. Реализация на `binary64` честно отделена: ошибка +//! round-trip ограничена выведенной оценкой `8·ε/α` и проверяется тестом +//! `inversion_identity_respects_derived_binary64_error_bound`. После эмиссии тинт уже +//! лежит на sRGB8-сетке; побайтовый контракт ниже доказывается и проверяется +//! отдельно, а не приписывается непрерывной функции. //! На ином фоне композит другой — это и есть смысл альфы (адаптация к //! подложке), гарантия формулируется для фона, на котором решён солид. //! @@ -35,17 +37,28 @@ //! t ≤ 1 ⇔ α ≥ (c − b) / (1 − b) (канал с c > b; при b = 1 симметрично) //! ``` //! -//! [`min_alpha_encoded`] — максимум этих границ по каналам; ниже него -//! [`invert_composite_encoded`] честно возвращает `None`, а не клампит -//! (кламп молча сдвинул бы композит — подмена запрещена). +//! [`min_alpha_encoded`] начинает с максимума этих алгебраических границ, затем +//! возвращает первый `binary64`, на котором строгая binary64-инверсия реально +//! лежит в `[0,1]³`. [`invert_composite_encoded`] может также принять более +//! раннюю граничную пару только если повторный binary64-композит побитно равен +//! входному solid; глобального epsilon и безусловного clamp нет. Это не минимальная α +//! дискретного sRGB8-композита: `#010000` над чёрным уже округляется из белого +//! красного канала при `α=0.5/255`, тогда как strict-binary64 пол равен `1/255`. //! -//! Квантование: солид, прочитанный из 8-битного hex, несёт ошибку ≤ 0.5/255 на -//! канал; инверсия масштабирует её в 1/α раз, поэтому восстановленный тинт -//! отклоняется от истинного не более чем на `0.5/(255·α)` на канал. Граница -//! запинена тестом `quantisation_error_bound_is_honoured`; при малых α точное -//! побайтное восстановление тинта не гарантируется — гарантируется прямой ход -//! (композит канонического тинта воспроизводит опорный hex побайтно, 12/12 -//! Figma-пар в `figma_neutral_ladder_pairs_roundtrip`). +//! Для побайтового пути сначала квантуются солид и фон. Пусть их канал — целые +//! `S, B`, непрерывная инверсия дала `T`, а эмитируемый тинт — +//! `Q = round(T)`. Тогда до итогового round композит равен +//! `S + α·(Q−T)`. При `α < 1` его отклонение строго меньше половины байта; +//! при `α = 1` имеем `T = S` и `Q = S`. Поэтому итоговый +//! [`composite_over_srgb8`] возвращает ровно `S`. Теорема исчерпывающе проверена +//! на всех 65 536 парах `(S, B)` для нескольких α, включая ближайшую к единице +//! снизу. Production-путь всё равно проверяет равенство после композиции и +//! отказывается эмитировать пару при нарушении инварианта: ошибка float не +//! должна превращаться в правдоподобно неверный цвет. +//! +//! Это не обещает восстановить исходный тинт: солид из 8-битного hex несёт +//! ошибку ≤ 0.5/255, которую инверсия масштабирует в `1/α` раз. Граница +//! `0.5/(255·α)` запинена тестом `quantisation_error_bound_is_honoured`. use crate::spaces::srgb::{hex_from_srgb_encoded, srgb_encoded_from_hex}; @@ -56,17 +69,14 @@ fn is_encoded_rgb(v: [f64; 3]) -> bool { .all(|x| x.is_finite() && (0.0..=1.0).contains(&x)) } -/// Прямой ход: straight-alpha композит `α·tint + (1−α)·bg` по кодированным -/// каналам — закон Figma/браузера (см. модульную документацию). -/// -/// Домен: `tint`/`bg` — кодированные цвета в `[0,1]³`, `alpha ∈ [0,1]`; -/// вне домена — `debug_assert` (горячий путь чистой алгебры не платит за -/// проверки в релизе; строгие Option-контракты — у инверсии и `min_alpha`). -pub fn composite_over_encoded(tint: [f64; 3], alpha: f64, bg: [f64; 3]) -> [f64; 3] { - debug_assert!( - is_encoded_rgb(tint) && is_encoded_rgb(bg) && (0.0..=1.0).contains(&alpha), - "composite_over_encoded: вход вне домена кодированного sRGB" - ); +/// Непрерывная алгебра straight-alpha до квантования. Она нужна инверсии и +/// материалам; путь эмитируемого 8-битного пикселя использует +/// [`composite_over_srgb8`], чтобы нормализация `byte/255` не сдвигала half-tie. +pub(crate) fn composite_over_encoded_unchecked( + tint: [f64; 3], + alpha: f64, + bg: [f64; 3], +) -> [f64; 3] { [ alpha * tint[0] + (1.0 - alpha) * bg[0], alpha * tint[1] + (1.0 - alpha) * bg[1], @@ -74,73 +84,238 @@ pub fn composite_over_encoded(tint: [f64; 3], alpha: f64, bg: [f64; 3]) -> [f64; ] } +/// Прямой непрерывный ход: `α·tint + (1−α)·bg` по кодированным каналам. +/// +/// Это алгебра над `[0,1]`, не финальное округление sRGB8-reference. Для +/// эмитированных sRGB8-цветов используй [`composite_over_srgb8`]. +/// +/// # Errors +/// +/// `Err`, если канал не конечен/вне `[0,1]` либо `alpha` не конечна/вне +/// `[0,1]`. Публичная граница не зажимает мусор и одинакова в debug/release. +pub fn composite_over_encoded( + tint: [f64; 3], + alpha: f64, + bg: [f64; 3], +) -> Result<[f64; 3], String> { + if !is_encoded_rgb(tint) { + return Err(format!("tint вне конечного encoded-sRGB [0,1]: {tint:?}")); + } + if !is_encoded_rgb(bg) { + return Err(format!("bg вне конечного encoded-sRGB [0,1]: {bg:?}")); + } + if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) { + return Err(format!("alpha вне конечного [0,1]: {alpha}")); + } + Ok(composite_over_encoded_unchecked(tint, alpha, bg)) +} + +/// Straight-alpha непрозрачного sRGB8-фона в шкале `0..255`, которую несут +/// каналы эмитированного CSS-цвета. +/// +/// Округление выполняется один раз, после композиции. Это существенно на +/// half-tie: `(250/255)·0.122·255` может стать `30.499…`, хотя эталонная +/// byte-reference `250·0.122` равен `30.5` и по round-half-up даёт байт 31. +/// +/// # Errors +/// +/// `Err`, если `alpha` не конечна или лежит вне `[0,1]`. +pub fn composite_over_srgb8(tint: [u8; 3], alpha: f64, bg: [u8; 3]) -> Result<[u8; 3], String> { + if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) { + return Err(format!("alpha вне конечного [0,1]: {alpha}")); + } + let channel = |index: usize| { + (f64::from(tint[index]) * alpha + f64::from(bg[index]) * (1.0 - alpha)).round() as u8 + }; + Ok([channel(0), channel(1), channel(2)]) +} + +fn encoded_to_srgb8(rgb: [f64; 3], label: &str) -> Result<[u8; 3], String> { + if !is_encoded_rgb(rgb) { + return Err(format!("{label} вне конечного encoded-sRGB [0,1]: {rgb:?}")); + } + Ok(rgb.map(|channel| (channel * 255.0).round() as u8)) +} + +fn hex_from_srgb8(rgb: [u8; 3]) -> String { + format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2]) +} + +/// Общий внутренний путь для semantic-метрик: вход уже квантован для эмиссии, +/// но повторная проверка не позволяет новому call site протащить NaN или clamp. +pub(crate) fn composite_srgb8_from_encoded( + tint: [f64; 3], + alpha: f64, + bg: [f64; 3], +) -> Result<[u8; 3], String> { + composite_over_srgb8( + encoded_to_srgb8(tint, "tint")?, + alpha, + encoded_to_srgb8(bg, "bg")?, + ) +} + +pub(crate) fn composite_hex_from_encoded( + tint: [f64; 3], + alpha: f64, + bg: [f64; 3], +) -> Result { + composite_srgb8_from_encoded(tint, alpha, bg).map(hex_from_srgb8) +} + /// Обратный ход: тинт, чей композит с `alpha` на `bg` равен `solid`. /// /// `None`, если вход вне домена (`solid`/`bg` не кодированные цвета `[0,1]³` /// или не конечные), `alpha` не в `(0, 1]` (при α=0 инверсия вырождена — /// композит не зависит от тинта), либо хотя бы один канал тинта выходит из -/// гамута `[0,1]` (α ниже [`min_alpha_encoded`]). +/// гамута `[0,1]`. Значение за границей канонизируется в 0/1 только при +/// побитном постусловии прямого binary64-хода; иначе возвращается `None`. pub fn invert_composite_encoded(solid: [f64; 3], alpha: f64, bg: [f64; 3]) -> Option<[f64; 3]> { if !(is_encoded_rgb(solid) && is_encoded_rgb(bg) && alpha > 0.0 && alpha <= 1.0) { return None; } let mut tint = [0.0; 3]; + let mut projected_boundary = [false; 3]; for c in 0..3 { - let t = (solid[c] - (1.0 - alpha) * bg[c]) / alpha; - // EPS: допуск на плавающую арифметику самой инверсии (не на квантование - // входа) — значения, вышедшие за [0,1] на машинный шум, клампятся к - // границе; настоящий выход из гамута отвергается. - const EPS: f64 = 1e-12; - if !(-EPS..=1.0 + EPS).contains(&t) { + let t = bg[c] + (solid[c] - bg[c]) / alpha; + if (0.0..=1.0).contains(&t) { + tint[c] = t; + } else { + // Никакого epsilon: пробуем только математическую границу gamut. + // Она допустима лишь когда ТОТ ЖЕ binary64-прямой ход побитно + // воспроизводит входной solid; иначе это была бы подмена цвета. + tint[c] = if t < 0.0 { 0.0 } else { 1.0 }; + projected_boundary[c] = true; + } + } + if projected_boundary.into_iter().any(|projected| projected) { + let recomposed = composite_over_encoded_unchecked(tint, alpha, bg); + if (0..3).any(|c| projected_boundary[c] && recomposed[c].to_bits() != solid[c].to_bits()) { return None; } - tint[c] = t.clamp(0.0, 1.0); } Some(tint) } -/// Минимальная α, при которой инверсия `solid` над `bg` разрешима в гамуте -/// (все каналы тинта в `[0,1]`). Для `solid == bg` равна 0 (любой видимый -/// эффект отсутствует, тинт = фон при любой α). +/// Строгая binary64-инверсия уже проверенного домена. /// -/// `None` при входе вне домена (не кодированный цвет `[0,1]³` / не конечный) — -/// молчаливый ответ на мусор был бы ложным обещанием разрешимости. -pub fn min_alpha_encoded(solid: [f64; 3], bg: [f64; 3]) -> Option { - if !is_encoded_rgb(solid) || !is_encoded_rgb(bg) { - return None; +/// Форма `bg + (solid-bg)/alpha` уменьшает отмену близких величин. Для каждого +/// знака `solid-bg` правильно округлённые IEEE-операции монотонны по alpha; +/// поэтому предикат попадания в `[0,1]` пригоден для exact lower-bound ниже. +fn invert_composite_strict(solid: [f64; 3], alpha: f64, bg: [f64; 3]) -> Option<[f64; 3]> { + let mut tint = [0.0; 3]; + for c in 0..3 { + let t = bg[c] + (solid[c] - bg[c]) / alpha; + if !(0.0..=1.0).contains(&t) { + return None; + } + tint[c] = t; } + Some(tint) +} + +/// Binary64-вычисление алгебраической границы непрерывной инверсии. +/// +/// Это только стартовая точка поиска: округление деления может поставить её +/// как выше, так и ниже первого `binary64`, проходящего фактический строгий +/// предикат [`invert_composite_strict`]. +fn analytic_alpha_candidate(solid: [f64; 3], bg: [f64; 3]) -> f64 { let mut lo = 0.0f64; for c in 0..3 { let (s, b) = (solid[c], bg[c]); let bound = if s < b { - (b - s) / b // b > s ≥ 0 ⇒ b > 0, деление определено + (b - s) / b } else if s > b { - (s - b) / (1.0 - b) // s > b ⇒ b < 1, деление определено + (s - b) / (1.0 - b) } else { 0.0 }; lo = lo.max(bound); } - Some(lo.clamp(0.0, 1.0)) + lo.clamp(0.0, 1.0) +} + +/// Первый `binary64`, на котором строгая binary64-инверсия `solid` над `bg` +/// возвращает тинт в `[0,1]³`. Для `solid == bg` по определению равен 0. +/// Предыдущее представимое число обязательно не проходит тот же предикат. +/// Это точный дискретный минимум реализации, но не точный вещественный инфимум: +/// округление операций способно сдвинуть его относительно алгебраической +/// границы в обе стороны. +/// +/// Это также не минимальная α на sRGB8-сетке: итоговый round иногда делает +/// целевой байт достижимым раньше. +/// Например, `solid=1/255`, `bg=0` даёт здесь `1/255`, хотя tint byte 255 при +/// `α=0.5/255` уже композитится в byte 1. +/// +/// `None` при входе вне домена (не кодированный цвет `[0,1]³` / не конечный) — +/// молчаливый ответ на мусор был бы ложным обещанием разрешимости. +pub fn min_alpha_encoded(solid: [f64; 3], bg: [f64; 3]) -> Option { + if !is_encoded_rgb(solid) || !is_encoded_rgb(bg) { + return None; + } + if solid == bg { + return Some(0.0); + } + let analytic = analytic_alpha_candidate(solid, bg); + debug_assert!(analytic > 0.0); + + // Положительные finite f64 упорядочены unsigned-битами. Сначала + // экспоненциально находим ближайшую пару «не проходит / проходит» вокруг + // аналитического кандидата; так обычный случай занимает несколько проб, + // но ответ не зависит от эвристического лимита числа ULP. + let analytic_bits = analytic.to_bits(); + let one_bits = 1.0f64.to_bits(); + let (mut failing_bits, mut passing_bits) = + if invert_composite_strict(solid, analytic, bg).is_some() { + let mut passing = analytic_bits; + let mut step = 1_u64; + loop { + let candidate = passing.saturating_sub(step); + if invert_composite_strict(solid, f64::from_bits(candidate), bg).is_none() { + break (candidate, passing); + } + passing = candidate; + step = step.saturating_mul(2); + } + } else { + let mut failing = analytic_bits; + let mut step = 1_u64; + loop { + let candidate = failing.saturating_add(step).min(one_bits); + if invert_composite_strict(solid, f64::from_bits(candidate), bg).is_some() { + break (failing, candidate); + } + failing = candidate; + step = step.saturating_mul(2); + } + }; + + // Монотонность строгой формы превращает найденный интервал в обычный + // exact lower-bound по целочисленным битам — epsilon и float-midpoint не + // участвуют. + while passing_bits - failing_bits > 1 { + let mid_bits = failing_bits + (passing_bits - failing_bits) / 2; + let mid = f64::from_bits(mid_bits); + if invert_composite_strict(solid, mid, bg).is_some() { + passing_bits = mid_bits; + } else { + failing_bits = mid_bits; + } + } + Some(f64::from_bits(passing_bits)) } /// Hex-обёртка прямого хода: композит `tint_hex @ alpha` над `bg_hex`, -/// квантованный до 8-битного hex — цвет, который реально покажет дисплей. +/// квантованный до 8-битного hex — канонический source-over reference-композит. /// /// # Errors /// -/// `Err` при невалидном hex на любом входе или `alpha` вне `[0,1]`/NaN — -/// публичная поверхность не пропускает мусор в release-алгебру (внутри только -/// `debug_assert`). +/// `Err` при невалидном hex на любом входе либо неконечной/внедиапазонной +/// `alpha`; ни одна сборка не подменяет такой ввод clamp-результатом. pub fn composite_hex(tint_hex: &str, alpha: f64, bg_hex: &str) -> Result { - if !(alpha.is_finite() && (0.0..=1.0).contains(&alpha)) { - return Err(format!("alpha must be in [0,1], got {alpha}")); - } let tint = srgb_encoded_from_hex(tint_hex)?; let bg = srgb_encoded_from_hex(bg_hex)?; - Ok(hex_from_srgb_encoded(composite_over_encoded( - tint, alpha, bg, - ))) + composite_hex_from_encoded(tint, alpha, bg) } /// Hex-обёртка обратного хода: тинт для `solid_hex @ alpha` над `bg_hex`, @@ -148,18 +323,25 @@ pub fn composite_hex(tint_hex: &str, alpha: f64, bg_hex: &str) -> Result Result, String> { + if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) { + return Err(format!("alpha вне конечного [0,1]: {alpha}")); + } let solid = srgb_encoded_from_hex(solid_hex)?; let bg = srgb_encoded_from_hex(bg_hex)?; Ok(invert_composite_encoded(solid, alpha, bg).map(hex_from_srgb_encoded)) } -/// Hex-обёртка [`min_alpha_encoded`]. +/// Hex-обёртка strict-binary64 пола [`min_alpha_encoded`]. Значение не следует +/// интерпретировать как вещественный инфимум или минимальную α дискретного +/// sRGB8-композита. /// /// # Errors /// @@ -172,16 +354,19 @@ pub fn min_alpha_hex(solid_hex: &str, bg_hex: &str) -> Result { .expect("hex-вход всегда в домене byte/255 — None недостижим по построению")) } -/// Альфа-аналог солида: тинт + ФАКТИЧЕСКАЯ α. +/// Непрерывный альфа-аналог солида: тинт + ФАКТИЧЕСКАЯ α. /// /// Продуктовый слой поверх строгого закона: потребитель всегда получает -/// пригодный ответ, и ответ никогда не врёт о цвете. +/// пригодный ответ без подмены цели клампом. Вещественная формула сохраняет +/// заданный цвет, а ошибка её binary64-вычисления ограничена доказанной выше +/// оценкой. Побайтовая эмиссия в hex/semantic использует отдельный проверяющий +/// sRGB8-путь с точным постусловием по байтам. #[derive(Debug, Clone, Copy, PartialEq)] pub struct AlphaAnalog { /// Кодированный тинт `[0,1]³`. pub tint: [f64; 3], - /// Фактическая α: запрошенная, если она разрешима, иначе минимально - /// разрешимая (композит при ней остаётся точно равным солиду). + /// Фактическая α: запрошенная, если она разрешима, иначе strict-binary64 + /// пол (не вещественный инфимум и не минимальная byte-grid α). pub alpha: f64, } @@ -189,25 +374,39 @@ pub struct AlphaAnalog { /// /// «Приблизить» можно двумя способами, и только один честен: кламп тинта при /// запрошенной α тихо сдвинул бы композит (система соврала бы о цвете — -/// запрещённая подмена), а подъём α до [`min_alpha_encoded`] сохраняет -/// композит ПОБАЙТНО равным солиду — двигается только прозрачность, и -/// фактическая α возвращается явно ([`AlphaAnalog::alpha`]). Запрошенная α -/// клампится в `[0,1]` (α=0 вырожденна и поднимается до α_min как «слишком -/// низкая»). +/// запрещённая подмена), а подъём α до strict-binary64 пола +/// [`min_alpha_encoded`] сохраняет исходную цель вещественной инверсии; в +/// binary64 остаётся только ограниченная ошибка округления, а не произвольный +/// цветовой сдвиг. Двигается прозрачность, и +/// фактическая α возвращается явно ([`AlphaAnalog::alpha`]). Запрошенная α вне +/// `[0,1]` отвергается: clamp скрыл бы ошибку вызывающего кода. `α=0` входит в +/// библиотечный домен и поднимается до strict-binary64 пола, если solid +/// отличается от фона; при `solid == bg` возвращается вырожденная пара +/// `tint=bg, α=0`. /// -/// `None` — только на входе вне домена (не кодированный цвет `[0,1]³` / -/// не конечный); для валидных цветов ответ существует всегда (в худшем -/// случае α=1, тинт=солид). +/// `None` — только на входе вне домена (цвет не конечен/не в `[0,1]³` либо +/// запрошенная α не конечна/не в `[0,1]`). Для валидного входа ответ существует +/// всегда (в худшем случае α=1, тинт=солид). pub fn resolve_alpha_analog( solid: [f64; 3], requested_alpha: f64, bg: [f64; 3], ) -> Option { - let floor = min_alpha_encoded(solid, bg)?; // None только на мусор-входах - if !requested_alpha.is_finite() { + if !requested_alpha.is_finite() || !(0.0..=1.0).contains(&requested_alpha) { return None; } - let alpha = requested_alpha.clamp(0.0, 1.0).max(floor); + let floor = min_alpha_encoded(solid, bg)?; // None только на мусор-входах + // Если запрошенная binary64-пара уже является точной обратной к нашему + // прямому ходу (включая честную gamut-границу), не поднимаем прозрачность. + if requested_alpha > 0.0 { + if let Some(tint) = invert_composite_encoded(solid, requested_alpha, bg) { + return Some(AlphaAnalog { + tint, + alpha: requested_alpha, + }); + } + } + let alpha = requested_alpha.max(floor); // При α == floor == 0 солид равен фону: любой видимый эффект отсутствует, // тинт = фон (инверсия при α=0 вырожденна — отвечаем без неё). if alpha == 0.0 { @@ -218,11 +417,68 @@ pub fn resolve_alpha_analog( Some(AlphaAnalog { tint, alpha }) } -/// Hex-обёртка [`resolve_alpha_analog`]: `(tint_hex, фактическая α)`. +/// Эмиссионный sRGB8-путь альфа-аналога. +/// +/// Входные цвета сначала квантуются до той же byte-сетки, в которой будет +/// эмитирован тинт. Затем выполняется непрерывная инверсия, тинт квантуется и +/// итог проверяется через production-композитор [`composite_over_srgb8`]. Это +/// отделяет continuous API от более сильного побайтового контракта hex/semantic. +/// +/// Недоменная запрошенная α и нарушение точного постусловия возвращаются как +/// `Err`: такой результат нельзя отдавать наружу. `Ok(None)` сохранён в форме +/// внутреннего API как защитный исход, но при валидных цветах и α недостижим. +pub(crate) fn resolve_alpha_analog_srgb8( + solid: [f64; 3], + requested_alpha: f64, + bg: [f64; 3], +) -> Result, String> { + if !requested_alpha.is_finite() || !(0.0..=1.0).contains(&requested_alpha) { + return Err(format!( + "requested_alpha вне конечного [0,1]: {requested_alpha}" + )); + } + let solid_srgb8 = encoded_to_srgb8(solid, "solid")?; + let bg_srgb8 = encoded_to_srgb8(bg, "bg")?; + let to_encoded = |rgb: [u8; 3]| rgb.map(|channel| f64::from(channel) / 255.0); + + let Some(analog) = resolve_alpha_analog( + to_encoded(solid_srgb8), + requested_alpha, + to_encoded(bg_srgb8), + ) else { + return Ok(None); + }; + let tint_srgb8 = encoded_to_srgb8(analog.tint, "resolved tint")?; + verify_srgb8_alpha_analog(solid_srgb8, tint_srgb8, analog.alpha, bg_srgb8)?; + Ok(Some((tint_srgb8, analog.alpha))) +} + +/// Глубинная проверка постусловия именно в эмитируемой byte-арифметике. +/// Отдельная функция нужна, чтобы тест мог доказать: неверная пара отвергается, +/// а не только что текущий солвер обычно выдаёт верную. +fn verify_srgb8_alpha_analog( + solid: [u8; 3], + tint: [u8; 3], + alpha: f64, + bg: [u8; 3], +) -> Result<(), String> { + let composite = composite_over_srgb8(tint, alpha, bg)?; + if composite != solid { + return Err(format!( + "alpha-analog не воспроизвёл sRGB8-цель: solid={solid:?}, tint={tint:?}, alpha={alpha}, bg={bg:?}, composite={composite:?}" + )); + } + Ok(()) +} + +/// Hex-обёртка эмиссионного sRGB8-резолвера: `(tint_hex, фактическая α)`. +/// Возвращённая пара побайтно воспроизводит +/// `solid_hex` через [`composite_over_srgb8`]; постусловие проверено до возврата. /// /// # Errors /// -/// `Err` при невалидном hex на любом входе. +/// `Err` при невалидном hex, неконечной/внедиапазонной запрошенной α либо при +/// нарушении точного sRGB8-постусловия (защитная ветка против численного дрейфа). pub fn resolve_alpha_analog_hex( solid_hex: &str, requested_alpha: f64, @@ -230,8 +486,8 @@ pub fn resolve_alpha_analog_hex( ) -> Result, String> { let solid = srgb_encoded_from_hex(solid_hex)?; let bg = srgb_encoded_from_hex(bg_hex)?; - Ok(resolve_alpha_analog(solid, requested_alpha, bg) - .map(|a| (hex_from_srgb_encoded(a.tint), a.alpha))) + Ok(resolve_alpha_analog_srgb8(solid, requested_alpha, bg)? + .map(|(tint, alpha)| (hex_from_srgb8(tint), alpha))) } #[cfg(test)] @@ -273,6 +529,84 @@ mod tests { } } + /// Контрпример на точной половине байта: source-over reference считает + /// каналы в шкале 0..255, поэтому `250 × 0.122 = 30.5` даёт 31. + /// Нормализация через `(250/255)×255` теряла tie и давала 30. + #[test] + fn emitted_composite_uses_byte_reference_arithmetic_at_half_tie() { + assert_eq!( + composite_hex("#C0B2FA", 0.122, "#000000").unwrap(), + "#17161F" + ); + } + + /// Теорема для всей одноканальной области известной α: эталон считается + /// целыми как `(122·t + 878·b)/1000`, поэтому тест не повторяет f64-путь + /// production-кода и ловит нормализацию через `/255·255` на всех half-tie. + #[test] + fn byte_reference_alpha_0122_matches_exact_rational_for_all_channel_pairs() { + for tint in u8::MIN..=u8::MAX { + for bg in u8::MIN..=u8::MAX { + let got = composite_over_srgb8([tint, 0, 0], 0.122, [bg, 0, 0]).unwrap()[0]; + let numerator = 122_u32 * u32::from(tint) + 878_u32 * u32::from(bg); + let expected = ((numerator + 500) / 1_000) as u8; + assert_eq!(got, expected, "tint={tint}, bg={bg}"); + } + } + } + + /// Конечная sRGB8-теорема альфа-аналога: для каждой из 65 536 пар + /// `(solid, bg)` и каждой представительной запрошенной α проверяющий + /// production-резолвер обязан вернуть тинт, который публичный byte- + /// композитор складывает ТОЧНО в исходный solid. `near_one` — соседнее + /// представимое число перед `1.0`; оно закрывает минимальный запас до + /// половины байта, где float-дрейф опаснее всего. + #[test] + fn srgb8_alpha_analog_is_exact_for_every_channel_pair() { + let near_one = f64::from_bits(1.0_f64.to_bits() - 1); + for requested_alpha in [0.0, 0.01, 0.122, 0.5, 0.9, near_one, 1.0] { + for solid in u8::MIN..=u8::MAX { + for bg in u8::MIN..=u8::MAX { + let solid_encoded = [f64::from(solid) / 255.0; 3]; + let bg_encoded = [f64::from(bg) / 255.0; 3]; + let (tint, actual_alpha) = + resolve_alpha_analog_srgb8(solid_encoded, requested_alpha, bg_encoded) + .unwrap_or_else(|error| { + panic!( + "solid={solid}, bg={bg}, requested={requested_alpha}: {error}" + ) + }) + .expect("конечная α и sRGB8-цвета всегда разрешимы"); + let got = composite_over_srgb8(tint, actual_alpha, [bg; 3]) + .expect("резолвер возвращает α в [0,1]"); + assert_eq!( + got, [solid; 3], + "solid={solid}, bg={bg}, requested={requested_alpha}, actual={actual_alpha}, tint={tint:?}" + ); + } + } + } + } + + /// Контрпример прежнего смешения доменов: `0.25/255` квантуется в byte 0, + /// но инверсия НЕквантованной цели при α=0.5 дала tint byte 1, чей + /// production-композит равен 1. Эмиссионный путь обязан сначала квантовать + /// цель, а глубинная проверка — отвергать заведомо неверную пару. + #[test] + fn srgb8_resolver_quantises_target_before_inversion_and_guard_rejects_drift() { + let off_grid_solid = [0.25 / 255.0; 3]; + let (tint, alpha) = resolve_alpha_analog_srgb8(off_grid_solid, 0.5, [0.0; 3]) + .expect("валидный числовой домен") + .expect("конечная α разрешима"); + assert_eq!(tint, [0; 3]); + assert_eq!(composite_over_srgb8(tint, alpha, [0; 3]).unwrap(), [0; 3]); + + assert!( + verify_srgb8_alpha_analog([0; 3], [1; 3], 0.5, [0; 3]).is_err(), + "пара из старого off-grid пути обязана быть отвергнута" + ); + } + /// Обратный ход на живых парах: восстановленный тинт отклоняется от /// канонического не более чем на границу квантования 0.5/(255·α) на канал, /// а его композит воспроизводит опорный hex побайтно (что и является @@ -293,18 +627,19 @@ mod tests { (tint[c] - true_tint[c]).abs() ); } - // Продуктовая гарантия: композит восстановленного тинта == опорный hex. - let recomposed = hex_from_srgb_encoded(composite_over_encoded(tint, *alpha, b)); + // Продуктовая гарантия проверяется production byte-композитором, + // а не повторением непрерывной алгебры инверсии. + let recomposed = composite_hex_from_encoded(tint, *alpha, b).unwrap(); assert_eq!(&recomposed, solid, "{solid}@{alpha}: re-композит разошёлся"); } } - /// Теорема тождества на непрерывных значениях: инверсия прямого хода - /// возвращает исходный тинт с машинной точностью для любых α ≥ α_min — - /// значит контраст альфа-аналога на каноническом фоне тождественен солиду - /// по построению. Свип — детерминированная сетка кодированных значений. + /// Алгебраическое тождество на непрерывных значениях с явной binary64- + /// границей ошибки. Прямой ход содержит три базовые операции, обратный — + /// ещё три; стандартная first-order оценка каждой правильно округлённой + /// операции даёт консервативные `8·ε/α` на восстановленный канал. #[test] - fn inversion_identity_is_exact_on_continuous_values() { + fn inversion_identity_respects_derived_binary64_error_bound() { let grid: Vec = (0..=10).map(|i| f64::from(i) / 10.0).collect(); for &tr in &grid { for &tb in &grid { @@ -312,16 +647,16 @@ mod tests { let tint = [tr, 0.5, tb]; let bg = [br, 0.25, 0.9]; for alpha in [0.05, 0.2, 0.5, 0.85, 1.0] { - let solid = composite_over_encoded(tint, alpha, bg); - let back = invert_composite_encoded(solid, alpha, bg) - .expect("прямой ход всегда обратим при той же α"); + let solid = composite_over_encoded_unchecked(tint, alpha, bg); + let back = invert_composite_encoded(solid, alpha, bg).unwrap_or_else(|| { + panic!( + "прямой ход не обратим: tint={tint:?}, bg={bg:?}, alpha={alpha}, solid={solid:?}" + ) + }); for c in 0..3 { - assert!( - (back[c] - tint[c]).abs() < 1e-9, - "identity: канал {c}: {} != {}", - back[c], - tint[c] - ); + let error = (back[c] - tint[c]).abs(); + let bound = 8.0 * f64::EPSILON / alpha; + assert!(error <= bound, "канал {c}: error={error} > {bound}"); } } } @@ -329,34 +664,62 @@ mod tests { } } - /// Аналитика α_min: на границе инверсия разрешима, чуть ниже — нет. - /// Свип по сетке пар (solid, bg), исключая вырожденный случай solid == bg - /// (там α_min = 0 и «чуть ниже» не существует). + /// Полный одноканальный sRGB8-домен фиксирует точное определение пола: + /// возвращённый `binary64` проходит строгую инверсию, а непосредственно + /// предшествующий — нет. Проверка predecessor принципиальна: округлённый + /// аналитический кандидат нередко проходит вместе с несколькими числами + /// перед ним, поэтому простого `candidate is feasible` недостаточно. #[test] - fn min_alpha_is_the_exact_feasibility_boundary() { - let grid: Vec = (0..=8).map(|i| f64::from(i) / 8.0).collect(); - for &s in &grid { - for &b in &grid { - if (s - b).abs() < 1e-15 { + fn min_alpha_is_first_passing_binary64_for_every_srgb8_channel_pair() { + for solid_byte in u8::MIN..=u8::MAX { + for bg_byte in u8::MIN..=u8::MAX { + let s = f64::from(solid_byte) / 255.0; + let b = f64::from(bg_byte) / 255.0; + let solid = [s, 0.25, 0.75]; + let bg = [b, 0.25, 0.75]; + let floor = min_alpha_encoded(solid, bg).expect("sRGB8 всегда в домене"); + + if solid_byte == bg_byte { + assert_eq!(floor.to_bits(), 0.0_f64.to_bits()); continue; } - let solid = [s, 0.3, 0.7]; - let bg = [b, 0.3, 0.7]; - let a_min = min_alpha_encoded(solid, bg).expect("вход в домене"); + assert!( - invert_composite_encoded(solid, a_min.max(1e-9), bg).is_some(), - "solid={s}, bg={b}: неразрешимо на собственной α_min={a_min}" + invert_composite_strict(solid, floor, bg).is_some(), + "solid={solid_byte}, bg={bg_byte}: floor={floor} не проходит" + ); + let predecessor = f64::from_bits(floor.to_bits() - 1); + assert!( + invert_composite_strict(solid, predecessor, bg).is_none(), + "solid={solid_byte}, bg={bg_byte}: predecessor={predecessor} тоже проходит; floor={floor} не минимален" ); - if a_min > 1e-6 { - assert!( - invert_composite_encoded(solid, a_min * 0.999, bg).is_none(), - "solid={s}, bg={b}: разрешимо НИЖЕ α_min={a_min} — граница не точна" - ); - } } } } + /// `min_alpha_encoded` нельзя называть минимумом sRGB8-композита: + /// финальное округление расширяет достижимое множество на половину кода. + /// Контрпример также защищает смысл `alphaCoerced`: это сознательно + /// отдельный strict-binary64 пол. + #[test] + fn strict_binary64_floor_is_not_the_byte_grid_minimum() { + let solid = [1.0 / 255.0, 0.0, 0.0]; + let bg = [0.0; 3]; + let strict_floor = min_alpha_encoded(solid, bg).unwrap(); + let byte_alpha = 0.5 / 255.0; + + assert_eq!(strict_floor, 1.0 / 255.0); + assert!(byte_alpha < strict_floor); + assert_eq!( + composite_over_srgb8([255, 0, 0], byte_alpha, [0; 3]).unwrap(), + [1, 0, 0] + ); + assert!( + invert_composite_encoded(solid, byte_alpha, bg).is_none(), + "byte достижим округлением, но continuous-тинт был бы вне gamut" + ); + } + /// Вырожденные α честно отвергаются, кламп не подменяет ответ. #[test] fn degenerate_alpha_is_rejected_not_clamped() { @@ -369,10 +732,20 @@ mod tests { assert_eq!(invert_composite_encoded(s, 1.0, b), Some(s)); } - /// Продуктовый резолвер никогда не врёт о цвете: при разрешимой + /// Выход из gamut даже на доли триллионной не является «машинным шумом»: + /// глобальный epsilon раньше превращал неразрешимую пару в ложный ответ. + #[test] + fn inversion_never_expands_gamut_with_epsilon() { + let alpha = 1.0 - 5e-13; + assert!(alpha < min_alpha_encoded([0.0; 3], [1.0; 3]).unwrap()); + assert!(invert_composite_encoded([0.0; 3], alpha, [1.0; 3]).is_none()); + } + + /// Непрерывный резолвер не подменяет целевой цвет: при разрешимой /// запрошенной α возвращает её саму; при неразрешимой — поднимает α ровно - /// до α_min, и композит остаётся ТОЧНО равным солиду (двигается - /// прозрачность, не цвет). Кламп-подмена тинта не происходит. + /// до α_min. Ошибка binary64-композита остаётся внутри доказанной оценки + /// (двигается прозрачность, не целевой цвет). Побайтовый постусловный тест живёт отдельно в + /// `srgb8_alpha_analog_is_exact_for_every_channel_pair`. #[test] fn resolver_moves_alpha_never_the_colour() { let grid: Vec = (0..=8).map(|i| f64::from(i) / 8.0).collect(); @@ -382,24 +755,35 @@ mod tests { let bg = [b, 0.4, 0.6]; let floor = min_alpha_encoded(solid, bg).expect("в домене"); for requested in [0.0, 0.05, 0.3, 0.9, 1.0] { + let requested_is_exact = + requested > 0.0 && invert_composite_encoded(solid, requested, bg).is_some(); let a = resolve_alpha_analog(solid, requested, bg).expect("в домене"); - // Фактическая α: запрошенная, если разрешима, иначе ровно α_min. - let want = requested.max(floor); - assert!( - (a.alpha - want).abs() < 1e-12, + // Фактическая α: запрошенная, если строгая или побитно-точная + // граничная инверсия разрешима; иначе ровно conservative floor. + let want = if requested_is_exact { + requested + } else { + requested.max(floor) + }; + assert_eq!( + a.alpha.to_bits(), + want.to_bits(), "solid={s},bg={b},req={requested}: α={} != {want}", a.alpha ); - // Композит НИКОГДА не отклоняется от солида. + // Отклонение — только ограниченная ошибка binary64, а не + // результат клампа или смены целевого цвета. let c = if a.alpha == 0.0 { bg // вырожденный случай solid==bg } else { - composite_over_encoded(a.tint, a.alpha, bg) + composite_over_encoded_unchecked(a.tint, a.alpha, bg) }; for ch in 0..3 { + let error = (c[ch] - solid[ch]).abs(); + let bound = 8.0 * f64::EPSILON; assert!( - (c[ch] - solid[ch]).abs() < 1e-9, - "solid={s},bg={b},req={requested}: композит уехал на канале {ch}" + error <= bound, + "solid={s},bg={b},req={requested}: канал {ch}, error={error} > {bound}" ); } } @@ -418,6 +802,23 @@ mod tests { } } + /// Обе публичные прямые границы одинаково отвергают мусор в debug/release; + /// непрерывная алгебра не доступна как panic/clamp-лазейка. + #[test] + fn public_compositors_reject_invalid_numeric_inputs() { + let valid = [0.25, 0.5, 0.75]; + for alpha in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.1, 1.1] { + assert!(composite_over_encoded(valid, alpha, valid).is_err()); + assert!(composite_over_srgb8([1, 2, 3], alpha, [4, 5, 6]).is_err()); + } + for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.1, 1.1] { + let mut rgb = valid; + rgb[0] = bad; + assert!(composite_over_encoded(rgb, 0.5, valid).is_err()); + assert!(composite_over_encoded(valid, 0.5, rgb).is_err()); + } + } + /// Все три hex-обёртки публичной поверхности: roundtrip на живой Figma-паре, /// плюс честный Err (не паника) на невалидном hex — .expect в min_alpha_hex /// недостижим, парсинг падает раньше через `?`. @@ -431,7 +832,8 @@ mod tests { // min_alpha_hex: для равных цветов пол = 0; для контрастной пары > 0. assert_eq!(min_alpha_hex("#FFFFFF", "#FFFFFF").unwrap(), 0.0); assert!(min_alpha_hex("#101012", "#FFFFFF").unwrap() > 0.9); - // resolve_alpha_analog_hex: неразрешимая α поднимается, композит равен солиду. + // Hex-путь: неразрешимая α поднимается, а итоговый sRGB8-композит + // побайтно равен солиду. let (tint2, actual) = resolve_alpha_analog_hex("#101012", 0.05, "#FFFFFF") .expect("валидный hex") .expect("цвета в домене"); @@ -445,19 +847,52 @@ mod tests { ] { assert!(f.is_some(), "невалидный hex обязан дать Err"); } + + // Ошибка вызывающего кода не должна выглядеть как физическая + // неразрешимость корректной пары цветов. + for bad in [ + f64::NAN, + f64::NEG_INFINITY, + f64::INFINITY, + -f64::EPSILON, + 1.0 + f64::EPSILON, + ] { + assert!( + invert_composite_hex("#808080", bad, "#FFFFFF").is_err(), + "alpha={bad} обязана дать Err, а не Ok(None)" + ); + } + assert_eq!( + invert_composite_hex("#808080", 0.0, "#FFFFFF").unwrap(), + None, + "нулевая alpha валидна, но обратный ход вырожден" + ); } - /// Резолвер отвергает только мусор; NaN-α — тоже мусор, не «поднять до α_min». + /// Резолвер строго отвергает α вне `[0,1]`: ни NaN, ни конечное значение за + /// границей не должны превращаться clamp-ом в правдоподобный ответ. #[test] - fn resolver_rejects_only_out_of_domain() { + fn resolver_rejects_every_out_of_domain_alpha() { let ok = [0.5, 0.5, 0.5]; assert!(resolve_alpha_analog([1.5, 0.0, 0.0], 0.5, ok).is_none()); - assert!(resolve_alpha_analog(ok, f64::NAN, ok).is_none()); - // Запрошенная α вне [0,1] клампится, не отвергается (пригодный ответ). - assert_eq!( - resolve_alpha_analog(ok, 5.0, ok).map(|a| a.alpha), - Some(1.0) - ); + for bad in [ + f64::NAN, + f64::NEG_INFINITY, + f64::INFINITY, + -f64::EPSILON, + 1.0 + f64::EPSILON, + -1.0, + 5.0, + ] { + assert!( + resolve_alpha_analog(ok, bad, ok).is_none(), + "requested α={bad} обязана быть отвергнута" + ); + assert!( + resolve_alpha_analog_hex("#808080", bad, "#808080").is_err(), + "hex-граница обязана вернуть Err для α={bad}" + ); + } } /// Домен ядра закреплён: внегамутные и неконечные входы отвергаются @@ -499,7 +934,7 @@ mod tests { let tint = [0.4, 0.6, 0.2]; let bg = [1.0, 1.0, 1.0]; for alpha in [0.08, 0.2, 0.5] { - let solid = composite_over_encoded(tint, alpha, bg); + let solid = composite_over_encoded_unchecked(tint, alpha, bg); let shifted = [solid[0] + 0.5 / 255.0, solid[1], solid[2]]; let back = invert_composite_encoded(shifted, alpha, bg) .expect("сдвиг на пол-кода не выбивает из гамута на этих значениях"); diff --git a/crates/labcolors-core/src/glow.rs b/crates/labcolors-core/src/glow.rs index 4c10a341..b10b0069 100644 --- a/crates/labcolors-core/src/glow.rs +++ b/crates/labcolors-core/src/glow.rs @@ -1,11 +1,15 @@ -//! Свечение — добавление СВЕТА, не наложение краски (labui ADR-0002 §5, -//! решение владельца 2026-07-03). +//! Конечный point-reference примитив для screen-рецепта glow. +//! +//! Модуль рассчитывает encoded-композит двух слоёв. Он не моделирует +//! физическое излучение, blur-поле или восприятие полного пространственного +//! эффекта: эти утверждения требуют render/context-контракта #221. //! //! # Модель //! //! Слой свечения — цвет G с непрозрачностью α и `mix-blend-mode: screen` //! над непрозрачным фоном. CSS Compositing 1 (blend → simple alpha -//! compositing) в device-пространстве даёт покомпонентно: +//! compositing) в объявленном reference-домене encoded sRGB даёт +//! покомпонентно: //! //! ```text //! result = (1−α)·bg + α·screen(G, bg) @@ -15,40 +19,35 @@ //! Свойства по построению (не проверкой): //! - **никогда не темнит**: α·G·(1−bg) ≥ 0 — вырождение «светлый тинт темнит //! светлый фон» у нормальной альфы здесь невозможно конструкцией; -//! - **монотонно и ЛИНЕЙНО по α** на канал — солвер интенсивности -//! тривиально сходится (по перцептивной цели — бисекция, J' монотонен); -//! - асимметрия тем бесплатно: на белом (1−bg)=0 — свечение физически -//! гаснет; на тёмном цветёт. Среда свечения — тёмная тема (закон -//! асимметрии ADR-0002 §1). +//! - **монотонно и ЛИНЕЙНО по α** на канал. Из этого НЕ следует монотонность +//! CAM16 J': солвер поэтому перебирает конечное множество реально эмитируемых +//! sRGB8-композитов, а не предполагает форму перцептивной функции; +//! - на белом (1−bg)=0 слой является point-no-op именно по формуле screen; +//! это численное свойство reference-профиля, не утверждение о физическом +//! свете или неизвестном renderer. //! -//! # Контрактные ступени (зеркальная деривация) +//! # Lab UI preset ступеней //! -//! **glow-k (dark) := фактический композит-шаг shadow-k (light)** — свечение -//! на тёмном обязано давать тот же перцептивный шаг |ΔJ'|, что тень на -//! светлом. Значения ИЗМЕРЕНЫ от -//! владельческих альф стека теней; отображение subtle:=minor, base:=ambient, -//! bloom:=major (penumbra — анатомическая ступень именно тени, у излучения -//! света полутени нет). Ноль придуманных чисел. +//! Текущие |ΔJ′| взяты из point-композитов клиентского стека теней Lab UI: +//! `subtle:=minor`, `base:=ambient`, `bloom:=major`. Это воспроизводимая +//! история preset, но не универсальный psychophysical law и не taxonomy +//! generic core; перенос в client profile отслеживается #221. //! //! # Анатомия (двухслойный bloom) //! -//! core — малый радиус, светлота поднята к белому (пересвет центра — сигнатура -//! реального света); halo — большой радиус, оттенок источника на его светлоте. -//! Оттенок оба слоя наследуют от источника: свечение не имеет собственного -//! цвета. +//! В текущем клиентском recipe core — цвет с поднятой к белому светлотой, halo +//! — исходный цвет. Радиусы и blur здесь отсутствуют: названия обозначают +//! назначение слоёв у потребителя, а не измеренную геометрию. //! -//! Центр подчинён ЕДИНОМУ ЗАКОНУ БАЛАНСА ([`crate::accent_balance`]): его -//! светлота — функциональный пол (полпути к белому по J'), а хрома на этой -//! светлоте — НЕ фикс-доля источника, а МАКСИМАЛЬНАЯ в гамуте -//! (`max_chroma`). Прежняя фикс-дельта «половина M'» размывала -//! центр к серому у пересвета (оттенок терялся); закон держит идентичность так -//! сильно, как позволяет гамут, а вырождение у стены белого честно флагуется -//! примитивом. Ноль новых констант: пол яркости — та же деривация «полпути к -//! белому», хрома — существующий `max_chroma`. +//! Core строится существующей policy [`crate::accent_balance`]: J′ берётся +//! посередине между источником и 100, chroma — на sRGB-границе данного hue. +//! Это явный versioned recipe, а не оптимум, проверенный на наблюдателях. Point-метрики +//! core и halo поэтому возвращаются раздельно. use crate::lcs::LcsColor; -use crate::spaces::srgb::{hex_from_srgb_encoded, srgb_encoded_from_hex}; +use crate::spaces::srgb::{decode_8bit, hex_from_srgb_encoded, srgb_encoded_from_hex, srgb_to_xyz}; use crate::spaces::vc::ViewingConditions; +use std::cmp::Ordering; /// Контрактный шаг glow-subtle (|ΔJ'| композита от фона): зеркало /// стек-композита fx-shadow-minor на светлом якоре labui. @@ -61,6 +60,73 @@ pub const GLOW_BASE_DJ: f64 = 2.3006; // SSOT-TRACKED — зеркальная деривация от владельческих альф теней. pub const GLOW_BLOOM_DJ: f64 = 13.3251; +/// Версия конечного reference-домена, в котором решается point-композит glow. +/// +/// Это не сертификат конкретного браузера или дисплея: профиль фиксирует +/// encoded sRGB8, оператор screen и CAM16-UCS J′ под переданными условиями +/// просмотра. Пространственный render-контракт живёт отдельно в #221. +pub const GLOW_REFERENCE_PROFILE: &str = "encoded-srgb8-screen-cam16ucs-jprime-v1"; + +/// Слой, по которому point-солвер держит целевой ΔJ′. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum GlowConstraintLayer { + /// Внешний ореол; core измеряется отдельно и не выдаётся за ту же цель. + Halo, +} + +impl GlowConstraintLayer { + /// Стабильный wire-ключ. + pub fn key(self) -> &'static str { + match self { + Self::Halo => "halo", + } + } +} + +/// Итог проверки target по конечному reference-домену. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum GlowTargetStatus { + /// Найден первый по интенсивности sRGB8-state, держащий цель. + Reached, + /// Цель не держит ни один state; возвращён глобальный максимум |ΔJ′|, а + /// при точном равенстве максимумов — первый state по alpha. + Unreachable, +} + +/// Статус представления midpoint-core на конечной сетке sRGB8. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GlowCoreStatus { + /// Эмитированный цвет сохранил целевой J′ побитно в арифметике движка. + Exact, + /// Непрерывный корень найден, но `#RRGGBB` неизбежно изменил J′. + Quantized, +} + +/// Проверяемый результат двухслойного recipe. +#[derive(Debug, Clone, PartialEq)] +pub struct GlowLayers { + pub core_hex: String, + pub halo_hex: String, + pub target_jp: f64, + pub continuous_jp: f64, + pub continuous_error: f64, + pub emitted_jp: f64, + pub emitted_error: f64, + pub status: GlowCoreStatus, +} + +impl GlowTargetStatus { + /// Стабильный wire-ключ. + pub fn key(self) -> &'static str { + match self { + Self::Reached => "reached", + Self::Unreachable => "unreachable", + } + } +} + /// Ступень контрактного стека свечения (зеркальная деривация, см. шапку). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GlowStep { @@ -73,7 +139,7 @@ pub enum GlowStep { } impl GlowStep { - /// Целевой перцептивный шаг |ΔJ'| композита ступени. + /// Целевой модуль ΔJ′ CAM16-UCS изолированного halo point-композита. pub fn target_dj(self) -> f64 { match self { GlowStep::Subtle => GLOW_SUBTLE_DJ, @@ -106,120 +172,613 @@ impl GlowStep { } } +fn validate_encoded_rgb(label: &str, rgb: [f64; 3]) -> Result<(), String> { + for (channel, value) in rgb.into_iter().enumerate() { + if !value.is_finite() || !(0.0..=1.0).contains(&value) { + return Err(format!( + "{label}[{channel}] вне конечного encoded-sRGB [0,1]: {value}" + )); + } + } + Ok(()) +} + +fn validate_viewing_numerics(vc: &ViewingConditions) -> Result<(), String> { + // Это только числовая защита CAM16-пути. Она не доказывает полноту или + // физическую применимость viewing context: такая граница принадлежит #230. + for (name, value) in [ + ("n", vc.n), + ("aw", vc.aw), + ("nbb", vc.nbb), + ("ncb", vc.ncb), + ("fl", vc.fl), + ("z", vc.z), + ("c", vc.c), + ("nc", vc.nc), + ("fl_pow_025", vc.fl_pow_025), + ("t_inner", vc.t_inner), + ] { + if !value.is_finite() || value <= 0.0 { + return Err(format!( + "ViewingConditions.{name} вне конечного положительного домена: {value}" + )); + } + } + for (channel, value) in vc.rgb_d.into_iter().enumerate() { + if !value.is_finite() || value <= 0.0 { + return Err(format!( + "ViewingConditions.rgb_d[{channel}] вне конечного положительного домена: {value}" + )); + } + } + Ok(()) +} + +/// Только J′ для горячего пути конечного солвера. Oklab-hue и M′ здесь не +/// участвуют в objective, поэтому полный `LcsColor` был бы лишней работой на +/// каждом из сотен sRGB8-state. Формула J′ остаётся тем же SSOT из `cam16`. +fn jp_from_srgb8(bytes: [u8; 3], vc: &ViewingConditions) -> Result { + let rgb = bytes.map(decode_8bit); + let (j, _, _) = crate::spaces::cam16::forward(srgb_to_xyz(rgb), vc); + let jp = crate::spaces::cam16::ucs_j(j); + if jp.is_finite() { + Ok(jp) + } else { + Err(format!( + "условия просмотра дали неконечный J′ для {}", + composite_hex(bytes) + )) + } +} + +fn jp_from_hex(hex: &str, vc: &ViewingConditions) -> Result { + let encoded = srgb_encoded_from_hex(hex)?; + jp_from_srgb8(encoded_bytes(encoded), vc) +} + /// Screen-слой над непрозрачным фоном: `bg + α·G·(1−bg)` покомпонентно -/// (device-пространство, закон лестницы — композитит браузер). -pub fn screen_layer_over_encoded(glow: [f64; 3], alpha: f64, bg: [f64; 3]) -> [f64; 3] { - debug_assert!((0.0..=1.0).contains(&alpha), "α вне [0,1]"); - [ +/// в reference-домене encoded sRGB. Функция не утверждает, что неизвестный +/// renderer использует тот же compositing/output profile. +/// +/// # Errors +/// +/// `Err`, если α или компонент входного encoded-sRGB не конечен либо лежит +/// вне `[0, 1]`. Граница не зажимает мусор: иначе вызывающий получил бы другой +/// цвет без явного сигнала. +pub fn screen_layer_over_encoded( + glow: [f64; 3], + alpha: f64, + bg: [f64; 3], +) -> Result<[f64; 3], String> { + validate_encoded_rgb("glow", glow)?; + validate_encoded_rgb("bg", bg)?; + if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) { + return Err(format!("alpha вне конечного [0,1]: {alpha}")); + } + Ok([ bg[0] + alpha * glow[0] * (1.0 - bg[0]), bg[1] + alpha * glow[1] * (1.0 - bg[1]), bg[2] + alpha * glow[2] * (1.0 - bg[2]), - ] + ]) +} + +/// Изолированный point-замер одного screen-слоя в reference-профиле glow. +/// Пространственное перекрытие core/halo сюда намеренно не входит: без +/// геометрии и порядка слоёв его нельзя восстановить честно. +pub(crate) struct ScreenLayerMeasurement { + pub(crate) composite_hex: String, + pub(crate) achieved_dj: f64, +} + +pub(crate) fn measure_screen_layer_at_alpha( + tint_hex: &str, + bg_hex: &str, + alpha: f64, + vc: &ViewingConditions, +) -> Result { + validate_viewing_numerics(vc)?; + let tint = srgb_encoded_from_hex(tint_hex)?; + let bg = srgb_encoded_from_hex(bg_hex)?; + let composite_hex = hex_from_srgb_encoded(screen_layer_over_encoded(tint, alpha, bg)?); + let bg_jp = jp_from_hex(bg_hex, vc)?; + let composite_jp = jp_from_hex(&composite_hex, vc)?; + let achieved_dj = (composite_jp - bg_jp).abs(); + if !achieved_dj.is_finite() { + return Err(format!( + "условия просмотра дали неконечный ΔJ' для {composite_hex}" + )); + } + Ok(ScreenLayerMeasurement { + composite_hex, + achieved_dj, + }) +} + +/// Рациональная стенка квантования α. Целые здесь принципиальны: двоичная +/// аппроксимация десятичной границы не должна решать, какой байт существует. +#[derive(Debug, Clone, Copy)] +struct AlphaWall { + numerator: u64, + denominator: u64, +} + +impl AlphaWall { + const ZERO: Self = Self { + numerator: 0, + denominator: 1, + }; + const ONE: Self = Self { + numerator: 1, + denominator: 1, + }; + + fn cmp(self, other: Self) -> Ordering { + (self.numerator * other.denominator).cmp(&(other.numerator * self.denominator)) + } + + fn same_value(self, other: Self) -> bool { + self.cmp(other) == Ordering::Equal + } + + fn as_f64(self) -> f64 { + self.numerator as f64 / self.denominator as f64 + } + + /// Центр интервала максимизирует запас до обеих стенок. Это канонизация + /// представления, а не утверждение о перцептивно «лучшей» непрозрачности. + fn midpoint(self, other: Self) -> f64 { + if self.same_value(other) { + return self.as_f64(); + } + let numerator = self.numerator * other.denominator + other.numerator * self.denominator; + let denominator = 2 * self.denominator * other.denominator; + numerator as f64 / denominator as f64 + } +} + +#[derive(Debug, Clone, Copy)] +struct QuantisedComposite { + bytes: [u8; 3], + lower: AlphaWall, + upper: AlphaWall, +} + +fn encoded_bytes(rgb: [f64; 3]) -> [u8; 3] { + rgb.map(|channel| (channel * 255.0).round() as u8) +} + +fn composite_hex(bytes: [u8; 3]) -> String { + format!("#{:02X}{:02X}{:02X}", bytes[0], bytes[1], bytes[2]) +} + +/// Поток всех достижимых sRGB8-композитов без промежуточных коллекций. +/// +/// На каждом канале стенки строго возрастают, поэтому три уже упорядоченных +/// потока достаточно слить тремя курсорами. Общая сортировка не добавляет +/// математической информации, зато затягивает в WASM аллокатор и универсальную +/// сортировку. Сравнение и группировка остаются рациональными: решение о байте +/// никогда не зависит от погрешности binary64. +struct QuantisedComposites { + background: [u8; 3], + slopes: [u64; 3], + next_values: [u16; 3], + bytes: [u8; 3], + lower: AlphaWall, + finished: bool, +} + +impl QuantisedComposites { + fn new(glow: [u8; 3], background: [u8; 3]) -> Self { + let slopes = core::array::from_fn(|channel| { + u64::from(glow[channel]) * (255 - u64::from(background[channel])) + }); + let next_values = background.map(|byte| u16::from(byte) + 1); + Self { + background, + slopes, + next_values, + bytes: background, + lower: AlphaWall::ZERO, + finished: false, + } + } + + /// На канале `B + α·G·(255−B)/255` новый байт `v` начинается ровно на + /// `255·(2(v−B)−1) / (2·G·(255−B))`. Все множители ограничены 8 битами, + /// поэтому точное перекрёстное сравнение стенок помещается в `u64`. + fn next_wall(&self, channel: usize) -> Option { + let slope = self.slopes[channel]; + let value = self.next_values[channel]; + if slope == 0 || value > u16::from(u8::MAX) { + return None; + } + + let base = u64::from(self.background[channel]); + let delta = u64::from(value) - base; + let wall = AlphaWall { + numerator: 255 * (2 * delta - 1), + denominator: 2 * slope, + }; + (wall.cmp(AlphaWall::ONE) != Ordering::Greater).then_some(wall) + } + + /// Возвращает очередной интервал `[lower, upper)`; последний интервал + /// замкнут справа и заканчивается на `1`. Стенки с одинаковым рациональным + /// значением поглощаются вместе, как в прежней сортированной реализации. + fn next_state(&mut self) -> Result, String> { + if self.finished { + return Ok(None); + } + + let upper = (0..3) + .filter_map(|channel| self.next_wall(channel).map(|wall| (wall, channel))) + .min_by(|(left_wall, left_channel), (right_wall, right_channel)| { + left_wall + .cmp(*right_wall) + .then_with(|| left_channel.cmp(right_channel)) + }) + .map(|(wall, _)| wall); + + let Some(upper) = upper else { + self.finished = true; + return Ok(Some(QuantisedComposite { + bytes: self.bytes, + lower: self.lower, + upper: AlphaWall::ONE, + })); + }; + + let state = QuantisedComposite { + bytes: self.bytes, + lower: self.lower, + upper, + }; + for channel in 0..3 { + if self + .next_wall(channel) + .is_some_and(|wall| wall.same_value(upper)) + { + self.bytes[channel] = self.bytes[channel].checked_add(1).ok_or_else(|| { + format!("внутренняя стенка переполнила sRGB8-канал {channel}") + })?; + self.next_values[channel] += 1; + } + } + self.lower = upper; + Ok(Some(state)) + } +} + +/// Тестовый сборщик сохраняет удобный независимый оракул, но `Vec` и сортировка +/// не входят в production-WASM. +#[cfg(test)] +fn quantised_composites( + glow: [u8; 3], + background: [u8; 3], +) -> Result, String> { + let mut stream = QuantisedComposites::new(glow, background); + let mut states = Vec::new(); + while let Some(state) = stream.next_state()? { + states.push(state); + } + Ok(states) } /// Результат glow-солвера (ADR-0002 «честный результат», закон 2). #[derive(Debug, Clone, PartialEq)] pub struct GlowSolve { - /// Интенсивность слоя `(0, 1]` — α screen-слоя. - pub alpha: f64, + /// Каноническая интенсивность слоя `(0, 1]`: центр устойчивого интервала + /// выбранного sRGB8-композита. Это не «оптимальный вкус», а максимальный + /// численный запас до ближайшей границы квантования. + alpha: f64, + /// Каноническая CSS-запись той же alpha; хранится вместе с числом, чтобы + /// downstream не вводил собственную политику округления. + alpha_css: String, + /// Запрошенный |ΔJ′| point-композита halo. + target_dj: f64, /// Фактический |ΔJ'| композита от фона, замерен на эмитируемом hex. - pub achieved_dj: f64, + achieved_dj: f64, /// Композит `screen(tint, α)` над фоном, `#RRGGBB`. - pub composite_hex: String, - /// Деградация: цель недостижима даже при α = 1 (например, фон близок к - /// белому — screen гаснет физически); возвращён ближайший достижимый - /// результат с честным флагом, НЕ ошибка и НЕ молчание. - pub degraded: bool, + composite_hex: String, + /// Деградация: ни один достижимый sRGB8-композит не держит цель (например, + /// над белым фоном слой является point-no-op в этом reference-профиле); + /// возвращён глобальный максимум |ΔJ'| с честным флагом, НЕ ошибка и НЕ + /// молчание. + status: GlowTargetStatus, } -/// Решить интенсивность screen-слоя под целевой перцептивный шаг. +impl GlowSolve { + /// Каноническая интенсивность, выбранная внутри устойчивого sRGB8-интервала. + pub fn alpha(&self) -> f64 { + self.alpha + } + + /// CSS-запись, восстанавливающая ту же binary64 alpha. + pub fn alpha_css(&self) -> &str { + &self.alpha_css + } + + /// Запрошенный |ΔJ′| halo-композита. + pub fn target_dj(&self) -> f64 { + self.target_dj + } + + /// Фактически достигнутый |ΔJ′| на возвращённом sRGB8-композите. + pub fn achieved_dj(&self) -> f64 { + self.achieved_dj + } + + /// Возвращённый reference-композит halo. + pub fn composite_hex(&self) -> &str { + &self.composite_hex + } + + /// Типизированный результат target-проверки. + pub fn status(&self) -> GlowTargetStatus { + self.status + } + + /// Геттер совместимости прежнего boolean-контракта. + pub fn degraded(&self) -> bool { + self.status == GlowTargetStatus::Unreachable + } + + /// Слой, по которому решалась цель. + pub fn constraint_layer(&self) -> GlowConstraintLayer { + GlowConstraintLayer::Halo + } + + /// Версия конечного домена расчёта. + pub fn reference_profile(&self) -> &'static str { + GLOW_REFERENCE_PROFILE + } +} + +/// Решить интенсивность screen-слоя под целевой численный модуль ΔJ′ CAM16-UCS. /// -/// Бисекция по α ∈ [0, 1]: J' композита монотонно растёт по α (screen -/// монотонен покомпонентно, J' монотонен по яркости стимула). Замер цели — -/// на КВАНТОВАННОМ композите (закон движка: честный шаг меряется на -/// отданном hex). +/// Канальные переходы screen выводятся как рациональные стенки округления до +/// sRGB8. Солвер проверяет все достижимые композиты в порядке α и потому не +/// предполагает монотонность CAM16 J'. Если цель недостижима, возвращается +/// глобальный максимум |ΔJ'| среди достижимых состояний; равные максимумы +/// детерминированно разрешаются первым state по alpha. /// /// # Errors /// -/// `Err` — программный мусор (закон 3 ADR-0002): невалидный hex, цель ≤ 0. +/// `Err` — программный мусор (закон 3 ADR-0002): невалидный hex, неконечная +/// или неположительная цель либо нечисловой результат переданных условий +/// просмотра. pub fn solve_screen_alpha_for_dj( glow_tint_hex: &str, bg_hex: &str, target_dj: f64, vc: &ViewingConditions, ) -> Result { - if target_dj.is_nan() || target_dj <= 0.0 { + if !target_dj.is_finite() || target_dj <= 0.0 { return Err(format!("целевой шаг вне домена: {target_dj}")); } + validate_viewing_numerics(vc)?; let glow = srgb_encoded_from_hex(glow_tint_hex)?; let bg = srgb_encoded_from_hex(bg_hex)?; - let bg_jp = LcsColor::from_hex_with_vc(bg_hex, vc)?.jp; - - let dj_at = |alpha: f64| -> Result<(f64, String), String> { - let hex = hex_from_srgb_encoded(screen_layer_over_encoded(glow, alpha, bg)); - let jp = LcsColor::from_hex_with_vc(&hex, vc)?.jp; - Ok(((jp - bg_jp).abs(), hex)) - }; - - let (max_dj, max_hex) = dj_at(1.0)?; - if max_dj < target_dj { - // Честная деградация: ближайший достижимый шаг + флаг (закон 2). + let glow_bytes = encoded_bytes(glow); + let bg_bytes = encoded_bytes(bg); + let slopes = core::array::from_fn::<_, 3, _>(|channel| { + u16::from(glow_bytes[channel]) * u16::from(u8::MAX - bg_bytes[channel]) + }); + if slopes == [0; 3] { + // Весь α-интервал отображается в один и тот же sRGB8-state. CAM16 + // forward ничего не добавит: ΔJ′ одного цвета с самим собой точно 0. + let alpha = AlphaWall::ZERO.midpoint(AlphaWall::ONE); return Ok(GlowSolve { - alpha: 1.0, - achieved_dj: max_dj, - composite_hex: max_hex, - degraded: true, + alpha, + alpha_css: crate::css_alpha_value(alpha)?, + target_dj, + achieved_dj: 0.0, + composite_hex: composite_hex(bg_bytes), + status: GlowTargetStatus::Unreachable, }); } + let bg_jp = jp_from_srgb8(bg_bytes, vc)?; - let (mut lo, mut hi) = (0.0_f64, 1.0_f64); - for _ in 0..48 { - let mid = 0.5 * (lo + hi); - let (dj, _) = dj_at(mid)?; - if dj < target_dj { - lo = mid; - } else { - hi = mid; + let measured = |bytes: [u8; 3]| -> Result { + let jp = jp_from_srgb8(bytes, vc)?; + let dj = (jp - bg_jp).abs(); + if !dj.is_finite() { + return Err(format!( + "условия просмотра дали неконечный ΔJ' для {}", + composite_hex(bytes) + )); + } + Ok(dj) + }; + + let mut states = QuantisedComposites::new(glow_bytes, bg_bytes); + let mut best: Option<(QuantisedComposite, f64)> = None; + let mut selected = None; + + while let Some(state) = states.next_state()? { + let achieved_dj = measured(state.bytes)?; + if achieved_dj >= target_dj { + selected = Some((state, achieved_dj)); + break; + } + if best + .as_ref() + .is_none_or(|(_, best_dj)| achieved_dj > *best_dj) + { + best = Some((state, achieved_dj)); } } - // Верхняя сторона брекета: на 8-битной сетке шаг дискретен, и середина - // могла бы лечь НИЖЕ цели без флага — hi по инварианту бисекции даёт - // квантованный шаг ≥ цели (ближайший достижимый сверху, закон 2 ADR-0002). - let alpha = hi; - let (achieved_dj, composite_hex) = dj_at(alpha)?; + + let (state, achieved_dj, status) = if let Some(selected) = selected { + let (state, achieved_dj) = selected; + (state, achieved_dj, GlowTargetStatus::Reached) + } else { + let (state, achieved_dj) = + best.ok_or_else(|| "конечное множество композитов оказалось пустым".to_string())?; + (state, achieved_dj, GlowTargetStatus::Unreachable) + }; + let composite_hex = composite_hex(state.bytes); + let alpha = state.lower.midpoint(state.upper); + + // Канонический CSS-сериализатор хранит ту же binary64 alpha. Production-проверка + // повторно композитит исходное число; строковый parse-round-trip проверяют + // граничные и межъязыковые тесты, не затягивая dec2flt-парсер в WASM. + let alpha_css = crate::css_alpha_value(alpha)?; + let roundtrip_hex = hex_from_srgb_encoded(screen_layer_over_encoded(glow, alpha, bg)?); + if roundtrip_hex != composite_hex { + return Err(format!( + "выбранная alpha не воспроизвела state: ожидался {composite_hex}, получен {roundtrip_hex}" + )); + } + Ok(GlowSolve { alpha, + alpha_css, + target_dj, achieved_dj, composite_hex, - degraded: false, + status, }) } -/// Двухслойная анатомия свечения от источника: `(core_hex, halo_hex)`. +/// Версионированный двухслойный recipe от источника: `(core_hex, halo_hex)`. /// -/// halo — сам источник (его оттенок на его светлоте); core — переэкспонирование -/// по ЗАКОНУ БАЛАНСА: светлота = функциональный пол «полпути к белому по J'» -/// (`J'core = (J'src + 100)/2`), а хрома на ней — МАКСИМАЛЬНАЯ в гамуте для -/// оттенка источника ([`crate::accent_balance::accent_balanced`]), не фикс-доля. -/// Оттенок (h_ok) — источника: свечение не имеет собственного цвета. +/// В recipe v1 halo буквально равен источнику. Для core координата J′ берётся +/// арифметической серединой между J′ источника и 100, а chroma — на sRGB-границе +/// его Oklab hue через [`crate::accent_balance::accent_balanced`]. Это точное +/// описание алгоритма, но не проверенный на наблюдателях оптимум и не модель +/// геометрии свечения. pub fn glow_layers_from_source( source_hex: &str, vc: &ViewingConditions, ) -> Result<(String, String), String> { - let src = LcsColor::from_hex_with_vc(source_hex, vc)?; - // Функциональный пол яркости центра: полпути к белому по J' (пересвет). - let jp_core = (src.jp + 100.0) * 0.5; - let l_core = crate::scale::jp_to_oklab_l(jp_core, vc); - // ЗАКОН БАЛАНСА: на этой яркости — максимум хромы оттенка источника (не ×0.5, - // размывавшая центр к серому). Идентичность держится, вырождение — флагом. - let core = crate::accent_balance::accent_balanced(l_core, src.h_ok, vc).color; - Ok((core.to_hex_with_vc(vc), source_hex.to_string())) + let layers = glow_layers_from_source_certified(source_hex, vc)?; + Ok((layers.core_hex, layers.halo_hex)) +} + +/// Строит core как ближайший binary64-корень фактической траектории +/// `accent_balanced(L,h).J′`, а не как сероосевой seed. +/// +/// Цель recipe остаётся явно версионированной арифметической серединой между +/// J′ источника и J′ белого. Сертификат отдельно сообщает непрерывный и +/// эмитированный остатки, не пряча квантизацию за допуском. +pub fn glow_layers_from_source_certified( + source_hex: &str, + vc: &ViewingConditions, +) -> Result { + validate_viewing_numerics(vc)?; + let source_encoded = srgb_encoded_from_hex(source_hex)?; + let canonical_source_hex = hex_from_srgb_encoded(source_encoded); + let src = LcsColor::from_hex_with_vc(&canonical_source_hex, vc)?; + let white = LcsColor::from_hex_with_vc("#FFFFFF", vc)?; + let target_jp = (src.jp() + white.jp()) * 0.5; + let hue = src.h_ok(); + + let at = |l: f64| crate::accent_balance::accent_balanced(l, hue, vc); + let mut lo = 0.0_f64; + let mut hi = 1.0_f64; + while hi.to_bits() - lo.to_bits() > 1 { + let mid = f64::from_bits((lo.to_bits() + hi.to_bits()) / 2); + if at(mid)?.color.jp() < target_jp { + lo = mid; + } else { + hi = mid; + } + } + let lo_color = at(lo)?.color; + let hi_color = at(hi)?.color; + let core = if (lo_color.jp() - target_jp).abs() <= (hi_color.jp() - target_jp).abs() { + lo_color + } else { + hi_color + }; + let continuous_jp = core.jp(); + let core_hex = core.to_hex_with_vc(vc); + let emitted_jp = LcsColor::from_hex_with_vc(&core_hex, vc)?.jp(); + let continuous_error = continuous_jp - target_jp; + let emitted_error = emitted_jp - target_jp; + let status = if emitted_error == 0.0 { + GlowCoreStatus::Exact + } else { + GlowCoreStatus::Quantized + }; + Ok(GlowLayers { + core_hex, + halo_hex: canonical_source_hex, + target_jp, + continuous_jp, + continuous_error, + emitted_jp, + emitted_error, + status, + }) } #[cfg(test)] mod tests { use super::*; + /// Независимый оракул не меняет байты по событиям production-алгоритма: + /// он строит открытые промежутки обычной формулой, пробует их середины через + /// публичный композитор и удаляет только соседние дубликаты hex. + fn oracle_states( + tint_hex: &str, + bg_hex: &str, + vc: &ViewingConditions, + ) -> Vec<(f64, String, f64)> { + let tint = srgb_encoded_from_hex(tint_hex).unwrap(); + let bg = srgb_encoded_from_hex(bg_hex).unwrap(); + let tint_bytes = encoded_bytes(tint); + let bg_bytes = encoded_bytes(bg); + let mut walls = vec![0.0, 1.0]; + + for channel in 0..3 { + let slope = f64::from(tint_bytes[channel]) * f64::from(u8::MAX - bg_bytes[channel]); + if slope == 0.0 { + continue; + } + for value in (u16::from(bg_bytes[channel]) + 1)..=u16::from(u8::MAX) { + let wall = 255.0 * (f64::from(value) - f64::from(bg_bytes[channel]) - 0.5) / slope; + if wall > 1.0 { + break; + } + walls.push(wall); + } + } + walls.sort_unstable_by(f64::total_cmp); + walls.dedup(); + + let bg_jp = LcsColor::from_hex_with_vc(bg_hex, vc).unwrap().jp; + let mut states: Vec<(f64, String, f64)> = Vec::new(); + for window in walls.windows(2) { + if window[0] == window[1] { + continue; + } + let alpha = window[0] + (window[1] - window[0]) * 0.5; + let hex = hex_from_srgb_encoded(screen_layer_over_encoded(tint, alpha, bg).unwrap()); + if states + .last() + .is_some_and(|(_, previous, _)| previous == &hex) + { + continue; + } + let jp = LcsColor::from_hex_with_vc(&hex, vc).unwrap().jp; + states.push((alpha, hex, (jp - bg_jp).abs())); + } + // Отдельный endpoint не даёт оракулу молча потерять состояние, которое + // теоретически могло бы существовать только при полностью непрозрачном слое. + let alpha = 1.0; + let hex = hex_from_srgb_encoded(screen_layer_over_encoded(tint, alpha, bg).unwrap()); + if states + .last() + .is_none_or(|(_, previous, _)| previous != &hex) + { + let jp = LcsColor::from_hex_with_vc(&hex, vc).unwrap().jp; + states.push((alpha, hex, (jp - bg_jp).abs())); + } + states + } + /// Screen никогда не темнит — свойство конструкции, sweep-замок. #[test] fn screen_never_darkens_any_channel() { @@ -231,7 +790,7 @@ mod tests { let bg = srgb_encoded_from_hex(bg_hex).unwrap(); for i in 0..=20 { let a = f64::from(i) / 20.0; - let out = screen_layer_over_encoded(tint, a, bg); + let out = screen_layer_over_encoded(tint, a, bg).unwrap(); for ch in 0..3 { assert!( out[ch] >= bg[ch] - 1e-12, @@ -244,25 +803,250 @@ mod tests { } } + /// Публичная числовая граница отвергает мусор, а не паникует в debug и не + /// превращает его в другой цвет через clamp. + #[test] + fn screen_rejects_every_non_finite_or_out_of_domain_component() { + let valid = [0.25, 0.5, 0.75]; + for alpha in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.1, 1.1] { + assert!(screen_layer_over_encoded(valid, alpha, valid).is_err()); + } + for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.1, 1.1] { + let mut rgb = valid; + rgb[1] = bad; + assert!(screen_layer_over_encoded(rgb, 0.5, valid).is_err()); + assert!(screen_layer_over_encoded(valid, 0.5, rgb).is_err()); + } + assert!(screen_layer_over_encoded(valid, 0.0, valid).is_ok()); + assert!(screen_layer_over_encoded(valid, 1.0, valid).is_ok()); + } + + #[test] + fn hot_jp_path_is_bit_identical_to_full_lcs_forward() { + for vc in [ViewingConditions::srgb(), ViewingConditions::dim_surround()] { + for byte in 0_u16..=255 { + let hex = format!("#{byte:02X}{byte:02X}{byte:02X}"); + let lean = jp_from_hex(&hex, &vc).unwrap(); + let full = LcsColor::from_hex_with_vc(&hex, &vc).unwrap().jp(); + assert_eq!(lean.to_bits(), full.to_bits(), "{hex}"); + } + for hex in ["#007AFF", "#FF3B30", "#34C759", "#FFD000", "#3E87FF"] { + let lean = jp_from_hex(hex, &vc).unwrap(); + let full = LcsColor::from_hex_with_vc(hex, &vc).unwrap().jp(); + assert_eq!(lean.to_bits(), full.to_bits(), "{hex}"); + } + } + } + + /// Точка `x.5` принадлежит новому байту. Проверка на представимой стенке + /// отделяет это правило от погрешности binary64, а `1/510` ниже проверяет + /// рациональную группировку непредставимой стенки трёх каналов. + #[test] + fn exact_walls_follow_round_half_up_without_binary_boundary_assumptions() { + let red = [1.0 / 255.0, 0.0, 0.0]; + let black = [0.0; 3]; + let at_half = hex_from_srgb_encoded(screen_layer_over_encoded(red, 0.5, black).unwrap()); + let just_below_half = f64::from_bits(0.5_f64.to_bits() - 1); + let below = + hex_from_srgb_encoded(screen_layer_over_encoded(red, just_below_half, black).unwrap()); + assert_eq!(below, "#000000"); + assert_eq!(at_half, "#010000", "round(0.5) обязан выбрать верхний байт"); + + let states = quantised_composites([1, 0, 0], [0; 3]).unwrap(); + assert_eq!(states.len(), 2); + assert_eq!(states[0].bytes, [0; 3]); + assert!(states[0].upper.same_value(AlphaWall { + numerator: 1, + denominator: 2, + })); + assert_eq!(states[1].bytes, [1, 0, 0]); + assert!(states[1].lower.same_value(states[0].upper)); + + let white_states = quantised_composites([255; 3], [0; 3]).unwrap(); + assert_eq!(white_states.len(), 256); + let first_wall = AlphaWall { + numerator: 1, + denominator: 510, + }; + assert!(white_states[0].upper.same_value(first_wall)); + assert!(white_states[1].lower.same_value(first_wall)); + assert_eq!(white_states[1].bytes, [1; 3]); + } + + /// Исчерпывающая теорема для одного канала: рациональное разбиение обязано + /// совпадать с публичной формулой для всех 65 536 пар `(background, glow)`. + /// Три канала независимы по определению screen, поэтому этот перебор + /// закрывает полный класс стенок без случайной выборки RGB-троек. + #[test] + fn every_rational_channel_partition_matches_the_public_compositor() { + for background in 0_u16..=255 { + for glow in 0_u16..=255 { + let b = background as u8; + let g = glow as u8; + let states = quantised_composites([g, 0, 0], [b, 0, 0]).unwrap(); + let at_one = screen_layer_over_encoded( + [f64::from(g) / 255.0, 0.0, 0.0], + 1.0, + [f64::from(b) / 255.0, 0.0, 0.0], + ) + .unwrap(); + let final_byte = (at_one[0] * 255.0).round() as u8; + + assert_eq!(states.first().unwrap().bytes[0], b); + assert_eq!(states.last().unwrap().bytes[0], final_byte); + assert_eq!(states.len(), usize::from(final_byte - b) + 1); + + for (index, state) in states.iter().enumerate() { + assert!(state.lower.cmp(state.upper) != Ordering::Greater); + if let Some(next) = states.get(index + 1) { + assert!(state.upper.same_value(next.lower)); + assert_eq!(next.bytes[0], state.bytes[0] + 1); + } + + let alpha = state.lower.midpoint(state.upper); + let out = screen_layer_over_encoded( + [f64::from(g) / 255.0, 0.0, 0.0], + alpha, + [f64::from(b) / 255.0, 0.0, 0.0], + ) + .unwrap(); + assert_eq!( + (out[0] * 255.0).round() as u8, + state.bytes[0], + "B={b}, G={g}, state={index}, alpha={alpha}" + ); + } + } + } + } + /// Солвер достигает контрактной цели на тёмном фоне (среда свечения). #[test] fn solver_hits_targets_on_dark() { let vc = ViewingConditions::dim_surround(); for target in [GLOW_SUBTLE_DJ, GLOW_BASE_DJ, GLOW_BLOOM_DJ] { let g = solve_screen_alpha_for_dj("#3E87FF", "#101012", target, &vc).unwrap(); - assert!(!g.degraded, "цель {target} недостижима на #101012"); - // Солвер возвращает ВЕРХНЮЮ сторону брекета: достигнутое ≥ цели - // (закон 2 ADR-0002 — ближайший достижимый СВЕРХУ), перелёт ограничен - // шагом 8-битной сетки на тёмной базе (≲0.3 J' на канал-инкремент). + assert!(!g.degraded(), "цель {target} недостижима на #101012"); + // Первый достижимый sRGB8-композит обязан держать цель. Его + // минимальность независимо проверяется sweep-оракулом ниже. assert!( - g.achieved_dj >= target - 1e-9 && g.achieved_dj - target < 0.5, - "цель {target}: достигнуто {:.4} (ожидалось [цель, цель+0.5))", - g.achieved_dj + g.achieved_dj() >= target, + "цель {target}: достигнуто {:.4}", + g.achieved_dj() ); - assert!(g.alpha > 0.0 && g.alpha <= 1.0); + assert!(g.alpha() > 0.0 && g.alpha() <= 1.0); } } + /// Регрессия реальной границы квантования: альфа, округлённая отдельно от + /// рассчитанного hex, не имеет права незаметно вернуть предыдущий байт. + #[test] + fn serialised_alpha_keeps_the_quantised_target() { + let vc = ViewingConditions::dim_surround(); + let tint = srgb_encoded_from_hex("#4A8FFF").unwrap(); + let bg = srgb_encoded_from_hex("#101012").unwrap(); + let bg_jp = LcsColor::from_hex_with_vc("#101012", &vc).unwrap().jp; + let measured_at = |alpha: f64| { + let hex = hex_from_srgb_encoded(screen_layer_over_encoded(tint, alpha, bg).unwrap()); + let jp = LcsColor::from_hex_with_vc(&hex, &vc).unwrap().jp; + ((jp - bg_jp).abs(), hex) + }; + + // Прежняя бисекция сходилась к рациональной стенке красного канала + // 1275/35372. Формат DTO с четырьмя знаками превращал её в 0.0360. + let legacy_boundary_alpha = 1_275.0 / 35_372.0; + let legacy_css = format!("{legacy_boundary_alpha:.4}"); + assert_eq!(legacy_css, "0.0360"); + let (rounded_dj, rounded_hex) = measured_at(legacy_css.parse().unwrap()); + assert_eq!(rounded_hex, "#12151B"); + assert!( + rounded_dj < GLOW_BASE_DJ, + "контрпример обязан лежать ниже цели: {rounded_dj}" + ); + + let solved = solve_screen_alpha_for_dj("#4A8FFF", "#101012", GLOW_BASE_DJ, &vc).unwrap(); + let alpha_css = solved.alpha_css(); + let serialised = alpha_css.parse::().unwrap(); + assert_eq!(serialised.to_bits(), solved.alpha().to_bits()); + let (serialised_dj, serialised_hex) = measured_at(serialised); + assert_eq!(serialised_hex, solved.composite_hex()); + assert!( + serialised_dj >= GLOW_BASE_DJ, + "эмитируемая alpha={alpha_css} дала {serialised_hex} с ΔJ'={serialised_dj}, ниже цели {}", + GLOW_BASE_DJ + ); + } + + /// Матрица свойств сверяет минимальный успешный state и глобальный + /// максимум при деградации с независимым оракулом. В ней есть нулевой тинт, + /// белый фон и разные surround — ветви, где предположение `alpha=1` особенно + /// легко скрывает ошибку выбора. + #[test] + fn solver_matches_independent_finite_state_oracle() { + let vcs = [ViewingConditions::srgb(), ViewingConditions::dim_surround()]; + for tint_hex in ["#000000", "#4A8FFF", "#FF3B30", "#FFFFFF"] { + for bg_hex in ["#000000", "#101012", "#808080", "#FFFFFF"] { + for vc in vcs { + let oracle = oracle_states(tint_hex, bg_hex, &vc); + assert!(!oracle.is_empty()); + let mut global_best = &oracle[0]; + for state in &oracle[1..] { + if state.2 > global_best.2 { + global_best = state; + } + } + + for target in [0.01, 1.0, 10.0, global_best.2 + 1.0] { + let solved = + solve_screen_alpha_for_dj(tint_hex, bg_hex, target, &vc).unwrap(); + let expected = oracle.iter().find(|state| state.2 >= target); + let (expected_state, degraded) = match expected { + Some(state) => (state, false), + None => (global_best, true), + }; + assert_eq!( + solved.composite_hex(), + expected_state.1, + "tint={tint_hex}, bg={bg_hex}, target={target}" + ); + assert_eq!(solved.achieved_dj().to_bits(), expected_state.2.to_bits()); + assert_eq!(solved.degraded(), degraded); + + let emitted_alpha = solved.alpha_css().parse::().unwrap(); + assert_eq!( + emitted_alpha.to_bits(), + solved.alpha().to_bits(), + "CSS round-trip обязан сохранять binary64: tint={tint_hex}, bg={bg_hex}, target={target}" + ); + let recomposed = hex_from_srgb_encoded( + screen_layer_over_encoded( + srgb_encoded_from_hex(tint_hex).unwrap(), + emitted_alpha, + srgb_encoded_from_hex(bg_hex).unwrap(), + ) + .unwrap(), + ); + assert_eq!(recomposed, solved.composite_hex()); + } + } + } + } + } + + #[test] + fn solver_rejects_non_finite_and_non_positive_targets() { + let vc = ViewingConditions::dim_surround(); + for target in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1.0, 0.0] { + assert!(solve_screen_alpha_for_dj("#4A8FFF", "#101012", target, &vc).is_err()); + } + + let mut invalid_vc = vc; + invalid_vc.n = f64::NAN; + assert!( + solve_screen_alpha_for_dj("#4A8FFF", "#101012", GLOW_BASE_DJ, &invalid_vc).is_err() + ); + } + /// Ступени контракта строго возрастают и по цели, и по решённой α. #[test] fn glow_stack_is_strictly_progressive() { @@ -272,51 +1056,60 @@ mod tests { for target in [GLOW_SUBTLE_DJ, GLOW_BASE_DJ, GLOW_BLOOM_DJ] { let g = solve_screen_alpha_for_dj("#FF3B30", "#101012", target, &vc).unwrap(); assert!( - g.alpha > prev, + g.alpha() > prev, "α стека не прогрессивна: {} после {prev}", - g.alpha + g.alpha() ); - prev = g.alpha; + prev = g.alpha(); } } - /// На белом фоне свечение честно деградирует с флагом (физика screen), - /// а не молчит и не ошибается — ADR-0002, закон 2. + /// На белом фоне screen-слой является point-no-op по объявленной формуле; + /// недостижимость поэтому должна быть явной, а не замаскированной ошибкой. #[test] fn glow_on_white_degrades_honestly() { let vc = ViewingConditions::srgb(); let g = solve_screen_alpha_for_dj("#3E87FF", "#FFFFFF", GLOW_BASE_DJ, &vc).unwrap(); - assert!(g.degraded, "на белом screen обязан гаснуть"); - assert!(g.achieved_dj < GLOW_BASE_DJ); - assert_eq!(g.composite_hex, "#FFFFFF", "screen над белым — тождество"); + assert!(g.degraded(), "над белым screen обязан быть point-no-op"); + assert!(g.achieved_dj() < GLOW_BASE_DJ); + assert_eq!(g.composite_hex(), "#FFFFFF", "screen над белым — тождество"); + assert_eq!( + g.alpha(), + 0.5, + "единственный state канонизируется центром [0,1]" + ); } - /// Анатомия core по ЗАКОНУ БАЛАНСА: светлее источника (пересвет), оттенок - /// унаследован, цвет центра ВЗЯТ ИЗ примитива баланса (не своя фикс-дельта), - /// насыщенный источник не вырождается, и хрома центра ПО ПОСТРОЕНИЮ в гамуте - /// (стена гамута — без тихого клипа, класс бага прежней ×0.5-доли). + /// Recipe v1: core светлее источника, использует его Oklab hue и + /// gamut-boundary chroma без тихого клипа. Тест фиксирует алгоритм, а не + /// проверенный на наблюдателях закон оптимального свечения. #[test] fn core_is_balanced_overexposed_source() { let vc = ViewingConditions::dim_surround(); - let (core_hex, halo_hex) = glow_layers_from_source("#FF3B30", &vc).unwrap(); - assert_eq!(halo_hex, "#FF3B30", "halo — сам источник"); + let layers = glow_layers_from_source_certified("#FF3B30", &vc).unwrap(); + let core_hex = layers.core_hex.clone(); + assert_eq!(layers.halo_hex, "#FF3B30", "halo — сам источник"); let src = LcsColor::from_hex_with_vc("#FF3B30", &vc).unwrap(); let core = LcsColor::from_hex_with_vc(&core_hex, &vc).unwrap(); - assert!(core.jp > src.jp, "core светлее источника (пересвет)"); - let dh = (core.h_ok - src.h_ok + 180.0).rem_euclid(360.0) - 180.0; + assert!(core.jp() > src.jp(), "core светлее источника (пересвет)"); + let dh = (core.h_ok() - src.h_ok() + 180.0).rem_euclid(360.0) - 180.0; assert!(dh.abs() < 6.0, "оттенок унаследован: Δh = {dh:.2}°"); - // Центр ВЗЯТ из примитива баланса — доказательство wiring'а. - let l_core = crate::scale::jp_to_oklab_l((src.jp + 100.0) * 0.5, &vc); - let balanced = crate::accent_balance::accent_balanced(l_core, src.h_ok, &vc); + // Сертификат пересчитывается из эмитированного hex и не смешивает + // непрерывный корень с ошибкой конечного представления. + let white = LcsColor::from_hex_with_vc("#FFFFFF", &vc).unwrap(); assert_eq!( - core_hex, - balanced.color.to_hex_with_vc(&vc), - "центр glow обязан быть цветом примитива баланса" + layers.target_jp.to_bits(), + ((src.jp() + white.jp()) * 0.5).to_bits() ); - assert!( - !balanced.hue_vanished, - "насыщенный источник не вырождается в центре" + assert_eq!(layers.emitted_jp.to_bits(), core.jp().to_bits()); + assert_eq!( + layers.emitted_error.to_bits(), + (layers.emitted_jp - layers.target_jp).to_bits() + ); + assert_eq!( + layers.status == GlowCoreStatus::Exact, + layers.emitted_error == 0.0 ); // Хрома баланса = стена гамута ⇒ эмиссия в гамуте, без тихого клипа @@ -328,4 +1121,13 @@ mod tests { "центр в гамуте: round-trip красочности стабилен (без тихого клипа)" ); } + + #[test] + fn layer_recipe_canonicalises_the_public_source_hex() { + let vc = ViewingConditions::srgb(); + let layers = glow_layers_from_source_certified("ff3b30", &vc).unwrap(); + assert_eq!(layers.halo_hex, "#FF3B30"); + assert!(layers.core_hex.starts_with('#')); + assert_eq!(layers.core_hex.len(), 7); + } } diff --git a/crates/labcolors-core/src/spaces/srgb.rs b/crates/labcolors-core/src/spaces/srgb.rs index 291683bc..2d8b1cb1 100644 --- a/crates/labcolors-core/src/spaces/srgb.rs +++ b/crates/labcolors-core/src/spaces/srgb.rs @@ -103,7 +103,7 @@ mod gamma_data; /// is therefore the *exact* decode for every reachable input — a table lookup /// that replaces the per-channel `powf` with zero loss (gated by /// `decode_table_matches_live_math` and `decode_reproduces_legacy_powf_path`). -fn decode_8bit(byte: u8) -> f64 { +pub(crate) fn decode_8bit(byte: u8) -> f64 { gamma_data::DECODE_8BIT[byte as usize] } diff --git a/docs/decisions/0004-finite-alpha-glow-reference.md b/docs/decisions/0004-finite-alpha-glow-reference.md new file mode 100644 index 00000000..1927b52e --- /dev/null +++ b/docs/decisions/0004-finite-alpha-glow-reference.md @@ -0,0 +1,131 @@ +# ADR-0004: конечный sRGB8-контракт alpha и point-glow + +- Статус: принято +- Дата: 2026-07-10 +- Связанные задачи: #41, #218, #221, #223, #233, #241, #258, #259 + +## Контекст + +Прежний glow-солвер выполнял фиксированные 48 шагов бисекции. Это требовало +неподтверждённой монотонности CAM16-UCS J′ по alpha. Затем числовая alpha +округлялась до четырёх знаков независимо от уже выбранного sRGB8-композита. +На воспроизводимом входе `halo=#4A8FFF`, `bg=#101012`, `target=2.3006` +граница `1275/35372` превращалась в `0.0360`, возвращала предыдущий байт +`#12151B` и нарушала собственную цель. + +У straight-alpha был отдельный дефект той же природы. Нормализованный путь +`(byte/255) · alpha · 255` менял точную половину из-за binary64-округления: +для `#C0B2FA @ 0.122` над чёрным канал `250 · 0.122 = 30.5` обязан дать 31 в +объявленном byte-reference, но старый путь давал 30. Отчётные +`compositeHex`, Lc и WCAG могли относиться к соседнему байту. + +## Решение + +Непрерывная алгебра опирается на W3C Compositing and Blending Level 1: +[source-over §9.1.4](https://www.w3.org/TR/compositing-1/#porterduffcompositingoperators_srcover) +и [screen §10.1.3](https://www.w3.org/TR/compositing-1/#blending-screen). +Спецификация задаёт операторы, но не нашу политику финального sRGB8-round; +выбор encoded sRGB как blending space и байтовая сетка ниже явно являются +версионированным reference-профилем продукта, а не приписываются W3C или +неизвестному browser/output pipeline. + +### 1. Два численных домена не смешиваются + +Непрерывная алгебра encoded-sRGB `[0,1]` остаётся для обратного хода alpha и +материальных интервальных расчётов. Финальный эмитируемый композит имеет +отдельный домен `encoded-sRGB8`: + +```text +source-over: round(tint_byte · alpha + bg_byte · (1 − alpha)) +screen: round(bg_byte + alpha · tint_byte · (255 − bg_byte) / 255) +``` + +Порядок операций является частью reference-контракта. Он не выдаётся за +гарантию неизвестного renderer или color-management pipeline; такую +применимость проверяют #233/#241. + +Публичные числовые функции отвергают NaN, бесконечности и выход из домена через +`Result`. Различная семантика debug/release запрещена. + +### 2. Glow перечисляет все достижимые состояния + +Для каждого канала screen переход в новый байт `v` начинается на рациональной +стенке: + +```text +255 · (2(v − B) − 1) / (2 · G · (255 − B)) +``` + +Три уже упорядоченных потока канальных стенок сливаются точным перекрёстным +умножением целых без общей сортировки и промежуточных коллекций. Солвер +проверяет все достижимые RGB-состояния в порядке alpha и выбирает: + +1. первое состояние, где `|ΔJ′| >= target`; +2. если такого нет — глобальный максимум `|ΔJ′|`; +3. при равном максимуме — первое состояние по alpha. + +Так выбор не зависит от предположения о монотонности J′. CAM16-UCS остаётся +binary64-метрикой текущей реализации; cross-runtime границы и интервальные +гарантии принадлежат #223. + +### 3. Alpha канонизируется внутри выбранного интервала + +Каждое квантованное состояние имеет интервал `[lower, upper)`, последнее — +`[lower, 1]`. Каноническая alpha — середина интервала: она максимизирует +численный запас до обеих стенок, но не объявляется эстетическим оптимумом. + +`alphaCss` — кратчайшая десятичная запись, восстанавливающая тот же `f64`. +CSS-переменная alpha копирует её буквально. Повторное округление downstream +запрещено. Побитовый parse-round-trip проверяется в Rust/JS-тестах; production +не включает универсальный десятичный парсер только ради повторной проверки +контракта стандартного shortest-roundtrip сериализатора. + +### 4. Point-метрики не подменяют spatial effect + +Цель glow относится только к изолированному halo. Результат отдельно сообщает +композит и `|ΔJ′|` для halo и core, `targetStatus`, `constraintLayer` и +`referenceProfile`. Совместный blur/overlap/backdrop/HDR-эффект без геометрии не +сертифицируется; это отдельный `SpatialField` из #221. + +Midpoint J′ и gamut-boundary chroma для core — версионированный recipe v1, а не +проверенный на наблюдателях «закон красивого свечения». + +### 5. Доказательства являются исполняемыми + +- все 65 536 пар одного screen-канала проверяют рациональное разбиение; +- независимый finite-state oracle проверяет выбор reached/unreachable; +- source-over при `alpha=0.122` сверяется с точной рациональной формулой на всех + 65 536 парах байтов; +- JS chain требует нулевого расхождения engine ↔ официальный consumer; +- OKLCH и Display P3 проходят все 16 777 216 sRGB8-входов отдельными обратными + путями в фактической среде release-CI с закреплённым Rust toolchain; +- изменение precision имеет быстрые mutation-killer sentinels. + +Full-cube PASS относится только к target, toolchain и системной `libm` +конкретного запуска: он не переносится на непроверенную среду. + +## Последствия и миграция + +Изменение намеренно breaking до 1.0 и не публикуется как patch (#259). + +Rust: + +- `composite_over_encoded(...)` теперь возвращает `Result`; +- для финального байтового reference используется `composite_over_srgb8(...)`; +- поля `GlowSolve` читаются через getters; +- alpha берётся из `alpha_css()`, статус — из `status()`. + +WASM/TypeScript: + +- mocks и exhaustive unions обязаны добавить `alphaCss`, `referenceProfile`, + `constraintLayer`, `targetDj`, `targetStatus`, оба composite и оба `achievedDj`; +- legacy `achievedDj` и `degraded` остаются точными aliases на период миграции; +- изменение строк alpha и соответствующих golden ожидаемо. + +Общий conformance pack повышен до `2.0.0`: alpha-family содержит half-tie +`#C0B2FA @ 0.122` над чёрным, поэтому Rust FFI и все нативные биндинги обязаны +вернуть `#17161F`, а старый нормализованный путь больше не считается conformant. + +Открытыми остаются: полный viewing/context contract (#230), cross-runtime +численные гарантии (#223), фактический renderer/output pipeline (#233/#241) и +пространственная модель glow (#221). From 1568b4c2e2782e5591644b643f449694236a07f4 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 10 Jul 2026 13:19:56 +0300 Subject: [PATCH 02/11] =?UTF-8?q?fix(css):=20=D1=81=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20alpha?= =?UTF-8?q?=20=D0=B1=D0=B5=D0=B7=20=D0=BF=D0=BE=D1=82=D0=B5=D1=80=D0=B8=20?= =?UTF-8?q?binary64?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/labcolors-core/src/lib.rs | 2 +- crates/labcolors-core/src/spaces/oklch.rs | 97 +++++++++++++++-------- 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 523373be..5f532445 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -108,7 +108,7 @@ pub use solve::{ BgInput, ChromaPolicy, Contract, Floor, Gamut, Hue, SolveJob, Solved, TypographicContext, Unreachable, solve, solve_many, }; -pub use spaces::oklch::{oklch_css_from_hex, oklch_from_hex}; +pub use spaces::oklch::{css_alpha_value, oklch_css_from_hex, oklch_from_hex}; pub use spaces::p3::{p3_css_from_hex, p3_from_hex}; pub use spaces::srgb::{hex_from_srgb_encoded, srgb_encoded_from_hex}; pub use spaces::vc::ViewingConditions; diff --git a/crates/labcolors-core/src/spaces/oklch.rs b/crates/labcolors-core/src/spaces/oklch.rs index bfd8f2c4..12fd4135 100644 --- a/crates/labcolors-core/src/spaces/oklch.rs +++ b/crates/labcolors-core/src/spaces/oklch.rs @@ -8,8 +8,11 @@ //! //! Значения остаются решёнными в sRGB-гамуте: oklch здесь — система координат //! записи, не расширение гамута. Точность цифр подобрана под БАЙТ-ТОЧНЫЙ -//! round-trip: `oklch_css_from_hex` → парс → sRGB даёт исходные 8-битные -//! байты на всём кубе (доказательство — тест `round_trip_is_byte_exact_*`). +//! round-trip: `oklch_css_from_hex` → независимый парс и обратные матрицы → +//! sRGB8. Полный release-гейт `oklch_full_cube` перечисляет все 16 777 216 +//! входов; CI запускает его явно в фактической Linux-среде с закреплённым Rust. +//! PASS относится к target, toolchain и системной `libm` конкретного запуска, а +//! не объявляет переносимость без проверки. Быстрый debug-тест остаётся smoke. use super::oklab::srgb_linear_to_oklab; use super::srgb::srgb_from_hex; @@ -35,15 +38,17 @@ pub fn oklch_from_hex(hex: &str) -> Result<[f64; 3], String> { /// CSS-строка `oklch(L% C H)` / `oklch(L% C H / A)` из hex-солида и /// опциональной альфы. /// -/// Точность: L — 5 знаков процента, C — 6, H — 3; запас относительно -/// полушага 8-битного канала, доказан round-trip тестом на всём кубе. -/// Альфа — до 4 знаков с обрезкой хвостовых нулей: литералы рампы проходят -/// как есть (`0.122` → `0.122`), а вычисленная альфа (альфа-аналог) не -/// тащит float-хвост в CSS-строку. +/// Точность: L — 5 знаков процента, C — 6, H — 3. Байт-точность для конкретных +/// target, toolchain и системной `libm` проверяет полное перечисление в release-тесте +/// `oklch_full_cube`; CI вызывает его с `--ignored`, а debug-решётка служит +/// быстрым smoke-тестом. +/// Альфа печатается кратчайшей десятичной записью, которая восстанавливает +/// исходный `f64`. Вычисленная прозрачность участвует в сертификате композита, +/// поэтому произвольное число знаков после запятой здесь недопустимо. /// /// # Errors /// -/// `Err` — невалидный hex. +/// `Err` — невалидный hex, не-конечная альфа либо альфа вне `[0, 1]`. pub fn oklch_css_from_hex(hex: &str, alpha: Option) -> Result { let [l, c, h] = oklch_from_hex(hex)?; let base = format!("oklch({:.5}% {:.6} {:.3}", l * 100.0, c, h); @@ -53,27 +58,37 @@ pub fn oklch_css_from_hex(hex: &str, alpha: Option) -> Result) -> Result { match alpha { None => Ok(String::new()), - Some(a) => { - const ALPHA_NOISE: f64 = 1e-6; - if !a.is_finite() || !(-ALPHA_NOISE..=1.0 + ALPHA_NOISE).contains(&a) { - return Err(format!("альфа вне [0, 1]: {a}")); - } - let a4 = format!("{:.4}", a.clamp(0.0, 1.0)); - let a4 = a4.trim_end_matches('0').trim_end_matches('.'); - Ok(format!(" / {a4}")) - } + Some(a) => Ok(format!(" / {}", css_alpha_value(a)?)), } } +/// Каноническая CSS-запись альфы без потери binary64-точности. +/// +/// Сериализатор не исправляет значения вызывающего: даже малое отклонение за +/// `[0, 1]` означает, что сертификат был построен в другом домене. Знаковый +/// ноль нормализуется, потому что `-0` и `0` задают одну прозрачность, но первая +/// форма создаёт ненужный платформенный дрейф строк. +/// +/// # Errors +/// +/// `Err`, если значение не конечно или лежит вне `[0, 1]`. +pub fn css_alpha_value(alpha: f64) -> Result { + if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) { + return Err(format!("альфа вне [0, 1]: {alpha}")); + } + let canonical = if alpha == 0.0 { 0.0 } else { alpha }; + Ok(canonical.to_string()) +} + /// Обратный путь (только для доказательства round-trip): (L, C, H°) → hex. #[cfg(test)] fn hex_from_oklch(l: f64, c: f64, h_deg: f64) -> String { @@ -86,8 +101,9 @@ fn hex_from_oklch(l: f64, c: f64, h_deg: f64) -> String { mod tests { use super::*; - /// Парсер эмитированной строки — эталонная реконструкция потребителя - /// (браузер парсит те же компоненты тем же путём Oklab → sRGB). + /// Независимая test-транскрипция CSS Color 4 Oklab → sRGB. Она проверяет + /// численный контракт эмиттера; паритет конкретного браузера — отдельный + /// WPT/headless gate. fn parse_emitted(css: &str) -> (String, Option) { let inner = css .strip_prefix("oklch(") @@ -144,24 +160,41 @@ mod tests { } } - /// Гард альфы: NaN — честная ошибка, конечный шум за краями — кламп - /// без артефакта "-0". + /// Вычисленная альфа является частью сертификата композита: CSS-строка + /// обязана восстанавливать то же binary64-значение, а не соседнюю точку. #[test] - fn alpha_guard_rejects_nan_and_clamps_noise() { + fn computed_alpha_round_trips_without_precision_loss() { + let alpha = 0.036_045_459_685_627_89_f64; + let css = oklch_css_from_hex("#4A8FFF", Some(alpha)).unwrap(); + let (_, parsed) = parse_emitted(&css); + assert_eq!( + parsed.unwrap().to_bits(), + alpha.to_bits(), + "CSS потерял точность вычисленной альфы: {css}" + ); + } + + /// Граница альфы не маскирует ошибку вычисления клампом; знаковый ноль + /// канонизируется без изменения значения прозрачности. + #[test] + fn alpha_guard_rejects_every_out_of_domain_value() { assert!(oklch_css_from_hex("#101012", Some(f64::NAN)).is_err()); assert!(oklch_css_from_hex("#101012", Some(f64::INFINITY)).is_err()); - // Грубо невалидная альфа — дефект вызывающего, не шум: ошибка, не кламп. + assert!(oklch_css_from_hex("#101012", Some(f64::NEG_INFINITY)).is_err()); assert!(oklch_css_from_hex("#101012", Some(-10.0)).is_err()); assert!(oklch_css_from_hex("#101012", Some(2.0)).is_err()); + assert!(oklch_css_from_hex("#101012", Some(-f64::EPSILON)).is_err()); + assert!(oklch_css_from_hex("#101012", Some(1.0 + f64::EPSILON)).is_err()); // Знаковый ноль H не просачивается в печать. assert!( !oklch_css_from_hex("#FFFFFF", None).unwrap().contains("-0."), "signed zero в компонентах запрещён" ); - let noisy = oklch_css_from_hex("#101012", Some(-1e-7)).unwrap(); - assert!(noisy.ends_with(" / 0)"), "шум у нуля клампится: {noisy}"); - let over = oklch_css_from_hex("#101012", Some(1.0 + 1e-9)).unwrap(); - assert!(over.ends_with(" / 1)"), "шум у единицы клампится: {over}"); + let zero = oklch_css_from_hex("#101012", Some(-0.0)).unwrap(); + assert!( + zero.ends_with(" / 0)"), + "знаковый ноль нормализован: {zero}" + ); } /// Форма строки — контракт потребителя: процент у L, слэш-альфа, From 2aa6a0c253b0b078f3012189bc3d6df14d8597a1 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 10 Jul 2026 13:42:18 +0300 Subject: [PATCH 03/11] =?UTF-8?q?fix(core):=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=20=D1=82=D0=BE=D1=87=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20alpha/glow=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D1=81=D0=B5?= =?UTF-8?q?=D0=BC=D0=B0=D0=BD=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../examples/figma_anchor_provenance.rs | 1 + crates/labcolors-core/src/alpha.rs | 14 +- crates/labcolors-core/src/config.rs | 70 ++++++- crates/labcolors-core/src/config/tests.rs | 157 ++++++++++++++- crates/labcolors-core/src/glow.rs | 139 +++---------- crates/labcolors-core/src/lib.rs | 5 +- crates/labcolors-core/src/material.rs | 9 +- crates/labcolors-core/src/semantic.rs | 190 +++++++++++------- crates/labcolors-core/src/spaces/p3.rs | 21 +- .../tests/data/labui_emission_golden.txt | 92 ++++----- .../0004-finite-alpha-glow-reference.md | 7 +- 11 files changed, 448 insertions(+), 257 deletions(-) diff --git a/crates/labcolors-core/examples/figma_anchor_provenance.rs b/crates/labcolors-core/examples/figma_anchor_provenance.rs index 8e4bc992..afdd6969 100644 --- a/crates/labcolors-core/examples/figma_anchor_provenance.rs +++ b/crates/labcolors-core/examples/figma_anchor_provenance.rs @@ -74,6 +74,7 @@ fn srgb_gamma_encode(v: f64) -> f64 { /// потребляет её, а не держит вторую копию формулы. fn composite_gamma(fg: [f64; 3], alpha: f64, bg: [f64; 3]) -> [f64; 3] { labcolors_core::alpha::composite_over_encoded(fg, alpha, bg) + .expect("данные provenance-примера лежат в encoded-sRGB домене") } /// Композит в линейном свете: каналы декодируются из gamma, смешиваются, diff --git a/crates/labcolors-core/src/alpha.rs b/crates/labcolors-core/src/alpha.rs index b0114602..acaffef3 100644 --- a/crates/labcolors-core/src/alpha.rs +++ b/crates/labcolors-core/src/alpha.rs @@ -398,13 +398,13 @@ pub fn resolve_alpha_analog( let floor = min_alpha_encoded(solid, bg)?; // None только на мусор-входах // Если запрошенная binary64-пара уже является точной обратной к нашему // прямому ходу (включая честную gamut-границу), не поднимаем прозрачность. - if requested_alpha > 0.0 { - if let Some(tint) = invert_composite_encoded(solid, requested_alpha, bg) { - return Some(AlphaAnalog { - tint, - alpha: requested_alpha, - }); - } + if requested_alpha > 0.0 + && let Some(tint) = invert_composite_encoded(solid, requested_alpha, bg) + { + return Some(AlphaAnalog { + tint, + alpha: requested_alpha, + }); } let alpha = requested_alpha.max(floor); // При α == floor == 0 солид равен фону: любой видимый эффект отсутствует, diff --git a/crates/labcolors-core/src/config.rs b/crates/labcolors-core/src/config.rs index 45ad6ddb..adc48517 100644 --- a/crates/labcolors-core/src/config.rs +++ b/crates/labcolors-core/src/config.rs @@ -192,8 +192,9 @@ pub enum ConfigError { }, /// Ссылка (алиас/alpha_analog) на роль, которой нет в `roles`. UnknownRole { referenced_by: String, role: String }, - /// Дубликат ключа в словаре конфига: повтор имени сделал бы lookup и - /// эмиссию неоднозначными (какая запись выиграла — вопрос порядка, тихо). + /// Дубликат ключа в словаре конфига или итоговом CSS-namespace: повтор имени + /// сделал бы lookup/эмиссию неоднозначными (какая запись выиграла — вопрос + /// порядка, тихо). DuplicateKey { dictionary: &'static str, key: String, @@ -598,6 +599,47 @@ pub enum RoleRecipe { Zero, } +/// Суффиксы CSS-переменных, которые один успешно решённый рецепт резервирует. +/// +/// Namespace выводится из типа outcome до резолва: иначе коллизия могла бы +/// зависеть от фона (на одном роль unreachable и «всё работает», на другом два +/// писателя молча делят один `--lab-*`). `Zero` переменных не эмитит никогда. +fn emitted_css_suffixes(recipe: &RoleRecipe) -> &'static [&'static str] { + const PRIMARY: &[&str] = &[""]; + const GLOW: &[&str] = &["", "-core", "-alpha"]; + const MATERIAL: &[&str] = &["", "-01", "-02"]; + const NONE: &[&str] = &[]; + + match recipe { + RoleRecipe::Glow { .. } => GLOW, + RoleRecipe::Material { .. } => MATERIAL, + RoleRecipe::Zero => NONE, + _ => PRIMARY, + } +} + +/// Зарезервировать один shape эмиссии в общем namespace. +/// +/// Отдельный примитив не знает сегодняшних суффиксов и потому не опирается на +/// случайное свойство, что `-core/-alpha/-01/-02` пока не пересекаются друг с +/// другом: будущая derived↔derived коллизия попадёт в тот же гард. +fn reserve_emitted_css_names( + emitted: &mut std::collections::BTreeSet, + name: &str, + suffixes: &[&str], +) -> Result<(), ConfigError> { + for suffix in suffixes { + let key = format!("--lab-{name}{suffix}"); + if !emitted.insert(key.clone()) { + return Err(ConfigError::DuplicateKey { + dictionary: "emitted CSS variables", + key, + }); + } + } + Ok(()) +} + /// Источник тинта лестницы/альфа-аналога: откуда берётся якорный цвет. /// /// Тинт bg-независим (это якорь источника), только пер-темен. Для [`Family`](Self::Family) @@ -1019,6 +1061,30 @@ impl ThemeConfig { } } + // Роль резервирует не только собственный `--lab-{name}`: Glow и Material + // создают сателлиты. Алиас клонирует outcome цели и потому создаёт тот же + // набор уже под СВОИМ именем. Проверяем итоговый namespace целиком до + // резолва/JSON, чтобы порядок писателей никогда не решал, чьё значение + // молча победит. Один общий set ловит role↔satellite, alias↔satellite и + // любые будущие satellite↔satellite пересечения без списка частных пар. + let mut emitted = std::collections::BTreeSet::new(); + + for (name, recipe) in &self.roles { + reserve_emitted_css_names(&mut emitted, name, emitted_css_suffixes(recipe))?; + } + for (alias, target) in &self.aliases { + let target_recipe = self + .roles + .iter() + .find(|(name, _)| name == target) + .map(|(_, recipe)| recipe) + .ok_or_else(|| ConfigError::UnknownRole { + referenced_by: format!("aliases.{alias}"), + role: target.clone(), + })?; + reserve_emitted_css_names(&mut emitted, alias, emitted_css_suffixes(target_recipe))?; + } + Ok(()) } diff --git a/crates/labcolors-core/src/config/tests.rs b/crates/labcolors-core/src/config/tests.rs index fa890505..5099ae50 100644 --- a/crates/labcolors-core/src/config/tests.rs +++ b/crates/labcolors-core/src/config/tests.rs @@ -1577,6 +1577,154 @@ fn validator_rejects_duplicate_dictionary_keys() { )); } +/// Имена конфига — не весь namespace эмиссии: Glow добавляет `-core/-alpha`, +/// Material — `-01/-02`. Роль или алиас с таким именем раньше проходили +/// preflight, а JSON-проекция молча записывала один `--lab-*` дважды; последний +/// писатель менял тип значения (например, alpha-число превращалось в цвет). +#[test] +fn validator_rejects_role_and_alias_collisions_with_emitted_satellites() { + let assert_collision = |cfg: ThemeConfig, expected_stem: &str| { + let expected = format!("--lab-{expected_stem}"); + assert!( + matches!( + cfg.validate(), + Err(ConfigError::DuplicateKey { + dictionary: "emitted CSS variables", + key, + }) if key == expected + ), + "коллизия эмитируемого ключа {expected} обязана быть отвергнута" + ); + }; + + // Реальный класс регрессии: существующий glow владеет двумя сателлитами. + for suffix in ["-core", "-alpha"] { + let colliding = format!("fx-glow-brand{suffix}"); + + let mut role_cfg = labui_reference(); + let ordinary_recipe = role_cfg + .roles + .iter() + .find(|(name, _)| name == "label-primary") + .expect("фикстура несёт label-primary") + .1 + .clone(); + role_cfg.roles.push((colliding.clone(), ordinary_recipe)); + assert_collision(role_cfg, &colliding); + + let mut alias_cfg = labui_reference(); + alias_cfg + .aliases + .push((colliding.clone(), "label-primary".to_string())); + assert_collision(alias_cfg, &colliding); + } + + // Тот же закон обязан закрывать второй многоключевой outcome, а не только + // конкретный найденный суффикс glow. + for suffix in ["-01", "-02"] { + let colliding = format!("probe-material{suffix}"); + + let mut role_cfg = labui_reference(); + role_cfg.roles.push(( + "probe-material".to_string(), + neutral_material(10.0, Floor::AaText), + )); + let ordinary_recipe = role_cfg + .roles + .iter() + .find(|(name, _)| name == "label-primary") + .expect("фикстура несёт label-primary") + .1 + .clone(); + role_cfg.roles.push((colliding.clone(), ordinary_recipe)); + assert_collision(role_cfg, &colliding); + + let mut alias_cfg = labui_reference(); + alias_cfg.roles.push(( + "probe-material".to_string(), + neutral_material(10.0, Floor::AaText), + )); + alias_cfg + .aliases + .push((colliding.clone(), "label-primary".to_string())); + assert_collision(alias_cfg, &colliding); + } + + // Алиас многоключевой цели сам становится владельцем полного shape. Это + // отдельная ветвь: проверка только рецептов ролей пропустила бы её. + for suffix in ["-core", "-alpha"] { + let owner = "probe-glow-alias"; + let colliding = format!("{owner}{suffix}"); + let mut cfg = labui_reference(); + cfg.aliases + .push((owner.to_string(), "fx-glow-brand".to_string())); + let ordinary_recipe = cfg + .roles + .iter() + .find(|(name, _)| name == "label-primary") + .expect("фикстура несёт label-primary") + .1 + .clone(); + cfg.roles.push((colliding.clone(), ordinary_recipe)); + assert_collision(cfg, &colliding); + } + + for suffix in ["-01", "-02"] { + let owner = "probe-material-alias"; + let colliding = format!("{owner}{suffix}"); + let mut cfg = labui_reference(); + cfg.roles.push(( + "probe-material".to_string(), + neutral_material(10.0, Floor::AaText), + )); + cfg.aliases + .push((owner.to_string(), "probe-material".to_string())); + cfg.aliases + .push((colliding.clone(), "label-primary".to_string())); + assert_collision(cfg, &colliding); + } +} + +/// Сегодняшние суффиксы не пересекаются друг с другом, но сам примитив +/// namespace не должен полагаться на это случайное свойство. Синтетические +/// shapes доказывают derived↔derived ветвь: два разных владельца строят один +/// итоговый ключ, и второй резерв немедленно падает. +#[test] +fn emitted_namespace_primitive_rejects_satellite_to_satellite_collision() { + let mut emitted = std::collections::BTreeSet::new(); + reserve_emitted_css_names(&mut emitted, "probe", &["-outer-inner"]) + .expect("первый сателлит свободен"); + let error = reserve_emitted_css_names(&mut emitted, "probe-outer", &["-inner"]) + .expect_err("derived↔derived коллизия обязана быть отвергнута"); + + assert!(matches!( + error, + ConfigError::DuplicateKey { + dictionary: "emitted CSS variables", + key, + } if key == "--lab-probe-outer-inner" + )); +} + +/// Гард не должен превращаться в запрет похожих префиксов: резервируются ровно +/// фактически эмитируемые имена, а не все строки, начинающиеся с имени роли. +#[test] +fn emitted_namespace_allows_non_colliding_near_misses() { + let mut cfg = labui_reference(); + cfg.aliases.push(( + "fx-glow-brand-alpha-extra".to_string(), + "label-primary".to_string(), + )); + cfg.roles.push(( + "probe-material".to_string(), + neutral_material(10.0, Floor::AaText), + )); + cfg.aliases + .push(("probe-material-03".to_string(), "label-primary".to_string())); + + assert_eq!(cfg.validate(), Ok(())); +} + /// preferred_side — закрытое меню {-1, +1}: 0 и 2 отвергаются. #[test] fn validator_rejects_preferred_side_outside_closed_menu() { @@ -1965,7 +2113,8 @@ fn fx_shadow_stack_composition_is_strictly_progressive_on_light() { .unwrap_or_else(|| panic!("{name} должен быть Translucent")); let tint = srgb_encoded_from_hex(t.tint_hex()).unwrap(); let prev_hex = hex_from_srgb_encoded(state); - state = composite_over_encoded(tint, t.alpha(), state); + state = composite_over_encoded(tint, t.alpha(), state) + .expect("эмитированные tint/alpha и предыдущий композит лежат в домене"); let state_hex = hex_from_srgb_encoded(state); // (1) слой меняет пиксели поверх уже наслоённого стека. assert_ne!( @@ -2244,8 +2393,10 @@ fn material_guarantee_recomputable_over_worst_backdrop() { let tint = crate::spaces::srgb::srgb_encoded_from_hex(m.tint_hex()).unwrap(); // ЭКСАКТНЫЙ композит квантованного тинта (как ядро и потребитель), без // переквантования — над двумя углами полного коридора. - let over_black = crate::alpha::composite_over_encoded(tint, m.alpha(), [0.0; 3]); - let over_white = crate::alpha::composite_over_encoded(tint, m.alpha(), [1.0; 3]); + let over_black = crate::alpha::composite_over_encoded(tint, m.alpha(), [0.0; 3]) + .expect("эмитированный материал лежит в домене композитора"); + let over_white = crate::alpha::composite_over_encoded(tint, m.alpha(), [1.0; 3]) + .expect("эмитированный материал лежит в домене композитора"); let pole_lum = if matches!(m.pole(), crate::material::Pole::White) { 1.0 } else { diff --git a/crates/labcolors-core/src/glow.rs b/crates/labcolors-core/src/glow.rs index b10b0069..4699106a 100644 --- a/crates/labcolors-core/src/glow.rs +++ b/crates/labcolors-core/src/glow.rs @@ -39,10 +39,11 @@ //! — исходный цвет. Радиусы и blur здесь отсутствуют: названия обозначают //! назначение слоёв у потребителя, а не измеренную геометрию. //! -//! Core строится существующей policy [`crate::accent_balance`]: J′ берётся -//! посередине между источником и 100, chroma — на sRGB-границе данного hue. -//! Это явный versioned recipe, а не оптимум, проверенный на наблюдателях. Point-метрики -//! core и halo поэтому возвращаются раздельно. +//! Core строится существующей policy [`crate::accent_balance`]: midpoint J′ +//! задаёт seed Oklab-светлоты, chroma берётся на sRGB-границе данного hue. +//! После хроматического преобразования и sRGB8-квантования итоговый J′ не +//! объявляется равным seed — фактическая point-метрика core возвращается +//! отдельно. Это versioned recipe, а не оптимум, проверенный на наблюдателях. use crate::lcs::LcsColor; use crate::spaces::srgb::{decode_8bit, hex_from_srgb_encoded, srgb_encoded_from_hex, srgb_to_xyz}; @@ -95,28 +96,6 @@ pub enum GlowTargetStatus { Unreachable, } -/// Статус представления midpoint-core на конечной сетке sRGB8. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum GlowCoreStatus { - /// Эмитированный цвет сохранил целевой J′ побитно в арифметике движка. - Exact, - /// Непрерывный корень найден, но `#RRGGBB` неизбежно изменил J′. - Quantized, -} - -/// Проверяемый результат двухслойного recipe. -#[derive(Debug, Clone, PartialEq)] -pub struct GlowLayers { - pub core_hex: String, - pub halo_hex: String, - pub target_jp: f64, - pub continuous_jp: f64, - pub continuous_error: f64, - pub emitted_jp: f64, - pub emitted_error: f64, - pub status: GlowCoreStatus, -} - impl GlowTargetStatus { /// Стабильный wire-ключ. pub fn key(self) -> &'static str { @@ -643,75 +622,25 @@ pub fn solve_screen_alpha_for_dj( /// Версионированный двухслойный recipe от источника: `(core_hex, halo_hex)`. /// -/// В recipe v1 halo буквально равен источнику. Для core координата J′ берётся -/// арифметической серединой между J′ источника и 100, а chroma — на sRGB-границе -/// его Oklab hue через [`crate::accent_balance::accent_balanced`]. Это точное -/// описание алгоритма, но не проверенный на наблюдателях оптимум и не модель -/// геометрии свечения. +/// В recipe v1 halo буквально равен источнику. Для core арифметическая середина +/// J′ источника и 100 задаёт seed Oklab-светлоты, а chroma — sRGB-границу его +/// Oklab hue через [`crate::accent_balance::accent_balanced`]. Фактический J′ +/// эмитированного core измеряется вызывающим кодом: он не обязан совпасть с seed +/// после смены координат и sRGB8-квантования. Это recipe, не наблюдательная +/// модель красоты или геометрии свечения. pub fn glow_layers_from_source( source_hex: &str, vc: &ViewingConditions, ) -> Result<(String, String), String> { - let layers = glow_layers_from_source_certified(source_hex, vc)?; - Ok((layers.core_hex, layers.halo_hex)) -} - -/// Строит core как ближайший binary64-корень фактической траектории -/// `accent_balanced(L,h).J′`, а не как сероосевой seed. -/// -/// Цель recipe остаётся явно версионированной арифметической серединой между -/// J′ источника и J′ белого. Сертификат отдельно сообщает непрерывный и -/// эмитированный остатки, не пряча квантизацию за допуском. -pub fn glow_layers_from_source_certified( - source_hex: &str, - vc: &ViewingConditions, -) -> Result { validate_viewing_numerics(vc)?; let source_encoded = srgb_encoded_from_hex(source_hex)?; let canonical_source_hex = hex_from_srgb_encoded(source_encoded); let src = LcsColor::from_hex_with_vc(&canonical_source_hex, vc)?; - let white = LcsColor::from_hex_with_vc("#FFFFFF", vc)?; - let target_jp = (src.jp() + white.jp()) * 0.5; - let hue = src.h_ok(); - - let at = |l: f64| crate::accent_balance::accent_balanced(l, hue, vc); - let mut lo = 0.0_f64; - let mut hi = 1.0_f64; - while hi.to_bits() - lo.to_bits() > 1 { - let mid = f64::from_bits((lo.to_bits() + hi.to_bits()) / 2); - if at(mid)?.color.jp() < target_jp { - lo = mid; - } else { - hi = mid; - } - } - let lo_color = at(lo)?.color; - let hi_color = at(hi)?.color; - let core = if (lo_color.jp() - target_jp).abs() <= (hi_color.jp() - target_jp).abs() { - lo_color - } else { - hi_color - }; - let continuous_jp = core.jp(); + let jp_core = (src.jp + 100.0) * 0.5; + let l_core = crate::scale::jp_to_oklab_l(jp_core, vc); + let core = crate::accent_balance::accent_balanced(l_core, src.h_ok, vc).color; let core_hex = core.to_hex_with_vc(vc); - let emitted_jp = LcsColor::from_hex_with_vc(&core_hex, vc)?.jp(); - let continuous_error = continuous_jp - target_jp; - let emitted_error = emitted_jp - target_jp; - let status = if emitted_error == 0.0 { - GlowCoreStatus::Exact - } else { - GlowCoreStatus::Quantized - }; - Ok(GlowLayers { - core_hex, - halo_hex: canonical_source_hex, - target_jp, - continuous_jp, - continuous_error, - emitted_jp, - emitted_error, - status, - }) + Ok((core_hex, canonical_source_hex)) } #[cfg(test)] @@ -827,12 +756,12 @@ mod tests { for byte in 0_u16..=255 { let hex = format!("#{byte:02X}{byte:02X}{byte:02X}"); let lean = jp_from_hex(&hex, &vc).unwrap(); - let full = LcsColor::from_hex_with_vc(&hex, &vc).unwrap().jp(); + let full = LcsColor::from_hex_with_vc(&hex, &vc).unwrap().jp; assert_eq!(lean.to_bits(), full.to_bits(), "{hex}"); } for hex in ["#007AFF", "#FF3B30", "#34C759", "#FFD000", "#3E87FF"] { let lean = jp_from_hex(hex, &vc).unwrap(); - let full = LcsColor::from_hex_with_vc(hex, &vc).unwrap().jp(); + let full = LcsColor::from_hex_with_vc(hex, &vc).unwrap().jp; assert_eq!(lean.to_bits(), full.to_bits(), "{hex}"); } } @@ -1086,32 +1015,14 @@ mod tests { #[test] fn core_is_balanced_overexposed_source() { let vc = ViewingConditions::dim_surround(); - let layers = glow_layers_from_source_certified("#FF3B30", &vc).unwrap(); - let core_hex = layers.core_hex.clone(); - assert_eq!(layers.halo_hex, "#FF3B30", "halo — сам источник"); + let (core_hex, halo_hex) = glow_layers_from_source("#FF3B30", &vc).unwrap(); + assert_eq!(halo_hex, "#FF3B30", "halo — сам источник"); let src = LcsColor::from_hex_with_vc("#FF3B30", &vc).unwrap(); let core = LcsColor::from_hex_with_vc(&core_hex, &vc).unwrap(); - assert!(core.jp() > src.jp(), "core светлее источника (пересвет)"); - let dh = (core.h_ok() - src.h_ok() + 180.0).rem_euclid(360.0) - 180.0; + assert!(core.jp > src.jp, "core светлее источника (пересвет)"); + let dh = (core.h_ok - src.h_ok + 180.0).rem_euclid(360.0) - 180.0; assert!(dh.abs() < 6.0, "оттенок унаследован: Δh = {dh:.2}°"); - // Сертификат пересчитывается из эмитированного hex и не смешивает - // непрерывный корень с ошибкой конечного представления. - let white = LcsColor::from_hex_with_vc("#FFFFFF", &vc).unwrap(); - assert_eq!( - layers.target_jp.to_bits(), - ((src.jp() + white.jp()) * 0.5).to_bits() - ); - assert_eq!(layers.emitted_jp.to_bits(), core.jp().to_bits()); - assert_eq!( - layers.emitted_error.to_bits(), - (layers.emitted_jp - layers.target_jp).to_bits() - ); - assert_eq!( - layers.status == GlowCoreStatus::Exact, - layers.emitted_error == 0.0 - ); - // Хрома баланса = стена гамута ⇒ эмиссия в гамуте, без тихого клипа // (прежняя ×0.5-доля могла запросить недостижимую красочность и молча // срезаться в to_hex). Round-trip красочности центра стабилен. @@ -1125,9 +1036,9 @@ mod tests { #[test] fn layer_recipe_canonicalises_the_public_source_hex() { let vc = ViewingConditions::srgb(); - let layers = glow_layers_from_source_certified("ff3b30", &vc).unwrap(); - assert_eq!(layers.halo_hex, "#FF3B30"); - assert!(layers.core_hex.starts_with('#')); - assert_eq!(layers.core_hex.len(), 7); + let (core_hex, halo_hex) = glow_layers_from_source("ff3b30", &vc).unwrap(); + assert_eq!(halo_hex, "#FF3B30"); + assert!(core_hex.starts_with('#')); + assert_eq!(core_hex.len(), 7); } } diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 5f532445..99886672 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -83,8 +83,9 @@ pub use config::{ }; pub use curve::ColorCurve; pub use glow::{ - GLOW_BASE_DJ, GLOW_BLOOM_DJ, GLOW_SUBTLE_DJ, GlowSolve, glow_layers_from_source, - screen_layer_over_encoded, solve_screen_alpha_for_dj, + GLOW_BASE_DJ, GLOW_BLOOM_DJ, GLOW_REFERENCE_PROFILE, GLOW_SUBTLE_DJ, GlowConstraintLayer, + GlowSolve, GlowTargetStatus, glow_layers_from_source, screen_layer_over_encoded, + solve_screen_alpha_for_dj, }; pub use hash::fnv1a_32; pub use ladder::{LadderPosition, LadderTint, ThemeAnchors}; diff --git a/crates/labcolors-core/src/material.rs b/crates/labcolors-core/src/material.rs index bf37667c..c2f4c375 100644 --- a/crates/labcolors-core/src/material.rs +++ b/crates/labcolors-core/src/material.rs @@ -313,7 +313,8 @@ mod tests { let t = enc(hex); let tq = quantise(t); for alpha in [0.01, 0.1, 0.5, 0.837, 1.0] { - let solid = composite_over_encoded(tq, alpha, tq); + let solid = composite_over_encoded(tq, alpha, tq) + .expect("тестовые sRGB-каналы и alpha лежат в домене"); assert_eq!( hex_from_srgb_encoded(solid), hex_from_srgb_encoded(tq), @@ -381,8 +382,10 @@ mod tests { let m = solve_material_alpha_hex(hex, AA_TEXT).unwrap(); let tint_q = quantise(enc(hex)); // Пересчёт «в лоб» как у потребителя: композит над двумя углами. - let over_black = composite_over_encoded(tint_q, m.alpha, [0.0; 3]); - let over_white = composite_over_encoded(tint_q, m.alpha, [1.0; 3]); + let over_black = composite_over_encoded(tint_q, m.alpha, [0.0; 3]) + .expect("решённый материал лежит в домене композитора"); + let over_white = composite_over_encoded(tint_q, m.alpha, [1.0; 3]) + .expect("решённый материал лежит в домене композитора"); let pole_lum = m.pole.luminance(); let recomputed = ratio_from_luminances(pole_lum, relative_luminance(over_black)).min( ratio_from_luminances(pole_lum, relative_luminance(over_white)), diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index 3a556654..56e6142f 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -1722,8 +1722,13 @@ pub struct GlowResolved { core_hex: String, halo_hex: String, alpha: f64, - achieved_dj: f64, - degraded: bool, + alpha_css: String, + target_dj: f64, + halo_composite_hex: String, + halo_achieved_dj: f64, + core_composite_hex: String, + core_achieved_dj: f64, + target_status: crate::glow::GlowTargetStatus, } impl GlowResolved { @@ -1739,13 +1744,49 @@ impl GlowResolved { pub fn alpha(&self) -> f64 { self.alpha } - /// Фактический |ΔJ'| композита от фона (замер на эмитируемом hex). + /// Каноническая CSS-запись alpha с точным обратным чтением в тот же `f64`. + pub fn alpha_css(&self) -> &str { + &self.alpha_css + } + /// Целевой |ΔJ′| изолированного halo-композита. + pub fn target_dj(&self) -> f64 { + self.target_dj + } + /// Версия конечного reference-домена point-расчёта. + pub fn reference_profile(&self) -> &'static str { + crate::glow::GLOW_REFERENCE_PROFILE + } + /// Слой, по которому решалась целевая ступень. + pub fn constraint_layer(&self) -> crate::glow::GlowConstraintLayer { + crate::glow::GlowConstraintLayer::Halo + } + /// Типизированный исход проверки цели. + pub fn target_status(&self) -> crate::glow::GlowTargetStatus { + self.target_status + } + /// Reference-композит изолированного halo на фоне резолва. + pub fn halo_composite_hex(&self) -> &str { + &self.halo_composite_hex + } + /// Фактический |ΔJ′| изолированного halo-композита. + pub fn halo_achieved_dj(&self) -> f64 { + self.halo_achieved_dj + } + /// Reference-композит изолированного core на том же фоне и alpha. + pub fn core_composite_hex(&self) -> &str { + &self.core_composite_hex + } + /// Фактический |ΔJ′| изолированного core-композита. + pub fn core_achieved_dj(&self) -> f64 { + self.core_achieved_dj + } + /// Alias совместимости: прежнее поле измеряло именно halo. pub fn achieved_dj(&self) -> f64 { - self.achieved_dj + self.halo_achieved_dj } - /// Цель недостижима — возвращён ближайший достижимый шаг (ADR-0002). + /// Alias совместимости прежнего boolean-контракта. pub fn degraded(&self) -> bool { - self.degraded + self.target_status == crate::glow::GlowTargetStatus::Unreachable } } @@ -2214,13 +2255,31 @@ fn resolve_spec_in( step.target_dj(), vc, ) { - Ok(g) => Resolved::Glow(GlowResolved { - core_hex, - halo_hex, - alpha: g.alpha, - achieved_dj: g.achieved_dj, - degraded: g.degraded, - }), + Ok(g) => { + let core_measurement = match crate::glow::measure_screen_layer_at_alpha( + &core_hex, + &bg_hex, + g.alpha(), + vc, + ) { + Ok(measurement) => measurement, + Err(e) => { + return Resolved::Unreachable(Unreachable::InvalidInput(e)); + } + }; + Resolved::Glow(GlowResolved { + core_hex, + halo_hex, + alpha: g.alpha(), + alpha_css: g.alpha_css().to_string(), + target_dj: g.target_dj(), + halo_composite_hex: g.composite_hex().to_string(), + halo_achieved_dj: g.achieved_dj(), + core_composite_hex: core_measurement.composite_hex, + core_achieved_dj: core_measurement.achieved_dj, + target_status: g.status(), + }) + } Err(e) => Resolved::Unreachable(Unreachable::InvalidInput(e)), }; } @@ -2334,13 +2393,11 @@ fn resolve_rgba_direct( )); } let bg_encoded = bg.encoded_display(); - // Тинт квантуется ДО композита: наружу уходит 8-битный tint_hex, и браузер - // скомпозитит именно его — замер обязан считаться из эмитируемого значения, - // иначе composite_hex/Lc/WCAG расходились бы с CSS-результатом на LSB. + // Тинт квантуется ДО композита: сертификат и эмиссия обязаны ссылаться на + // один и тот же encoded-sRGB8 reference-пиксель. let tint_q = quantise_encoded(tint_encoded); - let composite = crate::alpha::composite_over_encoded(tint_q, alpha, bg_encoded); // Прямая лестница эмитит запрошенную α как есть — коэрсии нет по построению. - finish_rgba(tint_q, alpha, composite, bg_encoded, vc, false, false) + finish_rgba(tint_q, alpha, bg_encoded, vc, false, false) } /// Резолв ЦВЕТНОГО текст/UI-лейбла (ратификация ch5c, M1). @@ -2487,8 +2544,7 @@ fn resolve_solid_with_ui_floor( Ok(solved) => { let shifted = crate::spaces::srgb::srgb_encoded_from_hex(solved.hex()) .expect("hex собственного солвера всегда валиден"); - let composite = crate::alpha::composite_over_encoded(shifted, 1.0, bg_encoded); - finish_rgba(shifted, 1.0, composite, bg_encoded, vc, false, true) + finish_rgba(shifted, 1.0, bg_encoded, vc, false, true) } Err(reason) => Resolved::Unreachable(reason), } @@ -2523,17 +2579,18 @@ fn resolve_pair_label( } else { surface_alpha_light }; - // Тинт квантуется ДО композита — браузер красит 8-битный tint, поэтому - // подложка обязана считаться из отдаваемого значения (тот же закон, что в - // resolve_rgba_direct). Композит квантуется тем же форматтером: лейбл решается - // против ТОГО ЖЕ пикселя, что уйдёт наружу. + // Тинт квантуется ДО композита: подложка обязана считаться из отдаваемого + // значения в едином encoded-sRGB8 reference-домене. let tint_q = quantise_encoded(tint.for_vc(vc)); - let surface = quantise_encoded(crate::alpha::composite_over_encoded( - tint_q, - alpha, - bg.encoded_display(), - )); - let surface_hex = crate::spaces::srgb::hex_from_srgb_encoded(surface); + let surface_hex = + match crate::alpha::composite_hex_from_encoded(tint_q, alpha, bg.encoded_display()) { + Ok(hex) => hex, + Err(error) => { + return Resolved::Unreachable(Unreachable::InvalidInput(format!( + "тинт-поверхность бейджа вне encoded-sRGB8 reference-домена: {error}" + ))); + } + }; let Ok(surface_bg) = BgInput::solid(&surface_hex) else { // Композит 8-битных каналов всегда в кубе — недостижимо, но честнее // отказ, чем правдоподобный мусор (RoleSpec публичен). @@ -2549,10 +2606,8 @@ fn resolve_pair_label( } /// Альфа-аналог: солид-цель `solid` (кодированный, по теме) на фоне резолва -/// инвертируется в `(tint, фактическая α)` через [`crate::alpha::resolve_alpha_analog`]. -/// Композит фактической пары равен солиду ПО ПОСТРОЕНИЮ, поэтому контраст -/// наследуется солидом; замер идёт на этом композите. `None`-инверсия -/// (вход вне гамута) физически недостижима — солид-тинт по теме всегда в гамуте. +/// инвертируется в `(tint, фактическая α)`. Перед инверсией цель квантуется до +/// эмитируемой sRGB8-сетки; production-композитор обязан побайтно вернуть её. fn resolve_rgba_inverted( solid_encoded: [f64; 3], requested_alpha: f64, @@ -2569,41 +2624,26 @@ fn resolve_rgba_inverted( )); } let bg_encoded = bg.encoded_display(); - let Some(analog) = - crate::alpha::resolve_alpha_analog(solid_encoded, requested_alpha, bg_encoded) - else { - // Тинт источника по теме — валидный кодированный цвет byte/255, поэтому - // домен инверсии не нарушается; None здесь означал бы мусорный вход. - return Resolved::Unreachable(Unreachable::InvalidInput( - "alpha-analog source out of encoded sRGB domain".to_string(), - )); - }; - // Тинт инверсии квантуется до композита (см. resolve_rgba_direct: замер из - // эмитируемого значения — браузер скомпозитит 8-битный tint_hex). Композит - // пересчитывается от квантованного тинта; равенство солиду держится в - // пределах LSB-границы квантования (#119). - let tint_q = quantise_encoded(analog.tint); - let composite = crate::alpha::composite_over_encoded(tint_q, analog.alpha, bg_encoded); - // Коэрсия α: фактическая (`analog.alpha`) строго выше запрошенной ⇔ пол - // `α_min` перекрыл запрошенную. `resolve_alpha_analog`: α = - // req.clamp(0,1).max(floor); под гардом `rgba_input_valid` (req ∈ (0,1]) - // clamp — no-op, т.е. α = req.max(floor). При floor ≤ req значения побайтно - // равны, при floor > req — строго больше. Сравнение точное, эпсилон не нужен: - // `max` возвращает либо тот же f64, либо floor. - let alpha_coerced = analog.alpha > requested_alpha; - finish_rgba( - tint_q, - analog.alpha, - composite, - bg_encoded, - vc, - alpha_coerced, - false, - ) + let solid_q = quantise_encoded(solid_encoded); + let analog = + match crate::alpha::resolve_alpha_analog_srgb8(solid_q, requested_alpha, bg_encoded) { + Ok(Some(analog)) => analog, + Ok(None) => { + return Resolved::Unreachable(Unreachable::InvalidInput( + "alpha-analog не удалось разрешить в конечном числовом домене".to_string(), + )); + } + Err(error) => return Resolved::Unreachable(Unreachable::InvalidInput(error)), + }; + let (tint_srgb8, actual_alpha) = analog; + let tint_q = tint_srgb8.map(|channel| f64::from(channel) / 255.0); + // Резолвер возвращает тот же binary64 либо строго больший точный пол. + let alpha_coerced = actual_alpha > requested_alpha; + finish_rgba(tint_q, actual_alpha, bg_encoded, vc, alpha_coerced, false) } -/// Собрать [`Resolved::Translucent`] из тинта, альфы и композита: квантовать тинт и -/// композит до hex, замерить контраст композита против фона резолва. +/// Собрать [`Resolved::Translucent`] из эмитируемых тинта и альфы: вывести их +/// encoded-sRGB8 reference-композит и замерить его против фона резолва. /// /// Контраст меряется в тех же метриках, что и у солид-роли: перцептивный `Lc` /// на линейном свете ([`measure_contrast`]) и WCAG на кодированном дисплее — так @@ -2611,17 +2651,23 @@ fn resolve_rgba_inverted( fn finish_rgba( tint_encoded: [f64; 3], alpha: f64, - composite_encoded: [f64; 3], bg_encoded: [f64; 3], vc: &ViewingConditions, alpha_coerced: bool, floor_coerced: bool, ) -> Resolved { use crate::spaces::srgb::{hex_from_srgb_encoded, srgb_encoded_from_hex, srgb_gamma_inv}; - // Замер идёт по КВАНТОВАННОМУ композиту — тому же 8-битному hex, который - // уходит наружу (закон движка: честный dJ' меряется на отданном цвете, - // как в solve_dj; неквантованный замер расходился бы с отданным на LSB). - let composite_hex = hex_from_srgb_encoded(composite_encoded); + // Единый byte-domain SSOT нужен и для hex, и для обеих метрик: нормализация + // `(byte/255)·255` способна изменить half-tie на один LSB. + let composite_hex = + match crate::alpha::composite_hex_from_encoded(tint_encoded, alpha, bg_encoded) { + Ok(hex) => hex, + Err(error) => { + return Resolved::Unreachable(Unreachable::InvalidInput(format!( + "rgba-композит вне encoded-sRGB8 reference-домена: {error}" + ))); + } + }; let composite_q = srgb_encoded_from_hex(&composite_hex).expect("hex собственного форматтера всегда валиден"); // Линейный свет из кодированного (per-channel gamma-декод) для перцептивного Lc. diff --git a/crates/labcolors-core/src/spaces/p3.rs b/crates/labcolors-core/src/spaces/p3.rs index e79ab2ae..28341a6b 100644 --- a/crates/labcolors-core/src/spaces/p3.rs +++ b/crates/labcolors-core/src/spaces/p3.rs @@ -286,17 +286,26 @@ mod tests { } } - /// Политика альфы — единая с oklch-эмиссией (общий дом css_alpha_suffix): - /// NaN/грубый выход — ошибка, шум у краёв — кламп. + /// Политика alpha едина с oklch-эмиссией: любое недоменное значение — + /// ошибка. Даже малый clamp изменил бы публичное число и мог бы выдать + /// соседний конечный композит за сертифицированный. #[test] fn alpha_guard_shared_with_oklch() { assert!(p3_css_from_hex("#101012", Some(f64::NAN)).is_err()); assert!(p3_css_from_hex("#101012", Some(-10.0)).is_err()); assert!(p3_css_from_hex("#101012", Some(2.0)).is_err()); - let noisy = p3_css_from_hex("#101012", Some(-1e-7)).unwrap(); - assert!(noisy.ends_with(" / 0)"), "шум у нуля клампится: {noisy}"); - let over = p3_css_from_hex("#101012", Some(1.0 + 1e-9)).unwrap(); - assert!(over.ends_with(" / 1)"), "шум у единицы клампится: {over}"); + assert!(p3_css_from_hex("#101012", Some(-1e-7)).is_err()); + assert!(p3_css_from_hex("#101012", Some(1.0 + 1e-9)).is_err()); + assert!( + p3_css_from_hex("#101012", Some(0.0)) + .unwrap() + .ends_with(" / 0)") + ); + assert!( + p3_css_from_hex("#101012", Some(1.0)) + .unwrap() + .ends_with(" / 1)") + ); } /// Форма строки — контракт потребителя: `color(display-p3 R G B [/ A])`, diff --git a/crates/labcolors-core/tests/data/labui_emission_golden.txt b/crates/labcolors-core/tests/data/labui_emission_golden.txt index 1f614862..afd1d758 100644 --- a/crates/labcolors-core/tests/data/labui_emission_golden.txt +++ b/crates/labcolors-core/tests/data/labui_emission_golden.txt @@ -77,10 +77,10 @@ srgb|#FFFFFF|fx-focus-ring-brand=rgba(#007AFF,1) srgb|#FFFFFF|fx-focus-ring-danger=rgba(#FF3B30,1) srgb|#FFFFFF|fx-focus-ring-warning=rgba(#FFA100,1) srgb|#FFFFFF|fx-focus-ring-neutral=rgba(#101012,1) -srgb|#FFFFFF|fx-glow-brand=glow(#8DBCFF,#007AFF,1.0000) -srgb|#FFFFFF|fx-glow-danger=glow(#FFAA9D,#FF3B30,1.0000) -srgb|#FFFFFF|fx-glow-warning=glow(#FFD19F,#FFA100,1.0000) -srgb|#FFFFFF|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,1.0000) +srgb|#FFFFFF|fx-glow-brand=glow(#8DBCFF,#007AFF,0.5000) +srgb|#FFFFFF|fx-glow-danger=glow(#FFAA9D,#FF3B30,0.5000) +srgb|#FFFFFF|fx-glow-warning=glow(#FFD19F,#FFA100,0.5000) +srgb|#FFFFFF|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.5000) srgb|#FFFFFF|fx-glow-inverted=rgba(#B0B0B9,0.522) srgb|#FFFFFF|fx-skeleton-highlight=rgba(#787880,0.039) srgb|#FFFFFF|fill-neutral=rgba(#787880,1) @@ -184,10 +184,10 @@ srgb|#F2F2F7|fx-focus-ring-brand=rgba(#007AFF,1) srgb|#F2F2F7|fx-focus-ring-danger=rgba(#FF3B30,1) srgb|#F2F2F7|fx-focus-ring-warning=rgba(#FFA100,1) srgb|#F2F2F7|fx-focus-ring-neutral=rgba(#101012,1) -srgb|#F2F2F7|fx-glow-brand=glow(#8DBCFF,#007AFF,1.0000) -srgb|#F2F2F7|fx-glow-danger=glow(#FFAA9D,#FF3B30,1.0000) -srgb|#F2F2F7|fx-glow-warning=glow(#FFD19F,#FFA100,0.9138) -srgb|#F2F2F7|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.6538) +srgb|#F2F2F7|fx-glow-brand=glow(#8DBCFF,#007AFF,0.9688) +srgb|#F2F2F7|fx-glow-danger=glow(#FFAA9D,#FF3B30,0.9980) +srgb|#F2F2F7|fx-glow-warning=glow(#FFD19F,#FFA100,0.9376) +srgb|#F2F2F7|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.6707) srgb|#F2F2F7|fx-glow-inverted=rgba(#B0B0B9,0.522) srgb|#F2F2F7|fx-skeleton-highlight=rgba(#787880,0.039) srgb|#F2F2F7|fill-neutral=rgba(#787880,1) @@ -291,10 +291,10 @@ srgb|#7F7F7F|fx-focus-ring-brand=rgba(#007AFF,1) srgb|#7F7F7F|fx-focus-ring-danger=rgba(#FF3B30,1) srgb|#7F7F7F|fx-focus-ring-warning=rgba(#FFA100,1) srgb|#7F7F7F|fx-focus-ring-neutral=rgba(#101012,1) -srgb|#7F7F7F|fx-glow-brand=glow(#8DBCFF,#007AFF,0.1061) -srgb|#7F7F7F|fx-glow-danger=glow(#FFAA9D,#FF3B30,0.1055) -srgb|#7F7F7F|fx-glow-warning=glow(#FFD19F,#FFA100,0.0681) -srgb|#7F7F7F|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0430) +srgb|#7F7F7F|fx-glow-brand=glow(#8DBCFF,#007AFF,0.1097) +srgb|#7F7F7F|fx-glow-danger=glow(#FFAA9D,#FF3B30,0.1094) +srgb|#7F7F7F|fx-glow-warning=glow(#FFD19F,#FFA100,0.0711) +srgb|#7F7F7F|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0469) srgb|#7F7F7F|fx-glow-inverted=rgba(#B0B0B9,0.522) srgb|#7F7F7F|fx-skeleton-highlight=rgba(#787880,0.039) srgb|#7F7F7F|fill-neutral=rgba(#787880,1) @@ -399,9 +399,9 @@ srgb|#1C1C1E|fx-focus-ring-danger=rgba(#FF3B30,1) srgb|#1C1C1E|fx-focus-ring-warning=rgba(#FFA100,1) srgb|#1C1C1E|fx-focus-ring-neutral=rgba(#101012,1) srgb|#1C1C1E|fx-glow-brand=glow(#8DBCFF,#007AFF,0.0599) -srgb|#1C1C1E|fx-glow-danger=glow(#FFAA9D,#FF3B30,0.0551) -srgb|#1C1C1E|fx-glow-warning=glow(#FFD19F,#FFA100,0.0374) -srgb|#1C1C1E|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0242) +srgb|#1C1C1E|fx-glow-danger=glow(#FFAA9D,#FF3B30,0.0570) +srgb|#1C1C1E|fx-glow-warning=glow(#FFD19F,#FFA100,0.0379) +srgb|#1C1C1E|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0243) srgb|#1C1C1E|fx-glow-inverted=rgba(#B0B0B9,0.522) srgb|#1C1C1E|fx-skeleton-highlight=rgba(#787880,0.039) srgb|#1C1C1E|fill-neutral=rgba(#787880,1) @@ -505,10 +505,10 @@ srgb|#101012|fx-focus-ring-brand=rgba(#007AFF,1) srgb|#101012|fx-focus-ring-danger=rgba(#FF3B30,1) srgb|#101012|fx-focus-ring-warning=rgba(#FFA100,1) srgb|#101012|fx-focus-ring-neutral=rgba(#101012,1) -srgb|#101012|fx-glow-brand=glow(#8DBCFF,#007AFF,0.0568) -srgb|#101012|fx-glow-danger=glow(#FFAA9D,#FF3B30,0.0560) -srgb|#101012|fx-glow-warning=glow(#FFD19F,#FFA100,0.0364) -srgb|#101012|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0230) +srgb|#101012|fx-glow-brand=glow(#8DBCFF,#007AFF,0.0569) +srgb|#101012|fx-glow-danger=glow(#FFAA9D,#FF3B30,0.0563) +srgb|#101012|fx-glow-warning=glow(#FFD19F,#FFA100,0.0381) +srgb|#101012|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0231) srgb|#101012|fx-glow-inverted=rgba(#B0B0B9,0.522) srgb|#101012|fx-skeleton-highlight=rgba(#787880,0.039) srgb|#101012|fill-neutral=rgba(#787880,1) @@ -612,10 +612,10 @@ srgb|#3478F6|fx-focus-ring-brand=rgba(#007AFF,1) srgb|#3478F6|fx-focus-ring-danger=rgba(#FF3B30,1) srgb|#3478F6|fx-focus-ring-warning=rgba(#FFA100,1) srgb|#3478F6|fx-focus-ring-neutral=rgba(#101012,1) -srgb|#3478F6|fx-glow-brand=glow(#8DBCFF,#007AFF,0.1316) -srgb|#3478F6|fx-glow-danger=glow(#FFAA9D,#FF3B30,0.1404) +srgb|#3478F6|fx-glow-brand=glow(#8DBCFF,#007AFF,0.1393) +srgb|#3478F6|fx-glow-danger=glow(#FFAA9D,#FF3B30,0.1422) srgb|#3478F6|fx-glow-warning=glow(#FFD19F,#FFA100,0.0763) -srgb|#3478F6|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0556) +srgb|#3478F6|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0561) srgb|#3478F6|fx-glow-inverted=rgba(#B0B0B9,0.522) srgb|#3478F6|fx-skeleton-highlight=rgba(#787880,0.039) srgb|#3478F6|fill-neutral=rgba(#787880,1) @@ -719,10 +719,10 @@ dim|#FFFFFF|fx-focus-ring-brand=rgba(#4A8FFF,1) dim|#FFFFFF|fx-focus-ring-danger=rgba(#FF3A3A,1) dim|#FFFFFF|fx-focus-ring-warning=rgba(#FF9008,1) dim|#FFFFFF|fx-focus-ring-neutral=rgba(#F6F8FA,1) -dim|#FFFFFF|fx-glow-brand=glow(#A0C5FF,#4A8FFF,1.0000) -dim|#FFFFFF|fx-glow-danger=glow(#FFA89F,#FF3A3A,1.0000) -dim|#FFFFFF|fx-glow-warning=glow(#FFC89D,#FF9008,1.0000) -dim|#FFFFFF|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,1.0000) +dim|#FFFFFF|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.5000) +dim|#FFFFFF|fx-glow-danger=glow(#FFA89F,#FF3A3A,0.5000) +dim|#FFFFFF|fx-glow-warning=glow(#FFC89D,#FF9008,0.5000) +dim|#FFFFFF|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.5000) dim|#FFFFFF|fx-glow-inverted=rgba(#3C3C43,0.522) dim|#FFFFFF|fx-skeleton-highlight=rgba(#787880,0.039) dim|#FFFFFF|fill-neutral=rgba(#787880,1) @@ -826,10 +826,10 @@ dim|#F2F2F7|fx-focus-ring-brand=rgba(#4A8FFF,1) dim|#F2F2F7|fx-focus-ring-danger=rgba(#FF3A3A,1) dim|#F2F2F7|fx-focus-ring-warning=rgba(#FF9008,1) dim|#F2F2F7|fx-focus-ring-neutral=rgba(#F6F8FA,1) -dim|#F2F2F7|fx-glow-brand=glow(#A0C5FF,#4A8FFF,1.0000) -dim|#F2F2F7|fx-glow-danger=glow(#FFA89F,#FF3A3A,1.0000) -dim|#F2F2F7|fx-glow-warning=glow(#FFC89D,#FF9008,1.0000) -dim|#F2F2F7|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.7308) +dim|#F2F2F7|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.9688) +dim|#F2F2F7|fx-glow-danger=glow(#FFA89F,#FF3A3A,0.9808) +dim|#F2F2F7|fx-glow-warning=glow(#FFC89D,#FF9008,0.9808) +dim|#F2F2F7|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.7692) dim|#F2F2F7|fx-glow-inverted=rgba(#3C3C43,0.522) dim|#F2F2F7|fx-skeleton-highlight=rgba(#787880,0.039) dim|#F2F2F7|fill-neutral=rgba(#787880,1) @@ -933,10 +933,10 @@ dim|#7F7F7F|fx-focus-ring-brand=rgba(#4A8FFF,1) dim|#7F7F7F|fx-focus-ring-danger=rgba(#FF3A3A,1) dim|#7F7F7F|fx-focus-ring-warning=rgba(#FF9008,1) dim|#7F7F7F|fx-focus-ring-neutral=rgba(#F6F8FA,1) -dim|#7F7F7F|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.0906) -dim|#7F7F7F|fx-glow-danger=glow(#FFA89F,#FF3A3A,0.1202) -dim|#7F7F7F|fx-glow-warning=glow(#FFC89D,#FF9008,0.0761) -dim|#7F7F7F|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0508) +dim|#7F7F7F|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.0924) +dim|#7F7F7F|fx-glow-danger=glow(#FFA89F,#FF3A3A,0.1207) +dim|#7F7F7F|fx-glow-warning=glow(#FFC89D,#FF9008,0.0791) +dim|#7F7F7F|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0547) dim|#7F7F7F|fx-glow-inverted=rgba(#3C3C43,0.522) dim|#7F7F7F|fx-skeleton-highlight=rgba(#787880,0.039) dim|#7F7F7F|fill-neutral=rgba(#787880,1) @@ -1040,10 +1040,10 @@ dim|#1C1C1E|fx-focus-ring-brand=rgba(#4A8FFF,1) dim|#1C1C1E|fx-focus-ring-danger=rgba(#FF3A3A,1) dim|#1C1C1E|fx-focus-ring-warning=rgba(#FF9008,1) dim|#1C1C1E|fx-focus-ring-neutral=rgba(#F6F8FA,1) -dim|#1C1C1E|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.0380) -dim|#1C1C1E|fx-glow-danger=glow(#FFA89F,#FF3A3A,0.0484) -dim|#1C1C1E|fx-glow-warning=glow(#FFC89D,#FF9008,0.0351) -dim|#1C1C1E|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0198) +dim|#1C1C1E|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.0401) +dim|#1C1C1E|fx-glow-danger=glow(#FFA89F,#FF3A3A,0.0486) +dim|#1C1C1E|fx-glow-warning=glow(#FFC89D,#FF9008,0.0363) +dim|#1C1C1E|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0199) dim|#1C1C1E|fx-glow-inverted=rgba(#3C3C43,0.522) dim|#1C1C1E|fx-skeleton-highlight=rgba(#787880,0.039) dim|#1C1C1E|fill-neutral=rgba(#787880,1) @@ -1147,10 +1147,10 @@ dim|#101012|fx-focus-ring-brand=rgba(#4A8FFF,1) dim|#101012|fx-focus-ring-danger=rgba(#FF3A3A,1) dim|#101012|fx-focus-ring-warning=rgba(#FF9008,1) dim|#101012|fx-focus-ring-neutral=rgba(#F6F8FA,1) -dim|#101012|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.0360) -dim|#101012|fx-glow-danger=glow(#FFA89F,#FF3A3A,0.0460) -dim|#101012|fx-glow-warning=glow(#FFC89D,#FF9008,0.0333) -dim|#101012|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0188) +dim|#101012|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.0381) +dim|#101012|fx-glow-danger=glow(#FFA89F,#FF3A3A,0.0462) +dim|#101012|fx-glow-warning=glow(#FFC89D,#FF9008,0.0345) +dim|#101012|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0189) dim|#101012|fx-glow-inverted=rgba(#3C3C43,0.522) dim|#101012|fx-skeleton-highlight=rgba(#787880,0.039) dim|#101012|fill-neutral=rgba(#787880,1) @@ -1254,10 +1254,10 @@ dim|#3478F6|fx-focus-ring-brand=rgba(#4A8FFF,1) dim|#3478F6|fx-focus-ring-danger=rgba(#FF3A3A,1) dim|#3478F6|fx-focus-ring-warning=rgba(#FF9008,1) dim|#3478F6|fx-focus-ring-neutral=rgba(#F6F8FA,1) -dim|#3478F6|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.1123) -dim|#3478F6|fx-glow-danger=glow(#FFA89F,#FF3A3A,0.1466) -dim|#3478F6|fx-glow-warning=glow(#FFC89D,#FF9008,0.0911) -dim|#3478F6|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0556) +dim|#3478F6|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.1189) +dim|#3478F6|fx-glow-danger=glow(#FFA89F,#FF3A3A,0.1484) +dim|#3478F6|fx-glow-warning=glow(#FFC89D,#FF9008,0.0936) +dim|#3478F6|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.0561) dim|#3478F6|fx-glow-inverted=rgba(#3C3C43,0.522) dim|#3478F6|fx-skeleton-highlight=rgba(#787880,0.039) dim|#3478F6|fill-neutral=rgba(#787880,1) diff --git a/docs/decisions/0004-finite-alpha-glow-reference.md b/docs/decisions/0004-finite-alpha-glow-reference.md index 1927b52e..758194c8 100644 --- a/docs/decisions/0004-finite-alpha-glow-reference.md +++ b/docs/decisions/0004-finite-alpha-glow-reference.md @@ -87,7 +87,9 @@ CSS-переменная alpha копирует её буквально. Пов `referenceProfile`. Совместный blur/overlap/backdrop/HDR-эффект без геометрии не сертифицируется; это отдельный `SpatialField` из #221. -Midpoint J′ и gamut-boundary chroma для core — версионированный recipe v1, а не +Midpoint J′ используется только как seed Oklab-светлоты, а gamut-boundary +chroma — как recipe core v1. Эмитированный core отдельно измеряется и не +объявляется точным midpoint после смены координат и sRGB8-квантования. Это не проверенный на наблюдателях «закон красивого свечения». ### 5. Доказательства являются исполняемыми @@ -122,7 +124,8 @@ WASM/TypeScript: - legacy `achievedDj` и `degraded` остаются точными aliases на период миграции; - изменение строк alpha и соответствующих golden ожидаемо. -Общий conformance pack повышен до `2.0.0`: alpha-family содержит half-tie +При выпуске breaking-версии по #259 общий conformance pack должен быть повышен +до `2.0.0`: alpha-family должна содержать half-tie `#C0B2FA @ 0.122` над чёрным, поэтому Rust FFI и все нативные биндинги обязаны вернуть `#17161F`, а старый нормализованный путь больше не считается conformant. From 5068f9d25716c4f3d981cb89a31037955e520464 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 10 Jul 2026 13:42:27 +0300 Subject: [PATCH 04/11] =?UTF-8?q?fix(wasm):=20=D1=8D=D0=BC=D0=B8=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D1=8F?= =?UTF-8?q?=D0=B5=D0=BC=D1=8B=D0=B9=20glow=20=D0=B1=D0=B5=D0=B7=20=D0=BE?= =?UTF-8?q?=D0=BA=D1=80=D1=83=D0=B3=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20alpha?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/labcolors-wasm/src/dto.rs | 28 +++- crates/labcolors-wasm/src/engine.rs | 51 +++++- crates/labcolors-wasm/src/lib.rs | 28 +++- crates/labcolors-wasm/src/projection.rs | 70 ++++++-- crates/labcolors-wasm/tests/wasm_parity.rs | 59 +++++++ packages/colors/README.md | 16 +- packages/colors/index.d.ts | 2 + .../colors/test/chain-invariants.test.mjs | 150 ++++++------------ .../colors/test/wasm-boundary.golden.json | 2 +- 9 files changed, 271 insertions(+), 135 deletions(-) diff --git a/crates/labcolors-wasm/src/dto.rs b/crates/labcolors-wasm/src/dto.rs index e2e2c958..d37be188 100644 --- a/crates/labcolors-wasm/src/dto.rs +++ b/crates/labcolors-wasm/src/dto.rs @@ -70,16 +70,32 @@ pub enum RoleOutcome { /// Слои свечения и решённая интенсивность (kind glow). #[derive(Debug, Clone, PartialEq)] pub struct GlowColor { - /// Слой пересвета (малый радиус), `#RRGGBB`. + /// Core-слой, предназначенный потребителем для меньшего blur, `#RRGGBB`; + /// сам движок геометрию не моделирует. pub core_hex: String, - /// Слой ореола — источник, `#RRGGBB`. + /// Halo-слой, предназначенный потребителем для большего blur, `#RRGGBB`; + /// в recipe v1 равен источнику. pub halo_hex: String, /// Интенсивность screen-слоя `(0, 1]`. pub alpha: f64, - /// Фактический |ΔJ'| композита от фона. - pub achieved_dj: f64, - /// Цель недостижима — ближайший достижимый шаг (ADR-0002, закон 2). - pub degraded: bool, + /// Каноническая CSS-запись той же alpha без повторного округления. + pub alpha_css: String, + /// Целевой |ΔJ′| изолированного halo-композита. + pub target_dj: f64, + /// Версия конечного reference-домена point-расчёта. + pub reference_profile: &'static str, + /// Слой, по которому решалась цель. + pub constraint_layer: labcolors_core::GlowConstraintLayer, + /// Типизированный результат target-проверки. + pub target_status: labcolors_core::GlowTargetStatus, + /// Reference-композит изолированного halo. + pub halo_composite_hex: String, + /// Фактический |ΔJ′| изолированного halo-композита. + pub halo_achieved_dj: f64, + /// Reference-композит изолированного core с той же alpha. + pub core_composite_hex: String, + /// Фактический |ΔJ′| изолированного core-композита. + pub core_achieved_dj: f64, } /// Двухслойный материал (kind material): тон + выведенная α + вердикт гарантии. diff --git a/crates/labcolors-wasm/src/engine.rs b/crates/labcolors-wasm/src/engine.rs index 99c5118d..85d73c30 100644 --- a/crates/labcolors-wasm/src/engine.rs +++ b/crates/labcolors-wasm/src/engine.rs @@ -279,8 +279,15 @@ fn map_resolved(resolved: Resolved, legal_floor: Option) -> RoleOutcome { core_hex: g.core_hex().to_string(), halo_hex: g.halo_hex().to_string(), alpha: g.alpha(), - achieved_dj: g.achieved_dj(), - degraded: g.degraded(), + alpha_css: g.alpha_css().to_string(), + target_dj: g.target_dj(), + reference_profile: g.reference_profile(), + constraint_layer: g.constraint_layer(), + target_status: g.target_status(), + halo_composite_hex: g.halo_composite_hex().to_string(), + halo_achieved_dj: g.halo_achieved_dj(), + core_composite_hex: g.core_composite_hex().to_string(), + core_achieved_dj: g.core_achieved_dj(), }), // Двухслойный материал (#89): тинт 01 (oklch/α) + опаковая база 02. Resolved::Material(m) => RoleOutcome::Material(crate::dto::MaterialColor { @@ -942,7 +949,20 @@ mod tests { assert_eq!(g.core_hex(), o.core_hex, "{name}: glow core"); assert_eq!(g.halo_hex(), o.halo_hex, "{name}: glow halo"); assert_eq!(g.alpha(), o.alpha, "{name}: glow alpha"); - assert_eq!(g.degraded(), o.degraded, "{name}: glow degraded"); + assert_eq!(g.alpha_css(), o.alpha_css, "{name}: glow alpha_css"); + assert_eq!(g.target_dj(), o.target_dj, "{name}: glow target_dj"); + assert_eq!(g.reference_profile(), o.reference_profile); + assert_eq!(g.constraint_layer(), o.constraint_layer); + assert_eq!(g.target_status(), o.target_status); + assert_eq!(g.halo_composite_hex(), o.halo_composite_hex); + assert_eq!(g.halo_achieved_dj(), o.halo_achieved_dj); + assert_eq!(g.core_composite_hex(), o.core_composite_hex); + assert_eq!(g.core_achieved_dj(), o.core_achieved_dj); + assert_eq!( + g.degraded(), + o.target_status == labcolors_core::GlowTargetStatus::Unreachable + ); + assert_eq!(g.achieved_dj().to_bits(), o.halo_achieved_dj.to_bits()); } (Resolved::Material(m), RoleOutcome::Material(o)) => { assert_eq!(m.tint_hex(), o.tone_hex, "{name}: material tone"); @@ -1033,4 +1053,29 @@ mod tests { let set = engine.resolve_theme("#FFFFFF", Theme::Light).unwrap(); assert!(set.roles.iter().any(|r| r.role_key == "accent-fill")); } + + /// Публичная JSON-граница обязана применять полный namespace-preflight + /// ядра: иначе алиас мог затереть числовой `-alpha` цветовой строкой уже в + /// `vars`, хотя оба отдельных имени выглядели валидными. + #[test] + fn load_config_rejects_alias_colliding_with_glow_satellite() { + let mut value: serde_json::Value = + serde_json::from_str(&labui_json()).expect("паспорт labui — валидный JSON"); + value["aliases"] + .as_array_mut() + .expect("aliases — массив") + .push(serde_json::json!({ + "alias": "fx-glow-brand-alpha", + "target": "label-primary" + })); + + let mut engine = Engine::new(); + match engine.load_config(&value.to_string()) { + Err(BindingError::InvalidConfig { reason }) => { + assert!(reason.contains("--lab-fx-glow-brand-alpha"), "{reason}"); + assert!(reason.contains("emitted CSS variables"), "{reason}"); + } + other => panic!("коллизия производного имени обязана быть отвергнута: {other:?}"), + } + } } diff --git a/crates/labcolors-wasm/src/lib.rs b/crates/labcolors-wasm/src/lib.rs index c90980c1..80cd7c34 100644 --- a/crates/labcolors-wasm/src/lib.rs +++ b/crates/labcolors-wasm/src/lib.rs @@ -135,17 +135,35 @@ export interface TranslucentRole { export interface GlowRole { readonly kind: "glow"; readonly cssVar: string; - /** Слой пересвета (малый радиус), #RRGGBB. */ + /** Core, предназначенный потребителем для меньшего blur; геометрия не моделируется. */ readonly coreHex: string; - /** Слой ореола — источник, #RRGGBB. */ + /** Halo, предназначенный потребителем для большего blur; в recipe v1 это источник. */ readonly haloHex: string; /** Интенсивность screen-слоя, (0, 1]. */ readonly alpha: number; - /** Фактический |ΔJ'| композита от фона. */ + /** Каноническая CSS-запись той же alpha; vars использует буквально её. */ + readonly alphaCss: string; + /** Конечный reference-домен point-расчёта; не сертификат renderer-а или дисплея. */ + readonly referenceProfile: "encoded-srgb8-screen-cam16ucs-jprime-v1"; + /** Цель решается только по изолированному halo. */ + readonly constraintLayer: "halo"; + /** Запрошенный |ΔJ'| halo-композита. */ + readonly targetDj: number; + /** Результат проверки цели во всём конечном reference-домене. */ + readonly targetStatus: "reached" | "unreachable"; + /** Reference-композит изолированного halo на фоне резолва. */ + readonly haloCompositeHex: string; + /** Фактический |ΔJ'| изолированного halo-композита. */ + readonly haloAchievedDj: number; + /** Reference-композит изолированного core с той же alpha. */ + readonly coreCompositeHex: string; + /** Фактический |ΔJ'| изолированного core-композита. */ + readonly coreAchievedDj: number; + /** @deprecated Alias `haloAchievedDj` для совместимости. */ readonly achievedDj: number; - /** Цель недостижима — ближайший достижимый шаг (ADR-0002, закон 2). */ + /** @deprecated Используйте targetStatus === "unreachable". */ readonly degraded: boolean; - /** Ready-to-serve CSS value халo: "oklch(L% C H)". */ + /** CSS-значение halo: `oklch(L% C H)`. */ readonly css: string; } diff --git a/crates/labcolors-wasm/src/projection.rs b/crates/labcolors-wasm/src/projection.rs index df14f38f..c115ad48 100644 --- a/crates/labcolors-wasm/src/projection.rs +++ b/crates/labcolors-wasm/src/projection.rs @@ -85,18 +85,42 @@ pub fn resolved_json(resolved: &ResolvedTheme) -> Result { field_str(&mut roles, "coreHex", &g.core_hex); field_str(&mut roles, "haloHex", &g.halo_hex); field_num(&mut roles, "alpha", g.alpha)?; - field_num(&mut roles, "achievedDj", g.achieved_dj)?; - field_bool(&mut roles, "degraded", g.degraded); + let canonical_alpha = + labcolors_core::css_alpha_value(g.alpha).map_err(|reason| { + BindingError::Internal { + reason: format!("glow alpha не сериализуется: {reason}"), + } + })?; + if canonical_alpha != g.alpha_css { + return Err(BindingError::Internal { + reason: format!( + "glow alphaCss рассинхронизирован: ожидался {canonical_alpha}, получен {}", + g.alpha_css + ), + }); + } + field_str(&mut roles, "alphaCss", &g.alpha_css); + field_str(&mut roles, "referenceProfile", g.reference_profile); + field_str(&mut roles, "constraintLayer", g.constraint_layer.key()); + field_num(&mut roles, "targetDj", g.target_dj)?; + field_str(&mut roles, "targetStatus", g.target_status.key()); + field_str(&mut roles, "haloCompositeHex", &g.halo_composite_hex); + field_num(&mut roles, "haloAchievedDj", g.halo_achieved_dj)?; + field_str(&mut roles, "coreCompositeHex", &g.core_composite_hex); + field_num(&mut roles, "coreAchievedDj", g.core_achieved_dj)?; + // Aliases совместимости старого неоднозначного контракта. + field_num(&mut roles, "achievedDj", g.halo_achieved_dj)?; + field_bool( + &mut roles, + "degraded", + g.target_status == labcolors_core::GlowTargetStatus::Unreachable, + ); let halo_css = oklch_css(&g.halo_hex, None)?; let core_css = oklch_css(&g.core_hex, None)?; field_str(&mut roles, "css", &halo_css); push_var(&mut vars, &css_var, &halo_css); push_var(&mut vars, &format!("{css_var}-core"), &core_css); - push_var( - &mut vars, - &format!("{css_var}-alpha"), - &format!("{:.4}", g.alpha), - ); + push_var(&mut vars, &format!("{css_var}-alpha"), &g.alpha_css); } RoleOutcome::Material(m) => { // Материал (#89): тинт 01 (oklch/α) над опаковой базой 02 (oklch). @@ -280,11 +304,18 @@ mod tests { RoleEntry { role_key: "pulse".to_string(), outcome: RoleOutcome::Glow(GlowColor { - core_hex: "#FFF1CC".to_string(), - halo_hex: "#FFB300".to_string(), - alpha: 0.5, - achieved_dj: 12.0625, - degraded: false, + core_hex: "#A0C5FF".to_string(), + halo_hex: "#4A8FFF".to_string(), + alpha: 0.036_045_459_685_627_89, + alpha_css: "0.03604545968562789".to_string(), + target_dj: 2.3006, + reference_profile: labcolors_core::GLOW_REFERENCE_PROFILE, + constraint_layer: labcolors_core::GlowConstraintLayer::Halo, + target_status: labcolors_core::GlowTargetStatus::Reached, + halo_composite_hex: "#13151B".to_string(), + halo_achieved_dj: 2.373_123_785_729_128, + core_composite_hex: "#15171B".to_string(), + core_achieved_dj: 3.235_504_076_619_437, }), }, RoleEntry { @@ -305,8 +336,8 @@ mod tests { let json = resolved_json(&fixture()).unwrap(); let css_label = labcolors_core::oklch_css_from_hex("#D5D5D7", None).unwrap(); let css_veil = labcolors_core::oklch_css_from_hex("#89CFF0", Some(0.35)).unwrap(); - let css_halo = labcolors_core::oklch_css_from_hex("#FFB300", None).unwrap(); - let css_core = labcolors_core::oklch_css_from_hex("#FFF1CC", None).unwrap(); + let css_halo = labcolors_core::oklch_css_from_hex("#4A8FFF", None).unwrap(); + let css_core = labcolors_core::oklch_css_from_hex("#A0C5FF", None).unwrap(); let expected = format!( concat!( "{{\"theme\":\"dark\",\"background\":\"#3A3A3C\",\"vars\":{{", @@ -314,7 +345,7 @@ mod tests { "\"--lab-veil\":\"{veil}\",", "\"--lab-pulse\":\"{halo}\",", "\"--lab-pulse-core\":\"{core}\",", - "\"--lab-pulse-alpha\":\"0.5000\"", + "\"--lab-pulse-alpha\":\"0.03604545968562789\"", "}},\"roles\":{{", "\"label-primary\":{{\"cssVar\":\"--lab-label-primary\",\"kind\":\"color\",", "\"hex\":\"#D5D5D7\",\"lc\":62.375,\"wcagRatio\":7.25,\"compressed\":false,", @@ -326,8 +357,13 @@ mod tests { "\"compositeLc\":-41.5,\"compositeWcag\":3.125,\"alphaCoerced\":true,", "\"floorCoerced\":false,\"css\":\"{veil}\"}},", "\"pulse\":{{\"cssVar\":\"--lab-pulse\",\"kind\":\"glow\",", - "\"coreHex\":\"#FFF1CC\",\"haloHex\":\"#FFB300\",\"alpha\":0.5,", - "\"achievedDj\":12.0625,\"degraded\":false,\"css\":\"{halo}\"}},", + "\"coreHex\":\"#A0C5FF\",\"haloHex\":\"#4A8FFF\",", + "\"alpha\":0.03604545968562789,\"alphaCss\":\"0.03604545968562789\",", + "\"referenceProfile\":\"encoded-srgb8-screen-cam16ucs-jprime-v1\",", + "\"constraintLayer\":\"halo\",\"targetDj\":2.3006,\"targetStatus\":\"reached\",", + "\"haloCompositeHex\":\"#13151B\",\"haloAchievedDj\":2.373123785729128,", + "\"coreCompositeHex\":\"#15171B\",\"coreAchievedDj\":3.235504076619437,", + "\"achievedDj\":2.373123785729128,\"degraded\":false,\"css\":\"{halo}\"}},", "\"impossible\":{{\"cssVar\":\"--lab-impossible\",\"kind\":\"unreachable\",", "\"code\":\"gamut_exhausted\",", "\"message\":\"нет цвета: \\\"предел\\\"\\nвторая строка\"}}", diff --git a/crates/labcolors-wasm/tests/wasm_parity.rs b/crates/labcolors-wasm/tests/wasm_parity.rs index 55b07a5e..87cc3a04 100644 --- a/crates/labcolors-wasm/tests/wasm_parity.rs +++ b/crates/labcolors-wasm/tests/wasm_parity.rs @@ -78,6 +78,18 @@ fn get_obj(obj: &JsValue, key: &str) -> JsValue { js_sys::Reflect::get(obj, &JsValue::from_str(key)).expect("property present") } +fn get_num(obj: &JsValue, key: &str) -> f64 { + get_obj(obj, key) + .as_f64() + .unwrap_or_else(|| panic!("{key} must be a number")) +} + +fn get_bool(obj: &JsValue, key: &str) -> bool { + get_obj(obj, key) + .as_bool() + .unwrap_or_else(|| panic!("{key} must be a boolean")) +} + /// Read the `message` of a rejected `JsError`. A `JsError` crosses as a JS /// `Error` object, so the human text (carrying our stable code) is its /// `.message` property, not the value's own string form. @@ -161,6 +173,53 @@ fn assert_parity(passport: &str, bg_hex: &str, theme: &str) { Some(g.core_hex()), "{name} glow core must match native" ); + assert_eq!( + get_str(&entry, "haloHex").as_deref(), + Some(g.halo_hex()), + "{name} glow halo must match native" + ); + assert_eq!(get_num(&entry, "alpha").to_bits(), g.alpha().to_bits()); + assert_eq!(get_str(&entry, "alphaCss").as_deref(), Some(g.alpha_css())); + assert_eq!( + get_str(&entry, "referenceProfile").as_deref(), + Some(g.reference_profile()) + ); + assert_eq!( + get_str(&entry, "constraintLayer").as_deref(), + Some(g.constraint_layer().key()) + ); + assert_eq!( + get_num(&entry, "targetDj").to_bits(), + g.target_dj().to_bits() + ); + assert_eq!( + get_str(&entry, "targetStatus").as_deref(), + Some(g.target_status().key()) + ); + assert_eq!( + get_str(&entry, "haloCompositeHex").as_deref(), + Some(g.halo_composite_hex()) + ); + assert_eq!( + get_num(&entry, "haloAchievedDj").to_bits(), + g.halo_achieved_dj().to_bits() + ); + assert_eq!( + get_str(&entry, "coreCompositeHex").as_deref(), + Some(g.core_composite_hex()) + ); + assert_eq!( + get_num(&entry, "coreAchievedDj").to_bits(), + g.core_achieved_dj().to_bits() + ); + assert_eq!( + get_num(&entry, "achievedDj").to_bits(), + g.halo_achieved_dj().to_bits() + ); + assert_eq!( + get_bool(&entry, "degraded"), + g.target_status() == labcolors_core::GlowTargetStatus::Unreachable + ); } // `Resolved` is `#[non_exhaustive]`: a future core variant must be // re-accounted here loudly, never masked. diff --git a/packages/colors/README.md b/packages/colors/README.md index 34868659..20592819 100644 --- a/packages/colors/README.md +++ b/packages/colors/README.md @@ -137,13 +137,21 @@ interface ResolvedTheme { roles: Record; // все роли с деталями } -type RoleResult = SolvedColor | TranslucentRole | NoneRole | UnreachableRole; +type RoleResult = + | SolvedColor + | TranslucentRole + | GlowRole + | MaterialRole + | NoneRole + | UnreachableRole; ``` -Каждая роль — одно из четырёх состояний: +Каждая роль — одно из шести состояний: - `SolvedColor` — цвет найден (`kind: "color"`, поля `css` — готовое `oklch(L% C H)`, `hex` — тот же цвет как данные, `lc`, `wcagRatio`, …). -- `TranslucentRole` — полупрозрачная роль лестницы или альфа-аналога (`kind: "translucent"`): `css` — готовое `oklch(L% C H / A)`, `tintHex` — тинт как данные, `alpha`, плюс `compositeHex` / `compositeLc` / `compositeWcag` — солид-композит на фоне резолва и его контраст (браузер композитит тинт на живой подложке). +- `TranslucentRole` — полупрозрачная роль лестницы или альфа-аналога (`kind: "translucent"`): `css` — готовое `oklch(L% C H / A)`, `tintHex` — тинт как данные, `alpha`, плюс `compositeHex` / `compositeLc` / `compositeWcag` — exact encoded-sRGB8 reference-композит на фоне резолва и его контраст. Конкретный renderer и color-management pipeline проверяются отдельно. +- `GlowRole` — два цвета для `mix-blend-mode: screen`: `cssVar` несёт halo, `${cssVar}-core` — core, `${cssVar}-alpha` — каноническую `alphaCss`. Цель относится только к изолированному halo; `targetStatus`, оба `*CompositeHex` и оба `*AchievedDj` не выдают point-расчёт за полный blur/overlap-эффект. `referenceProfile` фиксирует конечный расчётный домен, а не гарантирует любой браузер или дисплей. +- `MaterialRole` — тон двухслойного материала и выведенная alpha: primary — солид-канон, `-01` — тинт, `-02` — опаковая база; поля результата явно сообщают гарантию и её границы. - `NoneRole` — роль намеренно пустая по дизайну (`kind: "none"`), не ошибка. - `UnreachableRole` — ни один цвет не удовлетворяет требованиям для этого фона (`kind: "unreachable"`). @@ -159,7 +167,7 @@ type RoleResult = SolvedColor | TranslucentRole | NoneRole | UnreachableRole; ### `engine.loadConfig(json): string` -Загружает конфиг дизайн-системы (JSON по типу `ThemeConfig`; схема — в репозитории: `docs/decisions/0001-config-boundary.md`, TS-типы — в поставляемом `labcolors.d.ts`). Это **единственный** источник словаря ролей — встроенной таблицы в движке нет: до загрузки конфига `resolveTheme` отклоняется ошибкой `config_required`. Полный preflight: невалидный конфиг отклоняется структурной ошибкой `invalid_config: …` и НЕ меняет состояние. Возвращает отпечаток конфига — 16 hex-символов; разные конфиги дают разные отпечатки и разные кэш-пространства. +Загружает конфиг дизайн-системы (JSON по типу `ThemeConfig`; схема — в репозитории: `docs/decisions/0001-config-boundary.md`, TS-типы — в поставляемом `labcolors.d.ts`). Это **единственный** источник словаря ролей — встроенной таблицы в движке нет: до загрузки конфига `resolveTheme` отклоняется ошибкой `config_required`. Полный preflight проверяет не только имена ролей, но и итоговый CSS-namespace: `-core`/`-alpha` Glow и `-01`/`-02` Material не могут быть затёрты другой ролью или алиасом. Невалидный конфиг отклоняется структурной ошибкой `invalid_config: …` и НЕ меняет состояние. Возвращает отпечаток конфига — 16 hex-символов; разные конфиги дают разные отпечатки и разные кэш-пространства. --- diff --git a/packages/colors/index.d.ts b/packages/colors/index.d.ts index 920a18b5..a9ca98b0 100644 --- a/packages/colors/index.d.ts +++ b/packages/colors/index.d.ts @@ -15,6 +15,8 @@ export type { ThemeName, SolvedColor, TranslucentRole, + GlowRole, + MaterialRole, NoneRole, UnreachableRole, RoleResult, diff --git a/packages/colors/test/chain-invariants.test.mjs b/packages/colors/test/chain-invariants.test.mjs index 03be2dcb..74a3cd68 100644 --- a/packages/colors/test/chain-invariants.test.mjs +++ b/packages/colors/test/chain-invariants.test.mjs @@ -34,13 +34,6 @@ initSync({ module: new WebAssembly.Module(readFileSync(new URL("../pkg/labcolors_bg.wasm", import.meta.url))), }); -/// Максимальная поканальная дельта двух `#RRGGBB` в LSB (8-бит ступенях). -function channelDelta(hexA, hexB) { - const a = parseCssColor(hexA); - const b = parseCssColor(hexB); - return Math.max(Math.abs(a[0] - b[0]), Math.abs(a[1] - b[1]), Math.abs(a[2] - b[2])); -} - // Замороженный SSOT-паспорт labui — тот же, что читает wasm-parity. Единый вход // и для движка, и для декларации алиасов ниже, чтобы стороны не разошлись. const PASSPORT_PATH = new URL( @@ -123,39 +116,13 @@ test("legality survives serialization: each solid role's emitted var reparses to }); // ───────────────────────────────────────────────────────────────────────────── -// ЛЕГАЛЬНОСТЬ НАСКВОЗЬ — полупрозрачные роли (композит, как его соберёт браузер) +// ЛЕГАЛЬНОСТЬ НАСКВОЗЬ — полупрозрачные роли (encoded-sRGB8 reference) // ───────────────────────────────────────────────────────────────────────────── -// НАХОДКА (репро ниже, зафиксирована в PR; причина заземлена на исходники -// labcolors-core): для 3 из 1699 полупрозрачных сэмплов корпуса (все — тинт -// #C0B2FA при низкой α над чистым чёрным в dark-ic) композит, СОБРАННЫЙ -// браузером из эмитированной строки, расходится с движковым `compositeHex` -// ровно на 1 LSB (#17161F vs #17161E и т.п.). -// ПРИЧИНА — РАЗНОЕ ПРОСТРАНСТВО АРИФМЕТИКИ, не потеря точности тинта: тинт -// КВАНТУЕТСЯ в 8 бит В ОБОИХ путях (semantic.rs:1977 `quantise_encoded` ДО -// `composite_over_encoded`), α одна. Но движок композитит в нормализованном -// encoded-[0,1] (`α·(byte/255)`), затем hex_from_srgb_encoded делает `·255·round` -// (srgb.rs:196); браузер (`compositeOver`) — в 0–255 (`α·byte`). На α·byte ровно -// = 30.5 (тинт-канал 250, α=0.122): движок `0.122·(250/255)·255 = 30.4999… → -// round → 30` (#…1E), браузер `250·0.122 = 30.5 → round → 31` (#…1F). Округление -// точной половины расходится, потому что ÷255·255 стягивает 30.5 к 30.4999. -// Следствие: обещанный `compositeHex`/Lc/WCAG считаны в другом пространстве, -// чем то, что реально отрендерит браузер (суб-JND, но реальный выходной gap). -// Живёт в эмиссии labcolors-core — не чиним здесь (зона солвера), фиксируем -// границей. Возможный фикс ядра: композитить в том же (байтовом) пространстве, -// что и браузер, — тогда обещанное == отрендеренное. Инвариант ниже строгий на -// том, что ЭМИССИЯ СТРОКИ (тинт/α) ничего не теряет побайтно, и держит композит -// в истинной границе ≤1 LSB — дрейф ≥2 LSB или потеря тинта/α упадёт RED. -const COMPOSITE_QUANT_LSB = 1; -// Известная верхняя граница числа расхождений на текущем корпусе (THEMES× -// BACKGROUNDS×паспорт). Пин: разрастание gap (в другие темы/фоны/роли) поднимет -// число выше и упадёт RED — характеризация защищает и КОЛИЧЕСТВО, и ЛОКАЦИЮ. -// Волна 1 (закон категориальных зон) сменила info-сентимент с пурпура на синий -// фокус — прежние 3 near-black dark-ic расхождения исчезли, осталась РОВНО 1 -// benign ≤1-LSB точка (см. локацию ниже). Число снижено 3 → 1. -const KNOWN_COMPOSITE_DIVERGENCES = 1; - -test("translucent serialization fidelity: emitted tint+alpha round-trip exactly; browser composite matches the promise within the ≤1-LSB quantization bound", () => { +// Численный контракт здесь — encoded-sRGB8 source-over reference, реализованный +// штатным consumer-кодом. Это проверка единства engine↔package, а не заявление, +// что любой renderer без явно заданного color-management профиля совпадёт с ним. +test("translucent serialization fidelity: emitted tint+alpha, reference composite and reported metrics agree exactly", () => { const e = engine(); let translucentChecked = 0; @@ -185,18 +152,20 @@ test("translucent serialization fidelity: emitted tint+alpha round-trip exactly; role.tintHex, `${theme}/${bg}/${key}: reparsed tint != reported tintHex (serialization loss)`, ); - // HARD: α реконструируется в пределах точности эмиссии (4 знака). + // Вычисленная α — часть сертификата композита: строка обязана вернуть + // тот же binary64, а не близкое округлённое значение. assert.ok( - Math.abs(alpha - role.alpha) < 5e-5, - `${theme}/${bg}/${key}: reparsed alpha ${alpha} drifted from ${role.alpha}`, + Object.is(alpha, role.alpha), + `${theme}/${bg}/${key}: reparsed alpha ${alpha} != reported ${role.alpha}`, ); - // Композит, собранный браузером из ЭМИТИРОВАННОГО тинта, — в истинной - // границе ≤1 LSB от обещанного (см. находку выше). Дрейф ≥2 = регрессия. + // Reference-композит из эмитированных значений обязан совпасть с + // сертификатом побайтно: допуск скрыл бы другой цвет и другие метрики. const compHex = toHex(compositeOver(parsed, [bgParsed[0], bgParsed[1], bgParsed[2], 1])); - assert.ok( - channelDelta(compHex, role.compositeHex) <= COMPOSITE_QUANT_LSB, - `${theme}/${bg}/${key}: composite ${compHex} drifts >${COMPOSITE_QUANT_LSB} LSB from promised ${role.compositeHex}`, + assert.equal( + compHex, + role.compositeHex, + `${theme}/${bg}/${key}: reference composite ${compHex} != promised ${role.compositeHex}`, ); // Самосогласованность движка: перепроверка ЕГО ЖЕ compositeHex @@ -216,58 +185,6 @@ test("translucent serialization fidelity: emitted tint+alpha round-trip exactly; assert.ok(translucentChecked > 0, "no translucent roles exercised — sweep is vacuous"); }); -// Характеризация НАХОДКИ: пин ТЕКУЩЕГО поведения по трём осям — ВЕЛИЧИНА (≤1 LSB), -// КОЛИЧЕСТВО (≤ известного) и ЛОКАЦИЯ (единственная info-заливка light-ic/#FFFFFF; -// Волна 1 сместила её с near-black dark-ic). Тест НЕ узаконивает баг: -// • починят движок (композит в байтовом пространстве) → расхождения исчезнут, -// `divergent.length > 0` упадёт → форс-ревью; -// • разрастётся gap (другие темы/фоны/роли или >1 LSB) → величина/количество/ -// локация превысят пин → RED. -// Без пина количества и локации регрессия, размазавшая тот же ≤1-LSB зазор на -// сотню сэмплов или в light-темы, прошла бы зелёной — находка тихо сгнила бы. -test("characterization: the composite quantization gap is real, bounded to ≤1 LSB, count-capped, and confined to the light-ic info fill (pins current behaviour)", () => { - const e = engine(); - let maxObservedDelta = 0; - const divergent = []; - for (const theme of THEMES) { - for (const bg of BACKGROUNDS) { - const res = e.resolveTheme(bg, theme); - const bgP = parseCssColor(bg); - for (const [key, role] of Object.entries(res.roles)) { - if (role.kind !== "translucent") continue; - const parsed = parseCssColor(res.vars[role.cssVar]); - const compHex = toHex(compositeOver(parsed, [bgP[0], bgP[1], bgP[2], 1])); - const d = channelDelta(compHex, role.compositeHex); - maxObservedDelta = Math.max(maxObservedDelta, d); - if (d > 0) divergent.push(`${theme}/${bg}/${key}`); - } - } - } - // ВЕЛИЧИНА: никогда больше 1 LSB. - assert.ok(maxObservedDelta <= 1, `composite quantization gap exceeded 1 LSB: ${maxObservedDelta}`); - // СУЩЕСТВОВАНИЕ: расхождение реально сегодня (иначе движок, вероятно, починили). - assert.ok( - divergent.length > 0, - "composite gap no longer reproduces — engine may compose in byte space now; revisit the PR finding", - ); - // КОЛИЧЕСТВО: не больше известной верхней границы — разрастание gap → RED. - assert.ok( - divergent.length <= KNOWN_COMPOSITE_DIVERGENCES, - `composite gap spread: ${divergent.length} divergences > known ${KNOWN_COMPOSITE_DIVERGENCES} — ${divergent.join(", ")}`, - ); - // ЛОКАЦИЯ: единственное расхождение — низкоальфовая info-заливка на белом в - // light-ic (`fill-info-quaternary`, α≈0.02). Волна 1 (закон категориальных зон) - // сменила info-сентимент с пурпура на синий фокус — та же природа зазора - // (8-бит квантование браузерного композита), новая точка от нового цвета. - // Утечка в другую роль/тему/фон = смена природы находки → форс-ревью. - for (const loc of divergent) { - assert.ok( - loc === "light-ic/#FFFFFF/fill-info-quaternary", - `composite gap escaped the known info-fill point: ${loc}`, - ); - } -}); - // ───────────────────────────────────────────────────────────────────────────── // GLOW — сателлиты эмиссии // ───────────────────────────────────────────────────────────────────────────── @@ -290,18 +207,53 @@ test("glow roles emit halo primary + -core/-alpha satellites, all well-formed", `${theme}/${bg}/${key}: primary var must mirror halo css`, ); assert.ok(parseCssColor(res.vars[role.cssVar]), `${theme}/${bg}/${key}: halo must parse`); + assert.equal( + toHex(rgb(parseCssColor(res.vars[role.cssVar]))), + role.haloHex, + `${theme}/${bg}/${key}: halo css must reconstruct haloHex`, + ); // Сателлит -core: валидная oklch-строка. const core = res.vars[`${role.cssVar}-core`]; assert.ok(core && parseCssColor(core), `${theme}/${bg}/${key}: -core must be valid oklch`); + assert.equal( + toHex(rgb(parseCssColor(core))), + role.coreHex, + `${theme}/${bg}/${key}: core css must reconstruct coreHex`, + ); - // Сателлит -alpha: конечное число в (0,1]. + // Сателлит -alpha — буквальный SSOT, не повторное округление числа. const alphaVar = res.vars[`${role.cssVar}-alpha`]; const a = Number(alphaVar); assert.ok( Number.isFinite(a) && a > 0 && a <= 1, `${theme}/${bg}/${key}: -alpha must be a number in (0,1]: ${alphaVar}`, ); + assert.equal(alphaVar, role.alphaCss, `${theme}/${bg}/${key}: alpha var != alphaCss`); + assert.ok(Object.is(a, role.alpha), `${theme}/${bg}/${key}: alpha lost binary64 bits`); + + assert.equal(role.constraintLayer, "halo"); + assert.equal(role.referenceProfile, "encoded-srgb8-screen-cam16ucs-jprime-v1"); + assert.ok(["reached", "unreachable"].includes(role.targetStatus)); + assert.equal(role.degraded, role.targetStatus === "unreachable"); + assert.ok(Object.is(role.achievedDj, role.haloAchievedDj)); + if (role.targetStatus === "reached") { + assert.ok(role.haloAchievedDj >= role.targetDj); + } else { + assert.ok(role.haloAchievedDj < role.targetDj); + } + + // Оба поля — point-reference замеры ИЗОЛИРОВАННЫХ слоёв. Полный + // spatial stack без геометрии здесь намеренно не реконструируется. + const [br, bgc, bb] = parseCssColor(bg); + const screenHex = (layerCss) => { + const [lr, lg, lb] = parseCssColor(layerCss); + const channel = (background, layer) => + (background / 255 + a * (layer / 255) * (1 - background / 255)) * 255; + return toHex([channel(br, lr), channel(bgc, lg), channel(bb, lb)]); + }; + assert.equal(screenHex(res.vars[role.cssVar]), role.haloCompositeHex); + assert.equal(screenHex(core), role.coreCompositeHex); } } } diff --git a/packages/colors/test/wasm-boundary.golden.json b/packages/colors/test/wasm-boundary.golden.json index 4bdcef4c..2a473d16 100644 --- a/packages/colors/test/wasm-boundary.golden.json +++ b/packages/colors/test/wasm-boundary.golden.json @@ -1 +1 @@ -{"_meta":{"purpose":"Pre-optimisation byte-identity lock for the JS↔WASM recheck boundary (recheckContrast / _recheckContrastMulti) plus a resolveTheme vars snapshot.","regenerated":"2026-07-08","regen_reason":"Глава #64 (ADR-0003): ось читаемости активирована в домене Ys — реактивный recheck (measure_contrast/recheck_against) и solver-вывод считаются в Ys. recheck flat и resolveVars регенерированы против пересобранного движка; inputs (theme/bg/fgs) сохранены. semver-major по ADR.","drift_scope":"resolveVars: exactly 24 leaves changed, all --lab-fx-glow-{brand,danger,warning,neutral}-core (glow-centre colour, background-independent → 4 distinct values). No other var moved. glow base/alpha, labels, borders, fills, backgrounds all byte-identical.","recheck_unchanged":"recheck section: 2752/2752 contrast pairs (lc, wcag) byte-identical. No recheckContrast / contrast value changed. The WCAG relative-luminance + contrast_core + ratio_from_luminances chain is untouched by #185/#186.","numeric_provenance":{"glow-brand-core":"oklch(78.84894% 0.062240 265.472) -> oklch(78.76611% 0.108476 257.256): chroma rises to the blue gamut wall at the functional lightness (fixed-fraction under-saturated it); L ±0.08pp, hue shifts to the cusp.","glow-danger-core":"oklch(81.62208% 0.103545 27.533) -> oklch(81.77103% 0.102764 29.025): near-identical (red already sat near its wall).","glow-warning-core":"oklch(88.80612% 0.081204 68.866) -> oklch(88.88181% 0.082838 68.777): near-identical.","glow-neutral-core":"oklch(99.97226% 0.001314 106.423) -> oklch(100.00000% 0.000000 89.876): neutral hue is achromatic → gamut wall chroma is 0 (was fixed-fraction residue noise); hue_vanished flag fires honestly for the achromatic sentiment."},"wcag_floors_not_weakened":"Glow-centre is a decorative FX bloom colour, NOT a text/border/focus-ring role — no WCAG contrast floor or focus-appearance (WCAG 2.4.13) floor applies to it. Every actual contrast value (recheck section, 2752 pairs) is byte-identical, so no measured contrast dropped below any floor. North invariant #2 (accessibility-by-construction) is preserved: measured, not merely asserted.","isolated_verification_required":"Regen accepted only after an isolated adversarial verifier (not seeing the self-claim) confirms RED→GREEN and that no WCAG floor weakened.","drift_scope_wave1":"resolveVars: 69 info-* leaves changed across 6 theme×bg entries. Zero non-info drift (scope-checked). recheck untouched."},"recheck":[{"theme":"light","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#00030E","#0057BB","#037BFF","#78AFFF","#150000","#BE0005","#FF070E","#FF9A8C","#1B0D00","#A06300","#D28300","#FFAA3A","#000B01","#007C2D","#00A73F","#00D452","#020013","#4D00F9","#6D6EFF","#9FA9FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.59876563358553,20.60103636973117,79.91552226990729,6.816302360996974,63.95198968935827,3.97647793086433,41.89234722606997,2.2400949113548063,105.32470288110206,20.35108757719671,78.97384099389187,6.579727828852003,63.90360859563009,3.970705427078051,37.83221395673913,2.048194921340976,103.87315757816087,19.018778265876662,70.62528347734393,4.90716956443272,53.97808064225952,3.0056474129326713,34.232350404909745,1.8982213760304238,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.76444562516849,20.751302844974063,83.48658341134008,7.825697912025839,63.67798680778349,3.9439477616440737,40.86459333479343,2.1890112231357013]},{"theme":"light","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E6F0FF","#60A1FF","#0074F3","#004CA4","#FFF4F2","#FF8273","#F40009","#A80004","#FFF7EE","#F79C00","#C97E00","#8F5800","#C2FFC9","#00C84D","#00A03C","#006E26","#EEF0FF","#9099FF","#6662FF","#4300DB"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-97.97941453819666,18.27263411531394,-53.55180732861014,8.039306941172763,-34.54692380627581,4.792369408161928,-17.418806999279397,2.56984376276454,-102.41236835704379,19.474434420712562,-56.92703132210305,8.692613650917824,-34.93266406694178,4.850563950189369,-18.308962222075785,2.6667193472488013,-103.5613239139708,19.790915044333452,-61.97487030298502,9.71021814570805,-44.901407836051874,6.467843223430967,-25.783665577412222,3.5638173443979557,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.707239087904505,6.093571182387199,-23.370874926227668,3.2583602660985775,-98.9883689300502,18.54346384276384,-54.32153113615954,8.18635152260471,-34.304902287955215,4.756029101219059,-14.435475219756622,2.261552965854495]},{"theme":"light","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00163A","#003272","#004494","#040000","#3A0000","#760002","#990003","#0B0400","#261300","#512F00","#845100","#000200","#001D05","#004114","#006322","#000002","#160059","#2B0097","#3B00C5"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.89312592554248,4.521969910173767,28.754774555141132,3.1292028976206594,22.090354756391577,2.35851620111828,40.23106479600237,5.289901483963394,36.823385380889306,4.5064610721321205,27.786796188195932,3.002060282138028,21.02583740042693,2.2570951143110336,39.64148780057383,5.154352713354357,36.89828467875601,4.523118840482662,27.981450040156208,3.027209986332121,13.567318350507929,1.6836851754208233,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.3306726916817,5.312553092992316,36.84988795298528,4.512349526507044,30.916517468784825,3.4313626667845316,25.033605894158644,2.6690076358452903]},{"theme":"light","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#E7F1FF","#73ADFF","#2985FF","#0060CB","#FFF5F4","#FF9687","#FF4336","#CD0006","#FFF8F0","#FFA62C","#DC8A00","#AC6B00","#C7FFCC","#00D251","#00B043","#008631","#EFF1FF","#9CA6FF","#767AFF","#532BFF"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.32090210121694,9.956824779623638,-48.0046310941281,4.945145596338778,-30.285195552724065,3.1849116126306156,-14.342034884792934,1.9106934427040745,-91.71639880150975,10.602849175762582,-51.97724928251108,5.385053682831725,-31.570923815646545,3.301015045785777,-14.82740446463819,1.94471301206827,-92.86717481243348,10.774663089384916,-55.64998948221256,5.805759082702303,-40.41406674606042,4.149700467279057,-23.7537806706175,2.6249801387527425,-87.99158651504085,10.054343074329001,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-88.33220875070053,10.104016013712561,-48.80525652513833,5.032519885671211,-30.915189659062865,3.2415642790980215,-11.323318014385267,1.7063261265170042]},{"theme":"light","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000103","#001536","#003374","#0058BC","#070000","#360000","#730002","#BE0005","#0F0500","#241200","#4F2F00","#A16400","#000400","#001B05","#003F13","#007C2D","#000005","#140055","#3300AC","#4E00FA"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.65687576920386,5.198408498908002,36.59714090951085,4.504660010775197,27.83078380746869,3.029349552004155,13.31049770996219,1.6744770124926043,39.57933742278427,5.181007308853048,36.66186584356392,4.51899176883356,27.77510172821011,3.0221297438766705,12.721590912296357,1.6379804580418844,38.8564182220264,5.016999835671664,36.579572970042136,4.500775701626332,27.65113132417139,3.006116231224861,0,1.205042331050678,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.737197591996,5.216381929144518,36.59037871155487,4.5031645842195545,27.68585404738298,3.010592990868115,17.038043883395044,1.933101474636693]},{"theme":"light","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000104","#00204D","#003B83","#005CC4","#080000","#4C0001","#830002","#C70006","#0F0600","#331C00","#5B3600","#A76800","#000400","#002809","#004917","#00822F","#000005","#1F0075","#3A00C3","#511AFF"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.87966672163739,5.884401548408216,38.30767260595269,4.50632138120243,29.16726657041677,3.0308664461202253,15.823096179531001,1.7789458759332724,43.782795788734404,5.8597651770218455,38.402450836988166,4.527103297945568,29.016037258963053,3.011424325654164,14.9864556348382,1.7254955647131072,42.99821792375369,5.657954629703501,38.41622407485322,4.5301350068404425,28.93769973409828,3.0014083479801514,0,1.28054929135364,43.62397616937584,5.819206103341516,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.970091731453365,5.907320242919889,38.28628402517786,4.501651045763603,28.99214635836292,3.0083657301901288,19.11088367642911,2.0125842767555064]},{"theme":"light","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E6F0FF","#64A4FF","#0077F9","#0050AC","#FFF4F3","#FF8777","#F9000A","#B00005","#FFF7EF","#FA9E00","#CD8000","#955C00","#C3FFC9","#00CA4E","#00A33D","#007329","#EEF0FF","#939CFF","#6967FF","#4600E6"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.3221346483494,16.53807546575917,-53.23458352949761,7.5085332653696355,-34.299151794570456,4.531377660721796,-17.34040000814869,2.4827755446650244,-100.79897840003042,17.636700891614694,-56.73966900010428,8.131153426700068,-34.4661942600266,4.554628420782135,-18.26813498707333,2.5778416349364046,-101.94670279716274,17.922901715066683,-61.50032492399525,9.010497025774457,-44.48245067627942,6.049054371037485,-25.983086445749187,3.447217107352778,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-97.33108904020293,16.783196253636362,-53.9780650339559,7.638815670143083,-34.15649094558268,4.511565332868746,-14.178333309149869,2.174938899863266]},{"theme":"light","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#00030D","#0057B9","#0079FD","#74ADFF","#150000","#BC0005","#FD000A","#FF9788","#1B0D00","#9F6200","#CB7F00","#FFA730","#000B01","#007B2C","#00A63E","#00D251","#020013","#4C00F7","#6C6CFF","#9DA6FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.48497819102282,19.361251289328656,75.9364891028185,6.438321694919453,60.636932359482174,3.8278153397206016,38.76699274290234,2.152541489746932,101.19913515757631,19.11640864166299,75.28055256994783,6.281220843840182,60.46094873358925,3.807428346232839,34.70184973776556,1.9658801332164664,99.74758985463511,17.864929125608796,66.9127104243937,4.673002507305443,52.13428535623209,3.0001216660302674,31.16352525448906,1.8227359456035261,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.421867335088024,3.0235758695027473,33.32486172802245,1.9082022515525083,101.63887790164272,19.492343272893066,79.67393134344084,7.442688338927282,60.17031912503092,3.7741072518393395,37.967753096619255,2.113772626279229]},{"theme":"light","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E6F1FF","#67A5FF","#0079FD","#0053B2","#FFF5F3","#FF8A7A","#FD000A","#B60005","#FFF7EF","#FC9F00","#D08200","#9A5F00","#C4FFCA","#00CB4E","#00A53E","#00772A","#EEF1FF","#949EFF","#6B6BFF","#4900EE"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-94.97224440690107,14.900628115594163,-51.99577035710509,6.808135431331698,-33.457001357615106,4.175350120738554,-16.755883343489252,2.3335896778079332,-99.44736446714371,15.886233758087014,-55.869159717403925,7.428487313030341,-33.63398343987836,4.1977071628099685,-17.709012021143003,2.4235530906296177,-100.15616825242344,16.04465607216319,-60.39258141645385,8.181935837052826,-43.77686148964125,5.57032618168592,-25.67861999448382,3.2497240951569792,-95.39392806862337,14.992417327878547,-58.246405974587134,7.820609198173194,-41.3596549106396,5.227261702394524,-23.16505433719768,2.9752806858720673,-95.97861011098902,15.120061263203354,-52.9765437482588,6.963016139185695,-33.57839540296167,4.190679013723035,-13.485138200601865,2.040069034387161]},{"theme":"light","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EBF3FF","#D5E6FF","#92BFFF","#4291FF","#FFF8F7","#FFDDD8","#FFA597","#FF6253","#FFFAF5","#FFE0BE","#FFAA39","#E79100","#D0FFD4","#7DFF93","#00DA54","#00B947","#F1F4FF","#DFE4FF","#AEB8FF","#8087FF"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.43421936609613,5.100687932363686,-64.08586870464498,4.502412953317652,-41.11294430990105,3.017135229848019,-18.888486914529935,1.8251241221961068,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-40.940708789235714,3.0069340578770496,-21.122834074579163,1.9331720160112165,-77.70409895709017,5.493642615376888,-64.34147182620212,4.520282197700913,-40.849183913847796,3.0015190917179395,-28.18413111501551,2.292444746213784,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-41.2870784084647,3.027463512408998,-26.013778316259767,2.1791858289450103,-73.64084437140687,5.1896286230945075,-64.42349258712335,4.5260223420377566,-41.02780633656069,3.0120908713744514,-19.464187683477977,1.8526993919522168]},{"theme":"light","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#E8F1FF","#79B0FF","#368BFF","#0065D6","#FFF6F4","#FF9B8D","#FF5446","#D80007","#FFF8F1","#FFAB3F","#E18E00","#B47000","#C8FFCD","#00D552","#00B545","#008D34","#EFF1FF","#A0AAFF","#7B81FF","#573FFF"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-89.77514075602531,11.177468536043195,-51.89752791553575,5.733156384550998,-34.996215684604906,3.8114980409485155,-18.92394323577027,2.320895932067837,-94.48648764812391,11.954651961610136,-56.02375667309079,6.252768618847473,-36.81873931987068,4.002056146272223,-19.591172380553846,2.375625103310219,-95.24016771148194,12.080915286992608,-59.85145182180837,6.751641124414219,-44.82390104707481,4.887508677146329,-28.683175933367217,3.184369254481423,-90.45906258458693,11.288985892577246,-57.499124974387314,6.443154991463433,-42.83495757582209,4.660276446547384,-26.099142978345885,2.9428672820648862,-90.66219607331998,11.322193559065964,-52.9772743130274,5.867283737103295,-35.76790053375578,3.8916727925995533,-16.863337707955058,2.1560372721390864]},{"theme":"light","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#000209","#00479B","#0065D5","#3289FF","#100000","#9F0004","#D70007","#FF4F41","#170A00","#7A4A00","#9F6200","#DF8C00","#000801","#006322","#00822F","#00B344","#01000D","#3E00CE","#573EFF","#797EFF"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.22533906008685,12.552007092013115,56.99670466281028,5.352680994694784,44.43662180236468,3.3253647200326193,29.122033451491582,2.0641033245604916,76.01516726706762,12.436514137733148,55.97147975339624,5.133613537650522,43.91010121645754,3.2654000659595037,27.461071919951625,1.9716236837030474,74.81611736383091,11.770648316029503,52.816193854903005,4.528105409994289,41.51373908005361,3.011071841176318,19.679891349505557,1.6112041351931181,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.42233250009274,3.001939806510638,21.536768417458934,1.687644517337887,76.3513076320574,12.62087594492386,59.99704797613187,6.063965240585196,46.62694808670478,3.592268428661898,28.6381619838115,2.036506690369533]},{"theme":"light","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#DA7A00","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.42046624663428,20.43859328774187,80.31672424485741,6.9206271592755195,64.53598724808649,4.047139863973963,42.92136358171458,2.2930773168482466,105.12312344447312,20.1664284240786,79.32640956847692,6.666965706057784,64.47466861031803,4.0396342482899685,38.694434835096864,2.0867987099188317,103.94997559222902,19.08898290736351,71.78028267942507,5.099770465607366,55.19920557284124,3.104259347966041,35.17783363962355,1.9359471784913758,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.57179613789485,3.2209892436744982,37.49012931672191,2.03317973864909,105.58294145619523,20.58664787402852,83.26627791326895,7.757864847967472,64.57504887916579,4.051931818616954,42.090577540190225,2.2501559682241092]},{"theme":"dark","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#E3EEFF","#639EFF","#006DFE","#0048AD","#FFF4F3","#FF7F74","#F1001F","#A80012","#FFF7F1","#FF921B","#D17400","#955100","#B8FFBE","#00C649","#009F39","#006E25","#EDEEFF","#9794FF","#725BFF","#4E00D6"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.73192161780433,17.93999294470544,-52.63286661912575,7.865273702162647,-33.381528921151094,4.618607106385301,-17.0501576858861,2.5303719690800346,-102.4562582898777,19.486486384763445,-56.148341062851905,8.53993960492931,-34.330992536827104,4.759940239487347,-18.37234103122508,2.6737006902768288,-103.68997976483836,19.82648045930809,-60.35961607275431,9.379380118591195,-43.39157032733722,6.209218370112115,-24.93643207012505,3.4548840566407524,-97.27916136850891,18.085610527348415,-59.34389647192141,9.173841233004914,-42.25887199411116,6.018336366435839,-23.360562325895305,3.2570864547421428,-97.9872053947921,18.274719246988955,-53.323021296304695,7.99582368531923,-33.63351308594995,4.6559156780457185,-14.771084781300106,2.2949481607054216]},{"theme":"dark","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000104","#00153F","#00317C","#00429F","#070000","#3A0002","#750009","#9B0010","#0C0300","#291200","#562D00","#8B4B00","#000300","#001D05","#004112","#006521","#010005","#1A0057","#350098","#4700C4"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.20955594591163,5.284998875760269,36.83828085440759,4.509769828130583,28.330761142327287,3.0728704517581886,21.819504217645104,2.3321848000622474,40.142120786515875,5.269603471071473,36.80831529061993,4.503115584720142,27.930310922781615,3.0205822411426944,20.559717681283935,2.214401964672886,39.70511745091868,5.169077378914306,36.80795164205375,4.503034882134433,27.78871707797606,3.002307432697674,14.025581970431295,1.7128949100718707,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.795231110735635,4.500212645278562,30.09285126953815,3.3133648365325183,24.185949678819867,2.5748913454036786]},{"theme":"dark","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E4EFFF","#75AAFF","#3482FF","#005AD5","#FFF6F4","#FF9389","#FF3C3B","#CC0019","#FFF8F2","#FFA350","#E58000","#B36300","#BCFFC2","#00D04D","#00AF3F","#00862E","#EEEFFF","#A2A1FF","#7F74FF","#601DFF"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-86.07057705487603,9.776047393674569,-47.005198509651805,4.836992692850337,-29.617188814986996,3.125340410912816,-13.465221764046625,1.8500449052652255,-92.15650032550448,10.66842694645063,-51.122226750198095,5.289032001141636,-30.69564720892069,3.221769980092269,-14.697188723997265,1.9355550836437716,-92.95341233620333,10.787583067801071,-55.01595924446716,5.732183607436098,-39.01368610071904,4.00959458617392,-22.717127042375882,2.5408179478715844,-86.56463892830851,9.847321829129962,-52.766100540364825,5.474290510931042,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.32881842479753,9.957973594755346,-47.6452882768299,4.906141960838558,-29.99057738527554,3.1585747205458907,-10.990631160230787,1.6845762464445797]},{"theme":"dark","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000107","#00133B","#00317C","#0054C7","#0B0000","#360002","#730009","#BF0016","#100500","#261000","#542C00","#A85D00","#000501","#001B04","#004012","#007D2B","#010009","#180052","#3B00A6","#5A00F5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.616537893860624,5.189361828546554,36.671589094740156,4.521147554384925,27.767977778595686,3.0212072610618272,13.560368041515986,1.6902988819780091,39.46146930028483,5.1544680971312,36.646426655727396,4.515570174173857,27.733751845040576,3.016779185756929,12.426352071081123,1.6200937148513173,38.82022467282525,5.008731246970903,36.66757146424993,4.520256699490143,27.6755983348751,3.009270049666561,0,1.2356833222737893,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.666598999480925,5.200587158978297,36.5946359240296,4.504106003407952,27.684199915070796,3.0103795763779906,16.582944142840912,1.898781610023956]},{"theme":"dark","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000107","#001E53","#00398D","#0058CF","#0C0000","#4C0004","#83000C","#C60018","#110500","#371A00","#603300","#AE6000","#000601","#002808","#004916","#00822D","#01000A","#250070","#4400BC","#5E01FF"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.84943203331798,5.876721182230661,38.357196991852035,4.517162998132957,28.969046969390032,3.0054117916551304,16.078397205708793,1.7957132708136676,43.66281217184597,5.829141814712315,38.38176722933373,4.522556047220382,28.96308732518828,3.004650205577566,15.121014183458136,1.7339400568635355,43.01534021323247,5.66238735865634,38.36369120360285,4.518587529333456,28.956857085057415,3.003854272405285,0,1.32359073643957,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.88937786124038,5.8868666342953615,38.32550423955951,4.510220548286027,28.94413025668513,3.002229117722787,19.264807559936084,2.024522036885816]},{"theme":"dark","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E3EEFF","#66A0FF","#0E71FF","#004CB5","#FFF5F3","#FF8378","#F60020","#AF0013","#FFF7F1","#FF962A","#D57700","#9B5500","#B9FFBF","#00C849","#00A23A","#007226","#EDEEFF","#9997FF","#7460FF","#5200DF"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-95.07464172795706,16.23701078357789,-51.883866319617056,7.27432706128743,-33.10437599843017,4.366728841176053,-16.962076325331164,2.444617555127284,-101.23789901188303,17.745933910216884,-55.660852739640006,7.937252290960222,-33.86013630190819,4.470540437936136,-18.127983335051407,2.5633448249431416,-102.03269987499108,17.94442049171427,-60.01549050114797,8.732123022916136,-43.24478677485694,5.853976070416037,-25.11442835418257,3.342537791539289,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.852118066322895,5.637910373772879,-23.122294138224145,3.108849028833141,-96.32992550494482,16.539962663017093,-52.86610846158114,7.444323350911882,-33.336065162067605,4.398429825394501,-14.383943910910213,2.194178799785328]},{"theme":"dark","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#D67800","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.29489852310851,19.198605473400605,76.19115652133168,6.500760037097026,60.41041952456072,3.801604173546957,38.795795858188825,2.153958743950203,100.99755572094737,18.94295256384705,75.20084184495119,6.262487955668726,60.349100886792264,3.7945539156195487,34.56886711157111,1.9601948416949677,99.82440786870325,17.93087452582946,67.65471495589931,4.7903727911067815,52.29898260017819,3.0135204434714913,31.0522659160978,1.8184953129572932,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.44622841436909,3.025575236651027,33.364561593196164,1.9098288766402804,101.45737373266948,19.337677744697757,79.14071018974319,7.287203401685083,60.44948115564005,3.8061054053754897,37.96500981666448,2.1136413881018767]},{"theme":"dark","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E3EEFF","#69A2FF","#1975FF","#004FBB","#FFF5F3","#FF877C","#FA0021","#B50014","#FFF7F1","#FF9833","#D87900","#A05800","#B9FFC0","#00CA4A","#00A43B","#007628","#EDEEFF","#9B99FF","#7664FF","#5500E7"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-93.28410718321774,14.535439506623602,-51.011442228336165,6.654206660130301,-32.65955270222981,4.075313081844305,-16.370466566638967,2.297770413224217,-99.44736446714371,15.886233758087014,-55.07262883280597,7.299026812522581,-33.02825162884881,4.121422426501604,-17.569909707299868,2.4103015359642868,-100.24216533025177,16.063919770413232,-58.9183675206033,7.932997007393653,-42.52780974275247,5.391835753982905,-24.79898612463087,3.1522681596337323,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.905085629416114,5.1638402024196015,-22.782862127638978,2.93464773079297,-94.53939096020551,14.80664328764592,-51.95162400089297,6.801199303451522,-32.707141977036414,4.081250761977603,-13.69412377461175,2.0581050526656677]},{"theme":"dark","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E8F1FF","#D5E6FF","#95BEFF","#498FFF","#FFF9F8","#FFDDD8","#FFA59B","#FF5F57","#FFFAF6","#FFDFC5","#FFA95E","#F18700","#C6FFCB","#7DFF91","#00DA51","#00B943","#F0F1FF","#E1E3FF","#B4B6FF","#8982FF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-71.17583391665679,5.008590480308264,-64.08586870464498,4.502412953317652,-40.969901639859465,3.0086620775128243,-18.492326685553408,1.8062558438836862,-77.39176152066469,5.470028106981068,-64.06334225320066,4.50083949888721,-41.06186552000793,3.0141084284175577,-20.58109958398818,1.9067218663179524,-77.74802214310357,5.496966722090798,-64.16882972322054,4.508209630901898,-40.97688137217631,3.00907529195687,-26.93048349706687,2.2267208299060117,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.24499881268402,3.0249663274797034,-25.961518214862377,2.176489332337808,-72.19208007215988,5.082914152992658,-64.24580556994597,4.513590807561596,-40.91738125342747,3.005553523356943,-18.74578210381434,1.818317299241165]},{"theme":"dark","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E5EFFF","#7AADFF","#3D87FF","#005FDF","#FFF6F5","#FF988E","#FF4D48","#D5001A","#FFF8F3","#FFA75A","#EA8300","#BA6700","#BDFFC3","#00D34E","#00B341","#008C31","#EEEFFF","#A5A5FF","#837AFF","#6433FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.52359832003891,10.974551054175622,-50.82334067294272,5.601029591424501,-33.90019185783247,3.6989242542195706,-17.94937633357589,2.2421371448335408,-94.53061684959366,11.962030249162733,-55.11693338695639,6.136943021617957,-35.65662848796355,3.880065704029081,-19.010500723408704,2.3279588656978594,-95.32685818617963,12.095472485308715,-58.832789738116276,6.617312123916825,-43.099834270009964,4.690265818580579,-27.202021451205145,3.0448373808729228,-89.02366089613628,11.055448870118658,-56.507119659393844,6.314879036055816,-41.91393002442956,4.556652813501584,-25.675484951698337,2.9041378624661793,-89.65880574741696,11.158543725868578,-51.69548126800453,5.708204299589472,-34.40239467659594,3.750316223165759,-16.078998268001875,2.0949612264666513]},{"theme":"dark","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00020F","#0045A5","#0060E2","#3F88FF","#140000","#A10011","#D7001B","#FF4F49","#190900","#814600","#A85D00","#EA8300","#000A01","#006320","#00822D","#00B341","#030014","#4A00CC","#6537FF","#847BFF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.15878035868923,12.51550617725489,56.792209524954714,5.308053794065892,44.82231091099692,3.370285744389143,29.010809014184062,2.057710850183084,75.84645644595844,12.343365988484052,55.500674792931,5.036843578382145,43.81499067450534,3.25473208522667,27.348178994525146,1.9655632934834584,74.80511978941351,11.764533211175078,52.793708756474935,4.52413702940673,41.44647230556496,3.0043476386355414,20.422200521634597,1.6411239547419576,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.19495675605003,12.535354496536943,59.24767070343283,5.876074797996776,46.58341111523108,3.5866744685580465,28.54022915013225,2.030988108287992]},{"theme":"dark","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"light-ic","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#000110","#0042E2","#3B78FF","#85AEFF","#150000","#BD0011","#FF001B","#FF998D","#1A0500","#B04A00","#E96400","#FFA174","#000B01","#007C2D","#00A73F","#00D452","#00040B","#00639C","#0089D6","#4DB3FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.76809306682351,20.754602558566376,81.70242596755779,7.298044196933449,63.72862579130605,3.9499301799077116,41.321899652634976,2.211520376313395,105.32470288110206,20.35108757719671,79.14032751842986,6.6207260543602775,64.03544238362204,3.986463961054262,38.10332212672181,2.0602150617562085,104.62285904737456,19.706640245810163,73.93672186564804,5.489431142391079,57.895853978119064,3.3399091341000195,36.26437665881752,1.9807324696579875,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.53601527934143,20.543945015271454,78.36297683006073,6.432254688155311,62.2832355663668,3.784280156272109,42.68471757482252,2.28072562681704]},{"theme":"light-ic","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E8F0FF","#709FFF","#2D6DFF","#0038C6","#FFF4F3","#FF8275","#F30019","#A7000E","#FFF6F1","#FF8C52","#DF5F00","#9D4100","#C2FFC9","#00C84D","#009F3C","#006E27","#DFF0FF","#10A5FF","#0082CB","#005689"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-98.22794037003116,18.339196272629252,-53.8293996799751,8.092203727319841,-34.21813658812841,4.7430333536498335,-15.622162303180753,2.381112763636304,-102.4562582898777,19.486486384763445,-56.97432424245902,8.701923574139988,-34.76332175899368,4.824974728357804,-18.151652148372637,2.6494391947645526,-103.24826961310278,19.70448161329645,-59.10147589327934,9.125072654774526,-40.787696351505694,5.7744786453226835,-22.81212855995232,3.1897383862050708,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.29572232514961,6.0245037012352585,-23.381466511664765,3.259668821930569,-97.12909045511768,18.04563033112125,-52.54162933208091,7.848085216280267,-36.273216344700636,5.055412083856902,-18.54110013749204,2.69234396770907]},{"theme":"light-ic","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00104D","#002488","#0032B2","#040000","#3A0002","#750007","#98000B","#080100","#2E0E00","#602500","#8F3B00","#000200","#001D05","#004114","#006322","#000102","#00192E","#003A5F","#004D7C"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.799792142283025,4.501224406456354,30.04772679653348,3.3069997994324063,23.797727835383583,2.533076638038498,40.23106479600237,5.289901483963394,36.80831529061993,4.503115584720142,27.941932493369514,3.022087143525339,21.190215752118235,2.2723970292907865,40.01441400964806,5.240352767492206,36.83929336929951,4.509994812829007,27.908315541084118,3.0177360720288915,16.77490328531677,1.9034128352995776,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.22977489962569,5.289607582090012,36.84755783504637,4.511831555904809,27.796281383025352,3.0032808914145437,21.058698283639867,2.260143784790762]},{"theme":"light-ic","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#EAF1FF","#81ABFF","#4982FF","#0048F5","#FFF5F4","#FF9589","#FF403B","#CC0013","#FFF7F3","#FF9E6E","#F46900","#BD5000","#C6FFCC","#00D251","#00B043","#008631","#E1F1FF","#45B0FF","#0090E1","#006CA9"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.69551010135778,10.011246151186945,-48.263751486481056,4.97335266465752,-30.44825366943375,3.199530942583609,-12.043546933756078,1.753943193668956,-91.71639880150975,10.602849175762582,-51.72679347557047,5.3568510977778265,-31.21621892784593,3.268795397886287,-14.661523244642579,1.9330507439017046,-92.55409442667087,10.727809933310253,-53.89547122979923,5.603122076267749,-35.95806640240085,3.7112655792375078,-20.04367187639212,2.329915503818876,-87.89039906251095,10.039605852107476,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-86.58838125370931,9.850752189993974,-46.78872683576137,4.81370216625391,-31.383244337404896,3.2839492580871354,-15.736559790538532,2.009284883552937]},{"theme":"light-ic","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000004","#000E48","#002895","#0042E3","#070000","#360001","#730006","#BE0011","#0C0200","#2B0D00","#5F2400","#B04A00","#000400","#001B05","#003F13","#007C2D","#000103","#00172B","#00395C","#00639D"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.74741500641279,5.218664231374835,36.670023859840576,4.5208004701163915,27.672031623266886,3.008810094200282,15.351341296852958,1.8098737923818007,39.57933742278427,5.181007308853048,36.654144364688555,4.517280323585742,27.751464091733354,3.019069960427185,12.671992881770905,1.6349566993987985,39.23716411822194,5.10371254671437,36.601471319299684,4.505617849545331,27.652865692456807,3.006339685224649,7.684471784052528,1.3665581876464832,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.65687576920386,5.198408498908002,36.65150522712613,4.5166954713046765,27.64129393913477,3.0048491045091428,12.054359140378313,1.5979376774972005]},{"theme":"light-ic","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000004","#001864","#002FA8","#0045ED","#080000","#4C0003","#830008","#C60012","#0D0200","#3E1500","#6C2A00","#B84D00","#000401","#002809","#004917","#00822F","#000103","#00233D","#00416A","#0068A4"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.98030914587015,5.909904848562026,38.293478372508915,4.503221171740978,28.94317626341717,3.002107337321695,17.95394033246701,1.9258322867444517,43.782795788734404,5.8597651770218455,38.38866051296957,4.524070782124866,28.984608537472,3.0074014381148784,15.156957289606964,1.7362054736619685,43.437607341847624,5.771384990646562,38.33399448678559,4.512078832150148,28.981628167432756,3.007020263830111,9.892674202630067,1.4441010369850493,43.614187977218386,5.816700203526642,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.88976990866122,5.886966133555759,38.401524303038336,4.526899458852529,29.03631728859714,3.0140233900654434,14.306925700285792,1.683714708917422]},{"theme":"light-ic","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E9F0FF","#74A2FF","#3371FF","#003CD0","#FFF4F3","#FF8679","#F8001A","#AF000F","#FFF6F2","#FF9058","#E36100","#A44400","#C3FFC9","#00CA4E","#00A33D","#007329","#E0F0FF","#21A8FF","#0085D0","#005B90"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.69585202312288,16.62869936569993,-53.53835262595417,7.561646720391368,-33.90853669724825,4.477228400656394,-15.643424374972522,2.314414627530853,-100.79897840003042,17.636700891614694,-56.5097869254783,8.089667628698802,-34.29633291417122,4.530985783372177,-18.10601775891745,2.56107712884258,-101.63443561648342,17.844851007069916,-58.71630179001587,8.4915976550775,-40.34850051574898,5.4087697654311055,-22.889197327723014,3.0820931228283226,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-95.5914257275877,16.361459308701495,-52.26386479821355,7.339891411251783,-35.96637326948896,4.765958094131511,-18.710865986535588,2.6239504003951875]},{"theme":"light-ic","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#000110","#0041DF","#3876FF","#82ACFF","#140000","#BB0011","#FC001B","#FF968A","#1A0500","#AE4900","#E76300","#FF9F70","#000B01","#007B2C","#00A63F","#00D252","#00040B","#00629B","#0088D4","#47B1FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.64252534329776,19.49544279635548,78.08171358923367,6.990900026896573,60.31711036263554,3.7908833425501425,38.14751646036453,2.122399526754989,101.24542779029852,19.156162076239593,75.44574141529571,6.3202869230281635,60.62673294996029,3.82662941345977,34.94277926170305,1.9762445240799382,100.4972913238488,18.51105924752946,70.36281029021609,5.256397052662421,54.37780741212484,3.1906001595018556,32.840214506002376,1.8885138214456323,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.4093478352768,3.0225491335598114,33.31123457222728,1.9076443919396007,101.41044755581565,19.297565623167603,74.60712675826916,6.125340559948453,58.63177587043786,3.604653750954651,39.55009522975671,2.1915837971666887]},{"theme":"light-ic","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E9F1FF","#76A4FF","#3875FF","#003ED7","#FFF4F3","#FF897C","#FC001B","#B50010","#FFF6F2","#FF935D","#E66300","#A94600","#C4FFCA","#00CB4F","#00A53F","#00772B","#E0F0FF","#2BA9FF","#0087D3","#005E95"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-95.34499825068502,14.98175500729861,-52.621417231568934,6.906762415988252,-33.47849770408375,4.178062617945301,-14.893749254597887,2.163552488849563,-99.00844385529109,15.788447906049281,-55.63494162352738,7.390318911682041,-33.467257033822236,4.176644120449875,-17.547592326855693,2.408179342981014,-99.84390107174409,15.974784754119627,-57.911424929350886,7.764836527735112,-39.59013024218227,4.982347876116714,-22.33006111099907,2.8868875311347497,-95.39392806862337,14.992417327878547,-58.26045217149408,7.822951516486187,-41.372488477704024,5.229057297458628,-23.176065198031452,2.976455642863797,-93.8008911828484,14.646846343308852,-51.05892490110174,6.66159703590636,-35.06283530237813,4.380256350040106,-18.07990151348513,2.4590883821528062]},{"theme":"light-ic","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EDF3FF","#D8E6FF","#9ABDFF","#598FFF","#FFF8F7","#FFDDD8","#FFA59A","#FF6156","#FFF9F7","#FFDECE","#FFA77C","#FF7112","#D0FFD4","#7DFF93","#00D955","#00B947","#E5F3FF","#CEE8FF","#79C3FF","#0099EE"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.68698719753608,5.119268434409956,-64.43727527190525,4.526987198998707,-41.01457995095163,3.0113075347043563,-19.296469937711088,1.8446469211142276,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-41.0312066106476,3.012292267380192,-20.969079339398604,1.925648566411713,-77.34727635116013,5.466668081501757,-64.08510797117285,4.502359813071005,-40.89305028080999,3.004113887137535,-23.84545366268376,2.068528569757355,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-40.827825177303865,3.0002560129496776,-26.013778316259767,2.1791858289450103,-71.69069599862902,5.046190228559761,-64.15987367011111,4.5075837056249375,-40.85863800181002,3.0020782433474276,-19.258832894479415,1.8428420367238694]},{"theme":"light-ic","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#EAF1FF","#86AEFF","#5189FF","#0650FF","#FFF6F4","#FF9B8F","#FF5349","#D70015","#FFF7F3","#FFA376","#FA6C00","#C75400","#C8FFCD","#00D553","#00B445","#008D34","#E2F1FF","#50B4FF","#0094E7","#0072B3"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-90.02549742397721,11.218239018758677,-52.117590678997175,5.760385990810306,-35.453068631322715,3.8588722418561026,-17.16315496591595,2.1796295803105563,-94.48648764812391,11.954651961610136,-56.08049309972586,6.260045669338659,-36.68653001054492,3.9880923379680597,-19.42498133620683,2.3619323117143227,-94.88408174929032,12.021194381023134,-58.024655972390946,6.511549338355004,-40.19653971715428,4.366167533936967,-25.014063203920518,2.8441658803090872,-90.45906258458693,11.288985892577246,-57.51342916294683,6.44501257640493,-42.39985031117064,4.611196053798779,-26.099142978345885,2.9428672820648862,-89.03890865199516,11.05791933389335,-51.142776043878534,5.640183677550314,-35.55629387962385,3.86961291577043,-20.57349953874105,2.4573829383449617]},{"theme":"light-ic","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#00010B","#0035BB","#044EFF","#4E86FF","#0F0000","#9F000C","#D60015","#FF4D45","#150400","#903B00","#BB4F00","#F86B00","#000801","#006322","#008230","#00B344","#000307","#005182","#0071B2","#0093E5"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.30385323493446,12.594966222228084,58.55230095328762,5.70794052541694,46.62204712410211,3.5916381056717506,29.005214449343097,2.0573900879913625,76.05323451380515,12.457481581934719,55.93606418979168,5.126251708978212,44.077718465236906,3.284322001440957,27.730078776096413,1.986177453897667,75.43283879959024,12.113874624281086,52.77586207835023,4.520990578468311,41.44187963136357,3.003889327255304,23.868980516769618,1.7916338607346896,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.41072061989075,3.0007825637465393,21.536768417458934,1.687644517337887,76.14729893560808,12.509202438867769,55.739334564053664,5.085602713702602,43.07798550292857,3.173723442083943,28.271676351762963,2.015968674447834]},{"theme":"light-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark-ic","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#D38200","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E2","#8F5BFF","#B49FFF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.36973966240839,20.39226096069594,79.16304803673168,6.626348275559002,63.912632944603466,3.971781216419874,43.318131952880584,2.3140155027501375,105.16667360757414,20.20636839933495,79.43361118518992,6.693805381831162,64.79709340122825,4.079330310903514,38.90470830731088,2.096381256670562,103.76723276112338,18.92212884965907,70.7453629185683,4.926701923645705,54.109273202798526,3.0160152686700097,34.512224892089854,1.9092708836328856,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.58363991444959,3.2220248289870597,37.502971296697396,2.0337403797475795,105.5639998705614,20.569417077144905,82.76169093861434,7.605398027541734,65.05029978451972,4.11090668779679,42.03743585537794,2.2474520733771812]},{"theme":"dark-ic","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#DFEEFF","#4AA0FF","#007AE0","#005198","#FFF4F3","#FF7D79","#EF0030","#A6001E","#FFF7EF","#F99A00","#CA7D00","#915800","#B8FFBE","#00C648","#009F38","#006E24","#F0EDFF","#A98DFF","#894CFF","#6000C7"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.24720863865953,17.81140932471069,-52.09132740819334,7.763491481432673,-34.99617079306692,4.860177125212619,-17.977340963301042,2.6303715448994067,-102.4562582898777,19.486486384763445,-55.7461567674855,8.461541475204797,-34.016004840410574,4.712824540535038,-18.066789785866806,2.6401456993132153,-103.60398268701002,19.802704734429785,-61.68353878496926,9.650187857341214,-44.76775468944945,6.444757556916112,-26.060791619056427,3.599837857825526,-97.27916136850891,18.085610527348415,-59.33082759509665,9.171209294707863,-42.24705692654848,6.016359585841944,-23.350526713793172,3.2558471191579894,-97.94239544297146,18.262727694350296,-53.03355221198167,7.94095381214551,-32.857248309508414,4.541449578831631,-15.041171256747345,2.322063533056465]},{"theme":"dark-ic","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000103","#001834","#00376B","#004A8C","#070000","#390005","#750012","#99001B","#0D0400","#261300","#522F00","#865100","#000300","#001D05","#004112","#006521","#010005","#22004F","#43008F","#5800B7"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.21965913293546,5.287302224624218,36.81767378273908,4.505192880903515,27.950777846769416,3.023233051937069,21.000772856623527,2.25477321245894,40.142120786515875,5.269603471071473,36.88792093057972,4.5208109211231795,27.866909320024153,3.0123854877509646,20.876190217747006,2.243276608438576,39.57947058767665,5.139984882718895,36.89828467875601,4.523118840482662,27.8635036155662,3.011945822214043,13.292640012092916,1.6665035199669143,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.80600977189143,4.502603953746083,29.651273965169533,3.2515347547731954,23.802145579236715,2.533547958530533]},{"theme":"dark-ic","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E1EFFF","#62ACFF","#0087F7","#0065BB","#FFF5F4","#FF928C","#FF3644","#CA0027","#FFF8F1","#FFA533","#DD8900","#AD6A00","#BCFFC2","#00D04C","#00AF3F","#00862E","#F1EFFF","#B29CFF","#956AFF","#7700F5"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-85.70519954500536,9.72347127561709,-46.57313915269262,4.790554592637207,-29.772868283167547,3.1391774083572677,-14.678197720495364,1.9342213705917548,-91.71639880150975,10.602849175762582,-50.907190680193835,5.264997878479995,-30.103011927821978,3.1686137953283313,-14.381529067368087,1.9134497328938256,-92.9101803888625,10.781105347058572,-55.36632323767275,5.772792653136124,-40.27550194757439,4.135743433221995,-23.629833528615176,2.614848030728214,-86.56463892830851,9.847321829129962,-52.75275099572737,5.472775232316544,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.71836703917482,10.014570561299498,-47.715877818006604,4.91379345402092,-29.619990398118716,3.125589171245837,-10.872649839108671,1.6769004806806627]},{"theme":"dark-ic","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000106","#001632","#00366A","#005EAF","#0B0000","#360004","#730011","#BD0023","#100600","#241200","#502E00","#A36300","#000501","#001B04","#004012","#007D2B","#020009","#20004A","#48009A","#6F00E5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.62660370456575,5.191620542906115,36.592103069401674,4.503545882983851,27.695057695096033,3.0117807086970667,12.482735608358766,1.623488823202914,39.46146930028483,5.1544680971312,36.631002505660305,4.512153756975423,27.677944212691237,3.0095726068531357,12.751768680957815,1.6398240151911772,38.729462058672325,4.987982714010286,36.579572970042136,4.500775701626332,27.747483785710887,3.0185550274007946,0,1.2039987886628472,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.63684952401951,5.1939188146581685,36.601297620325695,4.505579426399357,27.609531401873983,3.0007614645527125,16.085712499624492,1.86220122646402]},{"theme":"dark-ic","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000206","#002245","#003E78","#0062B6","#0B0000","#4C0008","#820015","#C50025","#110600","#341C00","#5B3600","#A96700","#000601","#002808","#004916","#00822D","#02000A","#2E0067","#5300AD","#7400EE"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.7603252928922,5.854038847974277,38.29235457140463,4.502975854531618,29.083956028514045,3.020138635078335,15.054852030501879,1.729780743830801,43.694363439742204,5.837205585263101,38.354206470247746,4.516507237210082,29.087182234169767,3.020553277003921,15.230546565278194,1.740856512459411,42.92491991360228,5.63897133999608,38.33365423714702,4.512004338988724,28.93769973409828,3.0014083479801514,0,1.2794758858630608,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.85966524740055,5.879321607935794,38.335090707081115,4.512318847459295,28.952846517354917,3.003342035233188,18.7969313728254,1.9885281036480773]},{"theme":"dark-ic","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E0EEFF","#4FA3FF","#007DE4","#00559F","#FFF4F3","#FF817D","#F40031","#AD001F","#FFF7EF","#FB9C00","#CE7F00","#965B00","#B9FFBF","#00C849","#00A239","#007226","#F0EEFF","#AA90FF","#8C52FF","#6500D0"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-94.7101738760577,16.149472101206463,-51.76844025191021,7.254462182627787,-34.605057818633654,4.573999892517157,-17.84521283217997,2.534242228411278,-100.79897840003042,17.636700891614694,-55.24750846744172,7.863491890193123,-33.54190885081095,4.42668652015819,-17.815136446883052,2.5311583129351276,-101.94670279716274,17.922901715066683,-61.02785966423507,8.921519321161206,-44.34904450398409,6.02788949767693,-25.86724540779468,3.4331609270477186,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.840254848173814,5.636085625741139,-23.122294138224145,3.108849028833141,-96.71860922522349,16.634224259107555,-52.4354046324141,7.369572153296626,-32.71416296796099,4.3135863728502315,-14.766203746026353,2.2302399315399106]},{"theme":"dark-ic","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#CC7E00","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E1","#8F5BFF","#B49FFF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.24417193888262,19.155084079579783,75.03748031320593,6.224334741672483,59.78706522107772,3.730817455350078,39.19256422935483,2.1736266322828217,101.04110588404839,18.980469423093126,75.30804346166417,6.287699296730837,60.6715256777025,3.8318416601446055,34.77914058378512,1.9691960254812915,99.64166503759763,17.774143327140518,66.61979519504256,4.627804134340593,52.265155427331,3.010761189364384,30.386657168564103,1.79343744066295,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.45807219092383,3.026547994099155,33.37740357317164,1.9103555042369325,101.43843214703563,19.321492322017065,78.73738474892319,7.17242701812758,60.92473206099399,3.861502331684902,37.91186813185219,2.111101535692422]},{"theme":"dark-ic","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E0EEFF","#53A4FF","#007FE8","#0058A5","#FFF4F3","#FF8580","#F80032","#B30021","#FFF7F0","#FD9D00","#D18100","#9B5E00","#B9FFC0","#00CA4A","#00A43A","#007628","#F0EEFF","#AC93FF","#8D57FF","#6800D8"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-92.91963933131836,14.457074514504098,-50.53751898728166,6.580638229837842,-33.762466759799054,4.213972880586457,-17.265259153354464,2.3814244063048684,-99.00844385529109,15.788447906049281,-54.621704682580784,7.2261694596451385,-32.708249696736004,4.081389020198301,-17.26088719592284,2.381011454094406,-100.19905346524207,16.054261379481122,-59.919533209261445,8.101702037099825,-43.64240726631874,5.55098822531801,-25.561703474916055,3.236683846808585,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.893113879964474,5.162174617584753,-22.782862127638978,2.93464773079297,-94.92807468048419,14.891026660055605,-51.83505302329671,6.7828987244211,-32.07221654016657,4.002369709649521,-14.066989001346808,2.090531498955086]},{"theme":"dark-ic","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E5F1FF","#D2E7FF","#8AC0FF","#2091FF","#FFF9F8","#FFDDDA","#FFA49E","#FF5C5D","#FFFAF5","#FFE0C0","#FFAA42","#E99000","#C6FFCB","#7DFF91","#00DA50","#00B943","#F3F1FF","#E6E2FF","#C1B2FF","#9D7AFF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-70.80493468751378,4.981574213000164,-64.1741844582375,4.5085838821814495,-40.86541522684489,3.0024791016401666,-17.915271666932362,1.7789289638170434,-77.39176152066469,5.470028106981068,-64.14420274941838,4.506488572747314,-40.82594869447877,3.0001450551263997,-20.102672577261966,1.8834970356793017,-77.70409895709017,5.493642615376888,-64.40976920815079,4.525061719384059,-40.93902681557826,3.00683450902807,-28.2154352545988,2.2940964528512438,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.23137463469248,3.02415799426954,-25.961518214862377,2.176489332337808,-72.5833510184408,5.111647048756453,-64.45521953810703,4.528243512077896,-40.9348520371047,3.006587427640115,-18.521762858381592,1.807654812879132]},{"theme":"dark-ic","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E1EFFF","#68AEFF","#008BFD","#006AC4","#FFF6F5","#FF9791","#FF494F","#D30029","#FFF8F1","#FFA941","#E28C00","#B46E00","#BEFFC3","#00D34D","#00B340","#008C31","#F1EFFF","#B49FFF","#9971FF","#7D09FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.0351868676248,10.895768939711282,-50.01919534416472,5.502978516969682,-33.92042169291382,3.7009882121251625,-19.118481017052428,2.3367853131295435,-94.53061684959366,11.962030249162733,-54.89593536437227,6.108854061876529,-35.13675470036395,3.8260433618990066,-18.695569008603144,2.3023140000481574,-95.24016771148194,12.080915286992608,-59.18458546592085,6.6635751999987605,-44.37309985295556,4.8355935478331284,-28.125674388660414,3.1315039953558483,-89.11957165172129,11.070992163190612,-56.49372778683512,6.313154752438012,-41.901500594929324,4.555261373045625,-25.675484951698337,2.9041378624661793,-90.04835436179425,11.221964232051386,-51.2922333885473,5.658542947646642,-34.21126239038315,3.7307191421666435,-15.254686874750304,2.03178289437105]},{"theme":"dark-ic","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00030D","#004D92","#006CC7","#008BFE","#130001","#9F001C","#D60029","#FF4B50","#190B00","#7B4A00","#A06100","#E38C00","#000A01","#006320","#00822D","#00B341","#040013","#5B00BE","#7E16FF","#9971FF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.08600684668272,12.475516218126787,55.96473752983635,5.13221099097022,43.3369682989835,3.2018590596167122,29.318326289650187,2.0754572365320043,75.88156299830736,12.362776535253154,55.82217638546274,5.102669267083783,43.91931922104849,3.266436635421996,27.87409429449102,1.9940348577564484,74.63415754799017,11.66954886455628,52.68236753448497,4.504554642142053,41.632593181405234,3.023005829909607,19.021862742942993,1.5853635699211195,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.18160530883091,12.528031661692317,58.96992438550627,5.808209180198148,47.36214126331064,3.68860336114861,29.106756402593504,2.0632235490419992]},{"theme":"dark-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]}],"resolveVars":[{"theme":"light","bg":"#FFFFFF","vars":{"--lab-bg-tone-2":"oklch(97.84197% 0.010603 286.202)","--lab-bg-tone-3":"oklch(97.84197% 0.010603 286.202)","--lab-label-primary":"oklch(19.12257% 0.014100 291.556)","--lab-label-secondary":"oklch(56.54196% 0.013721 285.953)","--lab-label-tertiary":"oklch(66.97448% 0.014606 285.999)","--lab-label-quaternary":"oklch(81.39504% 0.013897 286.087)","--lab-border-strong":"oklch(19.12257% 0.014100 291.556)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(19.39430% 0.069681 257.297)","--lab-label-brand-secondary":"oklch(57.33316% 0.205939 257.291)","--lab-label-brand-tertiary":"oklch(67.12978% 0.175419 257.690)","--lab-label-brand-quaternary":"oklch(81.31388% 0.094313 257.967)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(20.16318% 0.082740 29.234)","--lab-label-danger-secondary":"oklch(59.62536% 0.244212 28.655)","--lab-label-danger-tertiary":"oklch(69.12968% 0.198650 28.574)","--lab-label-danger-quaternary":"oklch(82.55135% 0.097676 29.389)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(19.32631% 0.042728 66.944)","--lab-label-warning-secondary":"oklch(57.27465% 0.125021 68.799)","--lab-label-warning-tertiary":"oklch(67.73500% 0.148484 68.161)","--lab-label-warning-quaternary":"oklch(82.04043% 0.139600 68.374)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(67.73500% 0.148484 68.161 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(18.59864% 0.054816 147.311)","--lab-label-success-secondary":"oklch(54.91588% 0.161394 147.418)","--lab-label-success-tertiary":"oklch(64.88263% 0.190555 147.443)","--lab-label-success-quaternary":"oklch(79.02491% 0.232159 147.432)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(64.96409% 0.172888 147.450 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(19.54489% 0.077696 259.509)","--lab-label-info-secondary":"oklch(57.68997% 0.232691 259.838)","--lab-label-info-tertiary":"oklch(67.26428% 0.173598 259.980)","--lab-label-info-quaternary":"oklch(81.49851% 0.092914 259.576)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"1.0000","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"1.0000","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"1.0000","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"1.0000","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(66.97448% 0.014606 285.999)"}},{"theme":"light","bg":"#000000","vars":{"--lab-bg-tone-2":"oklch(7.11623% 0.013707 282.350)","--lab-bg-tone-3":"oklch(7.11623% 0.013707 282.350)","--lab-label-primary":"oklch(98.65156% 0.006606 286.278)","--lab-label-secondary":"oklch(80.14049% 0.013952 286.081)","--lab-label-tertiary":"oklch(69.23260% 0.013029 286.055)","--lab-label-quaternary":"oklch(55.16643% 0.013808 285.938)","--lab-border-strong":"oklch(98.65156% 0.006606 286.278)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-brand-secondary":"oklch(80.06451% 0.101154 257.822)","--lab-label-brand-tertiary":"oklch(69.35433% 0.162198 257.610)","--lab-label-brand-quaternary":"oklch(55.96293% 0.201872 257.393)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.64532% 0.006567 28.833)","--lab-label-danger-secondary":"oklch(81.38408% 0.105319 28.863)","--lab-label-danger-tertiary":"oklch(71.19739% 0.181099 28.350)","--lab-label-danger-quaternary":"oklch(58.10538% 0.238004 28.677)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.72985% 0.009577 72.664)","--lab-label-warning-secondary":"oklch(80.98123% 0.149940 68.750)","--lab-label-warning-tertiary":"oklch(70.11346% 0.152989 68.857)","--lab-label-warning-quaternary":"oklch(55.86543% 0.122417 68.219)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.31403% 0.027558 147.033)","--lab-label-success-secondary":"oklch(77.69544% 0.228159 147.448)","--lab-label-success-tertiary":"oklch(67.10085% 0.197388 147.383)","--lab-label-success-quaternary":"oklch(53.45797% 0.157009 147.442)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-info-secondary":"oklch(80.30515% 0.099317 259.806)","--lab-label-info-tertiary":"oklch(69.49027% 0.160387 260.058)","--lab-label-info-quaternary":"oklch(56.23879% 0.227606 259.853)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.0123","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.0104","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.0093","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.0059","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(69.23260% 0.013029 286.055)"}},{"theme":"light","bg":"#808080","vars":{"--lab-bg-tone-2":"oklch(58.21536% 0.012099 286.018)","--lab-bg-tone-3":"oklch(58.21536% 0.012099 286.018)","--lab-label-primary":"oklch(13.77177% 0.013406 284.503)","--lab-label-secondary":"oklch(20.89710% 0.013787 291.711)","--lab-label-tertiary":"oklch(33.70211% 0.013945 291.371)","--lab-label-quaternary":"oklch(49.18380% 0.012632 285.926)","--lab-border-strong":"oklch(13.77177% 0.013406 284.503)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.89968% 0.076454 257.725)","--lab-label-brand-tertiary":"oklch(34.17043% 0.122830 257.309)","--lab-label-brand-quaternary":"oklch(49.70427% 0.178878 257.337)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(21.88171% 0.089792 29.234)","--lab-label-danger-tertiary":"oklch(35.54757% 0.145582 28.627)","--lab-label-danger-quaternary":"oklch(51.73054% 0.211869 28.643)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.90753% 0.046395 66.438)","--lab-label-warning-tertiary":"oklch(34.00266% 0.074904 67.452)","--lab-label-warning-quaternary":"oklch(49.79045% 0.108745 68.715)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(91.20808% 0.064865 69.118 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(20.10741% 0.059351 147.255)","--lab-label-success-tertiary":"oklch(32.71649% 0.095649 147.616)","--lab-label-success-quaternary":"oklch(47.52181% 0.139141 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(88.67895% 0.198079 147.416 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(21.26627% 0.087439 260.146)","--lab-label-info-tertiary":"oklch(34.39884% 0.140838 260.069)","--lab-label-info-quaternary":"oklch(50.11242% 0.202893 259.861)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1070","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.1063","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.0686","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.0433","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.70211% 0.013945 291.371)"}},{"theme":"light","bg":"#3A3A3C","vars":{"--lab-bg-tone-2":"oklch(36.65084% 0.011912 285.797)","--lab-bg-tone-3":"oklch(36.65084% 0.011912 285.797)","--lab-label-primary":"oklch(98.92134% 0.005280 286.303)","--lab-label-secondary":"oklch(82.53513% 0.012530 281.039)","--lab-label-tertiary":"oklch(72.82755% 0.014297 286.039)","--lab-label-quaternary":"oklch(61.25907% 0.011943 286.042)","--lab-border-strong":"oklch(98.92134% 0.005280 286.303)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-brand-secondary":"oklch(82.44675% 0.088464 257.008)","--lab-label-brand-tertiary":"oklch(73.08383% 0.140495 257.442)","--lab-label-brand-quaternary":"oklch(61.77245% 0.208353 257.413)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(61.75781% 0.208491 257.338 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.86605% 0.005516 31.054)","--lab-label-danger-secondary":"oklch(83.38591% 0.092088 28.223)","--lab-label-danger-tertiary":"oklch(74.63167% 0.153784 28.842)","--lab-label-danger-quaternary":"oklch(64.10554% 0.244701 28.706)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.75683% 0.008520 67.727)","--lab-label-warning-secondary":"oklch(83.08426% 0.131366 68.787)","--lab-label-warning-tertiary":"oklch(73.86147% 0.161425 68.614)","--lab-label-warning-quaternary":"oklch(61.96370% 0.135496 68.532)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.51065% 0.024195 147.300)","--lab-label-success-secondary":"oklch(80.08887% 0.235054 147.469)","--lab-label-success-tertiary":"oklch(70.67977% 0.207519 147.455)","--lab-label-success-quaternary":"oklch(59.24524% 0.173588 147.533)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-info-secondary":"oklch(82.42821% 0.087905 259.666)","--lab-label-info-tertiary":"oklch(73.17409% 0.139071 259.898)","--lab-label-info-quaternary":"oklch(61.97090% 0.205878 259.740)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.0690","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.0584","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.0381","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.0279","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(72.82755% 0.014297 286.039)"}},{"theme":"light","bg":"#007AFF","vars":{"--lab-bg-tone-2":"oklch(57.87475% 0.012117 286.015)","--lab-bg-tone-3":"oklch(57.87475% 0.012117 286.015)","--lab-label-primary":"oklch(13.41536% 0.016099 291.497)","--lab-label-secondary":"oklch(20.01464% 0.013939 291.638)","--lab-label-tertiary":"oklch(33.31157% 0.013986 291.359)","--lab-label-quaternary":"oklch(48.82850% 0.012656 285.922)","--lab-border-strong":"oklch(13.41536% 0.016099 291.497)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.23588% 0.071518 256.892)","--lab-label-brand-tertiary":"oklch(33.52307% 0.122654 257.729)","--lab-label-brand-quaternary":"oklch(49.33014% 0.177503 257.333)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(20.90400% 0.085780 29.234)","--lab-label-danger-tertiary":"oklch(34.90075% 0.142917 28.593)","--lab-label-danger-quaternary":"oklch(51.33845% 0.210254 28.629)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.32691% 0.044866 67.175)","--lab-label-warning-tertiary":"oklch(33.70499% 0.073308 69.356)","--lab-label-warning-quaternary":"oklch(49.32505% 0.107532 68.994)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(90.50991% 0.070001 68.839 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(19.36939% 0.056254 147.869)","--lab-label-success-tertiary":"oklch(32.05045% 0.093836 147.561)","--lab-label-success-quaternary":"oklch(47.21271% 0.138736 147.423)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(87.96697% 0.214930 147.350 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(20.33878% 0.084139 260.257)","--lab-label-info-tertiary":"oklch(33.98919% 0.139378 260.097)","--lab-label-info-quaternary":"oklch(49.73960% 0.201561 259.877)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1336","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.1787","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.0893","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.0639","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.31157% 0.013986 291.359)"}},{"theme":"light","bg":"#FF3B30","vars":{"--lab-bg-tone-2":"oklch(63.60048% 0.011829 286.059)","--lab-bg-tone-3":"oklch(63.60048% 0.011829 286.059)","--lab-label-primary":"oklch(13.83495% 0.015576 284.180)","--lab-label-secondary":"oklch(25.12484% 0.015017 285.269)","--lab-label-tertiary":"oklch(36.40556% 0.013673 291.446)","--lab-label-quaternary":"oklch(51.30243% 0.012497 285.951)","--lab-border-strong":"oklch(13.83495% 0.015576 284.180)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(14.05977% 0.051776 257.879)","--lab-label-brand-secondary":"oklch(25.33060% 0.091490 257.423)","--lab-label-brand-tertiary":"oklch(36.69778% 0.133197 257.540)","--lab-label-brand-quaternary":"oklch(51.93456% 0.187074 257.359)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.49010% 0.059461 29.234)","--lab-label-danger-secondary":"oklch(26.17120% 0.107130 28.474)","--lab-label-danger-tertiary":"oklch(38.31782% 0.156989 28.748)","--lab-label-danger-quaternary":"oklch(53.87750% 0.220648 28.624)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(14.09840% 0.030367 70.955)","--lab-label-warning-secondary":"oklch(25.16306% 0.055216 68.014)","--lab-label-warning-tertiary":"oklch(36.95716% 0.080989 68.209)","--lab-label-warning-quaternary":"oklch(51.83499% 0.113346 68.533)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(94.55166% 0.039779 69.746 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.47470% 0.038038 148.984)","--lab-label-success-secondary":"oklch(24.13011% 0.070822 147.468)","--lab-label-success-tertiary":"oklch(35.33425% 0.103721 147.463)","--lab-label-success-quaternary":"oklch(49.62199% 0.145290 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(93.08890% 0.111383 147.112 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(14.07469% 0.058967 260.480)","--lab-label-info-secondary":"oklch(25.53067% 0.104920 260.137)","--lab-label-info-tertiary":"oklch(37.14168% 0.149591 259.762)","--lab-label-info-quaternary":"oklch(52.13660% 0.212021 259.943)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1812","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.4300","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.1737","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.0995","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(36.40556% 0.013673 291.446)"}}]} \ No newline at end of file +{"_meta":{"purpose":"Pre-optimisation byte-identity lock for the JS↔WASM recheck boundary (recheckContrast / _recheckContrastMulti) plus a resolveTheme vars snapshot.","regenerated":"2026-07-10","regen_reason":"Глава #64 (ADR-0003): ось читаемости активирована в домене Ys — реактивный recheck (measure_contrast/recheck_against) и solver-вывод считаются в Ys. recheck flat и resolveVars регенерированы против пересобранного движка; inputs (theme/bg/fgs) сохранены. semver-major по ADR.","drift_scope":"resolveVars: exactly 24 leaves changed, all --lab-fx-glow-{brand,danger,warning,neutral}-core (glow-centre colour, background-independent → 4 distinct values). No other var moved. glow base/alpha, labels, borders, fills, backgrounds all byte-identical.","recheck_unchanged":"recheck section: 2752/2752 contrast pairs (lc, wcag) byte-identical. No recheckContrast / contrast value changed. The WCAG relative-luminance + contrast_core + ratio_from_luminances chain is untouched by #185/#186.","numeric_provenance":{"glow-brand-core":"oklch(78.84894% 0.062240 265.472) -> oklch(78.76611% 0.108476 257.256): chroma rises to the blue gamut wall at the functional lightness (fixed-fraction under-saturated it); L ±0.08pp, hue shifts to the cusp.","glow-danger-core":"oklch(81.62208% 0.103545 27.533) -> oklch(81.77103% 0.102764 29.025): near-identical (red already sat near its wall).","glow-warning-core":"oklch(88.80612% 0.081204 68.866) -> oklch(88.88181% 0.082838 68.777): near-identical.","glow-neutral-core":"oklch(99.97226% 0.001314 106.423) -> oklch(100.00000% 0.000000 89.876): neutral hue is achromatic → gamut wall chroma is 0 (was fixed-fraction residue noise); hue_vanished flag fires honestly for the achromatic sentiment."},"wcag_floors_not_weakened":"Glow-centre is a decorative FX bloom colour, NOT a text/border/focus-ring role — no WCAG contrast floor or focus-appearance (WCAG 2.4.13) floor applies to it. Every actual contrast value (recheck section, 2752 pairs) is byte-identical, so no measured contrast dropped below any floor. North invariant #2 (accessibility-by-construction) is preserved: measured, not merely asserted.","isolated_verification_required":"Regen accepted only after an isolated adversarial verifier (not seeing the self-claim) confirms RED→GREEN and that no WCAG floor weakened.","drift_scope_wave1":"resolveVars: 69 info-* leaves changed across 6 theme×bg entries. Zero non-info drift (scope-checked). recheck untouched.","regen_reason_exact_alpha_glow":"ADR-0004: glow ?????? ??????????? ???????? encoded-sRGB8 screen-????????? ? ?????? ???????????? shortest-roundtrip binary64 alpha ?????? ???????????? ?????????? ?? 4 ??????.","drift_scope_exact_alpha_glow":"????? 24 ????? resolveVars: ?????? --lab-fx-glow-{brand,danger,warning,neutral}-alpha ?? 6 ?????. ?? ???? ????, core, ???? ??? ???? var ?? ?????????.","recheck_unchanged_exact_alpha_glow":"recheck: 2752/2752 ???????? ??????? ???????? ?????????."},"recheck":[{"theme":"light","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#00030E","#0057BB","#037BFF","#78AFFF","#150000","#BE0005","#FF070E","#FF9A8C","#1B0D00","#A06300","#D28300","#FFAA3A","#000B01","#007C2D","#00A73F","#00D452","#020013","#4D00F9","#6D6EFF","#9FA9FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.59876563358553,20.60103636973117,79.91552226990729,6.816302360996974,63.95198968935827,3.97647793086433,41.89234722606997,2.2400949113548063,105.32470288110206,20.35108757719671,78.97384099389187,6.579727828852003,63.90360859563009,3.970705427078051,37.83221395673913,2.048194921340976,103.87315757816087,19.018778265876662,70.62528347734393,4.90716956443272,53.97808064225952,3.0056474129326713,34.232350404909745,1.8982213760304238,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.76444562516849,20.751302844974063,83.48658341134008,7.825697912025839,63.67798680778349,3.9439477616440737,40.86459333479343,2.1890112231357013]},{"theme":"light","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E6F0FF","#60A1FF","#0074F3","#004CA4","#FFF4F2","#FF8273","#F40009","#A80004","#FFF7EE","#F79C00","#C97E00","#8F5800","#C2FFC9","#00C84D","#00A03C","#006E26","#EEF0FF","#9099FF","#6662FF","#4300DB"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-97.97941453819666,18.27263411531394,-53.55180732861014,8.039306941172763,-34.54692380627581,4.792369408161928,-17.418806999279397,2.56984376276454,-102.41236835704379,19.474434420712562,-56.92703132210305,8.692613650917824,-34.93266406694178,4.850563950189369,-18.308962222075785,2.6667193472488013,-103.5613239139708,19.790915044333452,-61.97487030298502,9.71021814570805,-44.901407836051874,6.467843223430967,-25.783665577412222,3.5638173443979557,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.707239087904505,6.093571182387199,-23.370874926227668,3.2583602660985775,-98.9883689300502,18.54346384276384,-54.32153113615954,8.18635152260471,-34.304902287955215,4.756029101219059,-14.435475219756622,2.261552965854495]},{"theme":"light","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00163A","#003272","#004494","#040000","#3A0000","#760002","#990003","#0B0400","#261300","#512F00","#845100","#000200","#001D05","#004114","#006322","#000002","#160059","#2B0097","#3B00C5"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.89312592554248,4.521969910173767,28.754774555141132,3.1292028976206594,22.090354756391577,2.35851620111828,40.23106479600237,5.289901483963394,36.823385380889306,4.5064610721321205,27.786796188195932,3.002060282138028,21.02583740042693,2.2570951143110336,39.64148780057383,5.154352713354357,36.89828467875601,4.523118840482662,27.981450040156208,3.027209986332121,13.567318350507929,1.6836851754208233,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.3306726916817,5.312553092992316,36.84988795298528,4.512349526507044,30.916517468784825,3.4313626667845316,25.033605894158644,2.6690076358452903]},{"theme":"light","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#E7F1FF","#73ADFF","#2985FF","#0060CB","#FFF5F4","#FF9687","#FF4336","#CD0006","#FFF8F0","#FFA62C","#DC8A00","#AC6B00","#C7FFCC","#00D251","#00B043","#008631","#EFF1FF","#9CA6FF","#767AFF","#532BFF"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.32090210121694,9.956824779623638,-48.0046310941281,4.945145596338778,-30.285195552724065,3.1849116126306156,-14.342034884792934,1.9106934427040745,-91.71639880150975,10.602849175762582,-51.97724928251108,5.385053682831725,-31.570923815646545,3.301015045785777,-14.82740446463819,1.94471301206827,-92.86717481243348,10.774663089384916,-55.64998948221256,5.805759082702303,-40.41406674606042,4.149700467279057,-23.7537806706175,2.6249801387527425,-87.99158651504085,10.054343074329001,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-88.33220875070053,10.104016013712561,-48.80525652513833,5.032519885671211,-30.915189659062865,3.2415642790980215,-11.323318014385267,1.7063261265170042]},{"theme":"light","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000103","#001536","#003374","#0058BC","#070000","#360000","#730002","#BE0005","#0F0500","#241200","#4F2F00","#A16400","#000400","#001B05","#003F13","#007C2D","#000005","#140055","#3300AC","#4E00FA"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.65687576920386,5.198408498908002,36.59714090951085,4.504660010775197,27.83078380746869,3.029349552004155,13.31049770996219,1.6744770124926043,39.57933742278427,5.181007308853048,36.66186584356392,4.51899176883356,27.77510172821011,3.0221297438766705,12.721590912296357,1.6379804580418844,38.8564182220264,5.016999835671664,36.579572970042136,4.500775701626332,27.65113132417139,3.006116231224861,0,1.205042331050678,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.737197591996,5.216381929144518,36.59037871155487,4.5031645842195545,27.68585404738298,3.010592990868115,17.038043883395044,1.933101474636693]},{"theme":"light","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000104","#00204D","#003B83","#005CC4","#080000","#4C0001","#830002","#C70006","#0F0600","#331C00","#5B3600","#A76800","#000400","#002809","#004917","#00822F","#000005","#1F0075","#3A00C3","#511AFF"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.87966672163739,5.884401548408216,38.30767260595269,4.50632138120243,29.16726657041677,3.0308664461202253,15.823096179531001,1.7789458759332724,43.782795788734404,5.8597651770218455,38.402450836988166,4.527103297945568,29.016037258963053,3.011424325654164,14.9864556348382,1.7254955647131072,42.99821792375369,5.657954629703501,38.41622407485322,4.5301350068404425,28.93769973409828,3.0014083479801514,0,1.28054929135364,43.62397616937584,5.819206103341516,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.970091731453365,5.907320242919889,38.28628402517786,4.501651045763603,28.99214635836292,3.0083657301901288,19.11088367642911,2.0125842767555064]},{"theme":"light","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E6F0FF","#64A4FF","#0077F9","#0050AC","#FFF4F3","#FF8777","#F9000A","#B00005","#FFF7EF","#FA9E00","#CD8000","#955C00","#C3FFC9","#00CA4E","#00A33D","#007329","#EEF0FF","#939CFF","#6967FF","#4600E6"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.3221346483494,16.53807546575917,-53.23458352949761,7.5085332653696355,-34.299151794570456,4.531377660721796,-17.34040000814869,2.4827755446650244,-100.79897840003042,17.636700891614694,-56.73966900010428,8.131153426700068,-34.4661942600266,4.554628420782135,-18.26813498707333,2.5778416349364046,-101.94670279716274,17.922901715066683,-61.50032492399525,9.010497025774457,-44.48245067627942,6.049054371037485,-25.983086445749187,3.447217107352778,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-97.33108904020293,16.783196253636362,-53.9780650339559,7.638815670143083,-34.15649094558268,4.511565332868746,-14.178333309149869,2.174938899863266]},{"theme":"light","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#00030D","#0057B9","#0079FD","#74ADFF","#150000","#BC0005","#FD000A","#FF9788","#1B0D00","#9F6200","#CB7F00","#FFA730","#000B01","#007B2C","#00A63E","#00D251","#020013","#4C00F7","#6C6CFF","#9DA6FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.48497819102282,19.361251289328656,75.9364891028185,6.438321694919453,60.636932359482174,3.8278153397206016,38.76699274290234,2.152541489746932,101.19913515757631,19.11640864166299,75.28055256994783,6.281220843840182,60.46094873358925,3.807428346232839,34.70184973776556,1.9658801332164664,99.74758985463511,17.864929125608796,66.9127104243937,4.673002507305443,52.13428535623209,3.0001216660302674,31.16352525448906,1.8227359456035261,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.421867335088024,3.0235758695027473,33.32486172802245,1.9082022515525083,101.63887790164272,19.492343272893066,79.67393134344084,7.442688338927282,60.17031912503092,3.7741072518393395,37.967753096619255,2.113772626279229]},{"theme":"light","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E6F1FF","#67A5FF","#0079FD","#0053B2","#FFF5F3","#FF8A7A","#FD000A","#B60005","#FFF7EF","#FC9F00","#D08200","#9A5F00","#C4FFCA","#00CB4E","#00A53E","#00772A","#EEF1FF","#949EFF","#6B6BFF","#4900EE"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-94.97224440690107,14.900628115594163,-51.99577035710509,6.808135431331698,-33.457001357615106,4.175350120738554,-16.755883343489252,2.3335896778079332,-99.44736446714371,15.886233758087014,-55.869159717403925,7.428487313030341,-33.63398343987836,4.1977071628099685,-17.709012021143003,2.4235530906296177,-100.15616825242344,16.04465607216319,-60.39258141645385,8.181935837052826,-43.77686148964125,5.57032618168592,-25.67861999448382,3.2497240951569792,-95.39392806862337,14.992417327878547,-58.246405974587134,7.820609198173194,-41.3596549106396,5.227261702394524,-23.16505433719768,2.9752806858720673,-95.97861011098902,15.120061263203354,-52.9765437482588,6.963016139185695,-33.57839540296167,4.190679013723035,-13.485138200601865,2.040069034387161]},{"theme":"light","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EBF3FF","#D5E6FF","#92BFFF","#4291FF","#FFF8F7","#FFDDD8","#FFA597","#FF6253","#FFFAF5","#FFE0BE","#FFAA39","#E79100","#D0FFD4","#7DFF93","#00DA54","#00B947","#F1F4FF","#DFE4FF","#AEB8FF","#8087FF"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.43421936609613,5.100687932363686,-64.08586870464498,4.502412953317652,-41.11294430990105,3.017135229848019,-18.888486914529935,1.8251241221961068,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-40.940708789235714,3.0069340578770496,-21.122834074579163,1.9331720160112165,-77.70409895709017,5.493642615376888,-64.34147182620212,4.520282197700913,-40.849183913847796,3.0015190917179395,-28.18413111501551,2.292444746213784,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-41.2870784084647,3.027463512408998,-26.013778316259767,2.1791858289450103,-73.64084437140687,5.1896286230945075,-64.42349258712335,4.5260223420377566,-41.02780633656069,3.0120908713744514,-19.464187683477977,1.8526993919522168]},{"theme":"light","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#E8F1FF","#79B0FF","#368BFF","#0065D6","#FFF6F4","#FF9B8D","#FF5446","#D80007","#FFF8F1","#FFAB3F","#E18E00","#B47000","#C8FFCD","#00D552","#00B545","#008D34","#EFF1FF","#A0AAFF","#7B81FF","#573FFF"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-89.77514075602531,11.177468536043195,-51.89752791553575,5.733156384550998,-34.996215684604906,3.8114980409485155,-18.92394323577027,2.320895932067837,-94.48648764812391,11.954651961610136,-56.02375667309079,6.252768618847473,-36.81873931987068,4.002056146272223,-19.591172380553846,2.375625103310219,-95.24016771148194,12.080915286992608,-59.85145182180837,6.751641124414219,-44.82390104707481,4.887508677146329,-28.683175933367217,3.184369254481423,-90.45906258458693,11.288985892577246,-57.499124974387314,6.443154991463433,-42.83495757582209,4.660276446547384,-26.099142978345885,2.9428672820648862,-90.66219607331998,11.322193559065964,-52.9772743130274,5.867283737103295,-35.76790053375578,3.8916727925995533,-16.863337707955058,2.1560372721390864]},{"theme":"light","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#000209","#00479B","#0065D5","#3289FF","#100000","#9F0004","#D70007","#FF4F41","#170A00","#7A4A00","#9F6200","#DF8C00","#000801","#006322","#00822F","#00B344","#01000D","#3E00CE","#573EFF","#797EFF"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.22533906008685,12.552007092013115,56.99670466281028,5.352680994694784,44.43662180236468,3.3253647200326193,29.122033451491582,2.0641033245604916,76.01516726706762,12.436514137733148,55.97147975339624,5.133613537650522,43.91010121645754,3.2654000659595037,27.461071919951625,1.9716236837030474,74.81611736383091,11.770648316029503,52.816193854903005,4.528105409994289,41.51373908005361,3.011071841176318,19.679891349505557,1.6112041351931181,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.42233250009274,3.001939806510638,21.536768417458934,1.687644517337887,76.3513076320574,12.62087594492386,59.99704797613187,6.063965240585196,46.62694808670478,3.592268428661898,28.6381619838115,2.036506690369533]},{"theme":"light","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#DA7A00","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.42046624663428,20.43859328774187,80.31672424485741,6.9206271592755195,64.53598724808649,4.047139863973963,42.92136358171458,2.2930773168482466,105.12312344447312,20.1664284240786,79.32640956847692,6.666965706057784,64.47466861031803,4.0396342482899685,38.694434835096864,2.0867987099188317,103.94997559222902,19.08898290736351,71.78028267942507,5.099770465607366,55.19920557284124,3.104259347966041,35.17783363962355,1.9359471784913758,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.57179613789485,3.2209892436744982,37.49012931672191,2.03317973864909,105.58294145619523,20.58664787402852,83.26627791326895,7.757864847967472,64.57504887916579,4.051931818616954,42.090577540190225,2.2501559682241092]},{"theme":"dark","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#E3EEFF","#639EFF","#006DFE","#0048AD","#FFF4F3","#FF7F74","#F1001F","#A80012","#FFF7F1","#FF921B","#D17400","#955100","#B8FFBE","#00C649","#009F39","#006E25","#EDEEFF","#9794FF","#725BFF","#4E00D6"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.73192161780433,17.93999294470544,-52.63286661912575,7.865273702162647,-33.381528921151094,4.618607106385301,-17.0501576858861,2.5303719690800346,-102.4562582898777,19.486486384763445,-56.148341062851905,8.53993960492931,-34.330992536827104,4.759940239487347,-18.37234103122508,2.6737006902768288,-103.68997976483836,19.82648045930809,-60.35961607275431,9.379380118591195,-43.39157032733722,6.209218370112115,-24.93643207012505,3.4548840566407524,-97.27916136850891,18.085610527348415,-59.34389647192141,9.173841233004914,-42.25887199411116,6.018336366435839,-23.360562325895305,3.2570864547421428,-97.9872053947921,18.274719246988955,-53.323021296304695,7.99582368531923,-33.63351308594995,4.6559156780457185,-14.771084781300106,2.2949481607054216]},{"theme":"dark","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000104","#00153F","#00317C","#00429F","#070000","#3A0002","#750009","#9B0010","#0C0300","#291200","#562D00","#8B4B00","#000300","#001D05","#004112","#006521","#010005","#1A0057","#350098","#4700C4"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.20955594591163,5.284998875760269,36.83828085440759,4.509769828130583,28.330761142327287,3.0728704517581886,21.819504217645104,2.3321848000622474,40.142120786515875,5.269603471071473,36.80831529061993,4.503115584720142,27.930310922781615,3.0205822411426944,20.559717681283935,2.214401964672886,39.70511745091868,5.169077378914306,36.80795164205375,4.503034882134433,27.78871707797606,3.002307432697674,14.025581970431295,1.7128949100718707,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.795231110735635,4.500212645278562,30.09285126953815,3.3133648365325183,24.185949678819867,2.5748913454036786]},{"theme":"dark","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E4EFFF","#75AAFF","#3482FF","#005AD5","#FFF6F4","#FF9389","#FF3C3B","#CC0019","#FFF8F2","#FFA350","#E58000","#B36300","#BCFFC2","#00D04D","#00AF3F","#00862E","#EEEFFF","#A2A1FF","#7F74FF","#601DFF"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-86.07057705487603,9.776047393674569,-47.005198509651805,4.836992692850337,-29.617188814986996,3.125340410912816,-13.465221764046625,1.8500449052652255,-92.15650032550448,10.66842694645063,-51.122226750198095,5.289032001141636,-30.69564720892069,3.221769980092269,-14.697188723997265,1.9355550836437716,-92.95341233620333,10.787583067801071,-55.01595924446716,5.732183607436098,-39.01368610071904,4.00959458617392,-22.717127042375882,2.5408179478715844,-86.56463892830851,9.847321829129962,-52.766100540364825,5.474290510931042,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.32881842479753,9.957973594755346,-47.6452882768299,4.906141960838558,-29.99057738527554,3.1585747205458907,-10.990631160230787,1.6845762464445797]},{"theme":"dark","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000107","#00133B","#00317C","#0054C7","#0B0000","#360002","#730009","#BF0016","#100500","#261000","#542C00","#A85D00","#000501","#001B04","#004012","#007D2B","#010009","#180052","#3B00A6","#5A00F5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.616537893860624,5.189361828546554,36.671589094740156,4.521147554384925,27.767977778595686,3.0212072610618272,13.560368041515986,1.6902988819780091,39.46146930028483,5.1544680971312,36.646426655727396,4.515570174173857,27.733751845040576,3.016779185756929,12.426352071081123,1.6200937148513173,38.82022467282525,5.008731246970903,36.66757146424993,4.520256699490143,27.6755983348751,3.009270049666561,0,1.2356833222737893,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.666598999480925,5.200587158978297,36.5946359240296,4.504106003407952,27.684199915070796,3.0103795763779906,16.582944142840912,1.898781610023956]},{"theme":"dark","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000107","#001E53","#00398D","#0058CF","#0C0000","#4C0004","#83000C","#C60018","#110500","#371A00","#603300","#AE6000","#000601","#002808","#004916","#00822D","#01000A","#250070","#4400BC","#5E01FF"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.84943203331798,5.876721182230661,38.357196991852035,4.517162998132957,28.969046969390032,3.0054117916551304,16.078397205708793,1.7957132708136676,43.66281217184597,5.829141814712315,38.38176722933373,4.522556047220382,28.96308732518828,3.004650205577566,15.121014183458136,1.7339400568635355,43.01534021323247,5.66238735865634,38.36369120360285,4.518587529333456,28.956857085057415,3.003854272405285,0,1.32359073643957,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.88937786124038,5.8868666342953615,38.32550423955951,4.510220548286027,28.94413025668513,3.002229117722787,19.264807559936084,2.024522036885816]},{"theme":"dark","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E3EEFF","#66A0FF","#0E71FF","#004CB5","#FFF5F3","#FF8378","#F60020","#AF0013","#FFF7F1","#FF962A","#D57700","#9B5500","#B9FFBF","#00C849","#00A23A","#007226","#EDEEFF","#9997FF","#7460FF","#5200DF"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-95.07464172795706,16.23701078357789,-51.883866319617056,7.27432706128743,-33.10437599843017,4.366728841176053,-16.962076325331164,2.444617555127284,-101.23789901188303,17.745933910216884,-55.660852739640006,7.937252290960222,-33.86013630190819,4.470540437936136,-18.127983335051407,2.5633448249431416,-102.03269987499108,17.94442049171427,-60.01549050114797,8.732123022916136,-43.24478677485694,5.853976070416037,-25.11442835418257,3.342537791539289,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.852118066322895,5.637910373772879,-23.122294138224145,3.108849028833141,-96.32992550494482,16.539962663017093,-52.86610846158114,7.444323350911882,-33.336065162067605,4.398429825394501,-14.383943910910213,2.194178799785328]},{"theme":"dark","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#D67800","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.29489852310851,19.198605473400605,76.19115652133168,6.500760037097026,60.41041952456072,3.801604173546957,38.795795858188825,2.153958743950203,100.99755572094737,18.94295256384705,75.20084184495119,6.262487955668726,60.349100886792264,3.7945539156195487,34.56886711157111,1.9601948416949677,99.82440786870325,17.93087452582946,67.65471495589931,4.7903727911067815,52.29898260017819,3.0135204434714913,31.0522659160978,1.8184953129572932,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.44622841436909,3.025575236651027,33.364561593196164,1.9098288766402804,101.45737373266948,19.337677744697757,79.14071018974319,7.287203401685083,60.44948115564005,3.8061054053754897,37.96500981666448,2.1136413881018767]},{"theme":"dark","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E3EEFF","#69A2FF","#1975FF","#004FBB","#FFF5F3","#FF877C","#FA0021","#B50014","#FFF7F1","#FF9833","#D87900","#A05800","#B9FFC0","#00CA4A","#00A43B","#007628","#EDEEFF","#9B99FF","#7664FF","#5500E7"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-93.28410718321774,14.535439506623602,-51.011442228336165,6.654206660130301,-32.65955270222981,4.075313081844305,-16.370466566638967,2.297770413224217,-99.44736446714371,15.886233758087014,-55.07262883280597,7.299026812522581,-33.02825162884881,4.121422426501604,-17.569909707299868,2.4103015359642868,-100.24216533025177,16.063919770413232,-58.9183675206033,7.932997007393653,-42.52780974275247,5.391835753982905,-24.79898612463087,3.1522681596337323,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.905085629416114,5.1638402024196015,-22.782862127638978,2.93464773079297,-94.53939096020551,14.80664328764592,-51.95162400089297,6.801199303451522,-32.707141977036414,4.081250761977603,-13.69412377461175,2.0581050526656677]},{"theme":"dark","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E8F1FF","#D5E6FF","#95BEFF","#498FFF","#FFF9F8","#FFDDD8","#FFA59B","#FF5F57","#FFFAF6","#FFDFC5","#FFA95E","#F18700","#C6FFCB","#7DFF91","#00DA51","#00B943","#F0F1FF","#E1E3FF","#B4B6FF","#8982FF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-71.17583391665679,5.008590480308264,-64.08586870464498,4.502412953317652,-40.969901639859465,3.0086620775128243,-18.492326685553408,1.8062558438836862,-77.39176152066469,5.470028106981068,-64.06334225320066,4.50083949888721,-41.06186552000793,3.0141084284175577,-20.58109958398818,1.9067218663179524,-77.74802214310357,5.496966722090798,-64.16882972322054,4.508209630901898,-40.97688137217631,3.00907529195687,-26.93048349706687,2.2267208299060117,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.24499881268402,3.0249663274797034,-25.961518214862377,2.176489332337808,-72.19208007215988,5.082914152992658,-64.24580556994597,4.513590807561596,-40.91738125342747,3.005553523356943,-18.74578210381434,1.818317299241165]},{"theme":"dark","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E5EFFF","#7AADFF","#3D87FF","#005FDF","#FFF6F5","#FF988E","#FF4D48","#D5001A","#FFF8F3","#FFA75A","#EA8300","#BA6700","#BDFFC3","#00D34E","#00B341","#008C31","#EEEFFF","#A5A5FF","#837AFF","#6433FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.52359832003891,10.974551054175622,-50.82334067294272,5.601029591424501,-33.90019185783247,3.6989242542195706,-17.94937633357589,2.2421371448335408,-94.53061684959366,11.962030249162733,-55.11693338695639,6.136943021617957,-35.65662848796355,3.880065704029081,-19.010500723408704,2.3279588656978594,-95.32685818617963,12.095472485308715,-58.832789738116276,6.617312123916825,-43.099834270009964,4.690265818580579,-27.202021451205145,3.0448373808729228,-89.02366089613628,11.055448870118658,-56.507119659393844,6.314879036055816,-41.91393002442956,4.556652813501584,-25.675484951698337,2.9041378624661793,-89.65880574741696,11.158543725868578,-51.69548126800453,5.708204299589472,-34.40239467659594,3.750316223165759,-16.078998268001875,2.0949612264666513]},{"theme":"dark","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00020F","#0045A5","#0060E2","#3F88FF","#140000","#A10011","#D7001B","#FF4F49","#190900","#814600","#A85D00","#EA8300","#000A01","#006320","#00822D","#00B341","#030014","#4A00CC","#6537FF","#847BFF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.15878035868923,12.51550617725489,56.792209524954714,5.308053794065892,44.82231091099692,3.370285744389143,29.010809014184062,2.057710850183084,75.84645644595844,12.343365988484052,55.500674792931,5.036843578382145,43.81499067450534,3.25473208522667,27.348178994525146,1.9655632934834584,74.80511978941351,11.764533211175078,52.793708756474935,4.52413702940673,41.44647230556496,3.0043476386355414,20.422200521634597,1.6411239547419576,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.19495675605003,12.535354496536943,59.24767070343283,5.876074797996776,46.58341111523108,3.5866744685580465,28.54022915013225,2.030988108287992]},{"theme":"dark","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"light-ic","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#000110","#0042E2","#3B78FF","#85AEFF","#150000","#BD0011","#FF001B","#FF998D","#1A0500","#B04A00","#E96400","#FFA174","#000B01","#007C2D","#00A73F","#00D452","#00040B","#00639C","#0089D6","#4DB3FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.76809306682351,20.754602558566376,81.70242596755779,7.298044196933449,63.72862579130605,3.9499301799077116,41.321899652634976,2.211520376313395,105.32470288110206,20.35108757719671,79.14032751842986,6.6207260543602775,64.03544238362204,3.986463961054262,38.10332212672181,2.0602150617562085,104.62285904737456,19.706640245810163,73.93672186564804,5.489431142391079,57.895853978119064,3.3399091341000195,36.26437665881752,1.9807324696579875,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.53601527934143,20.543945015271454,78.36297683006073,6.432254688155311,62.2832355663668,3.784280156272109,42.68471757482252,2.28072562681704]},{"theme":"light-ic","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E8F0FF","#709FFF","#2D6DFF","#0038C6","#FFF4F3","#FF8275","#F30019","#A7000E","#FFF6F1","#FF8C52","#DF5F00","#9D4100","#C2FFC9","#00C84D","#009F3C","#006E27","#DFF0FF","#10A5FF","#0082CB","#005689"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-98.22794037003116,18.339196272629252,-53.8293996799751,8.092203727319841,-34.21813658812841,4.7430333536498335,-15.622162303180753,2.381112763636304,-102.4562582898777,19.486486384763445,-56.97432424245902,8.701923574139988,-34.76332175899368,4.824974728357804,-18.151652148372637,2.6494391947645526,-103.24826961310278,19.70448161329645,-59.10147589327934,9.125072654774526,-40.787696351505694,5.7744786453226835,-22.81212855995232,3.1897383862050708,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.29572232514961,6.0245037012352585,-23.381466511664765,3.259668821930569,-97.12909045511768,18.04563033112125,-52.54162933208091,7.848085216280267,-36.273216344700636,5.055412083856902,-18.54110013749204,2.69234396770907]},{"theme":"light-ic","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00104D","#002488","#0032B2","#040000","#3A0002","#750007","#98000B","#080100","#2E0E00","#602500","#8F3B00","#000200","#001D05","#004114","#006322","#000102","#00192E","#003A5F","#004D7C"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.799792142283025,4.501224406456354,30.04772679653348,3.3069997994324063,23.797727835383583,2.533076638038498,40.23106479600237,5.289901483963394,36.80831529061993,4.503115584720142,27.941932493369514,3.022087143525339,21.190215752118235,2.2723970292907865,40.01441400964806,5.240352767492206,36.83929336929951,4.509994812829007,27.908315541084118,3.0177360720288915,16.77490328531677,1.9034128352995776,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.22977489962569,5.289607582090012,36.84755783504637,4.511831555904809,27.796281383025352,3.0032808914145437,21.058698283639867,2.260143784790762]},{"theme":"light-ic","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#EAF1FF","#81ABFF","#4982FF","#0048F5","#FFF5F4","#FF9589","#FF403B","#CC0013","#FFF7F3","#FF9E6E","#F46900","#BD5000","#C6FFCC","#00D251","#00B043","#008631","#E1F1FF","#45B0FF","#0090E1","#006CA9"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.69551010135778,10.011246151186945,-48.263751486481056,4.97335266465752,-30.44825366943375,3.199530942583609,-12.043546933756078,1.753943193668956,-91.71639880150975,10.602849175762582,-51.72679347557047,5.3568510977778265,-31.21621892784593,3.268795397886287,-14.661523244642579,1.9330507439017046,-92.55409442667087,10.727809933310253,-53.89547122979923,5.603122076267749,-35.95806640240085,3.7112655792375078,-20.04367187639212,2.329915503818876,-87.89039906251095,10.039605852107476,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-86.58838125370931,9.850752189993974,-46.78872683576137,4.81370216625391,-31.383244337404896,3.2839492580871354,-15.736559790538532,2.009284883552937]},{"theme":"light-ic","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000004","#000E48","#002895","#0042E3","#070000","#360001","#730006","#BE0011","#0C0200","#2B0D00","#5F2400","#B04A00","#000400","#001B05","#003F13","#007C2D","#000103","#00172B","#00395C","#00639D"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.74741500641279,5.218664231374835,36.670023859840576,4.5208004701163915,27.672031623266886,3.008810094200282,15.351341296852958,1.8098737923818007,39.57933742278427,5.181007308853048,36.654144364688555,4.517280323585742,27.751464091733354,3.019069960427185,12.671992881770905,1.6349566993987985,39.23716411822194,5.10371254671437,36.601471319299684,4.505617849545331,27.652865692456807,3.006339685224649,7.684471784052528,1.3665581876464832,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.65687576920386,5.198408498908002,36.65150522712613,4.5166954713046765,27.64129393913477,3.0048491045091428,12.054359140378313,1.5979376774972005]},{"theme":"light-ic","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000004","#001864","#002FA8","#0045ED","#080000","#4C0003","#830008","#C60012","#0D0200","#3E1500","#6C2A00","#B84D00","#000401","#002809","#004917","#00822F","#000103","#00233D","#00416A","#0068A4"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.98030914587015,5.909904848562026,38.293478372508915,4.503221171740978,28.94317626341717,3.002107337321695,17.95394033246701,1.9258322867444517,43.782795788734404,5.8597651770218455,38.38866051296957,4.524070782124866,28.984608537472,3.0074014381148784,15.156957289606964,1.7362054736619685,43.437607341847624,5.771384990646562,38.33399448678559,4.512078832150148,28.981628167432756,3.007020263830111,9.892674202630067,1.4441010369850493,43.614187977218386,5.816700203526642,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.88976990866122,5.886966133555759,38.401524303038336,4.526899458852529,29.03631728859714,3.0140233900654434,14.306925700285792,1.683714708917422]},{"theme":"light-ic","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E9F0FF","#74A2FF","#3371FF","#003CD0","#FFF4F3","#FF8679","#F8001A","#AF000F","#FFF6F2","#FF9058","#E36100","#A44400","#C3FFC9","#00CA4E","#00A33D","#007329","#E0F0FF","#21A8FF","#0085D0","#005B90"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.69585202312288,16.62869936569993,-53.53835262595417,7.561646720391368,-33.90853669724825,4.477228400656394,-15.643424374972522,2.314414627530853,-100.79897840003042,17.636700891614694,-56.5097869254783,8.089667628698802,-34.29633291417122,4.530985783372177,-18.10601775891745,2.56107712884258,-101.63443561648342,17.844851007069916,-58.71630179001587,8.4915976550775,-40.34850051574898,5.4087697654311055,-22.889197327723014,3.0820931228283226,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-95.5914257275877,16.361459308701495,-52.26386479821355,7.339891411251783,-35.96637326948896,4.765958094131511,-18.710865986535588,2.6239504003951875]},{"theme":"light-ic","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#000110","#0041DF","#3876FF","#82ACFF","#140000","#BB0011","#FC001B","#FF968A","#1A0500","#AE4900","#E76300","#FF9F70","#000B01","#007B2C","#00A63F","#00D252","#00040B","#00629B","#0088D4","#47B1FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.64252534329776,19.49544279635548,78.08171358923367,6.990900026896573,60.31711036263554,3.7908833425501425,38.14751646036453,2.122399526754989,101.24542779029852,19.156162076239593,75.44574141529571,6.3202869230281635,60.62673294996029,3.82662941345977,34.94277926170305,1.9762445240799382,100.4972913238488,18.51105924752946,70.36281029021609,5.256397052662421,54.37780741212484,3.1906001595018556,32.840214506002376,1.8885138214456323,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.4093478352768,3.0225491335598114,33.31123457222728,1.9076443919396007,101.41044755581565,19.297565623167603,74.60712675826916,6.125340559948453,58.63177587043786,3.604653750954651,39.55009522975671,2.1915837971666887]},{"theme":"light-ic","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E9F1FF","#76A4FF","#3875FF","#003ED7","#FFF4F3","#FF897C","#FC001B","#B50010","#FFF6F2","#FF935D","#E66300","#A94600","#C4FFCA","#00CB4F","#00A53F","#00772B","#E0F0FF","#2BA9FF","#0087D3","#005E95"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-95.34499825068502,14.98175500729861,-52.621417231568934,6.906762415988252,-33.47849770408375,4.178062617945301,-14.893749254597887,2.163552488849563,-99.00844385529109,15.788447906049281,-55.63494162352738,7.390318911682041,-33.467257033822236,4.176644120449875,-17.547592326855693,2.408179342981014,-99.84390107174409,15.974784754119627,-57.911424929350886,7.764836527735112,-39.59013024218227,4.982347876116714,-22.33006111099907,2.8868875311347497,-95.39392806862337,14.992417327878547,-58.26045217149408,7.822951516486187,-41.372488477704024,5.229057297458628,-23.176065198031452,2.976455642863797,-93.8008911828484,14.646846343308852,-51.05892490110174,6.66159703590636,-35.06283530237813,4.380256350040106,-18.07990151348513,2.4590883821528062]},{"theme":"light-ic","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EDF3FF","#D8E6FF","#9ABDFF","#598FFF","#FFF8F7","#FFDDD8","#FFA59A","#FF6156","#FFF9F7","#FFDECE","#FFA77C","#FF7112","#D0FFD4","#7DFF93","#00D955","#00B947","#E5F3FF","#CEE8FF","#79C3FF","#0099EE"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.68698719753608,5.119268434409956,-64.43727527190525,4.526987198998707,-41.01457995095163,3.0113075347043563,-19.296469937711088,1.8446469211142276,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-41.0312066106476,3.012292267380192,-20.969079339398604,1.925648566411713,-77.34727635116013,5.466668081501757,-64.08510797117285,4.502359813071005,-40.89305028080999,3.004113887137535,-23.84545366268376,2.068528569757355,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-40.827825177303865,3.0002560129496776,-26.013778316259767,2.1791858289450103,-71.69069599862902,5.046190228559761,-64.15987367011111,4.5075837056249375,-40.85863800181002,3.0020782433474276,-19.258832894479415,1.8428420367238694]},{"theme":"light-ic","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#EAF1FF","#86AEFF","#5189FF","#0650FF","#FFF6F4","#FF9B8F","#FF5349","#D70015","#FFF7F3","#FFA376","#FA6C00","#C75400","#C8FFCD","#00D553","#00B445","#008D34","#E2F1FF","#50B4FF","#0094E7","#0072B3"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-90.02549742397721,11.218239018758677,-52.117590678997175,5.760385990810306,-35.453068631322715,3.8588722418561026,-17.16315496591595,2.1796295803105563,-94.48648764812391,11.954651961610136,-56.08049309972586,6.260045669338659,-36.68653001054492,3.9880923379680597,-19.42498133620683,2.3619323117143227,-94.88408174929032,12.021194381023134,-58.024655972390946,6.511549338355004,-40.19653971715428,4.366167533936967,-25.014063203920518,2.8441658803090872,-90.45906258458693,11.288985892577246,-57.51342916294683,6.44501257640493,-42.39985031117064,4.611196053798779,-26.099142978345885,2.9428672820648862,-89.03890865199516,11.05791933389335,-51.142776043878534,5.640183677550314,-35.55629387962385,3.86961291577043,-20.57349953874105,2.4573829383449617]},{"theme":"light-ic","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#00010B","#0035BB","#044EFF","#4E86FF","#0F0000","#9F000C","#D60015","#FF4D45","#150400","#903B00","#BB4F00","#F86B00","#000801","#006322","#008230","#00B344","#000307","#005182","#0071B2","#0093E5"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.30385323493446,12.594966222228084,58.55230095328762,5.70794052541694,46.62204712410211,3.5916381056717506,29.005214449343097,2.0573900879913625,76.05323451380515,12.457481581934719,55.93606418979168,5.126251708978212,44.077718465236906,3.284322001440957,27.730078776096413,1.986177453897667,75.43283879959024,12.113874624281086,52.77586207835023,4.520990578468311,41.44187963136357,3.003889327255304,23.868980516769618,1.7916338607346896,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.41072061989075,3.0007825637465393,21.536768417458934,1.687644517337887,76.14729893560808,12.509202438867769,55.739334564053664,5.085602713702602,43.07798550292857,3.173723442083943,28.271676351762963,2.015968674447834]},{"theme":"light-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark-ic","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#D38200","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E2","#8F5BFF","#B49FFF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.36973966240839,20.39226096069594,79.16304803673168,6.626348275559002,63.912632944603466,3.971781216419874,43.318131952880584,2.3140155027501375,105.16667360757414,20.20636839933495,79.43361118518992,6.693805381831162,64.79709340122825,4.079330310903514,38.90470830731088,2.096381256670562,103.76723276112338,18.92212884965907,70.7453629185683,4.926701923645705,54.109273202798526,3.0160152686700097,34.512224892089854,1.9092708836328856,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.58363991444959,3.2220248289870597,37.502971296697396,2.0337403797475795,105.5639998705614,20.569417077144905,82.76169093861434,7.605398027541734,65.05029978451972,4.11090668779679,42.03743585537794,2.2474520733771812]},{"theme":"dark-ic","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#DFEEFF","#4AA0FF","#007AE0","#005198","#FFF4F3","#FF7D79","#EF0030","#A6001E","#FFF7EF","#F99A00","#CA7D00","#915800","#B8FFBE","#00C648","#009F38","#006E24","#F0EDFF","#A98DFF","#894CFF","#6000C7"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.24720863865953,17.81140932471069,-52.09132740819334,7.763491481432673,-34.99617079306692,4.860177125212619,-17.977340963301042,2.6303715448994067,-102.4562582898777,19.486486384763445,-55.7461567674855,8.461541475204797,-34.016004840410574,4.712824540535038,-18.066789785866806,2.6401456993132153,-103.60398268701002,19.802704734429785,-61.68353878496926,9.650187857341214,-44.76775468944945,6.444757556916112,-26.060791619056427,3.599837857825526,-97.27916136850891,18.085610527348415,-59.33082759509665,9.171209294707863,-42.24705692654848,6.016359585841944,-23.350526713793172,3.2558471191579894,-97.94239544297146,18.262727694350296,-53.03355221198167,7.94095381214551,-32.857248309508414,4.541449578831631,-15.041171256747345,2.322063533056465]},{"theme":"dark-ic","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000103","#001834","#00376B","#004A8C","#070000","#390005","#750012","#99001B","#0D0400","#261300","#522F00","#865100","#000300","#001D05","#004112","#006521","#010005","#22004F","#43008F","#5800B7"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.21965913293546,5.287302224624218,36.81767378273908,4.505192880903515,27.950777846769416,3.023233051937069,21.000772856623527,2.25477321245894,40.142120786515875,5.269603471071473,36.88792093057972,4.5208109211231795,27.866909320024153,3.0123854877509646,20.876190217747006,2.243276608438576,39.57947058767665,5.139984882718895,36.89828467875601,4.523118840482662,27.8635036155662,3.011945822214043,13.292640012092916,1.6665035199669143,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.80600977189143,4.502603953746083,29.651273965169533,3.2515347547731954,23.802145579236715,2.533547958530533]},{"theme":"dark-ic","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E1EFFF","#62ACFF","#0087F7","#0065BB","#FFF5F4","#FF928C","#FF3644","#CA0027","#FFF8F1","#FFA533","#DD8900","#AD6A00","#BCFFC2","#00D04C","#00AF3F","#00862E","#F1EFFF","#B29CFF","#956AFF","#7700F5"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-85.70519954500536,9.72347127561709,-46.57313915269262,4.790554592637207,-29.772868283167547,3.1391774083572677,-14.678197720495364,1.9342213705917548,-91.71639880150975,10.602849175762582,-50.907190680193835,5.264997878479995,-30.103011927821978,3.1686137953283313,-14.381529067368087,1.9134497328938256,-92.9101803888625,10.781105347058572,-55.36632323767275,5.772792653136124,-40.27550194757439,4.135743433221995,-23.629833528615176,2.614848030728214,-86.56463892830851,9.847321829129962,-52.75275099572737,5.472775232316544,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.71836703917482,10.014570561299498,-47.715877818006604,4.91379345402092,-29.619990398118716,3.125589171245837,-10.872649839108671,1.6769004806806627]},{"theme":"dark-ic","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000106","#001632","#00366A","#005EAF","#0B0000","#360004","#730011","#BD0023","#100600","#241200","#502E00","#A36300","#000501","#001B04","#004012","#007D2B","#020009","#20004A","#48009A","#6F00E5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.62660370456575,5.191620542906115,36.592103069401674,4.503545882983851,27.695057695096033,3.0117807086970667,12.482735608358766,1.623488823202914,39.46146930028483,5.1544680971312,36.631002505660305,4.512153756975423,27.677944212691237,3.0095726068531357,12.751768680957815,1.6398240151911772,38.729462058672325,4.987982714010286,36.579572970042136,4.500775701626332,27.747483785710887,3.0185550274007946,0,1.2039987886628472,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.63684952401951,5.1939188146581685,36.601297620325695,4.505579426399357,27.609531401873983,3.0007614645527125,16.085712499624492,1.86220122646402]},{"theme":"dark-ic","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000206","#002245","#003E78","#0062B6","#0B0000","#4C0008","#820015","#C50025","#110600","#341C00","#5B3600","#A96700","#000601","#002808","#004916","#00822D","#02000A","#2E0067","#5300AD","#7400EE"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.7603252928922,5.854038847974277,38.29235457140463,4.502975854531618,29.083956028514045,3.020138635078335,15.054852030501879,1.729780743830801,43.694363439742204,5.837205585263101,38.354206470247746,4.516507237210082,29.087182234169767,3.020553277003921,15.230546565278194,1.740856512459411,42.92491991360228,5.63897133999608,38.33365423714702,4.512004338988724,28.93769973409828,3.0014083479801514,0,1.2794758858630608,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.85966524740055,5.879321607935794,38.335090707081115,4.512318847459295,28.952846517354917,3.003342035233188,18.7969313728254,1.9885281036480773]},{"theme":"dark-ic","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E0EEFF","#4FA3FF","#007DE4","#00559F","#FFF4F3","#FF817D","#F40031","#AD001F","#FFF7EF","#FB9C00","#CE7F00","#965B00","#B9FFBF","#00C849","#00A239","#007226","#F0EEFF","#AA90FF","#8C52FF","#6500D0"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-94.7101738760577,16.149472101206463,-51.76844025191021,7.254462182627787,-34.605057818633654,4.573999892517157,-17.84521283217997,2.534242228411278,-100.79897840003042,17.636700891614694,-55.24750846744172,7.863491890193123,-33.54190885081095,4.42668652015819,-17.815136446883052,2.5311583129351276,-101.94670279716274,17.922901715066683,-61.02785966423507,8.921519321161206,-44.34904450398409,6.02788949767693,-25.86724540779468,3.4331609270477186,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.840254848173814,5.636085625741139,-23.122294138224145,3.108849028833141,-96.71860922522349,16.634224259107555,-52.4354046324141,7.369572153296626,-32.71416296796099,4.3135863728502315,-14.766203746026353,2.2302399315399106]},{"theme":"dark-ic","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#CC7E00","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E1","#8F5BFF","#B49FFF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.24417193888262,19.155084079579783,75.03748031320593,6.224334741672483,59.78706522107772,3.730817455350078,39.19256422935483,2.1736266322828217,101.04110588404839,18.980469423093126,75.30804346166417,6.287699296730837,60.6715256777025,3.8318416601446055,34.77914058378512,1.9691960254812915,99.64166503759763,17.774143327140518,66.61979519504256,4.627804134340593,52.265155427331,3.010761189364384,30.386657168564103,1.79343744066295,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.45807219092383,3.026547994099155,33.37740357317164,1.9103555042369325,101.43843214703563,19.321492322017065,78.73738474892319,7.17242701812758,60.92473206099399,3.861502331684902,37.91186813185219,2.111101535692422]},{"theme":"dark-ic","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E0EEFF","#53A4FF","#007FE8","#0058A5","#FFF4F3","#FF8580","#F80032","#B30021","#FFF7F0","#FD9D00","#D18100","#9B5E00","#B9FFC0","#00CA4A","#00A43A","#007628","#F0EEFF","#AC93FF","#8D57FF","#6800D8"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-92.91963933131836,14.457074514504098,-50.53751898728166,6.580638229837842,-33.762466759799054,4.213972880586457,-17.265259153354464,2.3814244063048684,-99.00844385529109,15.788447906049281,-54.621704682580784,7.2261694596451385,-32.708249696736004,4.081389020198301,-17.26088719592284,2.381011454094406,-100.19905346524207,16.054261379481122,-59.919533209261445,8.101702037099825,-43.64240726631874,5.55098822531801,-25.561703474916055,3.236683846808585,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.893113879964474,5.162174617584753,-22.782862127638978,2.93464773079297,-94.92807468048419,14.891026660055605,-51.83505302329671,6.7828987244211,-32.07221654016657,4.002369709649521,-14.066989001346808,2.090531498955086]},{"theme":"dark-ic","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E5F1FF","#D2E7FF","#8AC0FF","#2091FF","#FFF9F8","#FFDDDA","#FFA49E","#FF5C5D","#FFFAF5","#FFE0C0","#FFAA42","#E99000","#C6FFCB","#7DFF91","#00DA50","#00B943","#F3F1FF","#E6E2FF","#C1B2FF","#9D7AFF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-70.80493468751378,4.981574213000164,-64.1741844582375,4.5085838821814495,-40.86541522684489,3.0024791016401666,-17.915271666932362,1.7789289638170434,-77.39176152066469,5.470028106981068,-64.14420274941838,4.506488572747314,-40.82594869447877,3.0001450551263997,-20.102672577261966,1.8834970356793017,-77.70409895709017,5.493642615376888,-64.40976920815079,4.525061719384059,-40.93902681557826,3.00683450902807,-28.2154352545988,2.2940964528512438,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.23137463469248,3.02415799426954,-25.961518214862377,2.176489332337808,-72.5833510184408,5.111647048756453,-64.45521953810703,4.528243512077896,-40.9348520371047,3.006587427640115,-18.521762858381592,1.807654812879132]},{"theme":"dark-ic","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E1EFFF","#68AEFF","#008BFD","#006AC4","#FFF6F5","#FF9791","#FF494F","#D30029","#FFF8F1","#FFA941","#E28C00","#B46E00","#BEFFC3","#00D34D","#00B340","#008C31","#F1EFFF","#B49FFF","#9971FF","#7D09FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.0351868676248,10.895768939711282,-50.01919534416472,5.502978516969682,-33.92042169291382,3.7009882121251625,-19.118481017052428,2.3367853131295435,-94.53061684959366,11.962030249162733,-54.89593536437227,6.108854061876529,-35.13675470036395,3.8260433618990066,-18.695569008603144,2.3023140000481574,-95.24016771148194,12.080915286992608,-59.18458546592085,6.6635751999987605,-44.37309985295556,4.8355935478331284,-28.125674388660414,3.1315039953558483,-89.11957165172129,11.070992163190612,-56.49372778683512,6.313154752438012,-41.901500594929324,4.555261373045625,-25.675484951698337,2.9041378624661793,-90.04835436179425,11.221964232051386,-51.2922333885473,5.658542947646642,-34.21126239038315,3.7307191421666435,-15.254686874750304,2.03178289437105]},{"theme":"dark-ic","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00030D","#004D92","#006CC7","#008BFE","#130001","#9F001C","#D60029","#FF4B50","#190B00","#7B4A00","#A06100","#E38C00","#000A01","#006320","#00822D","#00B341","#040013","#5B00BE","#7E16FF","#9971FF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.08600684668272,12.475516218126787,55.96473752983635,5.13221099097022,43.3369682989835,3.2018590596167122,29.318326289650187,2.0754572365320043,75.88156299830736,12.362776535253154,55.82217638546274,5.102669267083783,43.91931922104849,3.266436635421996,27.87409429449102,1.9940348577564484,74.63415754799017,11.66954886455628,52.68236753448497,4.504554642142053,41.632593181405234,3.023005829909607,19.021862742942993,1.5853635699211195,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.18160530883091,12.528031661692317,58.96992438550627,5.808209180198148,47.36214126331064,3.68860336114861,29.106756402593504,2.0632235490419992]},{"theme":"dark-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]}],"resolveVars":[{"theme":"light","bg":"#FFFFFF","vars":{"--lab-bg-tone-2":"oklch(97.84197% 0.010603 286.202)","--lab-bg-tone-3":"oklch(97.84197% 0.010603 286.202)","--lab-label-primary":"oklch(19.12257% 0.014100 291.556)","--lab-label-secondary":"oklch(56.54196% 0.013721 285.953)","--lab-label-tertiary":"oklch(66.97448% 0.014606 285.999)","--lab-label-quaternary":"oklch(81.39504% 0.013897 286.087)","--lab-border-strong":"oklch(19.12257% 0.014100 291.556)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(19.39430% 0.069681 257.297)","--lab-label-brand-secondary":"oklch(57.33316% 0.205939 257.291)","--lab-label-brand-tertiary":"oklch(67.12978% 0.175419 257.690)","--lab-label-brand-quaternary":"oklch(81.31388% 0.094313 257.967)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(20.16318% 0.082740 29.234)","--lab-label-danger-secondary":"oklch(59.62536% 0.244212 28.655)","--lab-label-danger-tertiary":"oklch(69.12968% 0.198650 28.574)","--lab-label-danger-quaternary":"oklch(82.55135% 0.097676 29.389)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(19.32631% 0.042728 66.944)","--lab-label-warning-secondary":"oklch(57.27465% 0.125021 68.799)","--lab-label-warning-tertiary":"oklch(67.73500% 0.148484 68.161)","--lab-label-warning-quaternary":"oklch(82.04043% 0.139600 68.374)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(67.73500% 0.148484 68.161 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(18.59864% 0.054816 147.311)","--lab-label-success-secondary":"oklch(54.91588% 0.161394 147.418)","--lab-label-success-tertiary":"oklch(64.88263% 0.190555 147.443)","--lab-label-success-quaternary":"oklch(79.02491% 0.232159 147.432)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(64.96409% 0.172888 147.450 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(19.54489% 0.077696 259.509)","--lab-label-info-secondary":"oklch(57.68997% 0.232691 259.838)","--lab-label-info-tertiary":"oklch(67.26428% 0.173598 259.980)","--lab-label-info-quaternary":"oklch(81.49851% 0.092914 259.576)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.5","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.5","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.5","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.5","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(66.97448% 0.014606 285.999)"}},{"theme":"light","bg":"#000000","vars":{"--lab-bg-tone-2":"oklch(7.11623% 0.013707 282.350)","--lab-bg-tone-3":"oklch(7.11623% 0.013707 282.350)","--lab-label-primary":"oklch(98.65156% 0.006606 286.278)","--lab-label-secondary":"oklch(80.14049% 0.013952 286.081)","--lab-label-tertiary":"oklch(69.23260% 0.013029 286.055)","--lab-label-quaternary":"oklch(55.16643% 0.013808 285.938)","--lab-border-strong":"oklch(98.65156% 0.006606 286.278)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-brand-secondary":"oklch(80.06451% 0.101154 257.822)","--lab-label-brand-tertiary":"oklch(69.35433% 0.162198 257.610)","--lab-label-brand-quaternary":"oklch(55.96293% 0.201872 257.393)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.64532% 0.006567 28.833)","--lab-label-danger-secondary":"oklch(81.38408% 0.105319 28.863)","--lab-label-danger-tertiary":"oklch(71.19739% 0.181099 28.350)","--lab-label-danger-quaternary":"oklch(58.10538% 0.238004 28.677)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.72985% 0.009577 72.664)","--lab-label-warning-secondary":"oklch(80.98123% 0.149940 68.750)","--lab-label-warning-tertiary":"oklch(70.11346% 0.152989 68.857)","--lab-label-warning-quaternary":"oklch(55.86543% 0.122417 68.219)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.31403% 0.027558 147.033)","--lab-label-success-secondary":"oklch(77.69544% 0.228159 147.448)","--lab-label-success-tertiary":"oklch(67.10085% 0.197388 147.383)","--lab-label-success-quaternary":"oklch(53.45797% 0.157009 147.442)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-info-secondary":"oklch(80.30515% 0.099317 259.806)","--lab-label-info-tertiary":"oklch(69.49027% 0.160387 260.058)","--lab-label-info-quaternary":"oklch(56.23879% 0.227606 259.853)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.013010286081645773","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.01207107843137255","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.009560345877481427","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.00784313725490196","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(69.23260% 0.013029 286.055)"}},{"theme":"light","bg":"#808080","vars":{"--lab-bg-tone-2":"oklch(58.21536% 0.012099 286.018)","--lab-bg-tone-3":"oklch(58.21536% 0.012099 286.018)","--lab-label-primary":"oklch(13.77177% 0.013406 284.503)","--lab-label-secondary":"oklch(20.89710% 0.013787 291.711)","--lab-label-tertiary":"oklch(33.70211% 0.013945 291.371)","--lab-label-quaternary":"oklch(49.18380% 0.012632 285.926)","--lab-border-strong":"oklch(13.77177% 0.013406 284.503)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.89968% 0.076454 257.725)","--lab-label-brand-tertiary":"oklch(34.17043% 0.122830 257.309)","--lab-label-brand-quaternary":"oklch(49.70427% 0.178878 257.337)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(21.88171% 0.089792 29.234)","--lab-label-danger-tertiary":"oklch(35.54757% 0.145582 28.627)","--lab-label-danger-quaternary":"oklch(51.73054% 0.211869 28.643)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.90753% 0.046395 66.438)","--lab-label-warning-tertiary":"oklch(34.00266% 0.074904 67.452)","--lab-label-warning-quaternary":"oklch(49.79045% 0.108745 68.715)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(91.20808% 0.064865 69.118 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(20.10741% 0.059351 147.255)","--lab-label-success-tertiary":"oklch(32.71649% 0.095649 147.616)","--lab-label-success-quaternary":"oklch(47.52181% 0.139141 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(88.67895% 0.198079 147.416 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(21.26627% 0.087439 260.146)","--lab-label-info-tertiary":"oklch(34.39884% 0.140838 260.069)","--lab-label-info-quaternary":"oklch(50.11242% 0.202893 259.861)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.11057506131405706","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.11023622047244094","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.07169755954418741","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.047244094488188976","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.70211% 0.013945 291.371)"}},{"theme":"light","bg":"#3A3A3C","vars":{"--lab-bg-tone-2":"oklch(36.65084% 0.011912 285.797)","--lab-bg-tone-3":"oklch(36.65084% 0.011912 285.797)","--lab-label-primary":"oklch(98.92134% 0.005280 286.303)","--lab-label-secondary":"oklch(82.53513% 0.012530 281.039)","--lab-label-tertiary":"oklch(72.82755% 0.014297 286.039)","--lab-label-quaternary":"oklch(61.25907% 0.011943 286.042)","--lab-border-strong":"oklch(98.92134% 0.005280 286.303)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-brand-secondary":"oklch(82.44675% 0.088464 257.008)","--lab-label-brand-tertiary":"oklch(73.08383% 0.140495 257.442)","--lab-label-brand-quaternary":"oklch(61.77245% 0.208353 257.413)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(61.75781% 0.208491 257.338 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.86605% 0.005516 31.054)","--lab-label-danger-secondary":"oklch(83.38591% 0.092088 28.223)","--lab-label-danger-tertiary":"oklch(74.63167% 0.153784 28.842)","--lab-label-danger-quaternary":"oklch(64.10554% 0.244701 28.706)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.75683% 0.008520 67.727)","--lab-label-warning-secondary":"oklch(83.08426% 0.131366 68.787)","--lab-label-warning-tertiary":"oklch(73.86147% 0.161425 68.614)","--lab-label-warning-quaternary":"oklch(61.96370% 0.135496 68.532)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.51065% 0.024195 147.300)","--lab-label-success-secondary":"oklch(80.08887% 0.235054 147.469)","--lab-label-success-tertiary":"oklch(70.67977% 0.207519 147.455)","--lab-label-success-quaternary":"oklch(59.24524% 0.173588 147.533)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-info-secondary":"oklch(82.42821% 0.087905 259.666)","--lab-label-info-tertiary":"oklch(73.17409% 0.139071 259.898)","--lab-label-info-quaternary":"oklch(61.97090% 0.205878 259.740)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.06909778454881225","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.06091370558375635","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.04060913705583756","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.028061954965508264","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(72.82755% 0.014297 286.039)"}},{"theme":"light","bg":"#007AFF","vars":{"--lab-bg-tone-2":"oklch(57.87475% 0.012117 286.015)","--lab-bg-tone-3":"oklch(57.87475% 0.012117 286.015)","--lab-label-primary":"oklch(13.41536% 0.016099 291.497)","--lab-label-secondary":"oklch(20.01464% 0.013939 291.638)","--lab-label-tertiary":"oklch(33.31157% 0.013986 291.359)","--lab-label-quaternary":"oklch(48.82850% 0.012656 285.922)","--lab-border-strong":"oklch(13.41536% 0.016099 291.497)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.23588% 0.071518 256.892)","--lab-label-brand-tertiary":"oklch(33.52307% 0.122654 257.729)","--lab-label-brand-quaternary":"oklch(49.33014% 0.177503 257.333)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(20.90400% 0.085780 29.234)","--lab-label-danger-tertiary":"oklch(34.90075% 0.142917 28.593)","--lab-label-danger-quaternary":"oklch(51.33845% 0.210254 28.629)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.32691% 0.044866 67.175)","--lab-label-warning-tertiary":"oklch(33.70499% 0.073308 69.356)","--lab-label-warning-quaternary":"oklch(49.32505% 0.107532 68.994)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(90.50991% 0.070001 68.839 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(19.36939% 0.056254 147.869)","--lab-label-success-tertiary":"oklch(32.05045% 0.093836 147.561)","--lab-label-success-quaternary":"oklch(47.21271% 0.138736 147.423)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(87.96697% 0.214930 147.350 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(20.33878% 0.084139 260.257)","--lab-label-info-tertiary":"oklch(33.98919% 0.139378 260.097)","--lab-label-info-quaternary":"oklch(49.73960% 0.201561 259.877)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1414396647356095","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.180541833147175","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.09073588245366797","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.0643078283945157","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.31157% 0.013986 291.359)"}},{"theme":"light","bg":"#FF3B30","vars":{"--lab-bg-tone-2":"oklch(63.60048% 0.011829 286.059)","--lab-bg-tone-3":"oklch(63.60048% 0.011829 286.059)","--lab-label-primary":"oklch(13.83495% 0.015576 284.180)","--lab-label-secondary":"oklch(25.12484% 0.015017 285.269)","--lab-label-tertiary":"oklch(36.40556% 0.013673 291.446)","--lab-label-quaternary":"oklch(51.30243% 0.012497 285.951)","--lab-border-strong":"oklch(13.83495% 0.015576 284.180)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(14.05977% 0.051776 257.879)","--lab-label-brand-secondary":"oklch(25.33060% 0.091490 257.423)","--lab-label-brand-tertiary":"oklch(36.69778% 0.133197 257.540)","--lab-label-brand-quaternary":"oklch(51.93456% 0.187074 257.359)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.49010% 0.059461 29.234)","--lab-label-danger-secondary":"oklch(26.17120% 0.107130 28.474)","--lab-label-danger-tertiary":"oklch(38.31782% 0.156989 28.748)","--lab-label-danger-quaternary":"oklch(53.87750% 0.220648 28.624)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(14.09840% 0.030367 70.955)","--lab-label-warning-secondary":"oklch(25.16306% 0.055216 68.014)","--lab-label-warning-tertiary":"oklch(36.95716% 0.080989 68.209)","--lab-label-warning-quaternary":"oklch(51.83499% 0.113346 68.533)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(94.55166% 0.039779 69.746 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.47470% 0.038038 148.984)","--lab-label-success-secondary":"oklch(24.13011% 0.070822 147.468)","--lab-label-success-tertiary":"oklch(35.33425% 0.103721 147.463)","--lab-label-success-quaternary":"oklch(49.62199% 0.145290 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(93.08890% 0.111383 147.112 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(14.07469% 0.058967 260.480)","--lab-label-info-secondary":"oklch(25.53067% 0.104920 260.137)","--lab-label-info-tertiary":"oklch(37.14168% 0.149591 259.762)","--lab-label-info-quaternary":"oklch(52.13660% 0.212021 259.943)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.18357487922705315","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.4395613333149521","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.17777918620864494","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.10167726510894212","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(36.40556% 0.013673 291.446)"}}]} \ No newline at end of file From 1067e086aede954b132160c907b27930ff181477 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 10 Jul 2026 14:33:05 +0300 Subject: [PATCH 05/11] =?UTF-8?q?fix(core):=20=D0=B7=D0=B0=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BD=D0=B5=D1=87=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20alpha=20=D0=B8=20glow=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/labcolors-core/src/alpha.rs | 303 ++++++++++++++++-- crates/labcolors-core/src/config/tests.rs | 4 +- crates/labcolors-core/src/glow.rs | 258 ++++++++++++--- crates/labcolors-core/src/lib.rs | 2 +- crates/labcolors-core/src/material.rs | 9 +- crates/labcolors-core/src/semantic.rs | 48 ++- crates/labcolors-core/src/solve.rs | 7 +- crates/labcolors-core/src/spaces/oklch.rs | 15 +- crates/labcolors-core/src/spaces/p3.rs | 9 +- crates/labcolors-wasm/src/dto.rs | 12 +- crates/labcolors-wasm/src/lib.rs | 11 +- crates/labcolors-wasm/tests/wasm_parity.rs | 46 +-- .../0004-finite-alpha-glow-reference.md | 45 ++- docs/whitepaper.md | 12 +- packages/colors/README.md | 24 +- packages/colors/effective-bg.js | 10 +- .../colors/test/chain-invariants.test.mjs | 9 +- packages/colors/test/runtime.test.mjs | 18 +- .../colors/test/wasm-boundary-parity.test.mjs | 6 + .../colors/test/wasm-boundary.golden.json | 2 +- 20 files changed, 668 insertions(+), 182 deletions(-) diff --git a/crates/labcolors-core/src/alpha.rs b/crates/labcolors-core/src/alpha.rs index acaffef3..ee8b286b 100644 --- a/crates/labcolors-core/src/alpha.rs +++ b/crates/labcolors-core/src/alpha.rs @@ -45,16 +45,15 @@ //! дискретного sRGB8-композита: `#010000` над чёрным уже округляется из белого //! красного канала при `α=0.5/255`, тогда как strict-binary64 пол равен `1/255`. //! -//! Для побайтового пути сначала квантуются солид и фон. Пусть их канал — целые -//! `S, B`, непрерывная инверсия дала `T`, а эмитируемый тинт — -//! `Q = round(T)`. Тогда до итогового round композит равен -//! `S + α·(Q−T)`. При `α < 1` его отклонение строго меньше половины байта; -//! при `α = 1` имеем `T = S` и `Q = S`. Поэтому итоговый -//! [`composite_over_srgb8`] возвращает ровно `S`. Теорема исчерпывающе проверена -//! на всех 65 536 парах `(S, B)` для нескольких α, включая ближайшую к единице -//! снизу. Production-путь всё равно проверяет равенство после композиции и -//! отказывается эмитировать пару при нарушении инварианта: ошибка float не -//! должна превращаться в правдоподобно неверный цвет. +//! Эмиссионный путь НЕ использует continuous-пол как суррогат byte-grid пола. +//! После квантования solid/background он сначала исчерпывающе решает три +//! независимых одноканальных диапазона тинта на запрошенной alpha. Если решения +//! нет, lower-bound по упорядоченным битам `f64` находит первый `binary64`, +//! проходящий ТОТ ЖЕ [`composite_over_srgb8`]; predecessor обязан не проходить. +//! Поэтому округлительно разрешимые пары вроде `#FF0000 @ 0.12 → #1F0000` +//! сохраняют запрошенную прозрачность, а `alphaCoerced` не врёт о деградации. +//! Весь одноканальный домен из 65 536 `(S, B)` проверяет и точный композит, и +//! минимальность фактической alpha. //! //! Это не обещает восстановить исходный тинт: солид из 8-битного hex несёт //! ошибку ≤ 0.5/255, которую инверсия масштабирует в `1/α` раз. Граница @@ -69,6 +68,18 @@ fn is_encoded_rgb(v: [f64; 3]) -> bool { .all(|x| x.is_finite() && (0.0..=1.0).contains(&x)) } +/// Ровно тот же монотонный порядок binary64-операций, что у официального JS- +/// потребителя на непрозрачной подложке: `bg + alpha * (tint - bg)`. +/// Expanded-форма `tint*alpha + bg*(1-alpha)` немонотонна на отдельных ULP- +/// швах из-за двух округлений, поэтому не годится для доказуемого lower-bound. +fn source_over_channel_value(tint: u8, alpha: f64, bg: u8) -> f64 { + f64::from(bg) + alpha * (f64::from(tint) - f64::from(bg)) +} + +fn source_over_channel_srgb8(tint: u8, alpha: f64, bg: u8) -> u8 { + source_over_channel_value(tint, alpha, bg).round() as u8 +} + /// Непрерывная алгебра straight-alpha до квантования. Она нужна инверсии и /// материалам; путь эмитируемого 8-битного пикселя использует /// [`composite_over_srgb8`], чтобы нормализация `byte/255` не сдвигала half-tie. @@ -116,6 +127,10 @@ pub fn composite_over_encoded( /// Округление выполняется один раз, после композиции. Это существенно на /// half-tie: `(250/255)·0.122·255` может стать `30.499…`, хотя эталонная /// byte-reference `250·0.122` равен `30.5` и по round-half-up даёт байт 31. +/// Binary64-операции выполняются как монотонная affine-форма +/// `bg + alpha*(tint-bg)` — ровно тот же порядок использует официальный JS- +/// потребитель на непрозрачной подложке. Expanded-форма запрещена: на ULP-швах +/// она способна дать последовательность PASS→FAIL→PASS при росте alpha. /// /// # Errors /// @@ -124,10 +139,11 @@ pub fn composite_over_srgb8(tint: [u8; 3], alpha: f64, bg: [u8; 3]) -> Result<[u if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) { return Err(format!("alpha вне конечного [0,1]: {alpha}")); } - let channel = |index: usize| { - (f64::from(tint[index]) * alpha + f64::from(bg[index]) * (1.0 - alpha)).round() as u8 - }; - Ok([channel(0), channel(1), channel(2)]) + Ok([ + source_over_channel_srgb8(tint[0], alpha, bg[0]), + source_over_channel_srgb8(tint[1], alpha, bg[1]), + source_over_channel_srgb8(tint[2], alpha, bg[2]), + ]) } fn encoded_to_srgb8(rgb: [f64; 3], label: &str) -> Result<[u8; 3], String> { @@ -417,12 +433,128 @@ pub fn resolve_alpha_analog( Some(AlphaAnalog { tint, alpha }) } +/// Существование byte-тинта при фиксированной alpha проверяется только по +/// крайнему тинту нужного направления. Поканальный output монотонен по tint, а +/// соседние тинты отличаются до round не больше чем на `alpha ≤ 1`, поэтому +/// целевой байт нельзя «перепрыгнуть». +fn srgb8_target_is_feasible(solid: [u8; 3], alpha: f64, bg: [u8; 3]) -> bool { + (0..3).all(|channel| match solid[channel].cmp(&bg[channel]) { + std::cmp::Ordering::Equal => true, + std::cmp::Ordering::Greater => { + source_over_channel_srgb8(u8::MAX, alpha, bg[channel]) >= solid[channel] + } + std::cmp::Ordering::Less => { + source_over_channel_srgb8(u8::MIN, alpha, bg[channel]) <= solid[channel] + } + }) +} + +/// Первый `binary64`, на котором фактический reference-предикат допускает +/// byte-тинт. Положительные `f64` упорядочены своими битами, поэтому полный +/// lower-bound по `[0, 1]` занимает фиксированные 62 шага и не зависит от +/// эвристического epsilon или аналитического округления стенки. +fn first_srgb8_alpha(solid: [u8; 3], bg: [u8; 3]) -> f64 { + if solid == bg { + return 0.0; + } + let mut failing = 0.0_f64.to_bits(); + let mut passing = 1.0_f64.to_bits(); + debug_assert!(!srgb8_target_is_feasible(solid, 0.0, bg)); + debug_assert!(srgb8_target_is_feasible(solid, 1.0, bg)); + + while passing - failing > 1 { + let middle = failing + (passing - failing) / 2; + if srgb8_target_is_feasible(solid, f64::from_bits(middle), bg) { + passing = middle; + } else { + failing = middle; + } + } + f64::from_bits(passing) +} + +/// Канонический byte-тинт при фиксированной alpha. +/// +/// Каналы независимы. Монотонным двоичным поиском находится весь непрерывный +/// диапазон byte-тинтов, которые production-композитор округляет в цель. Из +/// него выбирается байт с минимальной ошибкой до непрерывной инверсии; при +/// точном равенстве — меньший байт. Затем тот же композитор проверяет ответ. +fn srgb8_tint_at_alpha(solid: [u8; 3], alpha: f64, bg: [u8; 3]) -> Option<[u8; 3]> { + if !srgb8_target_is_feasible(solid, alpha, bg) { + return None; + } + let mut tint = [0_u8; 3]; + + for channel in 0..3 { + let solid_channel = solid[channel]; + let bg_channel = bg[channel]; + if solid_channel == bg_channel { + tint[channel] = bg_channel; + continue; + } + if alpha == 0.0 { + return None; + } + + let output = |candidate: u8| source_over_channel_srgb8(candidate, alpha, bg_channel); + let mut lo = 0_u16; + let mut hi = 256_u16; + while lo < hi { + let mid = lo + (hi - lo) / 2; + if output(mid as u8) < solid_channel { + lo = mid + 1; + } else { + hi = mid; + } + } + if lo == 256 || output(lo as u8) != solid_channel { + return None; + } + let first = lo as u8; + + lo = u16::from(first); + hi = 256; + while lo < hi { + let mid = lo + (hi - lo) / 2; + if output(mid as u8) <= solid_channel { + lo = mid + 1; + } else { + hi = mid; + } + } + let last = (lo - 1) as u8; + + let ideal = + f64::from(bg_channel) + (f64::from(solid_channel) - f64::from(bg_channel)) / alpha; + let floor = ideal.floor().clamp(f64::from(first), f64::from(last)) as u8; + let ceil = ideal.ceil().clamp(f64::from(first), f64::from(last)) as u8; + let error = |candidate: u8| { + (source_over_channel_value(candidate, alpha, bg_channel) - f64::from(solid_channel)) + .abs() + }; + tint[channel] = if error(floor).total_cmp(&error(ceil)).is_le() { + floor + } else { + ceil + }; + } + + let composite = [ + source_over_channel_srgb8(tint[0], alpha, bg[0]), + source_over_channel_srgb8(tint[1], alpha, bg[1]), + source_over_channel_srgb8(tint[2], alpha, bg[2]), + ]; + (composite == solid).then_some(tint) +} + /// Эмиссионный sRGB8-путь альфа-аналога. /// /// Входные цвета сначала квантуются до той же byte-сетки, в которой будет -/// эмитирован тинт. Затем выполняется непрерывная инверсия, тинт квантуется и -/// итог проверяется через production-композитор [`composite_over_srgb8`]. Это -/// отделяет continuous API от более сильного побайтового контракта hex/semantic. +/// эмитирован тинт. Если запрошенная alpha допускает хотя бы один точный +/// byte-тинт, она сохраняется буквально. Иначе alpha поднимается до первого +/// `binary64`, который проходит тот же фиксированный Rust/JS reference- +/// композитор; непосредственный predecessor не проходит. Это отдельный, более +/// сильный контракт, чем continuous-инверсия [`resolve_alpha_analog`]. /// /// Недоменная запрошенная α и нарушение точного постусловия возвращаются как /// `Err`: такой результат нельзя отдавать наружу. `Ok(None)` сохранён в форме @@ -439,18 +571,17 @@ pub(crate) fn resolve_alpha_analog_srgb8( } let solid_srgb8 = encoded_to_srgb8(solid, "solid")?; let bg_srgb8 = encoded_to_srgb8(bg, "bg")?; - let to_encoded = |rgb: [u8; 3]| rgb.map(|channel| f64::from(channel) / 255.0); - - let Some(analog) = resolve_alpha_analog( - to_encoded(solid_srgb8), - requested_alpha, - to_encoded(bg_srgb8), - ) else { - return Ok(None); - }; - let tint_srgb8 = encoded_to_srgb8(analog.tint, "resolved tint")?; - verify_srgb8_alpha_analog(solid_srgb8, tint_srgb8, analog.alpha, bg_srgb8)?; - Ok(Some((tint_srgb8, analog.alpha))) + if let Some(tint) = srgb8_tint_at_alpha(solid_srgb8, requested_alpha, bg_srgb8) { + verify_srgb8_alpha_analog(solid_srgb8, tint, requested_alpha, bg_srgb8)?; + return Ok(Some((tint, requested_alpha))); + } + + let floor = first_srgb8_alpha(solid_srgb8, bg_srgb8); + debug_assert!(floor > requested_alpha); + let tint = srgb8_tint_at_alpha(solid_srgb8, floor, bg_srgb8) + .ok_or_else(|| "первая sRGB8-alpha не дала допустимый byte-тинт".to_string())?; + verify_srgb8_alpha_analog(solid_srgb8, tint, floor, bg_srgb8)?; + Ok(Some((tint, floor))) } /// Глубинная проверка постусловия именно в эмитируемой byte-арифметике. @@ -540,6 +671,38 @@ mod tests { ); } + /// Порядок source-over обязан совпадать с `effective-bg.js`. Affine- + /// reference вычисляет `5 + 0.1·(0−5) = 4.5` и округляет канал в 5. + #[test] + fn source_over_half_seam_matches_the_official_js_operation_order() { + assert_eq!(composite_hex("#000505", 0.1, "#050505").unwrap(), "#050505"); + let (tint, actual) = resolve_alpha_analog_hex("#040505", 0.1, "#050505") + .unwrap() + .unwrap(); + assert!( + actual > 0.1, + "запрошенная пара не воспроизводит красный байт 4" + ); + assert_eq!(composite_hex(&tint, actual, "#050505").unwrap(), "#040505"); + } + + /// Expanded-форма давала PASS→FAIL→PASS на трёх соседних `binary64`, что + /// делало обычный lower-bound ложным. Affine-reference монотонен. + #[test] + fn source_over_reference_is_monotone_across_the_known_ulp_seam() { + let centre = 0.812_992_125_984_252_f64; + let alphas = [ + f64::from_bits(centre.to_bits() - 1), + centre, + f64::from_bits(centre.to_bits() + 1), + ]; + let outputs = alphas.map(|alpha| source_over_channel_srgb8(255, alpha, 1)); + assert!( + outputs.windows(2).all(|pair| pair[0] <= pair[1]), + "{outputs:?}" + ); + } + /// Теорема для всей одноканальной области известной α: эталон считается /// целыми как `(122·t + 878·b)/1000`, поэтому тест не повторяет f64-путь /// production-кода и ловит нормализацию через `/255·255` на всех half-tie. @@ -569,6 +732,8 @@ mod tests { for bg in u8::MIN..=u8::MAX { let solid_encoded = [f64::from(solid) / 255.0; 3]; let bg_encoded = [f64::from(bg) / 255.0; 3]; + let requested_is_feasible = + srgb8_tint_at_alpha([solid; 3], requested_alpha, [bg; 3]).is_some(); let (tint, actual_alpha) = resolve_alpha_analog_srgb8(solid_encoded, requested_alpha, bg_encoded) .unwrap_or_else(|error| { @@ -583,6 +748,86 @@ mod tests { got, [solid; 3], "solid={solid}, bg={bg}, requested={requested_alpha}, actual={actual_alpha}, tint={tint:?}" ); + if requested_is_feasible { + assert_eq!( + actual_alpha.to_bits(), + requested_alpha.to_bits(), + "разрешимая requested alpha была изменена: solid={solid}, bg={bg}" + ); + } else { + assert!(actual_alpha > requested_alpha); + let predecessor = f64::from_bits(actual_alpha.to_bits() - 1); + assert!( + srgb8_tint_at_alpha([solid; 3], predecessor, [bg; 3]).is_none(), + "actual alpha не минимальна: solid={solid}, bg={bg}, actual={actual_alpha}" + ); + } + } + } + } + } + + /// Два контрпримера прежнего continuous-floor: byte-grid округление уже + /// делает цель точной, поэтому прозрачность нельзя увеличивать. + #[test] + fn byte_grid_resolver_preserves_every_already_feasible_requested_alpha() { + let tiny_red_alpha = 0.5 / 255.0; + let (tiny_tint, tiny_actual) = + resolve_alpha_analog_hex("#010000", tiny_red_alpha, "#000000") + .unwrap() + .unwrap(); + assert_eq!(tiny_tint, "#FF0000"); + assert_eq!(tiny_actual.to_bits(), tiny_red_alpha.to_bits()); + assert_eq!( + composite_hex(&tiny_tint, tiny_actual, "#000000").unwrap(), + "#010000" + ); + + let (tint, actual) = resolve_alpha_analog_hex("#1F0000", 0.12, "#000000") + .unwrap() + .unwrap(); + assert_eq!(tint, "#FF0000"); + assert_eq!(actual.to_bits(), 0.12_f64.to_bits()); + assert_eq!(composite_hex(&tint, actual, "#000000").unwrap(), "#1F0000"); + } + + /// Валидная, но заведомо слишком малая alpha не должна переполнять + /// целочисленную реконструкцию ни в debug, ни в release. Она поднимается до + /// первой разрешимой sRGB8-точки и сохраняет целевой байт. + #[test] + fn byte_grid_resolver_handles_tiny_normal_and_subnormal_alpha() { + for requested in [2_f64.powi(-100), f64::from_bits(1)] { + let (tint, actual) = resolve_alpha_analog_hex("#010000", requested, "#000000") + .unwrap() + .unwrap(); + assert!(actual > requested); + assert_eq!(composite_hex(&tint, actual, "#000000").unwrap(), "#010000"); + let predecessor = f64::from_bits(actual.to_bits() - 1); + assert!(srgb8_tint_at_alpha([1, 0, 0], predecessor, [0; 3]).is_none()); + } + } + + /// Исчерпывающий одноканальный сертификат reference-предиката: первый + /// `binary64` проходит, непосредственный predecessor — нет. + #[test] + fn byte_grid_alpha_floor_is_first_passing_for_every_channel_pair() { + for solid in u8::MIN..=u8::MAX { + for bg in u8::MIN..=u8::MAX { + let target = [solid, bg, bg]; + let background = [bg; 3]; + let floor = first_srgb8_alpha(target, background); + assert!( + srgb8_tint_at_alpha(target, floor, background).is_some(), + "solid={solid}, bg={bg}, floor={floor} не проходит" + ); + if solid == bg { + assert_eq!(floor.to_bits(), 0.0_f64.to_bits()); + } else { + let predecessor = f64::from_bits(floor.to_bits() - 1); + assert!( + srgb8_tint_at_alpha(target, predecessor, background).is_none(), + "solid={solid}, bg={bg}: predecessor={predecessor} тоже проходит" + ); } } } diff --git a/crates/labcolors-core/src/config/tests.rs b/crates/labcolors-core/src/config/tests.rs index 5099ae50..4412b401 100644 --- a/crates/labcolors-core/src/config/tests.rs +++ b/crates/labcolors-core/src/config/tests.rs @@ -2138,8 +2138,8 @@ fn fx_shadow_stack_composition_is_strictly_progressive_on_light() { /// Ladder@52-строк): (а) на тёмной базе паспорта свечение решается БЕЗ /// деградации, halo = пер-темный якорь источника, α ∈ (0, 1], фактический шаг /// в допуске квантования от контрактной ступени Base; (б) на белом фоне -/// белое нейтральное свечение деградирует ЧЕСТНО (screen гаснет физически) — -/// флаг degraded, не молчание и не ошибка (ADR-0002, закон 2). +/// белое нейтральное свечение деградирует ЧЕСТНО (на белом screen — point-no-op +/// reference-профиля) — флаг degraded, не молчание и не ошибка. #[test] fn glow_roles_resolve_screen_layers() { let cfg = labui_reference(); diff --git a/crates/labcolors-core/src/glow.rs b/crates/labcolors-core/src/glow.rs index 4699106a..a8906262 100644 --- a/crates/labcolors-core/src/glow.rs +++ b/crates/labcolors-core/src/glow.rs @@ -39,14 +39,19 @@ //! — исходный цвет. Радиусы и blur здесь отсутствуют: названия обозначают //! назначение слоёв у потребителя, а не измеренную геометрию. //! -//! Core строится существующей policy [`crate::accent_balance`]: midpoint J′ -//! задаёт seed Oklab-светлоты, chroma берётся на sRGB-границе данного hue. +//! Для хроматического источника core строится существующей policy +//! [`crate::accent_balance`]: midpoint J′ задаёт seed Oklab-светлоты, chroma +//! берётся на sRGB-границе данного hue. Точный sRGB8-нейтраль остаётся +//! нейтральным: у него нет hue, который можно было бы честно «усилить». //! После хроматического преобразования и sRGB8-квантования итоговый J′ не //! объявляется равным seed — фактическая point-метрика core возвращается //! отдельно. Это versioned recipe, а не оптимум, проверенный на наблюдателях. use crate::lcs::LcsColor; -use crate::spaces::srgb::{decode_8bit, hex_from_srgb_encoded, srgb_encoded_from_hex, srgb_to_xyz}; +use crate::spaces::oklab::oklab_to_srgb_linear; +use crate::spaces::srgb::{ + decode_8bit, hex_from_srgb, hex_from_srgb_encoded, srgb_encoded_from_hex, srgb_to_xyz, +}; use crate::spaces::vc::ViewingConditions; use std::cmp::Ordering; @@ -190,6 +195,37 @@ fn validate_viewing_numerics(vc: &ViewingConditions) -> Result<(), String> { )); } } + // Эти поля задокументированы как производные и должны меняться только + // вместе с исходником. `ViewingConditions` пока имеет публичные поля, так + // что проверяем инвариант на внешней границе вместо доверия структуре. + for (name, actual, expected) in [ + ("fl_pow_025", vc.fl_pow_025, vc.fl.powf(0.25)), + ( + "t_inner", + vc.t_inner, + (1.64 - 0.29_f64.powf(vc.n)).powf(0.73), + ), + ] { + if actual.to_bits() != expected.to_bits() { + return Err(format!( + "ViewingConditions.{name} не согласован с исходными полями: {actual} != {expected}" + )); + } + } + Ok(()) +} + +fn validate_lcs_numerics(label: &str, color: &LcsColor) -> Result<(), String> { + for (name, value) in [ + ("jp", color.jp), + ("h_ok", color.h_ok), + ("s", color.s), + ("h_cam", color.h_cam()), + ] { + if !value.is_finite() { + return Err(format!("{label}.{name} не конечен: {value}")); + } + } Ok(()) } @@ -215,9 +251,9 @@ fn jp_from_hex(hex: &str, vc: &ViewingConditions) -> Result { jp_from_srgb8(encoded_bytes(encoded), vc) } -/// Screen-слой над непрозрачным фоном: `bg + α·G·(1−bg)` покомпонентно -/// в reference-домене encoded sRGB. Функция не утверждает, что неизвестный -/// renderer использует тот же compositing/output profile. +/// Непрерывный screen-слой над непрозрачным фоном: `bg + α·G·(1−bg)` +/// покомпонентно в encoded sRGB. Это алгебра до финального sRGB8-round; для +/// эмитируемого reference-пикселя используй [`screen_layer_over_srgb8`]. /// /// # Errors /// @@ -241,6 +277,29 @@ pub fn screen_layer_over_encoded( ]) } +/// Screen-слой в конечном encoded-sRGB8 reference-домене. +/// +/// Формула вычисляется прямо в шкале байтов и округляется ровно один раз: +/// `round(bg + α·glow·(255−bg)/255)`. Указанный слева направо порядок +/// binary64-операций является частью reference-профиля и совпадает с JS- +/// проверкой официального пакета. Нормализация `byte/255` перед обратным +/// умножением способна сдвинуть half-tie на соседний LSB. +/// +/// # Errors +/// +/// `Err`, если `alpha` не конечна или лежит вне `[0,1]`. +pub fn screen_layer_over_srgb8(glow: [u8; 3], alpha: f64, bg: [u8; 3]) -> Result<[u8; 3], String> { + if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) { + return Err(format!("alpha вне конечного [0,1]: {alpha}")); + } + let channel = |index: usize| { + (f64::from(bg[index]) + + alpha * f64::from(glow[index]) * f64::from(u8::MAX - bg[index]) / f64::from(u8::MAX)) + .round() as u8 + }; + Ok([channel(0), channel(1), channel(2)]) +} + /// Изолированный point-замер одного screen-слоя в reference-профиле glow. /// Пространственное перекрытие core/halo сюда намеренно не входит: без /// геометрии и порядка слоёв его нельзя восстановить честно. @@ -256,9 +315,9 @@ pub(crate) fn measure_screen_layer_at_alpha( vc: &ViewingConditions, ) -> Result { validate_viewing_numerics(vc)?; - let tint = srgb_encoded_from_hex(tint_hex)?; - let bg = srgb_encoded_from_hex(bg_hex)?; - let composite_hex = hex_from_srgb_encoded(screen_layer_over_encoded(tint, alpha, bg)?); + let tint = encoded_bytes(srgb_encoded_from_hex(tint_hex)?); + let bg = encoded_bytes(srgb_encoded_from_hex(bg_hex)?); + let composite_hex = composite_hex(screen_layer_over_srgb8(tint, alpha, bg)?); let bg_jp = jp_from_hex(bg_hex, vc)?; let composite_jp = jp_from_hex(&composite_hex, vc)?; let achieved_dj = (composite_jp - bg_jp).abs(); @@ -596,17 +655,17 @@ pub fn solve_screen_alpha_for_dj( best.ok_or_else(|| "конечное множество композитов оказалось пустым".to_string())?; (state, achieved_dj, GlowTargetStatus::Unreachable) }; - let composite_hex = composite_hex(state.bytes); + let selected_hex = composite_hex(state.bytes); let alpha = state.lower.midpoint(state.upper); // Канонический CSS-сериализатор хранит ту же binary64 alpha. Production-проверка // повторно композитит исходное число; строковый parse-round-trip проверяют // граничные и межъязыковые тесты, не затягивая dec2flt-парсер в WASM. let alpha_css = crate::css_alpha_value(alpha)?; - let roundtrip_hex = hex_from_srgb_encoded(screen_layer_over_encoded(glow, alpha, bg)?); - if roundtrip_hex != composite_hex { + let roundtrip_hex = composite_hex(screen_layer_over_srgb8(glow_bytes, alpha, bg_bytes)?); + if roundtrip_hex != selected_hex { return Err(format!( - "выбранная alpha не воспроизвела state: ожидался {composite_hex}, получен {roundtrip_hex}" + "выбранная alpha не воспроизвела state: ожидался {selected_hex}, получен {roundtrip_hex}" )); } @@ -615,7 +674,7 @@ pub fn solve_screen_alpha_for_dj( alpha_css, target_dj, achieved_dj, - composite_hex, + composite_hex: selected_hex, status, }) } @@ -623,8 +682,10 @@ pub fn solve_screen_alpha_for_dj( /// Версионированный двухслойный recipe от источника: `(core_hex, halo_hex)`. /// /// В recipe v1 halo буквально равен источнику. Для core арифметическая середина -/// J′ источника и 100 задаёт seed Oklab-светлоты, а chroma — sRGB-границу его -/// Oklab hue через [`crate::accent_balance::accent_balanced`]. Фактический J′ +/// J′ источника и 100 задаёт seed Oklab-светлоты. У хроматического источника +/// chroma — sRGB-граница его Oklab hue через +/// [`crate::accent_balance::accent_balanced`]; у точного sRGB8-нейтраля chroma +/// остаётся нулевой, потому его численный hue не несёт цветового смысла. Фактический J′ /// эмитированного core измеряется вызывающим кодом: он не обязан совпасть с seed /// после смены координат и sRGB8-квантования. Это recipe, не наблюдательная /// модель красоты или геометрии свечения. @@ -634,12 +695,46 @@ pub fn glow_layers_from_source( ) -> Result<(String, String), String> { validate_viewing_numerics(vc)?; let source_encoded = srgb_encoded_from_hex(source_hex)?; + let source_bytes = encoded_bytes(source_encoded); let canonical_source_hex = hex_from_srgb_encoded(source_encoded); let src = LcsColor::from_hex_with_vc(&canonical_source_hex, vc)?; + validate_lcs_numerics("source", &src)?; let jp_core = (src.jp + 100.0) * 0.5; + if !jp_core.is_finite() { + return Err(format!("core J′ seed не конечен: {jp_core}")); + } let l_core = crate::scale::jp_to_oklab_l(jp_core, vc); - let core = crate::accent_balance::accent_balanced(l_core, src.h_ok, vc).color; - let core_hex = core.to_hex_with_vc(vc); + if !l_core.is_finite() || !(0.0..=1.0).contains(&l_core) { + return Err(format!("core Oklab L вне конечного [0,1]: {l_core}")); + } + + let core_rgb = if source_bytes[0] == source_bytes[1] && source_bytes[1] == source_bytes[2] { + // У точного sRGB8-нейтраля оттенка нет. Число atan2 от матричного шума + // нельзя превращать в насыщенный core: hue-отсутствие сохраняется. + oklab_to_srgb_linear([l_core, 0.0, 0.0]) + } else { + let balanced = crate::accent_balance::accent_balanced(l_core, src.h_ok, vc); + validate_lcs_numerics("core", &balanced.color)?; + for (name, value) in [ + ("l_ok", balanced.l_ok), + ("c_ok", balanced.c_ok), + ("hue_deg", balanced.hue_deg), + ] { + if !value.is_finite() { + return Err(format!("balanced core {name} не конечен: {value}")); + } + } + let hue = balanced.hue_deg.to_radians(); + oklab_to_srgb_linear([ + balanced.l_ok, + balanced.c_ok * hue.cos(), + balanced.c_ok * hue.sin(), + ]) + }; + if core_rgb.into_iter().any(|channel| !channel.is_finite()) { + return Err(format!("core linear-sRGB не конечен: {core_rgb:?}")); + } + let core_hex = hex_from_srgb(core_rgb); Ok((core_hex, canonical_source_hex)) } @@ -684,7 +779,7 @@ mod tests { continue; } let alpha = window[0] + (window[1] - window[0]) * 0.5; - let hex = hex_from_srgb_encoded(screen_layer_over_encoded(tint, alpha, bg).unwrap()); + let hex = composite_hex(screen_layer_over_srgb8(tint_bytes, alpha, bg_bytes).unwrap()); if states .last() .is_some_and(|(_, previous, _)| previous == &hex) @@ -697,7 +792,7 @@ mod tests { // Отдельный endpoint не даёт оракулу молча потерять состояние, которое // теоретически могло бы существовать только при полностью непрозрачном слое. let alpha = 1.0; - let hex = hex_from_srgb_encoded(screen_layer_over_encoded(tint, alpha, bg).unwrap()); + let hex = composite_hex(screen_layer_over_srgb8(tint_bytes, alpha, bg_bytes).unwrap()); if states .last() .is_none_or(|(_, previous, _)| previous != &hex) @@ -739,6 +834,7 @@ mod tests { let valid = [0.25, 0.5, 0.75]; for alpha in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.1, 1.1] { assert!(screen_layer_over_encoded(valid, alpha, valid).is_err()); + assert!(screen_layer_over_srgb8([1, 2, 3], alpha, [4, 5, 6]).is_err()); } for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.1, 1.1] { let mut rgb = valid; @@ -748,6 +844,8 @@ mod tests { } assert!(screen_layer_over_encoded(valid, 0.0, valid).is_ok()); assert!(screen_layer_over_encoded(valid, 1.0, valid).is_ok()); + assert!(screen_layer_over_srgb8([1, 2, 3], 0.0, [4, 5, 6]).is_ok()); + assert!(screen_layer_over_srgb8([1, 2, 3], 1.0, [4, 5, 6]).is_ok()); } #[test] @@ -772,12 +870,10 @@ mod tests { /// рациональную группировку непредставимой стенки трёх каналов. #[test] fn exact_walls_follow_round_half_up_without_binary_boundary_assumptions() { - let red = [1.0 / 255.0, 0.0, 0.0]; - let black = [0.0; 3]; - let at_half = hex_from_srgb_encoded(screen_layer_over_encoded(red, 0.5, black).unwrap()); + let at_half = composite_hex(screen_layer_over_srgb8([1, 0, 0], 0.5, [0; 3]).unwrap()); let just_below_half = f64::from_bits(0.5_f64.to_bits() - 1); let below = - hex_from_srgb_encoded(screen_layer_over_encoded(red, just_below_half, black).unwrap()); + composite_hex(screen_layer_over_srgb8([1, 0, 0], just_below_half, [0; 3]).unwrap()); assert_eq!(below, "#000000"); assert_eq!(at_half, "#010000", "round(0.5) обязан выбрать верхний байт"); @@ -802,6 +898,61 @@ mod tests { assert_eq!(white_states[1].bytes, [1; 3]); } + /// Известная десятичная alpha проверяется независимой целочисленной + /// формулой на всей одноканальной области. Это ловит нормализацию + /// `byte/255 → *255`, которая на `250 × 0.122` теряет точную половину. + #[test] + fn byte_reference_screen_alpha_0122_matches_exact_rational_for_all_channel_pairs() { + const ALPHA_NUMERATOR: u64 = 61; + const ALPHA_DENOMINATOR: u64 = 500; + const BYTE_MAX: u64 = 255; + let denominator = ALPHA_DENOMINATOR * BYTE_MAX; + + for glow in 0_u16..=255 { + for bg in 0_u16..=255 { + let glow = u64::from(glow); + let bg = u64::from(bg); + let numerator = bg * denominator + ALPHA_NUMERATOR * glow * (BYTE_MAX - bg); + let expected = ((2 * numerator + denominator) / (2 * denominator)) as u8; + let actual = screen_layer_over_srgb8([glow as u8, 0, 0], 0.122, [bg as u8, 0, 0]) + .unwrap()[0]; + assert_eq!(actual, expected, "glow={glow}, bg={bg}"); + } + } + + assert_eq!( + screen_layer_over_srgb8([192, 178, 250], 0.122, [0; 3]).unwrap(), + [23, 22, 31] + ); + let measurement = + measure_screen_layer_at_alpha("#C0B2FA", "#000000", 0.122, &ViewingConditions::srgb()) + .unwrap(); + assert_eq!(measurement.composite_hex, "#17161F"); + } + + /// На этой границе точное вещественное сравнение binary64 и фиксированный + /// порядок binary64-операций дают разные классификации. Reference намеренно + /// фиксирует второй вариант — тот же, который выполняет JS-потребитель; + /// солвер выбирает внутренние точки интервалов и перепроверяет результат. + #[test] + fn byte_screen_pins_reference_operation_order_at_float_wall() { + let below_wall = 0.501_968_503_937_007_9_f64; + let predecessor = f64::from_bits(below_wall.to_bits() - 1); + let successor = f64::from_bits(below_wall.to_bits() + 1); + assert_eq!( + screen_layer_over_srgb8([1, 0, 0], predecessor, [1, 0, 0]).unwrap()[0], + 2 + ); + assert_eq!( + screen_layer_over_srgb8([1, 0, 0], below_wall, [1, 0, 0]).unwrap()[0], + 2 + ); + assert_eq!( + screen_layer_over_srgb8([1, 0, 0], successor, [1, 0, 0]).unwrap()[0], + 2 + ); + } + /// Исчерпывающая теорема для одного канала: рациональное разбиение обязано /// совпадать с публичной формулой для всех 65 536 пар `(background, glow)`. /// Три канала независимы по определению screen, поэтому этот перебор @@ -813,13 +964,7 @@ mod tests { let b = background as u8; let g = glow as u8; let states = quantised_composites([g, 0, 0], [b, 0, 0]).unwrap(); - let at_one = screen_layer_over_encoded( - [f64::from(g) / 255.0, 0.0, 0.0], - 1.0, - [f64::from(b) / 255.0, 0.0, 0.0], - ) - .unwrap(); - let final_byte = (at_one[0] * 255.0).round() as u8; + let final_byte = screen_layer_over_srgb8([g, 0, 0], 1.0, [b, 0, 0]).unwrap()[0]; assert_eq!(states.first().unwrap().bytes[0], b); assert_eq!(states.last().unwrap().bytes[0], final_byte); @@ -833,15 +978,9 @@ mod tests { } let alpha = state.lower.midpoint(state.upper); - let out = screen_layer_over_encoded( - [f64::from(g) / 255.0, 0.0, 0.0], - alpha, - [f64::from(b) / 255.0, 0.0, 0.0], - ) - .unwrap(); + let out = screen_layer_over_srgb8([g, 0, 0], alpha, [b, 0, 0]).unwrap(); assert_eq!( - (out[0] * 255.0).round() as u8, - state.bytes[0], + out[0], state.bytes[0], "B={b}, G={g}, state={index}, alpha={alpha}" ); } @@ -872,11 +1011,11 @@ mod tests { #[test] fn serialised_alpha_keeps_the_quantised_target() { let vc = ViewingConditions::dim_surround(); - let tint = srgb_encoded_from_hex("#4A8FFF").unwrap(); - let bg = srgb_encoded_from_hex("#101012").unwrap(); + let tint = encoded_bytes(srgb_encoded_from_hex("#4A8FFF").unwrap()); + let bg = encoded_bytes(srgb_encoded_from_hex("#101012").unwrap()); let bg_jp = LcsColor::from_hex_with_vc("#101012", &vc).unwrap().jp; let measured_at = |alpha: f64| { - let hex = hex_from_srgb_encoded(screen_layer_over_encoded(tint, alpha, bg).unwrap()); + let hex = composite_hex(screen_layer_over_srgb8(tint, alpha, bg).unwrap()); let jp = LcsColor::from_hex_with_vc(&hex, &vc).unwrap().jp; ((jp - bg_jp).abs(), hex) }; @@ -947,11 +1086,11 @@ mod tests { solved.alpha().to_bits(), "CSS round-trip обязан сохранять binary64: tint={tint_hex}, bg={bg_hex}, target={target}" ); - let recomposed = hex_from_srgb_encoded( - screen_layer_over_encoded( - srgb_encoded_from_hex(tint_hex).unwrap(), + let recomposed = composite_hex( + screen_layer_over_srgb8( + encoded_bytes(srgb_encoded_from_hex(tint_hex).unwrap()), emitted_alpha, - srgb_encoded_from_hex(bg_hex).unwrap(), + encoded_bytes(srgb_encoded_from_hex(bg_hex).unwrap()), ) .unwrap(), ); @@ -1041,4 +1180,31 @@ mod tests { assert!(core_hex.starts_with('#')); assert_eq!(core_hex.len(), 7); } + + /// У sRGB8-серого нет определённого hue. Матричный шум Oklab не должен + /// превращаться в насыщенный core ни на одном байте и ни в одной штатной VC. + #[test] + fn exact_achromatic_sources_never_receive_an_invented_core_hue() { + for vc in [ViewingConditions::srgb(), ViewingConditions::dim_surround()] { + for byte in u8::MIN..=u8::MAX { + let source = format!("#{byte:02X}{byte:02X}{byte:02X}"); + let (core, halo) = glow_layers_from_source(&source, &vc).unwrap(); + let [r, g, b] = encoded_bytes(srgb_encoded_from_hex(&core).unwrap()); + assert_eq!(halo, source); + assert_eq!(r, g, "{source} дал цветной core {core}"); + assert_eq!(g, b, "{source} дал цветной core {core}"); + } + } + } + + /// Публичные поля VC позволяют внешнему коду создать несогласованное + /// производное состояние. Такое состояние раньше протаскивало NaN через + /// CAM16 и маскировало его квантизацией в правдоподобный `#000000`. + #[test] + fn layer_recipe_rejects_numerically_degenerate_viewing_conditions() { + let mut vc = ViewingConditions::srgb(); + vc.fl = 1.0e308; + vc.fl_pow_025 = 1.0e308; + assert!(glow_layers_from_source("#4A8FFF", &vc).is_err()); + } } diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 99886672..fa01f865 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -85,7 +85,7 @@ pub use curve::ColorCurve; pub use glow::{ GLOW_BASE_DJ, GLOW_BLOOM_DJ, GLOW_REFERENCE_PROFILE, GLOW_SUBTLE_DJ, GlowConstraintLayer, GlowSolve, GlowTargetStatus, glow_layers_from_source, screen_layer_over_encoded, - solve_screen_alpha_for_dj, + screen_layer_over_srgb8, solve_screen_alpha_for_dj, }; pub use hash::fnv1a_32; pub use ladder::{LadderPosition, LadderTint, ThemeAnchors}; diff --git a/crates/labcolors-core/src/material.rs b/crates/labcolors-core/src/material.rs index c2f4c375..7085c911 100644 --- a/crates/labcolors-core/src/material.rs +++ b/crates/labcolors-core/src/material.rs @@ -36,12 +36,13 @@ //! //! # Пространство //! -//! Композит — гамма-кодированный sRGB (device-пространство браузера/Figma), то же -//! измеренное пространство, что у [`crate::alpha`] (12 Figma-пар roundtrip). WCAG- +//! Композит — гамма-кодированный sRGB reference-профиля [`crate::alpha`], +//! заземлённого 12 Figma-парами, но не выдаваемого за любой browser pipeline. WCAG- //! светлота меряется на кодированном тоне-тинте (квантованном до 8-битного hex — //! эмитируемый цвет `01`), композит над фоном берётся ТОЧНО (без переквантования): -//! ровно так потребитель пересчитывает вердикт из эмитированных `01`/`02`, поэтому -//! гарантия ядра и re-check потребителя тождественны. +//! тем же алгоритмом официальный потребитель пересчитывает вердикт из +//! эмитированных `01`/`02`, поэтому его re-check тождествен ядру. Renderer и +//! color-management применимость принадлежат отдельному conformance-гейту. use crate::spaces::srgb::{hex_from_srgb_encoded, srgb_encoded_from_hex}; use crate::wcag::{ratio_from_luminances, relative_luminance}; diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index 56e6142f..cdb0f570 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -1613,8 +1613,9 @@ pub enum Resolved { /// /// Потребитель красит `--lab-{role}: rgba(tint, α)` — браузер композитит на /// фактической подложке. `composite` — то, во что этот rgba складывается на -/// ФОНЕ РЕЗОЛВА (`α·tint + (1−α)·bg`, кодированный sRGB — device-пространство -/// Figma/браузера); его контраст ([`TranslucentResolved::composite_lc`], +/// ФОНЕ РЕЗОЛВА (`α·tint + (1−α)·bg`) в объявленном encoded-sRGB8 reference- +/// профиле, заземлённом Figma-якорями, но не выдаваемом за универсальный browser +/// pipeline; его контраст ([`TranslucentResolved::composite_lc`], /// [`composite_wcag`](TranslucentResolved::composite_wcag)) — то, что фаза 1 AA меряет /// (контраст полупрозрачной роли определён её композитом, не тинтом). На ином /// фоне композит другой — это и есть смысл альфы; гарантия сформулирована для @@ -1623,8 +1624,8 @@ pub enum Resolved { pub struct TranslucentResolved { /// Тинт `#RRGGBB` — цвет, эмитируемый как `rgba(tint, α)` (без учёта α). tint_hex: String, - /// Фактическая α `(0, 1]` — запрошенная, если разрешима, иначе поднятая до - /// разрешимого минимума (для альфа-аналога; у прямой лестницы = альфа позиции). + /// Фактическая α `(0, 1]` — запрошенная, если существует точный byte-тинт, + /// иначе первый проходящий `binary64` (у прямой лестницы = альфа позиции). alpha: f64, /// Солид-композит `rgba(tint, α)` над фоном резолва, `#RRGGBB`. composite_hex: String, @@ -1640,15 +1641,15 @@ pub struct TranslucentResolved { /// до этого флага такие тени/свечения проходили как валидный резолв /// молча). Параметр-свободный замер: сетка дисплея, не политика. composite_distinct: bool, - /// Запрошенная α была поднята до разрешимого минимума (`α_min`), потому что - /// исходная не воспроизводима в гамуте — честный флаг деградации КОНТРАКТА + /// Запрошенная α была поднята до первого разрешимого sRGB8-значения, потому + /// что ни один byte-тинт не воспроизводит цель — честный флаг деградации КОНТРАКТА /// РОЛИ (симметрия с `compressed`/`degraded`). Ставится только на пути /// альфа-аналога ([`resolve_rgba_inverted`]), где солид-цель фиксирована, а /// α выводится; у прямой лестницы ([`resolve_rgba_direct`]) всегда `false`. /// /// Цвет при этом НЕ врёт: композит фактической пары остаётся ПОБАЙТНО равен /// солиду (двигается только прозрачность — см. [`crate::alpha`]). Флаг лишь - /// объявляет, что эмитированная α — не запрошенная, а минимально разрешимая + /// объявляет, что эмитированная α — не запрошенная, а первая разрешимая /// ([`alpha`](Self::alpha) несёт фактическое значение). alpha_coerced: bool, /// Солидная семейная граница (`border--strong`, M2 ch5c) была @@ -1695,7 +1696,8 @@ impl TranslucentResolved { self.composite_distinct } - /// Запрошенная α была поднята до `α_min` (альфа-аналог с неразрешимой + /// Запрошенная α была поднята до первого проходящего sRGB8-значения + /// (альфа-аналог с неразрешимой /// запрошенной прозрачностью). `false` у прямой лестницы и когда /// запрошенная α разрешима как есть. Цвет композита при этом равен солиду /// побайтно — коэрсится только прозрачность (см. поле-документацию). @@ -1714,9 +1716,10 @@ impl TranslucentResolved { /// /// Слои — [`crate::glow::glow_layers_from_source`] (halo = источник, core = /// пересвет); α — [`crate::glow::solve_screen_alpha_for_dj`] под контрактную -/// ступень на фактическом фоне; `degraded` — честный флаг закона 2 ADR-0002 -/// (цель недостижима даже при α = 1, например на белом — screen гаснет -/// физически; возвращён ближайший достижимый шаг). +/// ступень на фактическом фоне; `degraded` — честный флаг закона 2 ADR-0002: +/// цель не держит ни один достижимый sRGB8-state, поэтому возвращён глобально +/// лучший state. На белом screen является point-no-op только в объявленном +/// reference-профиле — это не утверждение о физическом свечении. #[derive(Debug, Clone, PartialEq)] pub struct GlowResolved { core_hex: String, @@ -2352,8 +2355,9 @@ fn resolve_spec_in( /// Лестница: rgba(`tint`, `alpha`) эмитится напрямую; его композит на фоне /// резолва замеряется для контраста. `tint` — кодированный (byte/255) sRGB. /// -/// Композитинг straight-alpha живёт в device-пространстве (гамма-кодированный -/// sRGB) — тот же путь, что Figma/браузер ([`crate::alpha`]). Контраст меряется +/// Композитинг straight-alpha живёт в версионированном encoded-sRGB8 reference- +/// профиле, который воспроизводит измеренные Figma-якоря ([`crate::alpha`]), но +/// не сертифицирует любой browser/color-management pipeline. Контраст меряется /// на КОМПОЗИТЕ (солид-эквивалент), не на тинте: контраст полупрозрачной роли /// определён тем, во что она складывается на подложке. /// Квантовать кодированный цвет до 8-битной сетки (hex-roundtrip без строки): @@ -2682,9 +2686,9 @@ fn finish_rgba( let bg_linear = decode(bg_encoded); let (composite_lc, _) = measure_contrast(bg_linear, composite_linear, vc); let composite_wcag = crate::wcag::contrast_ratio(composite_q, bg_encoded); - // Отличимость на сетке дисплея (ADR-0002): сравнение по 8-битным hex — тому - // же представлению, в котором браузер отдаст пиксели. Фон квантуется тем же - // форматтером (off-grid фон, напр. blur-среднее, честно садится на байт). + // Отличимость в encoded-sRGB8 reference (ADR-0002): сравнение по тем же + // 8-битным hex, из которых строится сертификат. Фон квантуется тем же + // форматтером; применимость к renderer проверяется отдельно (#241). let composite_distinct = composite_hex != hex_from_srgb_encoded(bg_encoded); Resolved::Translucent(TranslucentResolved { tint_hex: hex_from_srgb_encoded(tint_encoded), @@ -5529,7 +5533,7 @@ mod tests { #[test] fn alpha_coerced_flags_only_when_requested_alpha_raised_to_floor() { - // H1 (аудит 2026-07-03): поднятие α до α_min на пути альфа-аналога — + // H1 (аудит 2026-07-03): поднятие α до byte-grid пола на пути альфа-аналога — // деградация КОНТРАКТА роли (эмитируется не запрошенная α). До флага она // проходила молча: композит побайтно равен солиду, но обещанная // прозрачность подменялась без объявления. Флаг делает подмену видимой. @@ -5576,6 +5580,16 @@ mod tests { t_ok.alpha() ); + // Byte-grid разрешимость сильнее continuous-инверсии: белый красный + // тинт @ 0.12 уже округляется в #1F0000, поэтому коэрсии быть не должно. + let black = BgInput::solid("#000000").unwrap(); + let quantised_solid = srgb_encoded_from_hex("#1F0000").unwrap(); + let quantised = resolve_rgba_inverted(quantised_solid, 0.12, &black, &vc); + let t_quantised = quantised.translucent().unwrap(); + assert!(!t_quantised.alpha_coerced()); + assert_eq!(t_quantised.alpha().to_bits(), 0.12_f64.to_bits()); + assert_eq!(t_quantised.tint_hex(), "#FF0000"); + // Граница: α=1.0 всегда разрешима (тинт=солид) → флаг false даже для // насыщенного солида, который иначе коэрсил бы. let full = resolve_rgba_inverted(dark_solid, 1.0, &white, &vc); diff --git a/crates/labcolors-core/src/solve.rs b/crates/labcolors-core/src/solve.rs index 3741134e..d05a0cde 100644 --- a/crates/labcolors-core/src/solve.rs +++ b/crates/labcolors-core/src/solve.rs @@ -295,9 +295,10 @@ impl BgInput { } } - /// Гамма-кодированный 8-битный sRGB фона (`[0,1]³`, byte/255) — то самое - /// device-пространство, в котором Figma/браузер композитят straight-alpha - /// ([`crate::alpha`]). Альфа-роль ([`crate::semantic::RoleSpec::Ladder`] / + /// Гамма-кодированный 8-битный sRGB фона (`[0,1]³`, byte/255) — домен + /// reference-профиля [`crate::alpha`], заземлённого Figma-якорями без + /// универсального обещания browser pipeline. Альфа-роль + /// ([`crate::semantic::RoleSpec::Ladder`] / /// [`AlphaAnalog`](crate::semantic::RoleSpec::AlphaAnalog)) композитит свой /// тинт на этом фоне для честного замера контраста солид-эквивалента. Для /// [`Solid`](BgInput::Solid) это квантованный дисплей-цвет фона; будущие diff --git a/crates/labcolors-core/src/spaces/oklch.rs b/crates/labcolors-core/src/spaces/oklch.rs index 12fd4135..da4c2bed 100644 --- a/crates/labcolors-core/src/spaces/oklch.rs +++ b/crates/labcolors-core/src/spaces/oklch.rs @@ -9,10 +9,10 @@ //! Значения остаются решёнными в sRGB-гамуте: oklch здесь — система координат //! записи, не расширение гамута. Точность цифр подобрана под БАЙТ-ТОЧНЫЙ //! round-trip: `oklch_css_from_hex` → независимый парс и обратные матрицы → -//! sRGB8. Полный release-гейт `oklch_full_cube` перечисляет все 16 777 216 -//! входов; CI запускает его явно в фактической Linux-среде с закреплённым Rust. -//! PASS относится к target, toolchain и системной `libm` конкретного запуска, а -//! не объявляет переносимость без проверки. Быстрый debug-тест остаётся smoke. +//! sRGB8. Исполняемый тест проверяет детерминированную решётку с шагом 5 по +//! каждому каналу (около 140 тысяч цветов, включая края) и весь серый ramp. +//! Это сильная выборочная проверка, но не полный перебор 16 777 216 входов; +//! переносимость за пределы проверенных target/toolchain она не объявляет. use super::oklab::srgb_linear_to_oklab; use super::srgb::srgb_from_hex; @@ -38,10 +38,9 @@ pub fn oklch_from_hex(hex: &str) -> Result<[f64; 3], String> { /// CSS-строка `oklch(L% C H)` / `oklch(L% C H / A)` из hex-солида и /// опциональной альфы. /// -/// Точность: L — 5 знаков процента, C — 6, H — 3. Байт-точность для конкретных -/// target, toolchain и системной `libm` проверяет полное перечисление в release-тесте -/// `oklch_full_cube`; CI вызывает его с `--ignored`, а debug-решётка служит -/// быстрым smoke-тестом. +/// Точность: L — 5 знаков процента, C — 6, H — 3. Байт-точность проверяется +/// исполняемой решёткой с шагом 5 и полным серым ramp. Полный sRGB8-куб пока не +/// заявляется доказанным: выборочный тест нельзя переименовывать в exhaustive. /// Альфа печатается кратчайшей десятичной записью, которая восстанавливает /// исходный `f64`. Вычисленная прозрачность участвует в сертификате композита, /// поэтому произвольное число знаков после запятой здесь недопустимо. diff --git a/crates/labcolors-core/src/spaces/p3.rs b/crates/labcolors-core/src/spaces/p3.rs index 28341a6b..1d798e8d 100644 --- a/crates/labcolors-core/src/spaces/p3.rs +++ b/crates/labcolors-core/src/spaces/p3.rs @@ -28,8 +28,9 @@ //! [`super::srgb::srgb_gamma_inv`], вторых копий кривой нет. //! //! Точность цифр подобрана под БАЙТ-ТОЧНЫЙ round-trip: `p3_css_from_hex` → -//! парс → XYZ → sRGB даёт исходные 8-битные байты на решётке всего куба -//! (доказательство — тест `round_trip_is_byte_exact_on_lattice`). +//! парс → XYZ → sRGB даёт исходные 8-битные байты на решётке с шагом 5 по +//! каждому каналу (около 140 тысяч цветов, включая края; тест +//! `round_trip_is_byte_exact_on_lattice`). Это не полный перебор куба. use super::srgb::{srgb_from_hex, srgb_gamma, srgb_to_xyz}; @@ -113,12 +114,12 @@ pub fn p3_from_hex(hex: &str) -> Result<[f64; 3], String> { /// /// Точность: 6 знаков на компоненту — ошибка квантования печати ≤ 5·10⁻⁷ при /// полушаге 8-битного канала ≈ 2·10⁻³, запас > 3 порядков; байт-точность -/// round-trip доказана тестом на решётке всего куба. Политика альфы — единая +/// round-trip проверена тестом на решётке с шагом 5. Политика альфы — единая /// (`super::oklch::css_alpha_suffix`): та же, что у oklch-эмиссии. /// /// # Errors /// -/// `Err` — невалидный hex либо альфа вне [0, 1] сверх шумового эпсилона. +/// `Err` — невалидный hex, неконечная альфа либо альфа вне `[0, 1]`. pub fn p3_css_from_hex(hex: &str, alpha: Option) -> Result { let [r, g, b] = p3_from_hex(hex)?; let suffix = super::oklch::css_alpha_suffix(alpha)?; diff --git a/crates/labcolors-wasm/src/dto.rs b/crates/labcolors-wasm/src/dto.rs index d37be188..f4d1239d 100644 --- a/crates/labcolors-wasm/src/dto.rs +++ b/crates/labcolors-wasm/src/dto.rs @@ -141,13 +141,11 @@ pub struct RgbaColor { pub composite_lc: f64, /// The WCAG 2.1 ratio of the composite. pub composite_wcag: f64, - /// `true` when the requested alpha was raised to the smallest resolvable - /// value (`α_min`) because the requested transparency is not reproducible in - /// gamut — an honest, flagged degradation of the role contract (mirrors - /// `SolvedColor::compressed` / `GlowColor::degraded`). The colour never lies: - /// the composite still equals the target solid byte-for-byte; only the - /// alpha carried in `alpha` differs from what was asked. Always `false` for a - /// direct ladder emission. + /// `true`, если запрошенная alpha не допускает ни одного byte-тинта, + /// воспроизводящего цель в affine encoded-sRGB8 reference, и потому поднята + /// до первого проходящего `binary64`. Композит остаётся побайтно равен + /// целевому солиду; меняется только явно возвращённая `alpha`. У прямой + /// лестницы всегда `false`. pub alpha_coerced: bool, /// `true` when a solid family border (`border--strong`, M2 ch5c) was /// darkened along the family curve to meet the AA UI floor (3:1), because the diff --git a/crates/labcolors-wasm/src/lib.rs b/crates/labcolors-wasm/src/lib.rs index 80cd7c34..54a6c161 100644 --- a/crates/labcolors-wasm/src/lib.rs +++ b/crates/labcolors-wasm/src/lib.rs @@ -109,12 +109,11 @@ export interface TranslucentRole { /** WCAG 2.1 ratio of the composite. */ readonly compositeWcag: number; /** - * `true` when the requested alpha was raised to the smallest resolvable value - * (α_min) because the requested transparency is not reproducible in gamut — an - * honest, flagged contract degradation (mirrors `SolvedColor.compressed` / - * `GlowRole.degraded`). The colour never lies: the composite still equals the - * target solid byte-for-byte; only `alpha` differs from what was asked. Always - * `false` for a direct ladder emission. + * `true`, если запрошенная alpha не допускает ни одного byte-тинта, + * воспроизводящего цель в affine encoded-sRGB8 reference, и потому поднята до + * первого проходящего `binary64`. Композит остаётся побайтно равен целевому + * солиду; меняется только явно возвращённая `alpha`. У прямой лестницы всегда + * `false`. */ readonly alphaCoerced: boolean; /** diff --git a/crates/labcolors-wasm/tests/wasm_parity.rs b/crates/labcolors-wasm/tests/wasm_parity.rs index 87cc3a04..ac802618 100644 --- a/crates/labcolors-wasm/tests/wasm_parity.rs +++ b/crates/labcolors-wasm/tests/wasm_parity.rs @@ -1,5 +1,5 @@ -//! Headless-browser parity smoke: the binding's resolve must equal the native -//! `resolve_named_set` it wraps, role for role. +//! Headless-browser contract smoke: JS-binding resolve must equal the core +//! `resolve_named_set` it wraps, role for role, внутри одного wasm runtime. //! //! Run with `wasm-pack test --headless --chrome` (D1 default from the chapter). //! The engine is agnostic (ADR-0001 PR-c): it has no built-in table, so parity @@ -21,7 +21,7 @@ use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); /// The frozen labui passport — the config the built-in default table used to -/// hardcode. Both the boundary (via `loadConfig`) and the native expectation +/// hardcode. Both the boundary (via `loadConfig`) and the core expectation /// (via `ConfigDto` → `ThemeConfig` → `compile_named_role_table`) read this one /// SSOT, so the two sides cannot diverge on their input. /// @@ -40,18 +40,18 @@ const LABUI_JSON: &str = include_str!("data/labui.config.json"); /// real consumer uses must hold parity. Refresh on passport changes. const LABUI_PROD_JSON: &str = include_str!("data/labui.config.prod.json"); -/// Build the native role table for a passport through the same public path +/// Build the core role table for a passport through the same public path /// `loadConfig` uses, so the parity oracle is the core's own compile, not a /// parallel copy. -fn native_table(passport: &str) -> NamedRoleTable { +fn core_table(passport: &str) -> NamedRoleTable { let dto: ConfigDto = serde_json::from_str(passport).expect("passport parses"); let cfg = ThemeConfig::try_from(dto).expect("DTO → ThemeConfig"); cfg.compile_named_role_table().expect("passport compiles") } -/// Build the native labui role table (canonical fixture). -fn native_labui_table() -> NamedRoleTable { - native_table(LABUI_JSON) +/// Build the core labui role table (canonical fixture). +fn core_labui_table() -> NamedRoleTable { + core_table(LABUI_JSON) } /// A boundary engine with the given passport loaded. @@ -101,9 +101,9 @@ fn error_message(err: wasm_bindgen::JsError) -> String { } /// Shared parity assertion: for a passport, the binding's `resolveTheme` -/// must reproduce the native `resolve_named_set`, role for role. Expectations +/// must reproduce the core `resolve_named_set`, role for role. Expectations /// come straight from the core inside the same wasm runtime — never hand-typed. -/// The native side derives its ViewingConditions from the SAME core enum the +/// The core side derives its ViewingConditions from the SAME enum the /// boundary resolves through (`Theme::viewing_conditions()`): a hardcoded /// `srgb()` here silently diverges on any non-srgb theme (dark = dim surround) /// — exactly the miss that kept the old light-only test blind to dim parity. @@ -122,9 +122,9 @@ fn theme_vc(theme: &str) -> ViewingConditions { fn assert_parity(passport: &str, bg_hex: &str, theme: &str) { let bg = BgInput::solid(bg_hex).expect("bg is valid"); - let table = native_table(passport); + let table = core_table(passport); let vc = theme_vc(theme); - let native = resolve_named_set(&bg, &table, &vc); + let core_resolved = resolve_named_set(&bg, &table, &vc); // The binding result for the same inputs, from a loaded config. let engine = boundary_with(passport); @@ -140,7 +140,7 @@ fn assert_parity(passport: &str, bg_hex: &str, theme: &str) { "theme echoed back" ); - for (name, resolved) in &native { + for (name, resolved) in &core_resolved { let entry = get_obj(&roles, name); let kind = get_str(&entry, "kind").expect("every role has a kind"); match resolved { @@ -149,7 +149,7 @@ fn assert_parity(passport: &str, bg_hex: &str, theme: &str) { assert_eq!( get_str(&entry, "hex").as_deref(), Some(solved.hex()), - "{name} hex must match native" + "{name} hex must match core" ); } Resolved::None => { @@ -163,7 +163,7 @@ fn assert_parity(passport: &str, bg_hex: &str, theme: &str) { assert_eq!( get_str(&entry, "tintHex").as_deref(), Some(r.tint_hex()), - "{name} tint must match native" + "{name} tint must match core" ); } Resolved::Glow(g) => { @@ -171,12 +171,12 @@ fn assert_parity(passport: &str, bg_hex: &str, theme: &str) { assert_eq!( get_str(&entry, "coreHex").as_deref(), Some(g.core_hex()), - "{name} glow core must match native" + "{name} glow core must match core" ); assert_eq!( get_str(&entry, "haloHex").as_deref(), Some(g.halo_hex()), - "{name} glow halo must match native" + "{name} glow halo must match core" ); assert_eq!(get_num(&entry, "alpha").to_bits(), g.alpha().to_bits()); assert_eq!(get_str(&entry, "alphaCss").as_deref(), Some(g.alpha_css())); @@ -230,7 +230,7 @@ fn assert_parity(passport: &str, bg_hex: &str, theme: &str) { /// Canonical labui passport (target M1 text-anchor style), white/light. #[wasm_bindgen_test] -fn resolve_theme_matches_native_named_resolve() { +fn resolve_theme_matches_core_named_resolve() { assert_parity(LABUI_JSON, "#FFFFFF", "light"); } @@ -238,7 +238,7 @@ fn resolve_theme_matches_native_named_resolve() { /// path labui takes today must hold parity on both of its theme anchors. /// Guards the class: "a recipe style a real consumer uses is untested in wasm". #[wasm_bindgen_test] -fn resolve_theme_matches_native_on_prod_passport() { +fn resolve_theme_matches_core_on_prod_passport() { assert_parity(LABUI_PROD_JSON, "#FFFFFF", "light"); assert_parity(LABUI_PROD_JSON, "#101012", "dark"); } @@ -286,19 +286,19 @@ fn vars_mirror_reachable_roles_in_oklch() { } /// `recheckContrast` across the wasm boundary: the returned `Float64Array` -/// reproduces the native `resolve_named_set`'s own `(lc, wcag)` per role, accepts +/// reproduces the core `resolve_named_set`'s own `(lc, wcag)` per role, accepts /// the same shorthand hex forms as `resolveTheme`, and rejects a bad foreground. #[wasm_bindgen_test] fn recheck_contrast_boundary_matches_resolve_and_shares_hex_contract() { let bg = "#FFFFFF"; - let native = resolve_named_set( + let core_resolved = resolve_named_set( &BgInput::solid(bg).expect("white is valid"), - &native_labui_table(), + &core_labui_table(), &ViewingConditions::srgb(), ); let mut fgs: Vec = Vec::new(); let mut want: Vec<(f64, f64)> = Vec::new(); - for (_name, resolved) in &native { + for (_name, resolved) in &core_resolved { if let Resolved::Color { solved, .. } = resolved { fgs.push(solved.hex().to_string()); want.push((solved.lc(), solved.wcag_ratio())); diff --git a/docs/decisions/0004-finite-alpha-glow-reference.md b/docs/decisions/0004-finite-alpha-glow-reference.md index 758194c8..af6301ca 100644 --- a/docs/decisions/0004-finite-alpha-glow-reference.md +++ b/docs/decisions/0004-finite-alpha-glow-reference.md @@ -36,16 +36,32 @@ отдельный домен `encoded-sRGB8`: ```text -source-over: round(tint_byte · alpha + bg_byte · (1 − alpha)) +source-over: round(bg_byte + alpha · (tint_byte − bg_byte)) screen: round(bg_byte + alpha · tint_byte · (255 − bg_byte) / 255) ``` -Порядок операций является частью reference-контракта. Он не выдаётся за +Source-over affine-форма алгебраически равна оператору W3C, но в binary64 +монотонна по alpha. Expanded-запись с двумя произведениями на известных ULP- +швах давала PASS→FAIL→PASS и делала lower-bound ложным. Порядок операций +является частью reference-контракта. Он не выдаётся за гарантию неизвестного renderer или color-management pipeline; такую применимость проверяют #233/#241. -Публичные числовые функции отвергают NaN, бесконечности и выход из домена через -`Result`. Различная семантика debug/release запрещена. +Эмиссионный alpha-аналог решается прямо на этой конечной сетке. Сначала +проверяется запрошенная alpha: если существует хотя бы один byte-тинт, она не +меняется. Иначе полный lower-bound по упорядоченным битам `f64` находит первый +`binary64`, который проходит тот же reference-композитор; непосредственный +предшественник обязан не проходить. Каналы независимы, допустимый интервал +тинтов находится монотонным поиском, а канонический тинт минимизирует ошибку до +непрерывной инверсии (при равенстве выбирается меньший байт). Поэтому +`alphaCoerced` означает реальную недостижимость эмитируемой пары, а не более +строгой вспомогательной continuous-модели. + +Публичные прямые compositor/hex/CSS/glow-границы отвергают NaN, бесконечности и +выход из домена через `Result`. Legacy continuous API +`invert_composite_encoded` / `min_alpha_encoded` / `resolve_alpha_analog` +сохраняют прежний `Option`-контракт до отдельной миграции #41. Различная +семантика debug/release запрещена. ### 2. Glow перечисляет все достижимые состояния @@ -64,6 +80,12 @@ screen: round(bg_byte + alpha · tint_byte · (255 − bg_byte) / 255) 2. если такого нет — глобальный максимум `|ΔJ′|`; 3. при равном максимуме — первое состояние по alpha. +Рациональные стенки задают алгебраическое разбиение; финальная проверка выполняет +зафиксированное binary64-выражение в том же порядке, что официальный JS- +потребитель. На отдельных ULP-швах эти определения могут классифицировать саму +стенку по-разному, поэтому production-alpha берётся только из внутренности +интервала и перед возвратом обязана воспроизвести выбранный state. + Так выбор не зависит от предположения о монотонности J′. CAM16-UCS остаётся binary64-метрикой текущей реализации; cross-runtime границы и интервальные гарантии принадлежат #223. @@ -96,15 +118,16 @@ chroma — как recipe core v1. Эмитированный core отдельн - все 65 536 пар одного screen-канала проверяют рациональное разбиение; - независимый finite-state oracle проверяет выбор reached/unreachable; -- source-over при `alpha=0.122` сверяется с точной рациональной формулой на всех - 65 536 парах байтов; +- source-over и screen при `alpha=0.122` сверяются с независимой точной + рациональной формулой на всех 65 536 парах байтов каждый; - JS chain требует нулевого расхождения engine ↔ официальный consumer; -- OKLCH и Display P3 проходят все 16 777 216 sRGB8-входов отдельными обратными - путями в фактической среде release-CI с закреплённым Rust toolchain; +- OKLCH и Display P3 проходят решётку с шагом 5 по каждому каналу (около + 140 тысяч цветов на путь, включая края) и полный серый ramp; - изменение precision имеет быстрые mutation-killer sentinels. -Full-cube PASS относится только к target, toolchain и системной `libm` -конкретного запуска: он не переносится на непроверенную среду. +Полный куб из 16 777 216 входов не заявляется пройденным: выборочная решётка не +подменяет exhaustive-доказательство. Дальнейший conformance-gate отслеживает +#258 и обязан фиксировать target/toolchain, если такой перебор будет добавлен. ## Последствия и миграция @@ -114,6 +137,8 @@ Rust: - `composite_over_encoded(...)` теперь возвращает `Result`; - для финального байтового reference используется `composite_over_srgb8(...)`; +- `screen_layer_over_encoded(...)` остаётся непрерывной алгеброй, а финальный + point-пиксель вычисляет `screen_layer_over_srgb8(...)`; - поля `GlowSolve` читаются через getters; - alpha берётся из `alpha_css()`, статус — из `status()`. diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 5ebb4990..a5d5d151 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -490,8 +490,10 @@ Warning от сползания в красную (danger) зону. Полны - верификация мёртвой ручки `preferred_side` — прямым прогоном резолва (4 темы × 6 фонов × все роли): ноль сдвинутых байт ([`config.rs`](../crates/labcolors-core/src/config.rs)); -- WASM-обёртка гоняет headless-Chrome паритет-тест против нативного - `resolve_named_set`; наружная форма ролей едина — CSS-эмиссия oklch +- WASM-обёртка гоняет headless-Chrome паритет-тест JS-границы против core- + `resolve_named_set` внутри того же wasm runtime. Это проверка проекции и + контракта границы, не native↔WASM/libm доказательство (#223); наружная форма + ролей едина — CSS-эмиссия oklch ([`spaces/oklch.rs`](../crates/labcolors-core/src/spaces/oklch.rs); [`README.md`](../README.md), «Тесты»). @@ -514,8 +516,10 @@ GLASS-режиме база отброшена и `01` лежит над ЖИВ **Единственная решаемая величина — альфа**, и она НЕ рукописная. Солид-канон `01`-над-`02` = `α·T + (1−α)·T = T` — байт-точно равен тону при любой α (композит `T` над `T` есть `T`), поэтому солид тривиально верен; альфа выводится из -GLASS-режима. Тинт над фоном `B` даёт композит `α·T + (1−α)·B` (гамма-кодированный -sRGB — то же измеренное device-пространство, что у [`alpha.rs`](../crates/labcolors-core/src/alpha.rs)). +GLASS-режима. Тинт над фоном `B` даёт композит `α·T + (1−α)·B` в +гамма-кодированном sRGB reference-профиля +[`alpha.rs`](../crates/labcolors-core/src/alpha.rs). Профиль заземлён измеренными +Figma-парами, но не объявляет любой browser/color-management pipeline тождественным. Контракт: композит над ЛЮБЫМ разрешённым фоном обязан держать читаемость поверхности. Поверхность коммитит **ахроматический полюс** максимального контраста на тоне (белый на тёмном `T`, чёрный на светлом — параметр-свободный механизм diff --git a/packages/colors/README.md b/packages/colors/README.md index 20592819..10f49239 100644 --- a/packages/colors/README.md +++ b/packages/colors/README.md @@ -272,19 +272,23 @@ const bg2 = effectiveBackground(panel, { fallback: "#101012" }); ## Размер бандла -| Артефакт | raw | gzip | -|----------|-----|------| -| `labcolors_bg.wasm` | ~313 КБ | ~138 КБ | -| `labcolors.js` (JS-обёртка) | ~17 КБ | ~4 КБ | - -Замер CI-шага `report bundle size` (ci.yml) — raw и gzip. - -Это ВЕСЬ движок: перцептивная модель CAM16, солверы контраста, лестницы, граница конфига. `.wasm` — не JS-байты бандла, а ассет: он **не на критическом пути рендера** — грузится параллельно, компилируется потоково вне главного треда, кэшируется браузером после первой загрузки. Вспомогательные функции (`applyTheme`, `watchTheme`, `adaptTheme`, `effectiveBackground`) — несколько сотен байт чистого JavaScript с tree-shaking через именованные экспорты. +Размер не закреплён в документации приблизительным числом: оно устаревает при +любом изменении солвера. SSOT — шаг CI `report bundle size (gzip)` в `ci.yml`, +который для каждого коммита печатает точные raw-байты и результат `gzip -9` +отдельно для `labcolors_bg.wasm` и wasm-bindgen-обёртки `labcolors.js`. + +Это весь движок: CAM16, солверы контраста, лестницы и граница конфига. `.wasm` +поставляется отдельным ассетом. Будет ли его загрузка критическим путём первого +рендера, определяет интеграция: до первого `resolveTheme` инициализация обязана +завершиться; приложение может preload/кэшировать модуль. JS-хелперы +(`applyTheme`, `watchTheme`, `adaptTheme`, `effectiveBackground`) имеют +именованные экспорты и допускают tree-shaking, но их размер также следует мерить +сборкой, а не описывать приблизительно. Требует современных браузеров (2023+): сборщики Vite / webpack 5 / Next штатно понимают `new URL('….wasm', import.meta.url)` (вывод wasm-pack). В node передавайте wasm-байты напрямую в `init({ module_or_path })`. ### Для аудиторов цепочки поставки - **Network access (Socket и др.):** единственный `fetch` в пакете (`pkg/labcolors.js`) загружает СОБСТВЕННЫЙ `.wasm`-файл пакета при `init(url)` — стандартный лоадер wasm-bindgen. Ни внешних адресов, ни отправки данных, ни исполнения при импорте. В node-пути (передача байтов) `fetch` не вызывается. -- **Bundlephobia `BuildError`:** их webpack-конвейер не умеет `.wasm`-ассеты («loader customization needed») — так падает почти любой WASM-пакет. Реальные размеры — в таблице выше (замер CI-шага `report bundle size`). -- **Zero runtime JS-dependencies:** npm-поле `dependencies` пусто — транзитивной JS/npm-цепочки поставки нет. Rust-крейты сборки (`serde`, `serde_json` и др.) компилируются ВНУТРЬ `.wasm` (учтены в его размере выше); их цепочка аудируется на стороне сборки — `cargo audit` (RustSec) в CI lab-colors. +- **Bundlephobia `BuildError`:** их webpack-конвейер не умеет `.wasm`-ассеты («loader customization needed») — так падает почти любой WASM-пакет. Реальный размер конкретного коммита показывает CI-шаг `report bundle size (gzip)`. +- **Zero runtime JS-dependencies:** npm-поле `dependencies` пусто — транзитивной JS/npm-цепочки поставки нет. Rust-крейты сборки (`serde`, `serde_json` и др.) компилируются ВНУТРЬ `.wasm` (учтены CI-замером); их цепочка аудируется на стороне сборки — `cargo audit` (RustSec) в CI lab-colors. diff --git a/packages/colors/effective-bg.js b/packages/colors/effective-bg.js index 2a512b80..efee31dd 100644 --- a/packages/colors/effective-bg.js +++ b/packages/colors/effective-bg.js @@ -183,9 +183,15 @@ function oklchAlpha(tok) { export function compositeOver(top, bottom) { const at = top[3]; const ab = bottom[3]; - const a = at + ab * (1 - at); + // Affine-форма математически равна expanded source-over, но монотонна по + // top alpha в binary64. Expanded `at + ab*(1-at)` и две цветовые ветви могут + // дать PASS→FAIL→PASS на соседних ULP и разрушить конечный alpha lower-bound. + const a = ab + at * (1 - ab); if (a === 0) return [0, 0, 0, 0]; - const c = (i) => (top[i] * at + bottom[i] * ab * (1 - at)) / a; + const c = (i) => { + const bottomPremultiplied = bottom[i] * ab; + return (bottomPremultiplied + at * (top[i] - bottomPremultiplied)) / a; + }; return [c(0), c(1), c(2), a]; } diff --git a/packages/colors/test/chain-invariants.test.mjs b/packages/colors/test/chain-invariants.test.mjs index 74a3cd68..2c30ad5c 100644 --- a/packages/colors/test/chain-invariants.test.mjs +++ b/packages/colors/test/chain-invariants.test.mjs @@ -9,11 +9,12 @@ // внутри солвера, и без параллельной копии физики контраста. // // Что уже закрыто в другом месте (НЕ дублируем): -// - core `oklch.rs::round_trip_is_byte_exact_*` — emit↔parse байт-точны на КУБЕ; +// - core `oklch.rs::round_trip_is_byte_exact_*` — emit↔parse байт-точны на +// решётке шага 5 и полном сером ramp (не объявлен полный куб); // - `oklch-parse.test.mjs` — parseCssColor декодит эмиссию на 16 фикстурах; // - core `property_invariants.rs::every_floored_role_clears_its_wcag_floor…` — // пол на СОБСТВЕННОМ hex солвера (не на репарснутой строке); -// - wasm `wasm_parity.rs` — граница == нативный резолв, роль-в-роль. +// - wasm `wasm_parity.rs` — JS-граница == core-оракул внутри того же wasm runtime. // Дыра: КОМПОЗИЦИЯ этих доказательств на ЖИВОМ корпусе ролей — «цвет, который // браузер соберёт из emitted vars, всё ещё проходит свой пол/таргет» — как // единая цепочка, а не транзитивность двух изолированных проверок. Для @@ -248,8 +249,10 @@ test("glow roles emit halo primary + -core/-alpha satellites, all well-formed", const [br, bgc, bb] = parseCssColor(bg); const screenHex = (layerCss) => { const [lr, lg, lb] = parseCssColor(layerCss); + // Тот же конечный byte-reference, что у ядра: нормализация + // byte/255 перед обратным умножением сдвигает точные half-tie. const channel = (background, layer) => - (background / 255 + a * (layer / 255) * (1 - background / 255)) * 255; + background + a * layer * (255 - background) / 255; return toHex([channel(br, lr), channel(bgc, lg), channel(bb, lb)]); }; assert.equal(screenHex(res.vars[role.cssVar]), role.haloCompositeHex); diff --git a/packages/colors/test/runtime.test.mjs b/packages/colors/test/runtime.test.mjs index 19ebabb9..647af2bb 100644 --- a/packages/colors/test/runtime.test.mjs +++ b/packages/colors/test/runtime.test.mjs @@ -1,7 +1,7 @@ // Behaviour tests for the framework-free runtime — pure logic + the reactive // controller driven through injected fakes, so they run under plain `node --test` -// with no browser and no WASM. (The WASM↔native parity is covered separately by -// the headless-Chrome `wasm_parity` test.) +// with no browser and no WASM. JS-boundary↔core parity внутри одного wasm runtime +// отдельно проверяет headless-Chrome `wasm_parity` test. import { test } from "node:test"; import assert from "node:assert/strict"; @@ -37,6 +37,20 @@ test("compositeOver is true source-over alpha", () => { assert.equal(r[3], 1); // Fully transparent top → bottom unchanged. assert.deepEqual(compositeOver([9, 9, 9, 0], [40, 50, 60, 1]), [40, 50, 60, 1]); + + // Half-seam из Rust-регрессора: фиксирует тот же порядок binary64-операций, + // чтобы reference-композит и официальный потребитель не разошлись на LSB. + assert.equal(toHex(compositeOver([0, 5, 5, 0.1], [5, 5, 5, 1])), "#050505"); + + // Expanded-форма на этих соседних alpha давала 208→207→208. Affine- + // reference обязан быть монотонным, иначе first-passing alpha недоказуема. + const centre = 0.812992125984252; + const predecessor = centre - Number.EPSILON / 2; + const successor = centre + Number.EPSILON / 2; + const seam = [predecessor, centre, successor].map( + (alpha) => compositeOver([255, 0, 0, alpha], [1, 0, 0, 1])[0], + ); + assert.ok(seam[0] <= seam[1] && seam[1] <= seam[2], String(seam)); }); test("toHex rounds and clamps", () => { diff --git a/packages/colors/test/wasm-boundary-parity.test.mjs b/packages/colors/test/wasm-boundary-parity.test.mjs index 74567456..d5486060 100644 --- a/packages/colors/test/wasm-boundary-parity.test.mjs +++ b/packages/colors/test/wasm-boundary-parity.test.mjs @@ -39,6 +39,12 @@ test("wasm recheck boundary is byte-identical to the pre-optimisation golden", a "utf8", ); const golden = JSON.parse(readFileSync(goldenPath, "utf8")); + const metaText = JSON.stringify(golden._meta); + assert.doesNotMatch( + metaText, + /\?{3,}|\uFFFD/u, + "golden provenance не должен содержать следы потери UTF-8", + ); const engine = new LabColors(); engine.loadConfig(CONFIG); diff --git a/packages/colors/test/wasm-boundary.golden.json b/packages/colors/test/wasm-boundary.golden.json index 2a473d16..63840816 100644 --- a/packages/colors/test/wasm-boundary.golden.json +++ b/packages/colors/test/wasm-boundary.golden.json @@ -1 +1 @@ -{"_meta":{"purpose":"Pre-optimisation byte-identity lock for the JS↔WASM recheck boundary (recheckContrast / _recheckContrastMulti) plus a resolveTheme vars snapshot.","regenerated":"2026-07-10","regen_reason":"Глава #64 (ADR-0003): ось читаемости активирована в домене Ys — реактивный recheck (measure_contrast/recheck_against) и solver-вывод считаются в Ys. recheck flat и resolveVars регенерированы против пересобранного движка; inputs (theme/bg/fgs) сохранены. semver-major по ADR.","drift_scope":"resolveVars: exactly 24 leaves changed, all --lab-fx-glow-{brand,danger,warning,neutral}-core (glow-centre colour, background-independent → 4 distinct values). No other var moved. glow base/alpha, labels, borders, fills, backgrounds all byte-identical.","recheck_unchanged":"recheck section: 2752/2752 contrast pairs (lc, wcag) byte-identical. No recheckContrast / contrast value changed. The WCAG relative-luminance + contrast_core + ratio_from_luminances chain is untouched by #185/#186.","numeric_provenance":{"glow-brand-core":"oklch(78.84894% 0.062240 265.472) -> oklch(78.76611% 0.108476 257.256): chroma rises to the blue gamut wall at the functional lightness (fixed-fraction under-saturated it); L ±0.08pp, hue shifts to the cusp.","glow-danger-core":"oklch(81.62208% 0.103545 27.533) -> oklch(81.77103% 0.102764 29.025): near-identical (red already sat near its wall).","glow-warning-core":"oklch(88.80612% 0.081204 68.866) -> oklch(88.88181% 0.082838 68.777): near-identical.","glow-neutral-core":"oklch(99.97226% 0.001314 106.423) -> oklch(100.00000% 0.000000 89.876): neutral hue is achromatic → gamut wall chroma is 0 (was fixed-fraction residue noise); hue_vanished flag fires honestly for the achromatic sentiment."},"wcag_floors_not_weakened":"Glow-centre is a decorative FX bloom colour, NOT a text/border/focus-ring role — no WCAG contrast floor or focus-appearance (WCAG 2.4.13) floor applies to it. Every actual contrast value (recheck section, 2752 pairs) is byte-identical, so no measured contrast dropped below any floor. North invariant #2 (accessibility-by-construction) is preserved: measured, not merely asserted.","isolated_verification_required":"Regen accepted only after an isolated adversarial verifier (not seeing the self-claim) confirms RED→GREEN and that no WCAG floor weakened.","drift_scope_wave1":"resolveVars: 69 info-* leaves changed across 6 theme×bg entries. Zero non-info drift (scope-checked). recheck untouched.","regen_reason_exact_alpha_glow":"ADR-0004: glow ?????? ??????????? ???????? encoded-sRGB8 screen-????????? ? ?????? ???????????? shortest-roundtrip binary64 alpha ?????? ???????????? ?????????? ?? 4 ??????.","drift_scope_exact_alpha_glow":"????? 24 ????? resolveVars: ?????? --lab-fx-glow-{brand,danger,warning,neutral}-alpha ?? 6 ?????. ?? ???? ????, core, ???? ??? ???? var ?? ?????????.","recheck_unchanged_exact_alpha_glow":"recheck: 2752/2752 ???????? ??????? ???????? ?????????."},"recheck":[{"theme":"light","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#00030E","#0057BB","#037BFF","#78AFFF","#150000","#BE0005","#FF070E","#FF9A8C","#1B0D00","#A06300","#D28300","#FFAA3A","#000B01","#007C2D","#00A73F","#00D452","#020013","#4D00F9","#6D6EFF","#9FA9FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.59876563358553,20.60103636973117,79.91552226990729,6.816302360996974,63.95198968935827,3.97647793086433,41.89234722606997,2.2400949113548063,105.32470288110206,20.35108757719671,78.97384099389187,6.579727828852003,63.90360859563009,3.970705427078051,37.83221395673913,2.048194921340976,103.87315757816087,19.018778265876662,70.62528347734393,4.90716956443272,53.97808064225952,3.0056474129326713,34.232350404909745,1.8982213760304238,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.76444562516849,20.751302844974063,83.48658341134008,7.825697912025839,63.67798680778349,3.9439477616440737,40.86459333479343,2.1890112231357013]},{"theme":"light","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E6F0FF","#60A1FF","#0074F3","#004CA4","#FFF4F2","#FF8273","#F40009","#A80004","#FFF7EE","#F79C00","#C97E00","#8F5800","#C2FFC9","#00C84D","#00A03C","#006E26","#EEF0FF","#9099FF","#6662FF","#4300DB"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-97.97941453819666,18.27263411531394,-53.55180732861014,8.039306941172763,-34.54692380627581,4.792369408161928,-17.418806999279397,2.56984376276454,-102.41236835704379,19.474434420712562,-56.92703132210305,8.692613650917824,-34.93266406694178,4.850563950189369,-18.308962222075785,2.6667193472488013,-103.5613239139708,19.790915044333452,-61.97487030298502,9.71021814570805,-44.901407836051874,6.467843223430967,-25.783665577412222,3.5638173443979557,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.707239087904505,6.093571182387199,-23.370874926227668,3.2583602660985775,-98.9883689300502,18.54346384276384,-54.32153113615954,8.18635152260471,-34.304902287955215,4.756029101219059,-14.435475219756622,2.261552965854495]},{"theme":"light","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00163A","#003272","#004494","#040000","#3A0000","#760002","#990003","#0B0400","#261300","#512F00","#845100","#000200","#001D05","#004114","#006322","#000002","#160059","#2B0097","#3B00C5"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.89312592554248,4.521969910173767,28.754774555141132,3.1292028976206594,22.090354756391577,2.35851620111828,40.23106479600237,5.289901483963394,36.823385380889306,4.5064610721321205,27.786796188195932,3.002060282138028,21.02583740042693,2.2570951143110336,39.64148780057383,5.154352713354357,36.89828467875601,4.523118840482662,27.981450040156208,3.027209986332121,13.567318350507929,1.6836851754208233,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.3306726916817,5.312553092992316,36.84988795298528,4.512349526507044,30.916517468784825,3.4313626667845316,25.033605894158644,2.6690076358452903]},{"theme":"light","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#E7F1FF","#73ADFF","#2985FF","#0060CB","#FFF5F4","#FF9687","#FF4336","#CD0006","#FFF8F0","#FFA62C","#DC8A00","#AC6B00","#C7FFCC","#00D251","#00B043","#008631","#EFF1FF","#9CA6FF","#767AFF","#532BFF"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.32090210121694,9.956824779623638,-48.0046310941281,4.945145596338778,-30.285195552724065,3.1849116126306156,-14.342034884792934,1.9106934427040745,-91.71639880150975,10.602849175762582,-51.97724928251108,5.385053682831725,-31.570923815646545,3.301015045785777,-14.82740446463819,1.94471301206827,-92.86717481243348,10.774663089384916,-55.64998948221256,5.805759082702303,-40.41406674606042,4.149700467279057,-23.7537806706175,2.6249801387527425,-87.99158651504085,10.054343074329001,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-88.33220875070053,10.104016013712561,-48.80525652513833,5.032519885671211,-30.915189659062865,3.2415642790980215,-11.323318014385267,1.7063261265170042]},{"theme":"light","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000103","#001536","#003374","#0058BC","#070000","#360000","#730002","#BE0005","#0F0500","#241200","#4F2F00","#A16400","#000400","#001B05","#003F13","#007C2D","#000005","#140055","#3300AC","#4E00FA"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.65687576920386,5.198408498908002,36.59714090951085,4.504660010775197,27.83078380746869,3.029349552004155,13.31049770996219,1.6744770124926043,39.57933742278427,5.181007308853048,36.66186584356392,4.51899176883356,27.77510172821011,3.0221297438766705,12.721590912296357,1.6379804580418844,38.8564182220264,5.016999835671664,36.579572970042136,4.500775701626332,27.65113132417139,3.006116231224861,0,1.205042331050678,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.737197591996,5.216381929144518,36.59037871155487,4.5031645842195545,27.68585404738298,3.010592990868115,17.038043883395044,1.933101474636693]},{"theme":"light","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000104","#00204D","#003B83","#005CC4","#080000","#4C0001","#830002","#C70006","#0F0600","#331C00","#5B3600","#A76800","#000400","#002809","#004917","#00822F","#000005","#1F0075","#3A00C3","#511AFF"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.87966672163739,5.884401548408216,38.30767260595269,4.50632138120243,29.16726657041677,3.0308664461202253,15.823096179531001,1.7789458759332724,43.782795788734404,5.8597651770218455,38.402450836988166,4.527103297945568,29.016037258963053,3.011424325654164,14.9864556348382,1.7254955647131072,42.99821792375369,5.657954629703501,38.41622407485322,4.5301350068404425,28.93769973409828,3.0014083479801514,0,1.28054929135364,43.62397616937584,5.819206103341516,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.970091731453365,5.907320242919889,38.28628402517786,4.501651045763603,28.99214635836292,3.0083657301901288,19.11088367642911,2.0125842767555064]},{"theme":"light","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E6F0FF","#64A4FF","#0077F9","#0050AC","#FFF4F3","#FF8777","#F9000A","#B00005","#FFF7EF","#FA9E00","#CD8000","#955C00","#C3FFC9","#00CA4E","#00A33D","#007329","#EEF0FF","#939CFF","#6967FF","#4600E6"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.3221346483494,16.53807546575917,-53.23458352949761,7.5085332653696355,-34.299151794570456,4.531377660721796,-17.34040000814869,2.4827755446650244,-100.79897840003042,17.636700891614694,-56.73966900010428,8.131153426700068,-34.4661942600266,4.554628420782135,-18.26813498707333,2.5778416349364046,-101.94670279716274,17.922901715066683,-61.50032492399525,9.010497025774457,-44.48245067627942,6.049054371037485,-25.983086445749187,3.447217107352778,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-97.33108904020293,16.783196253636362,-53.9780650339559,7.638815670143083,-34.15649094558268,4.511565332868746,-14.178333309149869,2.174938899863266]},{"theme":"light","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#00030D","#0057B9","#0079FD","#74ADFF","#150000","#BC0005","#FD000A","#FF9788","#1B0D00","#9F6200","#CB7F00","#FFA730","#000B01","#007B2C","#00A63E","#00D251","#020013","#4C00F7","#6C6CFF","#9DA6FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.48497819102282,19.361251289328656,75.9364891028185,6.438321694919453,60.636932359482174,3.8278153397206016,38.76699274290234,2.152541489746932,101.19913515757631,19.11640864166299,75.28055256994783,6.281220843840182,60.46094873358925,3.807428346232839,34.70184973776556,1.9658801332164664,99.74758985463511,17.864929125608796,66.9127104243937,4.673002507305443,52.13428535623209,3.0001216660302674,31.16352525448906,1.8227359456035261,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.421867335088024,3.0235758695027473,33.32486172802245,1.9082022515525083,101.63887790164272,19.492343272893066,79.67393134344084,7.442688338927282,60.17031912503092,3.7741072518393395,37.967753096619255,2.113772626279229]},{"theme":"light","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E6F1FF","#67A5FF","#0079FD","#0053B2","#FFF5F3","#FF8A7A","#FD000A","#B60005","#FFF7EF","#FC9F00","#D08200","#9A5F00","#C4FFCA","#00CB4E","#00A53E","#00772A","#EEF1FF","#949EFF","#6B6BFF","#4900EE"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-94.97224440690107,14.900628115594163,-51.99577035710509,6.808135431331698,-33.457001357615106,4.175350120738554,-16.755883343489252,2.3335896778079332,-99.44736446714371,15.886233758087014,-55.869159717403925,7.428487313030341,-33.63398343987836,4.1977071628099685,-17.709012021143003,2.4235530906296177,-100.15616825242344,16.04465607216319,-60.39258141645385,8.181935837052826,-43.77686148964125,5.57032618168592,-25.67861999448382,3.2497240951569792,-95.39392806862337,14.992417327878547,-58.246405974587134,7.820609198173194,-41.3596549106396,5.227261702394524,-23.16505433719768,2.9752806858720673,-95.97861011098902,15.120061263203354,-52.9765437482588,6.963016139185695,-33.57839540296167,4.190679013723035,-13.485138200601865,2.040069034387161]},{"theme":"light","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EBF3FF","#D5E6FF","#92BFFF","#4291FF","#FFF8F7","#FFDDD8","#FFA597","#FF6253","#FFFAF5","#FFE0BE","#FFAA39","#E79100","#D0FFD4","#7DFF93","#00DA54","#00B947","#F1F4FF","#DFE4FF","#AEB8FF","#8087FF"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.43421936609613,5.100687932363686,-64.08586870464498,4.502412953317652,-41.11294430990105,3.017135229848019,-18.888486914529935,1.8251241221961068,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-40.940708789235714,3.0069340578770496,-21.122834074579163,1.9331720160112165,-77.70409895709017,5.493642615376888,-64.34147182620212,4.520282197700913,-40.849183913847796,3.0015190917179395,-28.18413111501551,2.292444746213784,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-41.2870784084647,3.027463512408998,-26.013778316259767,2.1791858289450103,-73.64084437140687,5.1896286230945075,-64.42349258712335,4.5260223420377566,-41.02780633656069,3.0120908713744514,-19.464187683477977,1.8526993919522168]},{"theme":"light","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#E8F1FF","#79B0FF","#368BFF","#0065D6","#FFF6F4","#FF9B8D","#FF5446","#D80007","#FFF8F1","#FFAB3F","#E18E00","#B47000","#C8FFCD","#00D552","#00B545","#008D34","#EFF1FF","#A0AAFF","#7B81FF","#573FFF"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-89.77514075602531,11.177468536043195,-51.89752791553575,5.733156384550998,-34.996215684604906,3.8114980409485155,-18.92394323577027,2.320895932067837,-94.48648764812391,11.954651961610136,-56.02375667309079,6.252768618847473,-36.81873931987068,4.002056146272223,-19.591172380553846,2.375625103310219,-95.24016771148194,12.080915286992608,-59.85145182180837,6.751641124414219,-44.82390104707481,4.887508677146329,-28.683175933367217,3.184369254481423,-90.45906258458693,11.288985892577246,-57.499124974387314,6.443154991463433,-42.83495757582209,4.660276446547384,-26.099142978345885,2.9428672820648862,-90.66219607331998,11.322193559065964,-52.9772743130274,5.867283737103295,-35.76790053375578,3.8916727925995533,-16.863337707955058,2.1560372721390864]},{"theme":"light","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#000209","#00479B","#0065D5","#3289FF","#100000","#9F0004","#D70007","#FF4F41","#170A00","#7A4A00","#9F6200","#DF8C00","#000801","#006322","#00822F","#00B344","#01000D","#3E00CE","#573EFF","#797EFF"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.22533906008685,12.552007092013115,56.99670466281028,5.352680994694784,44.43662180236468,3.3253647200326193,29.122033451491582,2.0641033245604916,76.01516726706762,12.436514137733148,55.97147975339624,5.133613537650522,43.91010121645754,3.2654000659595037,27.461071919951625,1.9716236837030474,74.81611736383091,11.770648316029503,52.816193854903005,4.528105409994289,41.51373908005361,3.011071841176318,19.679891349505557,1.6112041351931181,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.42233250009274,3.001939806510638,21.536768417458934,1.687644517337887,76.3513076320574,12.62087594492386,59.99704797613187,6.063965240585196,46.62694808670478,3.592268428661898,28.6381619838115,2.036506690369533]},{"theme":"light","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#DA7A00","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.42046624663428,20.43859328774187,80.31672424485741,6.9206271592755195,64.53598724808649,4.047139863973963,42.92136358171458,2.2930773168482466,105.12312344447312,20.1664284240786,79.32640956847692,6.666965706057784,64.47466861031803,4.0396342482899685,38.694434835096864,2.0867987099188317,103.94997559222902,19.08898290736351,71.78028267942507,5.099770465607366,55.19920557284124,3.104259347966041,35.17783363962355,1.9359471784913758,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.57179613789485,3.2209892436744982,37.49012931672191,2.03317973864909,105.58294145619523,20.58664787402852,83.26627791326895,7.757864847967472,64.57504887916579,4.051931818616954,42.090577540190225,2.2501559682241092]},{"theme":"dark","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#E3EEFF","#639EFF","#006DFE","#0048AD","#FFF4F3","#FF7F74","#F1001F","#A80012","#FFF7F1","#FF921B","#D17400","#955100","#B8FFBE","#00C649","#009F39","#006E25","#EDEEFF","#9794FF","#725BFF","#4E00D6"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.73192161780433,17.93999294470544,-52.63286661912575,7.865273702162647,-33.381528921151094,4.618607106385301,-17.0501576858861,2.5303719690800346,-102.4562582898777,19.486486384763445,-56.148341062851905,8.53993960492931,-34.330992536827104,4.759940239487347,-18.37234103122508,2.6737006902768288,-103.68997976483836,19.82648045930809,-60.35961607275431,9.379380118591195,-43.39157032733722,6.209218370112115,-24.93643207012505,3.4548840566407524,-97.27916136850891,18.085610527348415,-59.34389647192141,9.173841233004914,-42.25887199411116,6.018336366435839,-23.360562325895305,3.2570864547421428,-97.9872053947921,18.274719246988955,-53.323021296304695,7.99582368531923,-33.63351308594995,4.6559156780457185,-14.771084781300106,2.2949481607054216]},{"theme":"dark","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000104","#00153F","#00317C","#00429F","#070000","#3A0002","#750009","#9B0010","#0C0300","#291200","#562D00","#8B4B00","#000300","#001D05","#004112","#006521","#010005","#1A0057","#350098","#4700C4"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.20955594591163,5.284998875760269,36.83828085440759,4.509769828130583,28.330761142327287,3.0728704517581886,21.819504217645104,2.3321848000622474,40.142120786515875,5.269603471071473,36.80831529061993,4.503115584720142,27.930310922781615,3.0205822411426944,20.559717681283935,2.214401964672886,39.70511745091868,5.169077378914306,36.80795164205375,4.503034882134433,27.78871707797606,3.002307432697674,14.025581970431295,1.7128949100718707,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.795231110735635,4.500212645278562,30.09285126953815,3.3133648365325183,24.185949678819867,2.5748913454036786]},{"theme":"dark","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E4EFFF","#75AAFF","#3482FF","#005AD5","#FFF6F4","#FF9389","#FF3C3B","#CC0019","#FFF8F2","#FFA350","#E58000","#B36300","#BCFFC2","#00D04D","#00AF3F","#00862E","#EEEFFF","#A2A1FF","#7F74FF","#601DFF"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-86.07057705487603,9.776047393674569,-47.005198509651805,4.836992692850337,-29.617188814986996,3.125340410912816,-13.465221764046625,1.8500449052652255,-92.15650032550448,10.66842694645063,-51.122226750198095,5.289032001141636,-30.69564720892069,3.221769980092269,-14.697188723997265,1.9355550836437716,-92.95341233620333,10.787583067801071,-55.01595924446716,5.732183607436098,-39.01368610071904,4.00959458617392,-22.717127042375882,2.5408179478715844,-86.56463892830851,9.847321829129962,-52.766100540364825,5.474290510931042,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.32881842479753,9.957973594755346,-47.6452882768299,4.906141960838558,-29.99057738527554,3.1585747205458907,-10.990631160230787,1.6845762464445797]},{"theme":"dark","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000107","#00133B","#00317C","#0054C7","#0B0000","#360002","#730009","#BF0016","#100500","#261000","#542C00","#A85D00","#000501","#001B04","#004012","#007D2B","#010009","#180052","#3B00A6","#5A00F5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.616537893860624,5.189361828546554,36.671589094740156,4.521147554384925,27.767977778595686,3.0212072610618272,13.560368041515986,1.6902988819780091,39.46146930028483,5.1544680971312,36.646426655727396,4.515570174173857,27.733751845040576,3.016779185756929,12.426352071081123,1.6200937148513173,38.82022467282525,5.008731246970903,36.66757146424993,4.520256699490143,27.6755983348751,3.009270049666561,0,1.2356833222737893,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.666598999480925,5.200587158978297,36.5946359240296,4.504106003407952,27.684199915070796,3.0103795763779906,16.582944142840912,1.898781610023956]},{"theme":"dark","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000107","#001E53","#00398D","#0058CF","#0C0000","#4C0004","#83000C","#C60018","#110500","#371A00","#603300","#AE6000","#000601","#002808","#004916","#00822D","#01000A","#250070","#4400BC","#5E01FF"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.84943203331798,5.876721182230661,38.357196991852035,4.517162998132957,28.969046969390032,3.0054117916551304,16.078397205708793,1.7957132708136676,43.66281217184597,5.829141814712315,38.38176722933373,4.522556047220382,28.96308732518828,3.004650205577566,15.121014183458136,1.7339400568635355,43.01534021323247,5.66238735865634,38.36369120360285,4.518587529333456,28.956857085057415,3.003854272405285,0,1.32359073643957,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.88937786124038,5.8868666342953615,38.32550423955951,4.510220548286027,28.94413025668513,3.002229117722787,19.264807559936084,2.024522036885816]},{"theme":"dark","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E3EEFF","#66A0FF","#0E71FF","#004CB5","#FFF5F3","#FF8378","#F60020","#AF0013","#FFF7F1","#FF962A","#D57700","#9B5500","#B9FFBF","#00C849","#00A23A","#007226","#EDEEFF","#9997FF","#7460FF","#5200DF"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-95.07464172795706,16.23701078357789,-51.883866319617056,7.27432706128743,-33.10437599843017,4.366728841176053,-16.962076325331164,2.444617555127284,-101.23789901188303,17.745933910216884,-55.660852739640006,7.937252290960222,-33.86013630190819,4.470540437936136,-18.127983335051407,2.5633448249431416,-102.03269987499108,17.94442049171427,-60.01549050114797,8.732123022916136,-43.24478677485694,5.853976070416037,-25.11442835418257,3.342537791539289,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.852118066322895,5.637910373772879,-23.122294138224145,3.108849028833141,-96.32992550494482,16.539962663017093,-52.86610846158114,7.444323350911882,-33.336065162067605,4.398429825394501,-14.383943910910213,2.194178799785328]},{"theme":"dark","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#D67800","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.29489852310851,19.198605473400605,76.19115652133168,6.500760037097026,60.41041952456072,3.801604173546957,38.795795858188825,2.153958743950203,100.99755572094737,18.94295256384705,75.20084184495119,6.262487955668726,60.349100886792264,3.7945539156195487,34.56886711157111,1.9601948416949677,99.82440786870325,17.93087452582946,67.65471495589931,4.7903727911067815,52.29898260017819,3.0135204434714913,31.0522659160978,1.8184953129572932,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.44622841436909,3.025575236651027,33.364561593196164,1.9098288766402804,101.45737373266948,19.337677744697757,79.14071018974319,7.287203401685083,60.44948115564005,3.8061054053754897,37.96500981666448,2.1136413881018767]},{"theme":"dark","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E3EEFF","#69A2FF","#1975FF","#004FBB","#FFF5F3","#FF877C","#FA0021","#B50014","#FFF7F1","#FF9833","#D87900","#A05800","#B9FFC0","#00CA4A","#00A43B","#007628","#EDEEFF","#9B99FF","#7664FF","#5500E7"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-93.28410718321774,14.535439506623602,-51.011442228336165,6.654206660130301,-32.65955270222981,4.075313081844305,-16.370466566638967,2.297770413224217,-99.44736446714371,15.886233758087014,-55.07262883280597,7.299026812522581,-33.02825162884881,4.121422426501604,-17.569909707299868,2.4103015359642868,-100.24216533025177,16.063919770413232,-58.9183675206033,7.932997007393653,-42.52780974275247,5.391835753982905,-24.79898612463087,3.1522681596337323,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.905085629416114,5.1638402024196015,-22.782862127638978,2.93464773079297,-94.53939096020551,14.80664328764592,-51.95162400089297,6.801199303451522,-32.707141977036414,4.081250761977603,-13.69412377461175,2.0581050526656677]},{"theme":"dark","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E8F1FF","#D5E6FF","#95BEFF","#498FFF","#FFF9F8","#FFDDD8","#FFA59B","#FF5F57","#FFFAF6","#FFDFC5","#FFA95E","#F18700","#C6FFCB","#7DFF91","#00DA51","#00B943","#F0F1FF","#E1E3FF","#B4B6FF","#8982FF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-71.17583391665679,5.008590480308264,-64.08586870464498,4.502412953317652,-40.969901639859465,3.0086620775128243,-18.492326685553408,1.8062558438836862,-77.39176152066469,5.470028106981068,-64.06334225320066,4.50083949888721,-41.06186552000793,3.0141084284175577,-20.58109958398818,1.9067218663179524,-77.74802214310357,5.496966722090798,-64.16882972322054,4.508209630901898,-40.97688137217631,3.00907529195687,-26.93048349706687,2.2267208299060117,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.24499881268402,3.0249663274797034,-25.961518214862377,2.176489332337808,-72.19208007215988,5.082914152992658,-64.24580556994597,4.513590807561596,-40.91738125342747,3.005553523356943,-18.74578210381434,1.818317299241165]},{"theme":"dark","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E5EFFF","#7AADFF","#3D87FF","#005FDF","#FFF6F5","#FF988E","#FF4D48","#D5001A","#FFF8F3","#FFA75A","#EA8300","#BA6700","#BDFFC3","#00D34E","#00B341","#008C31","#EEEFFF","#A5A5FF","#837AFF","#6433FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.52359832003891,10.974551054175622,-50.82334067294272,5.601029591424501,-33.90019185783247,3.6989242542195706,-17.94937633357589,2.2421371448335408,-94.53061684959366,11.962030249162733,-55.11693338695639,6.136943021617957,-35.65662848796355,3.880065704029081,-19.010500723408704,2.3279588656978594,-95.32685818617963,12.095472485308715,-58.832789738116276,6.617312123916825,-43.099834270009964,4.690265818580579,-27.202021451205145,3.0448373808729228,-89.02366089613628,11.055448870118658,-56.507119659393844,6.314879036055816,-41.91393002442956,4.556652813501584,-25.675484951698337,2.9041378624661793,-89.65880574741696,11.158543725868578,-51.69548126800453,5.708204299589472,-34.40239467659594,3.750316223165759,-16.078998268001875,2.0949612264666513]},{"theme":"dark","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00020F","#0045A5","#0060E2","#3F88FF","#140000","#A10011","#D7001B","#FF4F49","#190900","#814600","#A85D00","#EA8300","#000A01","#006320","#00822D","#00B341","#030014","#4A00CC","#6537FF","#847BFF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.15878035868923,12.51550617725489,56.792209524954714,5.308053794065892,44.82231091099692,3.370285744389143,29.010809014184062,2.057710850183084,75.84645644595844,12.343365988484052,55.500674792931,5.036843578382145,43.81499067450534,3.25473208522667,27.348178994525146,1.9655632934834584,74.80511978941351,11.764533211175078,52.793708756474935,4.52413702940673,41.44647230556496,3.0043476386355414,20.422200521634597,1.6411239547419576,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.19495675605003,12.535354496536943,59.24767070343283,5.876074797996776,46.58341111523108,3.5866744685580465,28.54022915013225,2.030988108287992]},{"theme":"dark","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"light-ic","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#000110","#0042E2","#3B78FF","#85AEFF","#150000","#BD0011","#FF001B","#FF998D","#1A0500","#B04A00","#E96400","#FFA174","#000B01","#007C2D","#00A73F","#00D452","#00040B","#00639C","#0089D6","#4DB3FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.76809306682351,20.754602558566376,81.70242596755779,7.298044196933449,63.72862579130605,3.9499301799077116,41.321899652634976,2.211520376313395,105.32470288110206,20.35108757719671,79.14032751842986,6.6207260543602775,64.03544238362204,3.986463961054262,38.10332212672181,2.0602150617562085,104.62285904737456,19.706640245810163,73.93672186564804,5.489431142391079,57.895853978119064,3.3399091341000195,36.26437665881752,1.9807324696579875,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.53601527934143,20.543945015271454,78.36297683006073,6.432254688155311,62.2832355663668,3.784280156272109,42.68471757482252,2.28072562681704]},{"theme":"light-ic","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E8F0FF","#709FFF","#2D6DFF","#0038C6","#FFF4F3","#FF8275","#F30019","#A7000E","#FFF6F1","#FF8C52","#DF5F00","#9D4100","#C2FFC9","#00C84D","#009F3C","#006E27","#DFF0FF","#10A5FF","#0082CB","#005689"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-98.22794037003116,18.339196272629252,-53.8293996799751,8.092203727319841,-34.21813658812841,4.7430333536498335,-15.622162303180753,2.381112763636304,-102.4562582898777,19.486486384763445,-56.97432424245902,8.701923574139988,-34.76332175899368,4.824974728357804,-18.151652148372637,2.6494391947645526,-103.24826961310278,19.70448161329645,-59.10147589327934,9.125072654774526,-40.787696351505694,5.7744786453226835,-22.81212855995232,3.1897383862050708,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.29572232514961,6.0245037012352585,-23.381466511664765,3.259668821930569,-97.12909045511768,18.04563033112125,-52.54162933208091,7.848085216280267,-36.273216344700636,5.055412083856902,-18.54110013749204,2.69234396770907]},{"theme":"light-ic","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00104D","#002488","#0032B2","#040000","#3A0002","#750007","#98000B","#080100","#2E0E00","#602500","#8F3B00","#000200","#001D05","#004114","#006322","#000102","#00192E","#003A5F","#004D7C"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.799792142283025,4.501224406456354,30.04772679653348,3.3069997994324063,23.797727835383583,2.533076638038498,40.23106479600237,5.289901483963394,36.80831529061993,4.503115584720142,27.941932493369514,3.022087143525339,21.190215752118235,2.2723970292907865,40.01441400964806,5.240352767492206,36.83929336929951,4.509994812829007,27.908315541084118,3.0177360720288915,16.77490328531677,1.9034128352995776,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.22977489962569,5.289607582090012,36.84755783504637,4.511831555904809,27.796281383025352,3.0032808914145437,21.058698283639867,2.260143784790762]},{"theme":"light-ic","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#EAF1FF","#81ABFF","#4982FF","#0048F5","#FFF5F4","#FF9589","#FF403B","#CC0013","#FFF7F3","#FF9E6E","#F46900","#BD5000","#C6FFCC","#00D251","#00B043","#008631","#E1F1FF","#45B0FF","#0090E1","#006CA9"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.69551010135778,10.011246151186945,-48.263751486481056,4.97335266465752,-30.44825366943375,3.199530942583609,-12.043546933756078,1.753943193668956,-91.71639880150975,10.602849175762582,-51.72679347557047,5.3568510977778265,-31.21621892784593,3.268795397886287,-14.661523244642579,1.9330507439017046,-92.55409442667087,10.727809933310253,-53.89547122979923,5.603122076267749,-35.95806640240085,3.7112655792375078,-20.04367187639212,2.329915503818876,-87.89039906251095,10.039605852107476,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-86.58838125370931,9.850752189993974,-46.78872683576137,4.81370216625391,-31.383244337404896,3.2839492580871354,-15.736559790538532,2.009284883552937]},{"theme":"light-ic","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000004","#000E48","#002895","#0042E3","#070000","#360001","#730006","#BE0011","#0C0200","#2B0D00","#5F2400","#B04A00","#000400","#001B05","#003F13","#007C2D","#000103","#00172B","#00395C","#00639D"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.74741500641279,5.218664231374835,36.670023859840576,4.5208004701163915,27.672031623266886,3.008810094200282,15.351341296852958,1.8098737923818007,39.57933742278427,5.181007308853048,36.654144364688555,4.517280323585742,27.751464091733354,3.019069960427185,12.671992881770905,1.6349566993987985,39.23716411822194,5.10371254671437,36.601471319299684,4.505617849545331,27.652865692456807,3.006339685224649,7.684471784052528,1.3665581876464832,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.65687576920386,5.198408498908002,36.65150522712613,4.5166954713046765,27.64129393913477,3.0048491045091428,12.054359140378313,1.5979376774972005]},{"theme":"light-ic","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000004","#001864","#002FA8","#0045ED","#080000","#4C0003","#830008","#C60012","#0D0200","#3E1500","#6C2A00","#B84D00","#000401","#002809","#004917","#00822F","#000103","#00233D","#00416A","#0068A4"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.98030914587015,5.909904848562026,38.293478372508915,4.503221171740978,28.94317626341717,3.002107337321695,17.95394033246701,1.9258322867444517,43.782795788734404,5.8597651770218455,38.38866051296957,4.524070782124866,28.984608537472,3.0074014381148784,15.156957289606964,1.7362054736619685,43.437607341847624,5.771384990646562,38.33399448678559,4.512078832150148,28.981628167432756,3.007020263830111,9.892674202630067,1.4441010369850493,43.614187977218386,5.816700203526642,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.88976990866122,5.886966133555759,38.401524303038336,4.526899458852529,29.03631728859714,3.0140233900654434,14.306925700285792,1.683714708917422]},{"theme":"light-ic","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E9F0FF","#74A2FF","#3371FF","#003CD0","#FFF4F3","#FF8679","#F8001A","#AF000F","#FFF6F2","#FF9058","#E36100","#A44400","#C3FFC9","#00CA4E","#00A33D","#007329","#E0F0FF","#21A8FF","#0085D0","#005B90"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.69585202312288,16.62869936569993,-53.53835262595417,7.561646720391368,-33.90853669724825,4.477228400656394,-15.643424374972522,2.314414627530853,-100.79897840003042,17.636700891614694,-56.5097869254783,8.089667628698802,-34.29633291417122,4.530985783372177,-18.10601775891745,2.56107712884258,-101.63443561648342,17.844851007069916,-58.71630179001587,8.4915976550775,-40.34850051574898,5.4087697654311055,-22.889197327723014,3.0820931228283226,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-95.5914257275877,16.361459308701495,-52.26386479821355,7.339891411251783,-35.96637326948896,4.765958094131511,-18.710865986535588,2.6239504003951875]},{"theme":"light-ic","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#000110","#0041DF","#3876FF","#82ACFF","#140000","#BB0011","#FC001B","#FF968A","#1A0500","#AE4900","#E76300","#FF9F70","#000B01","#007B2C","#00A63F","#00D252","#00040B","#00629B","#0088D4","#47B1FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.64252534329776,19.49544279635548,78.08171358923367,6.990900026896573,60.31711036263554,3.7908833425501425,38.14751646036453,2.122399526754989,101.24542779029852,19.156162076239593,75.44574141529571,6.3202869230281635,60.62673294996029,3.82662941345977,34.94277926170305,1.9762445240799382,100.4972913238488,18.51105924752946,70.36281029021609,5.256397052662421,54.37780741212484,3.1906001595018556,32.840214506002376,1.8885138214456323,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.4093478352768,3.0225491335598114,33.31123457222728,1.9076443919396007,101.41044755581565,19.297565623167603,74.60712675826916,6.125340559948453,58.63177587043786,3.604653750954651,39.55009522975671,2.1915837971666887]},{"theme":"light-ic","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E9F1FF","#76A4FF","#3875FF","#003ED7","#FFF4F3","#FF897C","#FC001B","#B50010","#FFF6F2","#FF935D","#E66300","#A94600","#C4FFCA","#00CB4F","#00A53F","#00772B","#E0F0FF","#2BA9FF","#0087D3","#005E95"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-95.34499825068502,14.98175500729861,-52.621417231568934,6.906762415988252,-33.47849770408375,4.178062617945301,-14.893749254597887,2.163552488849563,-99.00844385529109,15.788447906049281,-55.63494162352738,7.390318911682041,-33.467257033822236,4.176644120449875,-17.547592326855693,2.408179342981014,-99.84390107174409,15.974784754119627,-57.911424929350886,7.764836527735112,-39.59013024218227,4.982347876116714,-22.33006111099907,2.8868875311347497,-95.39392806862337,14.992417327878547,-58.26045217149408,7.822951516486187,-41.372488477704024,5.229057297458628,-23.176065198031452,2.976455642863797,-93.8008911828484,14.646846343308852,-51.05892490110174,6.66159703590636,-35.06283530237813,4.380256350040106,-18.07990151348513,2.4590883821528062]},{"theme":"light-ic","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EDF3FF","#D8E6FF","#9ABDFF","#598FFF","#FFF8F7","#FFDDD8","#FFA59A","#FF6156","#FFF9F7","#FFDECE","#FFA77C","#FF7112","#D0FFD4","#7DFF93","#00D955","#00B947","#E5F3FF","#CEE8FF","#79C3FF","#0099EE"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.68698719753608,5.119268434409956,-64.43727527190525,4.526987198998707,-41.01457995095163,3.0113075347043563,-19.296469937711088,1.8446469211142276,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-41.0312066106476,3.012292267380192,-20.969079339398604,1.925648566411713,-77.34727635116013,5.466668081501757,-64.08510797117285,4.502359813071005,-40.89305028080999,3.004113887137535,-23.84545366268376,2.068528569757355,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-40.827825177303865,3.0002560129496776,-26.013778316259767,2.1791858289450103,-71.69069599862902,5.046190228559761,-64.15987367011111,4.5075837056249375,-40.85863800181002,3.0020782433474276,-19.258832894479415,1.8428420367238694]},{"theme":"light-ic","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#EAF1FF","#86AEFF","#5189FF","#0650FF","#FFF6F4","#FF9B8F","#FF5349","#D70015","#FFF7F3","#FFA376","#FA6C00","#C75400","#C8FFCD","#00D553","#00B445","#008D34","#E2F1FF","#50B4FF","#0094E7","#0072B3"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-90.02549742397721,11.218239018758677,-52.117590678997175,5.760385990810306,-35.453068631322715,3.8588722418561026,-17.16315496591595,2.1796295803105563,-94.48648764812391,11.954651961610136,-56.08049309972586,6.260045669338659,-36.68653001054492,3.9880923379680597,-19.42498133620683,2.3619323117143227,-94.88408174929032,12.021194381023134,-58.024655972390946,6.511549338355004,-40.19653971715428,4.366167533936967,-25.014063203920518,2.8441658803090872,-90.45906258458693,11.288985892577246,-57.51342916294683,6.44501257640493,-42.39985031117064,4.611196053798779,-26.099142978345885,2.9428672820648862,-89.03890865199516,11.05791933389335,-51.142776043878534,5.640183677550314,-35.55629387962385,3.86961291577043,-20.57349953874105,2.4573829383449617]},{"theme":"light-ic","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#00010B","#0035BB","#044EFF","#4E86FF","#0F0000","#9F000C","#D60015","#FF4D45","#150400","#903B00","#BB4F00","#F86B00","#000801","#006322","#008230","#00B344","#000307","#005182","#0071B2","#0093E5"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.30385323493446,12.594966222228084,58.55230095328762,5.70794052541694,46.62204712410211,3.5916381056717506,29.005214449343097,2.0573900879913625,76.05323451380515,12.457481581934719,55.93606418979168,5.126251708978212,44.077718465236906,3.284322001440957,27.730078776096413,1.986177453897667,75.43283879959024,12.113874624281086,52.77586207835023,4.520990578468311,41.44187963136357,3.003889327255304,23.868980516769618,1.7916338607346896,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.41072061989075,3.0007825637465393,21.536768417458934,1.687644517337887,76.14729893560808,12.509202438867769,55.739334564053664,5.085602713702602,43.07798550292857,3.173723442083943,28.271676351762963,2.015968674447834]},{"theme":"light-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark-ic","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#D38200","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E2","#8F5BFF","#B49FFF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.36973966240839,20.39226096069594,79.16304803673168,6.626348275559002,63.912632944603466,3.971781216419874,43.318131952880584,2.3140155027501375,105.16667360757414,20.20636839933495,79.43361118518992,6.693805381831162,64.79709340122825,4.079330310903514,38.90470830731088,2.096381256670562,103.76723276112338,18.92212884965907,70.7453629185683,4.926701923645705,54.109273202798526,3.0160152686700097,34.512224892089854,1.9092708836328856,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.58363991444959,3.2220248289870597,37.502971296697396,2.0337403797475795,105.5639998705614,20.569417077144905,82.76169093861434,7.605398027541734,65.05029978451972,4.11090668779679,42.03743585537794,2.2474520733771812]},{"theme":"dark-ic","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#DFEEFF","#4AA0FF","#007AE0","#005198","#FFF4F3","#FF7D79","#EF0030","#A6001E","#FFF7EF","#F99A00","#CA7D00","#915800","#B8FFBE","#00C648","#009F38","#006E24","#F0EDFF","#A98DFF","#894CFF","#6000C7"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.24720863865953,17.81140932471069,-52.09132740819334,7.763491481432673,-34.99617079306692,4.860177125212619,-17.977340963301042,2.6303715448994067,-102.4562582898777,19.486486384763445,-55.7461567674855,8.461541475204797,-34.016004840410574,4.712824540535038,-18.066789785866806,2.6401456993132153,-103.60398268701002,19.802704734429785,-61.68353878496926,9.650187857341214,-44.76775468944945,6.444757556916112,-26.060791619056427,3.599837857825526,-97.27916136850891,18.085610527348415,-59.33082759509665,9.171209294707863,-42.24705692654848,6.016359585841944,-23.350526713793172,3.2558471191579894,-97.94239544297146,18.262727694350296,-53.03355221198167,7.94095381214551,-32.857248309508414,4.541449578831631,-15.041171256747345,2.322063533056465]},{"theme":"dark-ic","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000103","#001834","#00376B","#004A8C","#070000","#390005","#750012","#99001B","#0D0400","#261300","#522F00","#865100","#000300","#001D05","#004112","#006521","#010005","#22004F","#43008F","#5800B7"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.21965913293546,5.287302224624218,36.81767378273908,4.505192880903515,27.950777846769416,3.023233051937069,21.000772856623527,2.25477321245894,40.142120786515875,5.269603471071473,36.88792093057972,4.5208109211231795,27.866909320024153,3.0123854877509646,20.876190217747006,2.243276608438576,39.57947058767665,5.139984882718895,36.89828467875601,4.523118840482662,27.8635036155662,3.011945822214043,13.292640012092916,1.6665035199669143,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.80600977189143,4.502603953746083,29.651273965169533,3.2515347547731954,23.802145579236715,2.533547958530533]},{"theme":"dark-ic","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E1EFFF","#62ACFF","#0087F7","#0065BB","#FFF5F4","#FF928C","#FF3644","#CA0027","#FFF8F1","#FFA533","#DD8900","#AD6A00","#BCFFC2","#00D04C","#00AF3F","#00862E","#F1EFFF","#B29CFF","#956AFF","#7700F5"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-85.70519954500536,9.72347127561709,-46.57313915269262,4.790554592637207,-29.772868283167547,3.1391774083572677,-14.678197720495364,1.9342213705917548,-91.71639880150975,10.602849175762582,-50.907190680193835,5.264997878479995,-30.103011927821978,3.1686137953283313,-14.381529067368087,1.9134497328938256,-92.9101803888625,10.781105347058572,-55.36632323767275,5.772792653136124,-40.27550194757439,4.135743433221995,-23.629833528615176,2.614848030728214,-86.56463892830851,9.847321829129962,-52.75275099572737,5.472775232316544,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.71836703917482,10.014570561299498,-47.715877818006604,4.91379345402092,-29.619990398118716,3.125589171245837,-10.872649839108671,1.6769004806806627]},{"theme":"dark-ic","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000106","#001632","#00366A","#005EAF","#0B0000","#360004","#730011","#BD0023","#100600","#241200","#502E00","#A36300","#000501","#001B04","#004012","#007D2B","#020009","#20004A","#48009A","#6F00E5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.62660370456575,5.191620542906115,36.592103069401674,4.503545882983851,27.695057695096033,3.0117807086970667,12.482735608358766,1.623488823202914,39.46146930028483,5.1544680971312,36.631002505660305,4.512153756975423,27.677944212691237,3.0095726068531357,12.751768680957815,1.6398240151911772,38.729462058672325,4.987982714010286,36.579572970042136,4.500775701626332,27.747483785710887,3.0185550274007946,0,1.2039987886628472,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.63684952401951,5.1939188146581685,36.601297620325695,4.505579426399357,27.609531401873983,3.0007614645527125,16.085712499624492,1.86220122646402]},{"theme":"dark-ic","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000206","#002245","#003E78","#0062B6","#0B0000","#4C0008","#820015","#C50025","#110600","#341C00","#5B3600","#A96700","#000601","#002808","#004916","#00822D","#02000A","#2E0067","#5300AD","#7400EE"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.7603252928922,5.854038847974277,38.29235457140463,4.502975854531618,29.083956028514045,3.020138635078335,15.054852030501879,1.729780743830801,43.694363439742204,5.837205585263101,38.354206470247746,4.516507237210082,29.087182234169767,3.020553277003921,15.230546565278194,1.740856512459411,42.92491991360228,5.63897133999608,38.33365423714702,4.512004338988724,28.93769973409828,3.0014083479801514,0,1.2794758858630608,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.85966524740055,5.879321607935794,38.335090707081115,4.512318847459295,28.952846517354917,3.003342035233188,18.7969313728254,1.9885281036480773]},{"theme":"dark-ic","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E0EEFF","#4FA3FF","#007DE4","#00559F","#FFF4F3","#FF817D","#F40031","#AD001F","#FFF7EF","#FB9C00","#CE7F00","#965B00","#B9FFBF","#00C849","#00A239","#007226","#F0EEFF","#AA90FF","#8C52FF","#6500D0"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-94.7101738760577,16.149472101206463,-51.76844025191021,7.254462182627787,-34.605057818633654,4.573999892517157,-17.84521283217997,2.534242228411278,-100.79897840003042,17.636700891614694,-55.24750846744172,7.863491890193123,-33.54190885081095,4.42668652015819,-17.815136446883052,2.5311583129351276,-101.94670279716274,17.922901715066683,-61.02785966423507,8.921519321161206,-44.34904450398409,6.02788949767693,-25.86724540779468,3.4331609270477186,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.840254848173814,5.636085625741139,-23.122294138224145,3.108849028833141,-96.71860922522349,16.634224259107555,-52.4354046324141,7.369572153296626,-32.71416296796099,4.3135863728502315,-14.766203746026353,2.2302399315399106]},{"theme":"dark-ic","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#CC7E00","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E1","#8F5BFF","#B49FFF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.24417193888262,19.155084079579783,75.03748031320593,6.224334741672483,59.78706522107772,3.730817455350078,39.19256422935483,2.1736266322828217,101.04110588404839,18.980469423093126,75.30804346166417,6.287699296730837,60.6715256777025,3.8318416601446055,34.77914058378512,1.9691960254812915,99.64166503759763,17.774143327140518,66.61979519504256,4.627804134340593,52.265155427331,3.010761189364384,30.386657168564103,1.79343744066295,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.45807219092383,3.026547994099155,33.37740357317164,1.9103555042369325,101.43843214703563,19.321492322017065,78.73738474892319,7.17242701812758,60.92473206099399,3.861502331684902,37.91186813185219,2.111101535692422]},{"theme":"dark-ic","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E0EEFF","#53A4FF","#007FE8","#0058A5","#FFF4F3","#FF8580","#F80032","#B30021","#FFF7F0","#FD9D00","#D18100","#9B5E00","#B9FFC0","#00CA4A","#00A43A","#007628","#F0EEFF","#AC93FF","#8D57FF","#6800D8"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-92.91963933131836,14.457074514504098,-50.53751898728166,6.580638229837842,-33.762466759799054,4.213972880586457,-17.265259153354464,2.3814244063048684,-99.00844385529109,15.788447906049281,-54.621704682580784,7.2261694596451385,-32.708249696736004,4.081389020198301,-17.26088719592284,2.381011454094406,-100.19905346524207,16.054261379481122,-59.919533209261445,8.101702037099825,-43.64240726631874,5.55098822531801,-25.561703474916055,3.236683846808585,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.893113879964474,5.162174617584753,-22.782862127638978,2.93464773079297,-94.92807468048419,14.891026660055605,-51.83505302329671,6.7828987244211,-32.07221654016657,4.002369709649521,-14.066989001346808,2.090531498955086]},{"theme":"dark-ic","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E5F1FF","#D2E7FF","#8AC0FF","#2091FF","#FFF9F8","#FFDDDA","#FFA49E","#FF5C5D","#FFFAF5","#FFE0C0","#FFAA42","#E99000","#C6FFCB","#7DFF91","#00DA50","#00B943","#F3F1FF","#E6E2FF","#C1B2FF","#9D7AFF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-70.80493468751378,4.981574213000164,-64.1741844582375,4.5085838821814495,-40.86541522684489,3.0024791016401666,-17.915271666932362,1.7789289638170434,-77.39176152066469,5.470028106981068,-64.14420274941838,4.506488572747314,-40.82594869447877,3.0001450551263997,-20.102672577261966,1.8834970356793017,-77.70409895709017,5.493642615376888,-64.40976920815079,4.525061719384059,-40.93902681557826,3.00683450902807,-28.2154352545988,2.2940964528512438,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.23137463469248,3.02415799426954,-25.961518214862377,2.176489332337808,-72.5833510184408,5.111647048756453,-64.45521953810703,4.528243512077896,-40.9348520371047,3.006587427640115,-18.521762858381592,1.807654812879132]},{"theme":"dark-ic","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E1EFFF","#68AEFF","#008BFD","#006AC4","#FFF6F5","#FF9791","#FF494F","#D30029","#FFF8F1","#FFA941","#E28C00","#B46E00","#BEFFC3","#00D34D","#00B340","#008C31","#F1EFFF","#B49FFF","#9971FF","#7D09FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.0351868676248,10.895768939711282,-50.01919534416472,5.502978516969682,-33.92042169291382,3.7009882121251625,-19.118481017052428,2.3367853131295435,-94.53061684959366,11.962030249162733,-54.89593536437227,6.108854061876529,-35.13675470036395,3.8260433618990066,-18.695569008603144,2.3023140000481574,-95.24016771148194,12.080915286992608,-59.18458546592085,6.6635751999987605,-44.37309985295556,4.8355935478331284,-28.125674388660414,3.1315039953558483,-89.11957165172129,11.070992163190612,-56.49372778683512,6.313154752438012,-41.901500594929324,4.555261373045625,-25.675484951698337,2.9041378624661793,-90.04835436179425,11.221964232051386,-51.2922333885473,5.658542947646642,-34.21126239038315,3.7307191421666435,-15.254686874750304,2.03178289437105]},{"theme":"dark-ic","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00030D","#004D92","#006CC7","#008BFE","#130001","#9F001C","#D60029","#FF4B50","#190B00","#7B4A00","#A06100","#E38C00","#000A01","#006320","#00822D","#00B341","#040013","#5B00BE","#7E16FF","#9971FF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.08600684668272,12.475516218126787,55.96473752983635,5.13221099097022,43.3369682989835,3.2018590596167122,29.318326289650187,2.0754572365320043,75.88156299830736,12.362776535253154,55.82217638546274,5.102669267083783,43.91931922104849,3.266436635421996,27.87409429449102,1.9940348577564484,74.63415754799017,11.66954886455628,52.68236753448497,4.504554642142053,41.632593181405234,3.023005829909607,19.021862742942993,1.5853635699211195,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.18160530883091,12.528031661692317,58.96992438550627,5.808209180198148,47.36214126331064,3.68860336114861,29.106756402593504,2.0632235490419992]},{"theme":"dark-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]}],"resolveVars":[{"theme":"light","bg":"#FFFFFF","vars":{"--lab-bg-tone-2":"oklch(97.84197% 0.010603 286.202)","--lab-bg-tone-3":"oklch(97.84197% 0.010603 286.202)","--lab-label-primary":"oklch(19.12257% 0.014100 291.556)","--lab-label-secondary":"oklch(56.54196% 0.013721 285.953)","--lab-label-tertiary":"oklch(66.97448% 0.014606 285.999)","--lab-label-quaternary":"oklch(81.39504% 0.013897 286.087)","--lab-border-strong":"oklch(19.12257% 0.014100 291.556)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(19.39430% 0.069681 257.297)","--lab-label-brand-secondary":"oklch(57.33316% 0.205939 257.291)","--lab-label-brand-tertiary":"oklch(67.12978% 0.175419 257.690)","--lab-label-brand-quaternary":"oklch(81.31388% 0.094313 257.967)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(20.16318% 0.082740 29.234)","--lab-label-danger-secondary":"oklch(59.62536% 0.244212 28.655)","--lab-label-danger-tertiary":"oklch(69.12968% 0.198650 28.574)","--lab-label-danger-quaternary":"oklch(82.55135% 0.097676 29.389)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(19.32631% 0.042728 66.944)","--lab-label-warning-secondary":"oklch(57.27465% 0.125021 68.799)","--lab-label-warning-tertiary":"oklch(67.73500% 0.148484 68.161)","--lab-label-warning-quaternary":"oklch(82.04043% 0.139600 68.374)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(67.73500% 0.148484 68.161 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(18.59864% 0.054816 147.311)","--lab-label-success-secondary":"oklch(54.91588% 0.161394 147.418)","--lab-label-success-tertiary":"oklch(64.88263% 0.190555 147.443)","--lab-label-success-quaternary":"oklch(79.02491% 0.232159 147.432)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(64.96409% 0.172888 147.450 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(19.54489% 0.077696 259.509)","--lab-label-info-secondary":"oklch(57.68997% 0.232691 259.838)","--lab-label-info-tertiary":"oklch(67.26428% 0.173598 259.980)","--lab-label-info-quaternary":"oklch(81.49851% 0.092914 259.576)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.5","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.5","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.5","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.5","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(66.97448% 0.014606 285.999)"}},{"theme":"light","bg":"#000000","vars":{"--lab-bg-tone-2":"oklch(7.11623% 0.013707 282.350)","--lab-bg-tone-3":"oklch(7.11623% 0.013707 282.350)","--lab-label-primary":"oklch(98.65156% 0.006606 286.278)","--lab-label-secondary":"oklch(80.14049% 0.013952 286.081)","--lab-label-tertiary":"oklch(69.23260% 0.013029 286.055)","--lab-label-quaternary":"oklch(55.16643% 0.013808 285.938)","--lab-border-strong":"oklch(98.65156% 0.006606 286.278)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-brand-secondary":"oklch(80.06451% 0.101154 257.822)","--lab-label-brand-tertiary":"oklch(69.35433% 0.162198 257.610)","--lab-label-brand-quaternary":"oklch(55.96293% 0.201872 257.393)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.64532% 0.006567 28.833)","--lab-label-danger-secondary":"oklch(81.38408% 0.105319 28.863)","--lab-label-danger-tertiary":"oklch(71.19739% 0.181099 28.350)","--lab-label-danger-quaternary":"oklch(58.10538% 0.238004 28.677)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.72985% 0.009577 72.664)","--lab-label-warning-secondary":"oklch(80.98123% 0.149940 68.750)","--lab-label-warning-tertiary":"oklch(70.11346% 0.152989 68.857)","--lab-label-warning-quaternary":"oklch(55.86543% 0.122417 68.219)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.31403% 0.027558 147.033)","--lab-label-success-secondary":"oklch(77.69544% 0.228159 147.448)","--lab-label-success-tertiary":"oklch(67.10085% 0.197388 147.383)","--lab-label-success-quaternary":"oklch(53.45797% 0.157009 147.442)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-info-secondary":"oklch(80.30515% 0.099317 259.806)","--lab-label-info-tertiary":"oklch(69.49027% 0.160387 260.058)","--lab-label-info-quaternary":"oklch(56.23879% 0.227606 259.853)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.013010286081645773","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.01207107843137255","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.009560345877481427","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.00784313725490196","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(69.23260% 0.013029 286.055)"}},{"theme":"light","bg":"#808080","vars":{"--lab-bg-tone-2":"oklch(58.21536% 0.012099 286.018)","--lab-bg-tone-3":"oklch(58.21536% 0.012099 286.018)","--lab-label-primary":"oklch(13.77177% 0.013406 284.503)","--lab-label-secondary":"oklch(20.89710% 0.013787 291.711)","--lab-label-tertiary":"oklch(33.70211% 0.013945 291.371)","--lab-label-quaternary":"oklch(49.18380% 0.012632 285.926)","--lab-border-strong":"oklch(13.77177% 0.013406 284.503)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.89968% 0.076454 257.725)","--lab-label-brand-tertiary":"oklch(34.17043% 0.122830 257.309)","--lab-label-brand-quaternary":"oklch(49.70427% 0.178878 257.337)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(21.88171% 0.089792 29.234)","--lab-label-danger-tertiary":"oklch(35.54757% 0.145582 28.627)","--lab-label-danger-quaternary":"oklch(51.73054% 0.211869 28.643)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.90753% 0.046395 66.438)","--lab-label-warning-tertiary":"oklch(34.00266% 0.074904 67.452)","--lab-label-warning-quaternary":"oklch(49.79045% 0.108745 68.715)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(91.20808% 0.064865 69.118 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(20.10741% 0.059351 147.255)","--lab-label-success-tertiary":"oklch(32.71649% 0.095649 147.616)","--lab-label-success-quaternary":"oklch(47.52181% 0.139141 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(88.67895% 0.198079 147.416 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(21.26627% 0.087439 260.146)","--lab-label-info-tertiary":"oklch(34.39884% 0.140838 260.069)","--lab-label-info-quaternary":"oklch(50.11242% 0.202893 259.861)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.11057506131405706","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.11023622047244094","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.07169755954418741","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.047244094488188976","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.70211% 0.013945 291.371)"}},{"theme":"light","bg":"#3A3A3C","vars":{"--lab-bg-tone-2":"oklch(36.65084% 0.011912 285.797)","--lab-bg-tone-3":"oklch(36.65084% 0.011912 285.797)","--lab-label-primary":"oklch(98.92134% 0.005280 286.303)","--lab-label-secondary":"oklch(82.53513% 0.012530 281.039)","--lab-label-tertiary":"oklch(72.82755% 0.014297 286.039)","--lab-label-quaternary":"oklch(61.25907% 0.011943 286.042)","--lab-border-strong":"oklch(98.92134% 0.005280 286.303)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-brand-secondary":"oklch(82.44675% 0.088464 257.008)","--lab-label-brand-tertiary":"oklch(73.08383% 0.140495 257.442)","--lab-label-brand-quaternary":"oklch(61.77245% 0.208353 257.413)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(61.75781% 0.208491 257.338 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.86605% 0.005516 31.054)","--lab-label-danger-secondary":"oklch(83.38591% 0.092088 28.223)","--lab-label-danger-tertiary":"oklch(74.63167% 0.153784 28.842)","--lab-label-danger-quaternary":"oklch(64.10554% 0.244701 28.706)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.75683% 0.008520 67.727)","--lab-label-warning-secondary":"oklch(83.08426% 0.131366 68.787)","--lab-label-warning-tertiary":"oklch(73.86147% 0.161425 68.614)","--lab-label-warning-quaternary":"oklch(61.96370% 0.135496 68.532)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.51065% 0.024195 147.300)","--lab-label-success-secondary":"oklch(80.08887% 0.235054 147.469)","--lab-label-success-tertiary":"oklch(70.67977% 0.207519 147.455)","--lab-label-success-quaternary":"oklch(59.24524% 0.173588 147.533)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-info-secondary":"oklch(82.42821% 0.087905 259.666)","--lab-label-info-tertiary":"oklch(73.17409% 0.139071 259.898)","--lab-label-info-quaternary":"oklch(61.97090% 0.205878 259.740)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.06909778454881225","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.06091370558375635","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.04060913705583756","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.028061954965508264","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(72.82755% 0.014297 286.039)"}},{"theme":"light","bg":"#007AFF","vars":{"--lab-bg-tone-2":"oklch(57.87475% 0.012117 286.015)","--lab-bg-tone-3":"oklch(57.87475% 0.012117 286.015)","--lab-label-primary":"oklch(13.41536% 0.016099 291.497)","--lab-label-secondary":"oklch(20.01464% 0.013939 291.638)","--lab-label-tertiary":"oklch(33.31157% 0.013986 291.359)","--lab-label-quaternary":"oklch(48.82850% 0.012656 285.922)","--lab-border-strong":"oklch(13.41536% 0.016099 291.497)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.23588% 0.071518 256.892)","--lab-label-brand-tertiary":"oklch(33.52307% 0.122654 257.729)","--lab-label-brand-quaternary":"oklch(49.33014% 0.177503 257.333)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(20.90400% 0.085780 29.234)","--lab-label-danger-tertiary":"oklch(34.90075% 0.142917 28.593)","--lab-label-danger-quaternary":"oklch(51.33845% 0.210254 28.629)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.32691% 0.044866 67.175)","--lab-label-warning-tertiary":"oklch(33.70499% 0.073308 69.356)","--lab-label-warning-quaternary":"oklch(49.32505% 0.107532 68.994)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(90.50991% 0.070001 68.839 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(19.36939% 0.056254 147.869)","--lab-label-success-tertiary":"oklch(32.05045% 0.093836 147.561)","--lab-label-success-quaternary":"oklch(47.21271% 0.138736 147.423)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(87.96697% 0.214930 147.350 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(20.33878% 0.084139 260.257)","--lab-label-info-tertiary":"oklch(33.98919% 0.139378 260.097)","--lab-label-info-quaternary":"oklch(49.73960% 0.201561 259.877)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1414396647356095","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.180541833147175","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.09073588245366797","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.0643078283945157","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.31157% 0.013986 291.359)"}},{"theme":"light","bg":"#FF3B30","vars":{"--lab-bg-tone-2":"oklch(63.60048% 0.011829 286.059)","--lab-bg-tone-3":"oklch(63.60048% 0.011829 286.059)","--lab-label-primary":"oklch(13.83495% 0.015576 284.180)","--lab-label-secondary":"oklch(25.12484% 0.015017 285.269)","--lab-label-tertiary":"oklch(36.40556% 0.013673 291.446)","--lab-label-quaternary":"oklch(51.30243% 0.012497 285.951)","--lab-border-strong":"oklch(13.83495% 0.015576 284.180)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(14.05977% 0.051776 257.879)","--lab-label-brand-secondary":"oklch(25.33060% 0.091490 257.423)","--lab-label-brand-tertiary":"oklch(36.69778% 0.133197 257.540)","--lab-label-brand-quaternary":"oklch(51.93456% 0.187074 257.359)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.49010% 0.059461 29.234)","--lab-label-danger-secondary":"oklch(26.17120% 0.107130 28.474)","--lab-label-danger-tertiary":"oklch(38.31782% 0.156989 28.748)","--lab-label-danger-quaternary":"oklch(53.87750% 0.220648 28.624)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(14.09840% 0.030367 70.955)","--lab-label-warning-secondary":"oklch(25.16306% 0.055216 68.014)","--lab-label-warning-tertiary":"oklch(36.95716% 0.080989 68.209)","--lab-label-warning-quaternary":"oklch(51.83499% 0.113346 68.533)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(94.55166% 0.039779 69.746 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.47470% 0.038038 148.984)","--lab-label-success-secondary":"oklch(24.13011% 0.070822 147.468)","--lab-label-success-tertiary":"oklch(35.33425% 0.103721 147.463)","--lab-label-success-quaternary":"oklch(49.62199% 0.145290 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(93.08890% 0.111383 147.112 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(14.07469% 0.058967 260.480)","--lab-label-info-secondary":"oklch(25.53067% 0.104920 260.137)","--lab-label-info-tertiary":"oklch(37.14168% 0.149591 259.762)","--lab-label-info-quaternary":"oklch(52.13660% 0.212021 259.943)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.18357487922705315","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.4395613333149521","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.17777918620864494","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.10167726510894212","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(36.40556% 0.013673 291.446)"}}]} \ No newline at end of file +{"_meta":{"purpose":"Байт-точный golden границы JS↔WASM: recheckContrast/_recheckContrastMulti и полный snapshot resolveTheme.vars.","regenerated":"2026-07-10","regen_reason":"ADR-0004: point-glow решается по конечным encoded-sRGB8 screen-state; alpha эмитируется кратчайшей decimal-записью без повторного округления.","drift_scope":"Относительно origin/main изменены ровно 24 листа resolveVars: только --lab-fx-glow-{brand,danger,warning,neutral}-alpha на 6 фонах. Ни один base/core, label, border, fill, background или другой var не изменён.","recheck_unchanged":"Секция recheck: 2752/2752 числовых значений, то есть 1376/1376 пар (lc, wcag), байт-идентичны origin/main; ни одно значение recheckContrast не изменено.","numeric_provenance":{"glow_alpha":"Старый fixed-48 bisection + 4-decimal alpha заменён перебором конечных sRGB8-state, внутренней alpha выбранного интервала и shortest-roundtrip binary64 CSS.","scope_check":"Фактический структурный diff вычислен по каждому листу resolveVars; 4 alpha-ключа × 6 записей = 24."},"wcag_floors_not_weakened":"Glow — декоративная роль без WCAG-пола. Отдельно подтверждено: все 1376 пар (2752 числовых значения) recheck неизменны, поэтому измеренный контраст ни одной проверяемой роли не ослаблен.","isolated_verification_required":"Регенерация принимается только после независимой проверки scope, recheck-инварианта и полного Rust/npm/browser gate.","history":{"previous_regenerated":"2026-07-10","previous_regen_reason":"Глава #64 (ADR-0003): ось читаемости активирована в домене Ys — реактивный recheck (measure_contrast/recheck_against) и solver-вывод считаются в Ys. recheck flat и resolveVars регенерированы против пересобранного движка; inputs (theme/bg/fgs) сохранены. semver-major по ADR.","previous_drift_scope":"resolveVars: exactly 24 leaves changed, all --lab-fx-glow-{brand,danger,warning,neutral}-core (glow-centre colour, background-independent → 4 distinct values). No other var moved. glow base/alpha, labels, borders, fills, backgrounds all byte-identical.","previous_recheck_unchanged":"Исторический regen также не менял recheck; прежний текст ошибочно называл 2752 числа парами. Фактический объём: 1376 пар (lc, wcag).","previous_numeric_provenance":{"glow-brand-core":"oklch(78.84894% 0.062240 265.472) -> oklch(78.76611% 0.108476 257.256): chroma rises to the blue gamut wall at the functional lightness (fixed-fraction under-saturated it); L ±0.08pp, hue shifts to the cusp.","glow-danger-core":"oklch(81.62208% 0.103545 27.533) -> oklch(81.77103% 0.102764 29.025): near-identical (red already sat near its wall).","glow-warning-core":"oklch(88.80612% 0.081204 68.866) -> oklch(88.88181% 0.082838 68.777): near-identical.","glow-neutral-core":"oklch(99.97226% 0.001314 106.423) -> oklch(100.00000% 0.000000 89.876): neutral hue is achromatic → gamut wall chroma is 0 (was fixed-fraction residue noise); hue_vanished flag fires honestly for the achromatic sentiment."},"previous_wave1_scope":"resolveVars: 69 info-* leaves changed across 6 theme×bg entries. Zero non-info drift (scope-checked). recheck untouched."}},"recheck":[{"theme":"light","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#00030E","#0057BB","#037BFF","#78AFFF","#150000","#BE0005","#FF070E","#FF9A8C","#1B0D00","#A06300","#D28300","#FFAA3A","#000B01","#007C2D","#00A73F","#00D452","#020013","#4D00F9","#6D6EFF","#9FA9FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.59876563358553,20.60103636973117,79.91552226990729,6.816302360996974,63.95198968935827,3.97647793086433,41.89234722606997,2.2400949113548063,105.32470288110206,20.35108757719671,78.97384099389187,6.579727828852003,63.90360859563009,3.970705427078051,37.83221395673913,2.048194921340976,103.87315757816087,19.018778265876662,70.62528347734393,4.90716956443272,53.97808064225952,3.0056474129326713,34.232350404909745,1.8982213760304238,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.76444562516849,20.751302844974063,83.48658341134008,7.825697912025839,63.67798680778349,3.9439477616440737,40.86459333479343,2.1890112231357013]},{"theme":"light","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E6F0FF","#60A1FF","#0074F3","#004CA4","#FFF4F2","#FF8273","#F40009","#A80004","#FFF7EE","#F79C00","#C97E00","#8F5800","#C2FFC9","#00C84D","#00A03C","#006E26","#EEF0FF","#9099FF","#6662FF","#4300DB"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-97.97941453819666,18.27263411531394,-53.55180732861014,8.039306941172763,-34.54692380627581,4.792369408161928,-17.418806999279397,2.56984376276454,-102.41236835704379,19.474434420712562,-56.92703132210305,8.692613650917824,-34.93266406694178,4.850563950189369,-18.308962222075785,2.6667193472488013,-103.5613239139708,19.790915044333452,-61.97487030298502,9.71021814570805,-44.901407836051874,6.467843223430967,-25.783665577412222,3.5638173443979557,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.707239087904505,6.093571182387199,-23.370874926227668,3.2583602660985775,-98.9883689300502,18.54346384276384,-54.32153113615954,8.18635152260471,-34.304902287955215,4.756029101219059,-14.435475219756622,2.261552965854495]},{"theme":"light","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00163A","#003272","#004494","#040000","#3A0000","#760002","#990003","#0B0400","#261300","#512F00","#845100","#000200","#001D05","#004114","#006322","#000002","#160059","#2B0097","#3B00C5"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.89312592554248,4.521969910173767,28.754774555141132,3.1292028976206594,22.090354756391577,2.35851620111828,40.23106479600237,5.289901483963394,36.823385380889306,4.5064610721321205,27.786796188195932,3.002060282138028,21.02583740042693,2.2570951143110336,39.64148780057383,5.154352713354357,36.89828467875601,4.523118840482662,27.981450040156208,3.027209986332121,13.567318350507929,1.6836851754208233,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.3306726916817,5.312553092992316,36.84988795298528,4.512349526507044,30.916517468784825,3.4313626667845316,25.033605894158644,2.6690076358452903]},{"theme":"light","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#E7F1FF","#73ADFF","#2985FF","#0060CB","#FFF5F4","#FF9687","#FF4336","#CD0006","#FFF8F0","#FFA62C","#DC8A00","#AC6B00","#C7FFCC","#00D251","#00B043","#008631","#EFF1FF","#9CA6FF","#767AFF","#532BFF"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.32090210121694,9.956824779623638,-48.0046310941281,4.945145596338778,-30.285195552724065,3.1849116126306156,-14.342034884792934,1.9106934427040745,-91.71639880150975,10.602849175762582,-51.97724928251108,5.385053682831725,-31.570923815646545,3.301015045785777,-14.82740446463819,1.94471301206827,-92.86717481243348,10.774663089384916,-55.64998948221256,5.805759082702303,-40.41406674606042,4.149700467279057,-23.7537806706175,2.6249801387527425,-87.99158651504085,10.054343074329001,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-88.33220875070053,10.104016013712561,-48.80525652513833,5.032519885671211,-30.915189659062865,3.2415642790980215,-11.323318014385267,1.7063261265170042]},{"theme":"light","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000103","#001536","#003374","#0058BC","#070000","#360000","#730002","#BE0005","#0F0500","#241200","#4F2F00","#A16400","#000400","#001B05","#003F13","#007C2D","#000005","#140055","#3300AC","#4E00FA"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.65687576920386,5.198408498908002,36.59714090951085,4.504660010775197,27.83078380746869,3.029349552004155,13.31049770996219,1.6744770124926043,39.57933742278427,5.181007308853048,36.66186584356392,4.51899176883356,27.77510172821011,3.0221297438766705,12.721590912296357,1.6379804580418844,38.8564182220264,5.016999835671664,36.579572970042136,4.500775701626332,27.65113132417139,3.006116231224861,0,1.205042331050678,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.737197591996,5.216381929144518,36.59037871155487,4.5031645842195545,27.68585404738298,3.010592990868115,17.038043883395044,1.933101474636693]},{"theme":"light","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000104","#00204D","#003B83","#005CC4","#080000","#4C0001","#830002","#C70006","#0F0600","#331C00","#5B3600","#A76800","#000400","#002809","#004917","#00822F","#000005","#1F0075","#3A00C3","#511AFF"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.87966672163739,5.884401548408216,38.30767260595269,4.50632138120243,29.16726657041677,3.0308664461202253,15.823096179531001,1.7789458759332724,43.782795788734404,5.8597651770218455,38.402450836988166,4.527103297945568,29.016037258963053,3.011424325654164,14.9864556348382,1.7254955647131072,42.99821792375369,5.657954629703501,38.41622407485322,4.5301350068404425,28.93769973409828,3.0014083479801514,0,1.28054929135364,43.62397616937584,5.819206103341516,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.970091731453365,5.907320242919889,38.28628402517786,4.501651045763603,28.99214635836292,3.0083657301901288,19.11088367642911,2.0125842767555064]},{"theme":"light","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E6F0FF","#64A4FF","#0077F9","#0050AC","#FFF4F3","#FF8777","#F9000A","#B00005","#FFF7EF","#FA9E00","#CD8000","#955C00","#C3FFC9","#00CA4E","#00A33D","#007329","#EEF0FF","#939CFF","#6967FF","#4600E6"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.3221346483494,16.53807546575917,-53.23458352949761,7.5085332653696355,-34.299151794570456,4.531377660721796,-17.34040000814869,2.4827755446650244,-100.79897840003042,17.636700891614694,-56.73966900010428,8.131153426700068,-34.4661942600266,4.554628420782135,-18.26813498707333,2.5778416349364046,-101.94670279716274,17.922901715066683,-61.50032492399525,9.010497025774457,-44.48245067627942,6.049054371037485,-25.983086445749187,3.447217107352778,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-97.33108904020293,16.783196253636362,-53.9780650339559,7.638815670143083,-34.15649094558268,4.511565332868746,-14.178333309149869,2.174938899863266]},{"theme":"light","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#00030D","#0057B9","#0079FD","#74ADFF","#150000","#BC0005","#FD000A","#FF9788","#1B0D00","#9F6200","#CB7F00","#FFA730","#000B01","#007B2C","#00A63E","#00D251","#020013","#4C00F7","#6C6CFF","#9DA6FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.48497819102282,19.361251289328656,75.9364891028185,6.438321694919453,60.636932359482174,3.8278153397206016,38.76699274290234,2.152541489746932,101.19913515757631,19.11640864166299,75.28055256994783,6.281220843840182,60.46094873358925,3.807428346232839,34.70184973776556,1.9658801332164664,99.74758985463511,17.864929125608796,66.9127104243937,4.673002507305443,52.13428535623209,3.0001216660302674,31.16352525448906,1.8227359456035261,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.421867335088024,3.0235758695027473,33.32486172802245,1.9082022515525083,101.63887790164272,19.492343272893066,79.67393134344084,7.442688338927282,60.17031912503092,3.7741072518393395,37.967753096619255,2.113772626279229]},{"theme":"light","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E6F1FF","#67A5FF","#0079FD","#0053B2","#FFF5F3","#FF8A7A","#FD000A","#B60005","#FFF7EF","#FC9F00","#D08200","#9A5F00","#C4FFCA","#00CB4E","#00A53E","#00772A","#EEF1FF","#949EFF","#6B6BFF","#4900EE"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-94.97224440690107,14.900628115594163,-51.99577035710509,6.808135431331698,-33.457001357615106,4.175350120738554,-16.755883343489252,2.3335896778079332,-99.44736446714371,15.886233758087014,-55.869159717403925,7.428487313030341,-33.63398343987836,4.1977071628099685,-17.709012021143003,2.4235530906296177,-100.15616825242344,16.04465607216319,-60.39258141645385,8.181935837052826,-43.77686148964125,5.57032618168592,-25.67861999448382,3.2497240951569792,-95.39392806862337,14.992417327878547,-58.246405974587134,7.820609198173194,-41.3596549106396,5.227261702394524,-23.16505433719768,2.9752806858720673,-95.97861011098902,15.120061263203354,-52.9765437482588,6.963016139185695,-33.57839540296167,4.190679013723035,-13.485138200601865,2.040069034387161]},{"theme":"light","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EBF3FF","#D5E6FF","#92BFFF","#4291FF","#FFF8F7","#FFDDD8","#FFA597","#FF6253","#FFFAF5","#FFE0BE","#FFAA39","#E79100","#D0FFD4","#7DFF93","#00DA54","#00B947","#F1F4FF","#DFE4FF","#AEB8FF","#8087FF"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.43421936609613,5.100687932363686,-64.08586870464498,4.502412953317652,-41.11294430990105,3.017135229848019,-18.888486914529935,1.8251241221961068,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-40.940708789235714,3.0069340578770496,-21.122834074579163,1.9331720160112165,-77.70409895709017,5.493642615376888,-64.34147182620212,4.520282197700913,-40.849183913847796,3.0015190917179395,-28.18413111501551,2.292444746213784,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-41.2870784084647,3.027463512408998,-26.013778316259767,2.1791858289450103,-73.64084437140687,5.1896286230945075,-64.42349258712335,4.5260223420377566,-41.02780633656069,3.0120908713744514,-19.464187683477977,1.8526993919522168]},{"theme":"light","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#E8F1FF","#79B0FF","#368BFF","#0065D6","#FFF6F4","#FF9B8D","#FF5446","#D80007","#FFF8F1","#FFAB3F","#E18E00","#B47000","#C8FFCD","#00D552","#00B545","#008D34","#EFF1FF","#A0AAFF","#7B81FF","#573FFF"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-89.77514075602531,11.177468536043195,-51.89752791553575,5.733156384550998,-34.996215684604906,3.8114980409485155,-18.92394323577027,2.320895932067837,-94.48648764812391,11.954651961610136,-56.02375667309079,6.252768618847473,-36.81873931987068,4.002056146272223,-19.591172380553846,2.375625103310219,-95.24016771148194,12.080915286992608,-59.85145182180837,6.751641124414219,-44.82390104707481,4.887508677146329,-28.683175933367217,3.184369254481423,-90.45906258458693,11.288985892577246,-57.499124974387314,6.443154991463433,-42.83495757582209,4.660276446547384,-26.099142978345885,2.9428672820648862,-90.66219607331998,11.322193559065964,-52.9772743130274,5.867283737103295,-35.76790053375578,3.8916727925995533,-16.863337707955058,2.1560372721390864]},{"theme":"light","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#000209","#00479B","#0065D5","#3289FF","#100000","#9F0004","#D70007","#FF4F41","#170A00","#7A4A00","#9F6200","#DF8C00","#000801","#006322","#00822F","#00B344","#01000D","#3E00CE","#573EFF","#797EFF"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.22533906008685,12.552007092013115,56.99670466281028,5.352680994694784,44.43662180236468,3.3253647200326193,29.122033451491582,2.0641033245604916,76.01516726706762,12.436514137733148,55.97147975339624,5.133613537650522,43.91010121645754,3.2654000659595037,27.461071919951625,1.9716236837030474,74.81611736383091,11.770648316029503,52.816193854903005,4.528105409994289,41.51373908005361,3.011071841176318,19.679891349505557,1.6112041351931181,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.42233250009274,3.001939806510638,21.536768417458934,1.687644517337887,76.3513076320574,12.62087594492386,59.99704797613187,6.063965240585196,46.62694808670478,3.592268428661898,28.6381619838115,2.036506690369533]},{"theme":"light","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#DA7A00","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.42046624663428,20.43859328774187,80.31672424485741,6.9206271592755195,64.53598724808649,4.047139863973963,42.92136358171458,2.2930773168482466,105.12312344447312,20.1664284240786,79.32640956847692,6.666965706057784,64.47466861031803,4.0396342482899685,38.694434835096864,2.0867987099188317,103.94997559222902,19.08898290736351,71.78028267942507,5.099770465607366,55.19920557284124,3.104259347966041,35.17783363962355,1.9359471784913758,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.57179613789485,3.2209892436744982,37.49012931672191,2.03317973864909,105.58294145619523,20.58664787402852,83.26627791326895,7.757864847967472,64.57504887916579,4.051931818616954,42.090577540190225,2.2501559682241092]},{"theme":"dark","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#E3EEFF","#639EFF","#006DFE","#0048AD","#FFF4F3","#FF7F74","#F1001F","#A80012","#FFF7F1","#FF921B","#D17400","#955100","#B8FFBE","#00C649","#009F39","#006E25","#EDEEFF","#9794FF","#725BFF","#4E00D6"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.73192161780433,17.93999294470544,-52.63286661912575,7.865273702162647,-33.381528921151094,4.618607106385301,-17.0501576858861,2.5303719690800346,-102.4562582898777,19.486486384763445,-56.148341062851905,8.53993960492931,-34.330992536827104,4.759940239487347,-18.37234103122508,2.6737006902768288,-103.68997976483836,19.82648045930809,-60.35961607275431,9.379380118591195,-43.39157032733722,6.209218370112115,-24.93643207012505,3.4548840566407524,-97.27916136850891,18.085610527348415,-59.34389647192141,9.173841233004914,-42.25887199411116,6.018336366435839,-23.360562325895305,3.2570864547421428,-97.9872053947921,18.274719246988955,-53.323021296304695,7.99582368531923,-33.63351308594995,4.6559156780457185,-14.771084781300106,2.2949481607054216]},{"theme":"dark","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000104","#00153F","#00317C","#00429F","#070000","#3A0002","#750009","#9B0010","#0C0300","#291200","#562D00","#8B4B00","#000300","#001D05","#004112","#006521","#010005","#1A0057","#350098","#4700C4"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.20955594591163,5.284998875760269,36.83828085440759,4.509769828130583,28.330761142327287,3.0728704517581886,21.819504217645104,2.3321848000622474,40.142120786515875,5.269603471071473,36.80831529061993,4.503115584720142,27.930310922781615,3.0205822411426944,20.559717681283935,2.214401964672886,39.70511745091868,5.169077378914306,36.80795164205375,4.503034882134433,27.78871707797606,3.002307432697674,14.025581970431295,1.7128949100718707,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.795231110735635,4.500212645278562,30.09285126953815,3.3133648365325183,24.185949678819867,2.5748913454036786]},{"theme":"dark","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E4EFFF","#75AAFF","#3482FF","#005AD5","#FFF6F4","#FF9389","#FF3C3B","#CC0019","#FFF8F2","#FFA350","#E58000","#B36300","#BCFFC2","#00D04D","#00AF3F","#00862E","#EEEFFF","#A2A1FF","#7F74FF","#601DFF"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-86.07057705487603,9.776047393674569,-47.005198509651805,4.836992692850337,-29.617188814986996,3.125340410912816,-13.465221764046625,1.8500449052652255,-92.15650032550448,10.66842694645063,-51.122226750198095,5.289032001141636,-30.69564720892069,3.221769980092269,-14.697188723997265,1.9355550836437716,-92.95341233620333,10.787583067801071,-55.01595924446716,5.732183607436098,-39.01368610071904,4.00959458617392,-22.717127042375882,2.5408179478715844,-86.56463892830851,9.847321829129962,-52.766100540364825,5.474290510931042,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.32881842479753,9.957973594755346,-47.6452882768299,4.906141960838558,-29.99057738527554,3.1585747205458907,-10.990631160230787,1.6845762464445797]},{"theme":"dark","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000107","#00133B","#00317C","#0054C7","#0B0000","#360002","#730009","#BF0016","#100500","#261000","#542C00","#A85D00","#000501","#001B04","#004012","#007D2B","#010009","#180052","#3B00A6","#5A00F5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.616537893860624,5.189361828546554,36.671589094740156,4.521147554384925,27.767977778595686,3.0212072610618272,13.560368041515986,1.6902988819780091,39.46146930028483,5.1544680971312,36.646426655727396,4.515570174173857,27.733751845040576,3.016779185756929,12.426352071081123,1.6200937148513173,38.82022467282525,5.008731246970903,36.66757146424993,4.520256699490143,27.6755983348751,3.009270049666561,0,1.2356833222737893,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.666598999480925,5.200587158978297,36.5946359240296,4.504106003407952,27.684199915070796,3.0103795763779906,16.582944142840912,1.898781610023956]},{"theme":"dark","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000107","#001E53","#00398D","#0058CF","#0C0000","#4C0004","#83000C","#C60018","#110500","#371A00","#603300","#AE6000","#000601","#002808","#004916","#00822D","#01000A","#250070","#4400BC","#5E01FF"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.84943203331798,5.876721182230661,38.357196991852035,4.517162998132957,28.969046969390032,3.0054117916551304,16.078397205708793,1.7957132708136676,43.66281217184597,5.829141814712315,38.38176722933373,4.522556047220382,28.96308732518828,3.004650205577566,15.121014183458136,1.7339400568635355,43.01534021323247,5.66238735865634,38.36369120360285,4.518587529333456,28.956857085057415,3.003854272405285,0,1.32359073643957,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.88937786124038,5.8868666342953615,38.32550423955951,4.510220548286027,28.94413025668513,3.002229117722787,19.264807559936084,2.024522036885816]},{"theme":"dark","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E3EEFF","#66A0FF","#0E71FF","#004CB5","#FFF5F3","#FF8378","#F60020","#AF0013","#FFF7F1","#FF962A","#D57700","#9B5500","#B9FFBF","#00C849","#00A23A","#007226","#EDEEFF","#9997FF","#7460FF","#5200DF"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-95.07464172795706,16.23701078357789,-51.883866319617056,7.27432706128743,-33.10437599843017,4.366728841176053,-16.962076325331164,2.444617555127284,-101.23789901188303,17.745933910216884,-55.660852739640006,7.937252290960222,-33.86013630190819,4.470540437936136,-18.127983335051407,2.5633448249431416,-102.03269987499108,17.94442049171427,-60.01549050114797,8.732123022916136,-43.24478677485694,5.853976070416037,-25.11442835418257,3.342537791539289,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.852118066322895,5.637910373772879,-23.122294138224145,3.108849028833141,-96.32992550494482,16.539962663017093,-52.86610846158114,7.444323350911882,-33.336065162067605,4.398429825394501,-14.383943910910213,2.194178799785328]},{"theme":"dark","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#D67800","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.29489852310851,19.198605473400605,76.19115652133168,6.500760037097026,60.41041952456072,3.801604173546957,38.795795858188825,2.153958743950203,100.99755572094737,18.94295256384705,75.20084184495119,6.262487955668726,60.349100886792264,3.7945539156195487,34.56886711157111,1.9601948416949677,99.82440786870325,17.93087452582946,67.65471495589931,4.7903727911067815,52.29898260017819,3.0135204434714913,31.0522659160978,1.8184953129572932,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.44622841436909,3.025575236651027,33.364561593196164,1.9098288766402804,101.45737373266948,19.337677744697757,79.14071018974319,7.287203401685083,60.44948115564005,3.8061054053754897,37.96500981666448,2.1136413881018767]},{"theme":"dark","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E3EEFF","#69A2FF","#1975FF","#004FBB","#FFF5F3","#FF877C","#FA0021","#B50014","#FFF7F1","#FF9833","#D87900","#A05800","#B9FFC0","#00CA4A","#00A43B","#007628","#EDEEFF","#9B99FF","#7664FF","#5500E7"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-93.28410718321774,14.535439506623602,-51.011442228336165,6.654206660130301,-32.65955270222981,4.075313081844305,-16.370466566638967,2.297770413224217,-99.44736446714371,15.886233758087014,-55.07262883280597,7.299026812522581,-33.02825162884881,4.121422426501604,-17.569909707299868,2.4103015359642868,-100.24216533025177,16.063919770413232,-58.9183675206033,7.932997007393653,-42.52780974275247,5.391835753982905,-24.79898612463087,3.1522681596337323,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.905085629416114,5.1638402024196015,-22.782862127638978,2.93464773079297,-94.53939096020551,14.80664328764592,-51.95162400089297,6.801199303451522,-32.707141977036414,4.081250761977603,-13.69412377461175,2.0581050526656677]},{"theme":"dark","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E8F1FF","#D5E6FF","#95BEFF","#498FFF","#FFF9F8","#FFDDD8","#FFA59B","#FF5F57","#FFFAF6","#FFDFC5","#FFA95E","#F18700","#C6FFCB","#7DFF91","#00DA51","#00B943","#F0F1FF","#E1E3FF","#B4B6FF","#8982FF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-71.17583391665679,5.008590480308264,-64.08586870464498,4.502412953317652,-40.969901639859465,3.0086620775128243,-18.492326685553408,1.8062558438836862,-77.39176152066469,5.470028106981068,-64.06334225320066,4.50083949888721,-41.06186552000793,3.0141084284175577,-20.58109958398818,1.9067218663179524,-77.74802214310357,5.496966722090798,-64.16882972322054,4.508209630901898,-40.97688137217631,3.00907529195687,-26.93048349706687,2.2267208299060117,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.24499881268402,3.0249663274797034,-25.961518214862377,2.176489332337808,-72.19208007215988,5.082914152992658,-64.24580556994597,4.513590807561596,-40.91738125342747,3.005553523356943,-18.74578210381434,1.818317299241165]},{"theme":"dark","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E5EFFF","#7AADFF","#3D87FF","#005FDF","#FFF6F5","#FF988E","#FF4D48","#D5001A","#FFF8F3","#FFA75A","#EA8300","#BA6700","#BDFFC3","#00D34E","#00B341","#008C31","#EEEFFF","#A5A5FF","#837AFF","#6433FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.52359832003891,10.974551054175622,-50.82334067294272,5.601029591424501,-33.90019185783247,3.6989242542195706,-17.94937633357589,2.2421371448335408,-94.53061684959366,11.962030249162733,-55.11693338695639,6.136943021617957,-35.65662848796355,3.880065704029081,-19.010500723408704,2.3279588656978594,-95.32685818617963,12.095472485308715,-58.832789738116276,6.617312123916825,-43.099834270009964,4.690265818580579,-27.202021451205145,3.0448373808729228,-89.02366089613628,11.055448870118658,-56.507119659393844,6.314879036055816,-41.91393002442956,4.556652813501584,-25.675484951698337,2.9041378624661793,-89.65880574741696,11.158543725868578,-51.69548126800453,5.708204299589472,-34.40239467659594,3.750316223165759,-16.078998268001875,2.0949612264666513]},{"theme":"dark","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00020F","#0045A5","#0060E2","#3F88FF","#140000","#A10011","#D7001B","#FF4F49","#190900","#814600","#A85D00","#EA8300","#000A01","#006320","#00822D","#00B341","#030014","#4A00CC","#6537FF","#847BFF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.15878035868923,12.51550617725489,56.792209524954714,5.308053794065892,44.82231091099692,3.370285744389143,29.010809014184062,2.057710850183084,75.84645644595844,12.343365988484052,55.500674792931,5.036843578382145,43.81499067450534,3.25473208522667,27.348178994525146,1.9655632934834584,74.80511978941351,11.764533211175078,52.793708756474935,4.52413702940673,41.44647230556496,3.0043476386355414,20.422200521634597,1.6411239547419576,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.19495675605003,12.535354496536943,59.24767070343283,5.876074797996776,46.58341111523108,3.5866744685580465,28.54022915013225,2.030988108287992]},{"theme":"dark","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"light-ic","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#000110","#0042E2","#3B78FF","#85AEFF","#150000","#BD0011","#FF001B","#FF998D","#1A0500","#B04A00","#E96400","#FFA174","#000B01","#007C2D","#00A73F","#00D452","#00040B","#00639C","#0089D6","#4DB3FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.76809306682351,20.754602558566376,81.70242596755779,7.298044196933449,63.72862579130605,3.9499301799077116,41.321899652634976,2.211520376313395,105.32470288110206,20.35108757719671,79.14032751842986,6.6207260543602775,64.03544238362204,3.986463961054262,38.10332212672181,2.0602150617562085,104.62285904737456,19.706640245810163,73.93672186564804,5.489431142391079,57.895853978119064,3.3399091341000195,36.26437665881752,1.9807324696579875,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.53601527934143,20.543945015271454,78.36297683006073,6.432254688155311,62.2832355663668,3.784280156272109,42.68471757482252,2.28072562681704]},{"theme":"light-ic","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E8F0FF","#709FFF","#2D6DFF","#0038C6","#FFF4F3","#FF8275","#F30019","#A7000E","#FFF6F1","#FF8C52","#DF5F00","#9D4100","#C2FFC9","#00C84D","#009F3C","#006E27","#DFF0FF","#10A5FF","#0082CB","#005689"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-98.22794037003116,18.339196272629252,-53.8293996799751,8.092203727319841,-34.21813658812841,4.7430333536498335,-15.622162303180753,2.381112763636304,-102.4562582898777,19.486486384763445,-56.97432424245902,8.701923574139988,-34.76332175899368,4.824974728357804,-18.151652148372637,2.6494391947645526,-103.24826961310278,19.70448161329645,-59.10147589327934,9.125072654774526,-40.787696351505694,5.7744786453226835,-22.81212855995232,3.1897383862050708,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.29572232514961,6.0245037012352585,-23.381466511664765,3.259668821930569,-97.12909045511768,18.04563033112125,-52.54162933208091,7.848085216280267,-36.273216344700636,5.055412083856902,-18.54110013749204,2.69234396770907]},{"theme":"light-ic","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00104D","#002488","#0032B2","#040000","#3A0002","#750007","#98000B","#080100","#2E0E00","#602500","#8F3B00","#000200","#001D05","#004114","#006322","#000102","#00192E","#003A5F","#004D7C"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.799792142283025,4.501224406456354,30.04772679653348,3.3069997994324063,23.797727835383583,2.533076638038498,40.23106479600237,5.289901483963394,36.80831529061993,4.503115584720142,27.941932493369514,3.022087143525339,21.190215752118235,2.2723970292907865,40.01441400964806,5.240352767492206,36.83929336929951,4.509994812829007,27.908315541084118,3.0177360720288915,16.77490328531677,1.9034128352995776,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.22977489962569,5.289607582090012,36.84755783504637,4.511831555904809,27.796281383025352,3.0032808914145437,21.058698283639867,2.260143784790762]},{"theme":"light-ic","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#EAF1FF","#81ABFF","#4982FF","#0048F5","#FFF5F4","#FF9589","#FF403B","#CC0013","#FFF7F3","#FF9E6E","#F46900","#BD5000","#C6FFCC","#00D251","#00B043","#008631","#E1F1FF","#45B0FF","#0090E1","#006CA9"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.69551010135778,10.011246151186945,-48.263751486481056,4.97335266465752,-30.44825366943375,3.199530942583609,-12.043546933756078,1.753943193668956,-91.71639880150975,10.602849175762582,-51.72679347557047,5.3568510977778265,-31.21621892784593,3.268795397886287,-14.661523244642579,1.9330507439017046,-92.55409442667087,10.727809933310253,-53.89547122979923,5.603122076267749,-35.95806640240085,3.7112655792375078,-20.04367187639212,2.329915503818876,-87.89039906251095,10.039605852107476,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-86.58838125370931,9.850752189993974,-46.78872683576137,4.81370216625391,-31.383244337404896,3.2839492580871354,-15.736559790538532,2.009284883552937]},{"theme":"light-ic","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000004","#000E48","#002895","#0042E3","#070000","#360001","#730006","#BE0011","#0C0200","#2B0D00","#5F2400","#B04A00","#000400","#001B05","#003F13","#007C2D","#000103","#00172B","#00395C","#00639D"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.74741500641279,5.218664231374835,36.670023859840576,4.5208004701163915,27.672031623266886,3.008810094200282,15.351341296852958,1.8098737923818007,39.57933742278427,5.181007308853048,36.654144364688555,4.517280323585742,27.751464091733354,3.019069960427185,12.671992881770905,1.6349566993987985,39.23716411822194,5.10371254671437,36.601471319299684,4.505617849545331,27.652865692456807,3.006339685224649,7.684471784052528,1.3665581876464832,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.65687576920386,5.198408498908002,36.65150522712613,4.5166954713046765,27.64129393913477,3.0048491045091428,12.054359140378313,1.5979376774972005]},{"theme":"light-ic","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000004","#001864","#002FA8","#0045ED","#080000","#4C0003","#830008","#C60012","#0D0200","#3E1500","#6C2A00","#B84D00","#000401","#002809","#004917","#00822F","#000103","#00233D","#00416A","#0068A4"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.98030914587015,5.909904848562026,38.293478372508915,4.503221171740978,28.94317626341717,3.002107337321695,17.95394033246701,1.9258322867444517,43.782795788734404,5.8597651770218455,38.38866051296957,4.524070782124866,28.984608537472,3.0074014381148784,15.156957289606964,1.7362054736619685,43.437607341847624,5.771384990646562,38.33399448678559,4.512078832150148,28.981628167432756,3.007020263830111,9.892674202630067,1.4441010369850493,43.614187977218386,5.816700203526642,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.88976990866122,5.886966133555759,38.401524303038336,4.526899458852529,29.03631728859714,3.0140233900654434,14.306925700285792,1.683714708917422]},{"theme":"light-ic","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E9F0FF","#74A2FF","#3371FF","#003CD0","#FFF4F3","#FF8679","#F8001A","#AF000F","#FFF6F2","#FF9058","#E36100","#A44400","#C3FFC9","#00CA4E","#00A33D","#007329","#E0F0FF","#21A8FF","#0085D0","#005B90"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.69585202312288,16.62869936569993,-53.53835262595417,7.561646720391368,-33.90853669724825,4.477228400656394,-15.643424374972522,2.314414627530853,-100.79897840003042,17.636700891614694,-56.5097869254783,8.089667628698802,-34.29633291417122,4.530985783372177,-18.10601775891745,2.56107712884258,-101.63443561648342,17.844851007069916,-58.71630179001587,8.4915976550775,-40.34850051574898,5.4087697654311055,-22.889197327723014,3.0820931228283226,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-95.5914257275877,16.361459308701495,-52.26386479821355,7.339891411251783,-35.96637326948896,4.765958094131511,-18.710865986535588,2.6239504003951875]},{"theme":"light-ic","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#000110","#0041DF","#3876FF","#82ACFF","#140000","#BB0011","#FC001B","#FF968A","#1A0500","#AE4900","#E76300","#FF9F70","#000B01","#007B2C","#00A63F","#00D252","#00040B","#00629B","#0088D4","#47B1FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.64252534329776,19.49544279635548,78.08171358923367,6.990900026896573,60.31711036263554,3.7908833425501425,38.14751646036453,2.122399526754989,101.24542779029852,19.156162076239593,75.44574141529571,6.3202869230281635,60.62673294996029,3.82662941345977,34.94277926170305,1.9762445240799382,100.4972913238488,18.51105924752946,70.36281029021609,5.256397052662421,54.37780741212484,3.1906001595018556,32.840214506002376,1.8885138214456323,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.4093478352768,3.0225491335598114,33.31123457222728,1.9076443919396007,101.41044755581565,19.297565623167603,74.60712675826916,6.125340559948453,58.63177587043786,3.604653750954651,39.55009522975671,2.1915837971666887]},{"theme":"light-ic","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E9F1FF","#76A4FF","#3875FF","#003ED7","#FFF4F3","#FF897C","#FC001B","#B50010","#FFF6F2","#FF935D","#E66300","#A94600","#C4FFCA","#00CB4F","#00A53F","#00772B","#E0F0FF","#2BA9FF","#0087D3","#005E95"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-95.34499825068502,14.98175500729861,-52.621417231568934,6.906762415988252,-33.47849770408375,4.178062617945301,-14.893749254597887,2.163552488849563,-99.00844385529109,15.788447906049281,-55.63494162352738,7.390318911682041,-33.467257033822236,4.176644120449875,-17.547592326855693,2.408179342981014,-99.84390107174409,15.974784754119627,-57.911424929350886,7.764836527735112,-39.59013024218227,4.982347876116714,-22.33006111099907,2.8868875311347497,-95.39392806862337,14.992417327878547,-58.26045217149408,7.822951516486187,-41.372488477704024,5.229057297458628,-23.176065198031452,2.976455642863797,-93.8008911828484,14.646846343308852,-51.05892490110174,6.66159703590636,-35.06283530237813,4.380256350040106,-18.07990151348513,2.4590883821528062]},{"theme":"light-ic","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EDF3FF","#D8E6FF","#9ABDFF","#598FFF","#FFF8F7","#FFDDD8","#FFA59A","#FF6156","#FFF9F7","#FFDECE","#FFA77C","#FF7112","#D0FFD4","#7DFF93","#00D955","#00B947","#E5F3FF","#CEE8FF","#79C3FF","#0099EE"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.68698719753608,5.119268434409956,-64.43727527190525,4.526987198998707,-41.01457995095163,3.0113075347043563,-19.296469937711088,1.8446469211142276,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-41.0312066106476,3.012292267380192,-20.969079339398604,1.925648566411713,-77.34727635116013,5.466668081501757,-64.08510797117285,4.502359813071005,-40.89305028080999,3.004113887137535,-23.84545366268376,2.068528569757355,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-40.827825177303865,3.0002560129496776,-26.013778316259767,2.1791858289450103,-71.69069599862902,5.046190228559761,-64.15987367011111,4.5075837056249375,-40.85863800181002,3.0020782433474276,-19.258832894479415,1.8428420367238694]},{"theme":"light-ic","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#EAF1FF","#86AEFF","#5189FF","#0650FF","#FFF6F4","#FF9B8F","#FF5349","#D70015","#FFF7F3","#FFA376","#FA6C00","#C75400","#C8FFCD","#00D553","#00B445","#008D34","#E2F1FF","#50B4FF","#0094E7","#0072B3"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-90.02549742397721,11.218239018758677,-52.117590678997175,5.760385990810306,-35.453068631322715,3.8588722418561026,-17.16315496591595,2.1796295803105563,-94.48648764812391,11.954651961610136,-56.08049309972586,6.260045669338659,-36.68653001054492,3.9880923379680597,-19.42498133620683,2.3619323117143227,-94.88408174929032,12.021194381023134,-58.024655972390946,6.511549338355004,-40.19653971715428,4.366167533936967,-25.014063203920518,2.8441658803090872,-90.45906258458693,11.288985892577246,-57.51342916294683,6.44501257640493,-42.39985031117064,4.611196053798779,-26.099142978345885,2.9428672820648862,-89.03890865199516,11.05791933389335,-51.142776043878534,5.640183677550314,-35.55629387962385,3.86961291577043,-20.57349953874105,2.4573829383449617]},{"theme":"light-ic","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#00010B","#0035BB","#044EFF","#4E86FF","#0F0000","#9F000C","#D60015","#FF4D45","#150400","#903B00","#BB4F00","#F86B00","#000801","#006322","#008230","#00B344","#000307","#005182","#0071B2","#0093E5"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.30385323493446,12.594966222228084,58.55230095328762,5.70794052541694,46.62204712410211,3.5916381056717506,29.005214449343097,2.0573900879913625,76.05323451380515,12.457481581934719,55.93606418979168,5.126251708978212,44.077718465236906,3.284322001440957,27.730078776096413,1.986177453897667,75.43283879959024,12.113874624281086,52.77586207835023,4.520990578468311,41.44187963136357,3.003889327255304,23.868980516769618,1.7916338607346896,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.41072061989075,3.0007825637465393,21.536768417458934,1.687644517337887,76.14729893560808,12.509202438867769,55.739334564053664,5.085602713702602,43.07798550292857,3.173723442083943,28.271676351762963,2.015968674447834]},{"theme":"light-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark-ic","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#D38200","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E2","#8F5BFF","#B49FFF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.36973966240839,20.39226096069594,79.16304803673168,6.626348275559002,63.912632944603466,3.971781216419874,43.318131952880584,2.3140155027501375,105.16667360757414,20.20636839933495,79.43361118518992,6.693805381831162,64.79709340122825,4.079330310903514,38.90470830731088,2.096381256670562,103.76723276112338,18.92212884965907,70.7453629185683,4.926701923645705,54.109273202798526,3.0160152686700097,34.512224892089854,1.9092708836328856,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.58363991444959,3.2220248289870597,37.502971296697396,2.0337403797475795,105.5639998705614,20.569417077144905,82.76169093861434,7.605398027541734,65.05029978451972,4.11090668779679,42.03743585537794,2.2474520733771812]},{"theme":"dark-ic","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#DFEEFF","#4AA0FF","#007AE0","#005198","#FFF4F3","#FF7D79","#EF0030","#A6001E","#FFF7EF","#F99A00","#CA7D00","#915800","#B8FFBE","#00C648","#009F38","#006E24","#F0EDFF","#A98DFF","#894CFF","#6000C7"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.24720863865953,17.81140932471069,-52.09132740819334,7.763491481432673,-34.99617079306692,4.860177125212619,-17.977340963301042,2.6303715448994067,-102.4562582898777,19.486486384763445,-55.7461567674855,8.461541475204797,-34.016004840410574,4.712824540535038,-18.066789785866806,2.6401456993132153,-103.60398268701002,19.802704734429785,-61.68353878496926,9.650187857341214,-44.76775468944945,6.444757556916112,-26.060791619056427,3.599837857825526,-97.27916136850891,18.085610527348415,-59.33082759509665,9.171209294707863,-42.24705692654848,6.016359585841944,-23.350526713793172,3.2558471191579894,-97.94239544297146,18.262727694350296,-53.03355221198167,7.94095381214551,-32.857248309508414,4.541449578831631,-15.041171256747345,2.322063533056465]},{"theme":"dark-ic","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000103","#001834","#00376B","#004A8C","#070000","#390005","#750012","#99001B","#0D0400","#261300","#522F00","#865100","#000300","#001D05","#004112","#006521","#010005","#22004F","#43008F","#5800B7"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.21965913293546,5.287302224624218,36.81767378273908,4.505192880903515,27.950777846769416,3.023233051937069,21.000772856623527,2.25477321245894,40.142120786515875,5.269603471071473,36.88792093057972,4.5208109211231795,27.866909320024153,3.0123854877509646,20.876190217747006,2.243276608438576,39.57947058767665,5.139984882718895,36.89828467875601,4.523118840482662,27.8635036155662,3.011945822214043,13.292640012092916,1.6665035199669143,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.80600977189143,4.502603953746083,29.651273965169533,3.2515347547731954,23.802145579236715,2.533547958530533]},{"theme":"dark-ic","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E1EFFF","#62ACFF","#0087F7","#0065BB","#FFF5F4","#FF928C","#FF3644","#CA0027","#FFF8F1","#FFA533","#DD8900","#AD6A00","#BCFFC2","#00D04C","#00AF3F","#00862E","#F1EFFF","#B29CFF","#956AFF","#7700F5"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-85.70519954500536,9.72347127561709,-46.57313915269262,4.790554592637207,-29.772868283167547,3.1391774083572677,-14.678197720495364,1.9342213705917548,-91.71639880150975,10.602849175762582,-50.907190680193835,5.264997878479995,-30.103011927821978,3.1686137953283313,-14.381529067368087,1.9134497328938256,-92.9101803888625,10.781105347058572,-55.36632323767275,5.772792653136124,-40.27550194757439,4.135743433221995,-23.629833528615176,2.614848030728214,-86.56463892830851,9.847321829129962,-52.75275099572737,5.472775232316544,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.71836703917482,10.014570561299498,-47.715877818006604,4.91379345402092,-29.619990398118716,3.125589171245837,-10.872649839108671,1.6769004806806627]},{"theme":"dark-ic","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000106","#001632","#00366A","#005EAF","#0B0000","#360004","#730011","#BD0023","#100600","#241200","#502E00","#A36300","#000501","#001B04","#004012","#007D2B","#020009","#20004A","#48009A","#6F00E5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.62660370456575,5.191620542906115,36.592103069401674,4.503545882983851,27.695057695096033,3.0117807086970667,12.482735608358766,1.623488823202914,39.46146930028483,5.1544680971312,36.631002505660305,4.512153756975423,27.677944212691237,3.0095726068531357,12.751768680957815,1.6398240151911772,38.729462058672325,4.987982714010286,36.579572970042136,4.500775701626332,27.747483785710887,3.0185550274007946,0,1.2039987886628472,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.63684952401951,5.1939188146581685,36.601297620325695,4.505579426399357,27.609531401873983,3.0007614645527125,16.085712499624492,1.86220122646402]},{"theme":"dark-ic","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000206","#002245","#003E78","#0062B6","#0B0000","#4C0008","#820015","#C50025","#110600","#341C00","#5B3600","#A96700","#000601","#002808","#004916","#00822D","#02000A","#2E0067","#5300AD","#7400EE"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.7603252928922,5.854038847974277,38.29235457140463,4.502975854531618,29.083956028514045,3.020138635078335,15.054852030501879,1.729780743830801,43.694363439742204,5.837205585263101,38.354206470247746,4.516507237210082,29.087182234169767,3.020553277003921,15.230546565278194,1.740856512459411,42.92491991360228,5.63897133999608,38.33365423714702,4.512004338988724,28.93769973409828,3.0014083479801514,0,1.2794758858630608,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.85966524740055,5.879321607935794,38.335090707081115,4.512318847459295,28.952846517354917,3.003342035233188,18.7969313728254,1.9885281036480773]},{"theme":"dark-ic","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E0EEFF","#4FA3FF","#007DE4","#00559F","#FFF4F3","#FF817D","#F40031","#AD001F","#FFF7EF","#FB9C00","#CE7F00","#965B00","#B9FFBF","#00C849","#00A239","#007226","#F0EEFF","#AA90FF","#8C52FF","#6500D0"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-94.7101738760577,16.149472101206463,-51.76844025191021,7.254462182627787,-34.605057818633654,4.573999892517157,-17.84521283217997,2.534242228411278,-100.79897840003042,17.636700891614694,-55.24750846744172,7.863491890193123,-33.54190885081095,4.42668652015819,-17.815136446883052,2.5311583129351276,-101.94670279716274,17.922901715066683,-61.02785966423507,8.921519321161206,-44.34904450398409,6.02788949767693,-25.86724540779468,3.4331609270477186,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.840254848173814,5.636085625741139,-23.122294138224145,3.108849028833141,-96.71860922522349,16.634224259107555,-52.4354046324141,7.369572153296626,-32.71416296796099,4.3135863728502315,-14.766203746026353,2.2302399315399106]},{"theme":"dark-ic","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#CC7E00","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E1","#8F5BFF","#B49FFF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.24417193888262,19.155084079579783,75.03748031320593,6.224334741672483,59.78706522107772,3.730817455350078,39.19256422935483,2.1736266322828217,101.04110588404839,18.980469423093126,75.30804346166417,6.287699296730837,60.6715256777025,3.8318416601446055,34.77914058378512,1.9691960254812915,99.64166503759763,17.774143327140518,66.61979519504256,4.627804134340593,52.265155427331,3.010761189364384,30.386657168564103,1.79343744066295,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.45807219092383,3.026547994099155,33.37740357317164,1.9103555042369325,101.43843214703563,19.321492322017065,78.73738474892319,7.17242701812758,60.92473206099399,3.861502331684902,37.91186813185219,2.111101535692422]},{"theme":"dark-ic","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E0EEFF","#53A4FF","#007FE8","#0058A5","#FFF4F3","#FF8580","#F80032","#B30021","#FFF7F0","#FD9D00","#D18100","#9B5E00","#B9FFC0","#00CA4A","#00A43A","#007628","#F0EEFF","#AC93FF","#8D57FF","#6800D8"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-92.91963933131836,14.457074514504098,-50.53751898728166,6.580638229837842,-33.762466759799054,4.213972880586457,-17.265259153354464,2.3814244063048684,-99.00844385529109,15.788447906049281,-54.621704682580784,7.2261694596451385,-32.708249696736004,4.081389020198301,-17.26088719592284,2.381011454094406,-100.19905346524207,16.054261379481122,-59.919533209261445,8.101702037099825,-43.64240726631874,5.55098822531801,-25.561703474916055,3.236683846808585,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.893113879964474,5.162174617584753,-22.782862127638978,2.93464773079297,-94.92807468048419,14.891026660055605,-51.83505302329671,6.7828987244211,-32.07221654016657,4.002369709649521,-14.066989001346808,2.090531498955086]},{"theme":"dark-ic","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E5F1FF","#D2E7FF","#8AC0FF","#2091FF","#FFF9F8","#FFDDDA","#FFA49E","#FF5C5D","#FFFAF5","#FFE0C0","#FFAA42","#E99000","#C6FFCB","#7DFF91","#00DA50","#00B943","#F3F1FF","#E6E2FF","#C1B2FF","#9D7AFF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-70.80493468751378,4.981574213000164,-64.1741844582375,4.5085838821814495,-40.86541522684489,3.0024791016401666,-17.915271666932362,1.7789289638170434,-77.39176152066469,5.470028106981068,-64.14420274941838,4.506488572747314,-40.82594869447877,3.0001450551263997,-20.102672577261966,1.8834970356793017,-77.70409895709017,5.493642615376888,-64.40976920815079,4.525061719384059,-40.93902681557826,3.00683450902807,-28.2154352545988,2.2940964528512438,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.23137463469248,3.02415799426954,-25.961518214862377,2.176489332337808,-72.5833510184408,5.111647048756453,-64.45521953810703,4.528243512077896,-40.9348520371047,3.006587427640115,-18.521762858381592,1.807654812879132]},{"theme":"dark-ic","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E1EFFF","#68AEFF","#008BFD","#006AC4","#FFF6F5","#FF9791","#FF494F","#D30029","#FFF8F1","#FFA941","#E28C00","#B46E00","#BEFFC3","#00D34D","#00B340","#008C31","#F1EFFF","#B49FFF","#9971FF","#7D09FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.0351868676248,10.895768939711282,-50.01919534416472,5.502978516969682,-33.92042169291382,3.7009882121251625,-19.118481017052428,2.3367853131295435,-94.53061684959366,11.962030249162733,-54.89593536437227,6.108854061876529,-35.13675470036395,3.8260433618990066,-18.695569008603144,2.3023140000481574,-95.24016771148194,12.080915286992608,-59.18458546592085,6.6635751999987605,-44.37309985295556,4.8355935478331284,-28.125674388660414,3.1315039953558483,-89.11957165172129,11.070992163190612,-56.49372778683512,6.313154752438012,-41.901500594929324,4.555261373045625,-25.675484951698337,2.9041378624661793,-90.04835436179425,11.221964232051386,-51.2922333885473,5.658542947646642,-34.21126239038315,3.7307191421666435,-15.254686874750304,2.03178289437105]},{"theme":"dark-ic","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00030D","#004D92","#006CC7","#008BFE","#130001","#9F001C","#D60029","#FF4B50","#190B00","#7B4A00","#A06100","#E38C00","#000A01","#006320","#00822D","#00B341","#040013","#5B00BE","#7E16FF","#9971FF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.08600684668272,12.475516218126787,55.96473752983635,5.13221099097022,43.3369682989835,3.2018590596167122,29.318326289650187,2.0754572365320043,75.88156299830736,12.362776535253154,55.82217638546274,5.102669267083783,43.91931922104849,3.266436635421996,27.87409429449102,1.9940348577564484,74.63415754799017,11.66954886455628,52.68236753448497,4.504554642142053,41.632593181405234,3.023005829909607,19.021862742942993,1.5853635699211195,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.18160530883091,12.528031661692317,58.96992438550627,5.808209180198148,47.36214126331064,3.68860336114861,29.106756402593504,2.0632235490419992]},{"theme":"dark-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]}],"resolveVars":[{"theme":"light","bg":"#FFFFFF","vars":{"--lab-bg-tone-2":"oklch(97.84197% 0.010603 286.202)","--lab-bg-tone-3":"oklch(97.84197% 0.010603 286.202)","--lab-label-primary":"oklch(19.12257% 0.014100 291.556)","--lab-label-secondary":"oklch(56.54196% 0.013721 285.953)","--lab-label-tertiary":"oklch(66.97448% 0.014606 285.999)","--lab-label-quaternary":"oklch(81.39504% 0.013897 286.087)","--lab-border-strong":"oklch(19.12257% 0.014100 291.556)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(19.39430% 0.069681 257.297)","--lab-label-brand-secondary":"oklch(57.33316% 0.205939 257.291)","--lab-label-brand-tertiary":"oklch(67.12978% 0.175419 257.690)","--lab-label-brand-quaternary":"oklch(81.31388% 0.094313 257.967)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(20.16318% 0.082740 29.234)","--lab-label-danger-secondary":"oklch(59.62536% 0.244212 28.655)","--lab-label-danger-tertiary":"oklch(69.12968% 0.198650 28.574)","--lab-label-danger-quaternary":"oklch(82.55135% 0.097676 29.389)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(19.32631% 0.042728 66.944)","--lab-label-warning-secondary":"oklch(57.27465% 0.125021 68.799)","--lab-label-warning-tertiary":"oklch(67.73500% 0.148484 68.161)","--lab-label-warning-quaternary":"oklch(82.04043% 0.139600 68.374)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(67.73500% 0.148484 68.161 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(18.59864% 0.054816 147.311)","--lab-label-success-secondary":"oklch(54.91588% 0.161394 147.418)","--lab-label-success-tertiary":"oklch(64.88263% 0.190555 147.443)","--lab-label-success-quaternary":"oklch(79.02491% 0.232159 147.432)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(64.96409% 0.172888 147.450 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(19.54489% 0.077696 259.509)","--lab-label-info-secondary":"oklch(57.68997% 0.232691 259.838)","--lab-label-info-tertiary":"oklch(67.26428% 0.173598 259.980)","--lab-label-info-quaternary":"oklch(81.49851% 0.092914 259.576)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.5","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.5","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.5","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.5","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(66.97448% 0.014606 285.999)"}},{"theme":"light","bg":"#000000","vars":{"--lab-bg-tone-2":"oklch(7.11623% 0.013707 282.350)","--lab-bg-tone-3":"oklch(7.11623% 0.013707 282.350)","--lab-label-primary":"oklch(98.65156% 0.006606 286.278)","--lab-label-secondary":"oklch(80.14049% 0.013952 286.081)","--lab-label-tertiary":"oklch(69.23260% 0.013029 286.055)","--lab-label-quaternary":"oklch(55.16643% 0.013808 285.938)","--lab-border-strong":"oklch(98.65156% 0.006606 286.278)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-brand-secondary":"oklch(80.06451% 0.101154 257.822)","--lab-label-brand-tertiary":"oklch(69.35433% 0.162198 257.610)","--lab-label-brand-quaternary":"oklch(55.96293% 0.201872 257.393)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.64532% 0.006567 28.833)","--lab-label-danger-secondary":"oklch(81.38408% 0.105319 28.863)","--lab-label-danger-tertiary":"oklch(71.19739% 0.181099 28.350)","--lab-label-danger-quaternary":"oklch(58.10538% 0.238004 28.677)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.72985% 0.009577 72.664)","--lab-label-warning-secondary":"oklch(80.98123% 0.149940 68.750)","--lab-label-warning-tertiary":"oklch(70.11346% 0.152989 68.857)","--lab-label-warning-quaternary":"oklch(55.86543% 0.122417 68.219)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.31403% 0.027558 147.033)","--lab-label-success-secondary":"oklch(77.69544% 0.228159 147.448)","--lab-label-success-tertiary":"oklch(67.10085% 0.197388 147.383)","--lab-label-success-quaternary":"oklch(53.45797% 0.157009 147.442)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-info-secondary":"oklch(80.30515% 0.099317 259.806)","--lab-label-info-tertiary":"oklch(69.49027% 0.160387 260.058)","--lab-label-info-quaternary":"oklch(56.23879% 0.227606 259.853)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.013010286081645773","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.01207107843137255","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.009560345877481427","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.00784313725490196","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(69.23260% 0.013029 286.055)"}},{"theme":"light","bg":"#808080","vars":{"--lab-bg-tone-2":"oklch(58.21536% 0.012099 286.018)","--lab-bg-tone-3":"oklch(58.21536% 0.012099 286.018)","--lab-label-primary":"oklch(13.77177% 0.013406 284.503)","--lab-label-secondary":"oklch(20.89710% 0.013787 291.711)","--lab-label-tertiary":"oklch(33.70211% 0.013945 291.371)","--lab-label-quaternary":"oklch(49.18380% 0.012632 285.926)","--lab-border-strong":"oklch(13.77177% 0.013406 284.503)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.89968% 0.076454 257.725)","--lab-label-brand-tertiary":"oklch(34.17043% 0.122830 257.309)","--lab-label-brand-quaternary":"oklch(49.70427% 0.178878 257.337)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(21.88171% 0.089792 29.234)","--lab-label-danger-tertiary":"oklch(35.54757% 0.145582 28.627)","--lab-label-danger-quaternary":"oklch(51.73054% 0.211869 28.643)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.90753% 0.046395 66.438)","--lab-label-warning-tertiary":"oklch(34.00266% 0.074904 67.452)","--lab-label-warning-quaternary":"oklch(49.79045% 0.108745 68.715)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(91.20808% 0.064865 69.118 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(20.10741% 0.059351 147.255)","--lab-label-success-tertiary":"oklch(32.71649% 0.095649 147.616)","--lab-label-success-quaternary":"oklch(47.52181% 0.139141 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(88.67895% 0.198079 147.416 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(21.26627% 0.087439 260.146)","--lab-label-info-tertiary":"oklch(34.39884% 0.140838 260.069)","--lab-label-info-quaternary":"oklch(50.11242% 0.202893 259.861)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.11057506131405706","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.11023622047244094","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.07169755954418741","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.047244094488188976","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.70211% 0.013945 291.371)"}},{"theme":"light","bg":"#3A3A3C","vars":{"--lab-bg-tone-2":"oklch(36.65084% 0.011912 285.797)","--lab-bg-tone-3":"oklch(36.65084% 0.011912 285.797)","--lab-label-primary":"oklch(98.92134% 0.005280 286.303)","--lab-label-secondary":"oklch(82.53513% 0.012530 281.039)","--lab-label-tertiary":"oklch(72.82755% 0.014297 286.039)","--lab-label-quaternary":"oklch(61.25907% 0.011943 286.042)","--lab-border-strong":"oklch(98.92134% 0.005280 286.303)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-brand-secondary":"oklch(82.44675% 0.088464 257.008)","--lab-label-brand-tertiary":"oklch(73.08383% 0.140495 257.442)","--lab-label-brand-quaternary":"oklch(61.77245% 0.208353 257.413)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(61.75781% 0.208491 257.338 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.86605% 0.005516 31.054)","--lab-label-danger-secondary":"oklch(83.38591% 0.092088 28.223)","--lab-label-danger-tertiary":"oklch(74.63167% 0.153784 28.842)","--lab-label-danger-quaternary":"oklch(64.10554% 0.244701 28.706)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.75683% 0.008520 67.727)","--lab-label-warning-secondary":"oklch(83.08426% 0.131366 68.787)","--lab-label-warning-tertiary":"oklch(73.86147% 0.161425 68.614)","--lab-label-warning-quaternary":"oklch(61.96370% 0.135496 68.532)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.51065% 0.024195 147.300)","--lab-label-success-secondary":"oklch(80.08887% 0.235054 147.469)","--lab-label-success-tertiary":"oklch(70.67977% 0.207519 147.455)","--lab-label-success-quaternary":"oklch(59.24524% 0.173588 147.533)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-info-secondary":"oklch(82.42821% 0.087905 259.666)","--lab-label-info-tertiary":"oklch(73.17409% 0.139071 259.898)","--lab-label-info-quaternary":"oklch(61.97090% 0.205878 259.740)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.06909778454881225","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.06091370558375635","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.04060913705583756","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.028061954965508264","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(72.82755% 0.014297 286.039)"}},{"theme":"light","bg":"#007AFF","vars":{"--lab-bg-tone-2":"oklch(57.87475% 0.012117 286.015)","--lab-bg-tone-3":"oklch(57.87475% 0.012117 286.015)","--lab-label-primary":"oklch(13.41536% 0.016099 291.497)","--lab-label-secondary":"oklch(20.01464% 0.013939 291.638)","--lab-label-tertiary":"oklch(33.31157% 0.013986 291.359)","--lab-label-quaternary":"oklch(48.82850% 0.012656 285.922)","--lab-border-strong":"oklch(13.41536% 0.016099 291.497)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.23588% 0.071518 256.892)","--lab-label-brand-tertiary":"oklch(33.52307% 0.122654 257.729)","--lab-label-brand-quaternary":"oklch(49.33014% 0.177503 257.333)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(20.90400% 0.085780 29.234)","--lab-label-danger-tertiary":"oklch(34.90075% 0.142917 28.593)","--lab-label-danger-quaternary":"oklch(51.33845% 0.210254 28.629)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.32691% 0.044866 67.175)","--lab-label-warning-tertiary":"oklch(33.70499% 0.073308 69.356)","--lab-label-warning-quaternary":"oklch(49.32505% 0.107532 68.994)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(90.50991% 0.070001 68.839 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(19.36939% 0.056254 147.869)","--lab-label-success-tertiary":"oklch(32.05045% 0.093836 147.561)","--lab-label-success-quaternary":"oklch(47.21271% 0.138736 147.423)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(87.96697% 0.214930 147.350 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(20.33878% 0.084139 260.257)","--lab-label-info-tertiary":"oklch(33.98919% 0.139378 260.097)","--lab-label-info-quaternary":"oklch(49.73960% 0.201561 259.877)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1414396647356095","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.180541833147175","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.09073588245366797","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.0643078283945157","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.31157% 0.013986 291.359)"}},{"theme":"light","bg":"#FF3B30","vars":{"--lab-bg-tone-2":"oklch(63.60048% 0.011829 286.059)","--lab-bg-tone-3":"oklch(63.60048% 0.011829 286.059)","--lab-label-primary":"oklch(13.83495% 0.015576 284.180)","--lab-label-secondary":"oklch(25.12484% 0.015017 285.269)","--lab-label-tertiary":"oklch(36.40556% 0.013673 291.446)","--lab-label-quaternary":"oklch(51.30243% 0.012497 285.951)","--lab-border-strong":"oklch(13.83495% 0.015576 284.180)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(14.05977% 0.051776 257.879)","--lab-label-brand-secondary":"oklch(25.33060% 0.091490 257.423)","--lab-label-brand-tertiary":"oklch(36.69778% 0.133197 257.540)","--lab-label-brand-quaternary":"oklch(51.93456% 0.187074 257.359)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.49010% 0.059461 29.234)","--lab-label-danger-secondary":"oklch(26.17120% 0.107130 28.474)","--lab-label-danger-tertiary":"oklch(38.31782% 0.156989 28.748)","--lab-label-danger-quaternary":"oklch(53.87750% 0.220648 28.624)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(14.09840% 0.030367 70.955)","--lab-label-warning-secondary":"oklch(25.16306% 0.055216 68.014)","--lab-label-warning-tertiary":"oklch(36.95716% 0.080989 68.209)","--lab-label-warning-quaternary":"oklch(51.83499% 0.113346 68.533)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(94.55166% 0.039779 69.746 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.47470% 0.038038 148.984)","--lab-label-success-secondary":"oklch(24.13011% 0.070822 147.468)","--lab-label-success-tertiary":"oklch(35.33425% 0.103721 147.463)","--lab-label-success-quaternary":"oklch(49.62199% 0.145290 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(93.08890% 0.111383 147.112 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(14.07469% 0.058967 260.480)","--lab-label-info-secondary":"oklch(25.53067% 0.104920 260.137)","--lab-label-info-tertiary":"oklch(37.14168% 0.149591 259.762)","--lab-label-info-quaternary":"oklch(52.13660% 0.212021 259.943)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.18357487922705315","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.4395613333149521","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.17777918620864494","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.10167726510894212","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(36.40556% 0.013673 291.446)"}}]} From 3b6adbb39118a65b163aeef2c7ebde1d602bc280 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 10 Jul 2026 14:49:46 +0300 Subject: [PATCH 06/11] =?UTF-8?q?fix(docs):=20=D1=81=D0=B8=D0=BD=D1=85?= =?UTF-8?q?=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D1=81=D1=87=D1=91=D1=82=D1=87=D0=B8=D0=BA=20ADR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/NAMING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/NAMING.md b/docs/NAMING.md index 620fa695..d92cd618 100644 --- a/docs/NAMING.md +++ b/docs/NAMING.md @@ -17,7 +17,7 @@ | крейтов семейства в crates/ | 5 | | экспорт-субпутей package.json @labpics/colors | 7 | | python-скриптов scripts/*.py | 2 | -| маркдаун-доков docs/**/*.md (включая этот канон) | 10 | +| маркдаун-доков docs/**/*.md (включая этот канон) | 11 | | векторов conformance/vectors/*.json (включая manifest) | 6 | | файлов вне закона имён | 4 | From 46c47b565b0d08376f5da81d0557b6b6de382215 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 10 Jul 2026 15:04:29 +0300 Subject: [PATCH 07/11] =?UTF-8?q?docs(science):=20=D0=B7=D0=B0=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D1=82=D1=8C=20=D0=B4=D1=80=D0=B5=D0=B9=D1=84=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=B0=20neutral=20progression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/labcolors-core/src/neutral.rs | 27 ++++++++++++++++----------- docs/empirical-inventory.md | 4 ++-- docs/empirical-residue.md | 8 +++++--- docs/whitepaper.md | 9 +++++---- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/crates/labcolors-core/src/neutral.rs b/crates/labcolors-core/src/neutral.rs index c1c51815..c895d2c9 100644 --- a/crates/labcolors-core/src/neutral.rs +++ b/crates/labcolors-core/src/neutral.rs @@ -61,14 +61,15 @@ const HUE_PURITY_MP_REF_RATIO: f64 = 1.5; // SSOT-TRACKED — показатель кривой чистоты, терминал (e) design-choice (форма мотивирована Abney; (0,1]), см. docs/empirical-inventory.md. const HUE_PURITY_EXPONENT: f64 = 0.6; -/// Legacy-параметры формы нейтральной кривой. +/// Унаследованные параметры формы нейтральной кривой. /// -/// Значения сохраняют текущую эмиссию до замены construction policy, но не являются +/// Значения сохраняют текущую эмиссию до замены политики построения, но не являются /// универсальной психофизикой или пользовательскими осями намерения. Равномерные -/// Oklab/sRGB/CAM16-J′ шаги и прямое Whittle/crispening-прочтение уже опровергнуты -/// по магнитуде; тёмная ветвь дополнительно вырождается у near-black. Корневая -/// задача #219/#261 должна вывести контекстную progression либо удалить gamma- -/// параметризацию. Новые production-механизмы не должны зависеть от этих чисел. +/// шаги Oklab/sRGB/CAM16-J′ и прямое прочтение метрики контрастного усиления Уиттла +/// уже опровергнуты по магнитуде; тёмная ветвь дополнительно вырождается около +/// чёрного. Задачи #219/#261 должны вывести зависящий от контекста закон прогрессии +/// либо удалить gamma-параметризацию. Новые рабочие механизмы не должны зависеть от +/// этих чисел. #[derive(Debug, Clone, Copy, PartialEq)] pub struct CurveParams { pub gamma_light: f64, @@ -81,14 +82,17 @@ impl Default for CurveParams { Self { // Показатель степени, которым гамма-кривая отображает t на светлотный // интервал [светлый якорь, базовый], при t <= 0.5. - // SSOT-TRACKED — frozen legacy gamma; root progression model open in #219/#261. + // SSOT-TRACKED — зафиксированная gamma прежней эмиссии; корневая модель + // прогрессии остаётся открытой в #219/#261. gamma_light: 1.75, // Показатель степени для тёмной ветви (t > 0.5): интервал [базовый, // тёмный якорь]. - // SSOT-TRACKED — frozen legacy gamma; root progression model open in #219/#261. + // SSOT-TRACKED — зафиксированная gamma прежней эмиссии; корневая модель + // прогрессии остаётся открытой в #219/#261. gamma_dark: 1.5, // Позиция вдоль параметра кривой t, где огибающая хромы достигает пика. - // SSOT-TRACKED — frozen legacy scalar; context hue-function research belongs to #219/#261. + // SSOT-TRACKED — зафиксированный скаляр прежней эмиссии; исследование + // зависимости от оттенка принадлежит #219/#261. chroma_peak_t: 0.35, } } @@ -860,8 +864,9 @@ mod wave2_e_locks { } // ───────────────────────────────────────────────────────────────────────────── -// Задача #108: активная гипотеза co-dependency для gamma_light/gamma_dark — -// crispening-метрика Уиттла (Whittle 1992 Vision Research 32(8):1493-1507, +// Задача #108: четвёртая проверенная и опровергнутая скалярная гипотеза для +// gamma_light/gamma_dark — метрика контрастного усиления Уиттла +// (Whittle 1992 Vision Research 32(8):1493-1507, // DOI 10.1016/0042-6989(92)90205-w; порогово-дискриминационная, НЕ appearance- // модель CAM16 — см. docs/empirical-inventory.md "требует параметр-free // метрики" / "не appearance-модель CAM16"). Три предыдущих скалярных метрики diff --git a/docs/empirical-inventory.md b/docs/empirical-inventory.md index 97db5414..3bbdd9d3 100644 --- a/docs/empirical-inventory.md +++ b/docs/empirical-inventory.md @@ -20,8 +20,8 @@ Marker column: `SSOT-TRACKED` = has a paper trail in this table but no citation- | 17 | `CUSP_HALF_WINDOW_DEG` | `40.0` | `semantic.rs` | SSOT-TRACKED | **(e) DESIGN-CHOICE** (НЕ (c), проверено ре-аудитом 2026-07-07) — Полу-окно поиска оттенка (°) в `cusp_attracted_hue`; держит undertone в сине-фиолетовой полосе. ⚠️ Замер: полный дрейф каспа гамута sRGB вблизи 286° достигает ≈42.5° по L-шкале — окно 40° клипует чуть ВНУТРИ полного дрейфа НАМЕРЕННО (движок держит оттенок близко к каноническому, не гонится за магента-каспом; см. комментарий `cusp_attracted_hue`). НЕ «окно покрывает дрейф»; тест `derivator_locks::cusp_window_is_near_measured_gamut_drift`. Калибровочный. **Отличие от `HUE_SEARCH_HALF_WINDOW` (строка 36):** там доказан интерьерный оптимум (окно НЕ связывает), здесь доказано ОБРАТНОЕ (дрейф строго превышает окно) — измеримо связывающий кап, значит точное значение окна материально для крайних светлот; (c) не применим. | | 19 | `STRICT_STEP` | `0.5` | `semantic.rs` | SSOT-TRACKED | **(e) DESIGN-CHOICE** (НЕ (c), проверено ре-аудитом 2026-07-07) — Граница квантования выходной сетки (Lc), НЕ JND-клейм. Замер: медианный Lc-шаг одного 8-бит кванта серых ≈0.44; STRICT_STEP=0.5 сидит чуть выше типичного шага сетки. (Максимальный шаг ≈7.85 — это обрыв мягкого клампа APCA, разрыв, а не шаг сетки.) Тест `derivator_locks::strict_step_sits_just_above_typical_grid_step`. Калибровочный. **Почему не (c):** в отличие от других низко-экспозиционных ручек, `STRICT_STEP` — прямое слагаемое цели демоции (`senior_mag − STRICT_STEP` в `demote_below`/`demote_below_hued`), поэтому оно НЕПРЕРЫВНО и напрямую сдвигает эмитируемый цвет junior-роли; никакого лока инвариантности выхода по интервалу не существует — честный терминал (e), не (c). | | 21 | `CHROMA_FRACTION` | `0.88` | `sentiment.rs` | SSOT-TRACKED | **(e) DESIGN-CHOICE** — доля максимума хромы в гамуте, которую несёт каждый сентимент на своей матч-светлоте; `< 1`, чтобы сидеть внутри стены гамута, а не «неоном». **Волна 2:** genuine свободная ручка; легальный диапазон **(0,1]** (валидатор). Sensitivity: свип [0.70,1.0] на реальных якорях danger/warning/success/info → max ΔE_ok≈**0.0421** (>1 JND, материальна) → честный (e), не (c) (лок `chroma_fraction_sensitivity_is_bounded`). Реестровый дефолт 0.88 — для клиентов без якорной калибровки; **labui=1.0** (danger `#FF3B30` > 0.88·C_max, см. S-01). Протокол: подобрать так, чтобы самый насыщенный якорь клиента сел чуть внутри стены. **(Строка #20 `DEFAULT_HARDNESS` УДАЛЕНА Волной 1: p-норма brand-displacement снесена законом категориальных зон — трекать константу удалённого механизма было бы ложью о живости кода.)** | -| 22 | `NEUTRAL_DEFAULT_GAMMA_LIGHT` | `1.75` | `neutral.rs` | SSOT-TRACKED | **OPEN ROOT MODEL — scalar gamma rejected.** `1.75` remains only as the frozen legacy-output baseline. Equal-step fits in Oklab, encoded sRGB and surround-CAM16 J′ miss the shipped magnitude; the direct Whittle/crispening fit is also refuted because it becomes affine above `L_b` and singular toward the dark anchor. It is neither an admitted universal law nor an active Whittle hypothesis. Owner #219/#261 must derive a context-conditioned progression or remove gamma parameterisation. Sensitivity: [1.3,2.2] changes emitted output materially (max ΔE_ok≈0.0466). | -| 23 | `NEUTRAL_DEFAULT_GAMMA_DARK` | `1.5` | `neutral.rs` | SSOT-TRACKED | **OPEN ROOT MODEL — scalar gamma rejected.** `1.5` remains only as the frozen legacy-output baseline. Equal-step Oklab/sRGB/CAM16-J′ fits and the direct Whittle/crispening fit all fail by a large margin; the latter runs away from the near-black singularity instead of explaining the progression. Owner #219/#261 must derive a context-conditioned progression or remove gamma parameterisation. Sensitivity: [1.2,1.9] changes emitted output materially (max ΔE_ok≈0.0325). | +| 22 | `NEUTRAL_DEFAULT_GAMMA_LIGHT` | `1.75` | `neutral.rs` | SSOT-TRACKED | **ОТКРЫТАЯ КОРНЕВАЯ МОДЕЛЬ — скалярная gamma опровергнута.** `1.75` сохраняется только как зафиксированная база совместимости прежней эмиссии. Подборы равных шагов в Oklab, кодированном sRGB и CAM16 J′ с окружением не воспроизводят отгружаемую магнитуду; прямой подбор по метрике контрастного усиления Уиттла тоже опровергнут, поскольку выше `L_b` становится аффинным, а к тёмному якорю — сингулярным. Это не принятый универсальный закон и не активная гипотеза Уиттла. #219/#261 должны вывести зависящую от контекста прогрессию либо удалить gamma-параметризацию. Свип [1.3,2.2] материально меняет эмиссию (max ΔE_ok≈0.0466). | +| 23 | `NEUTRAL_DEFAULT_GAMMA_DARK` | `1.5` | `neutral.rs` | SSOT-TRACKED | **ОТКРЫТАЯ КОРНЕВАЯ МОДЕЛЬ — скалярная gamma опровергнута.** `1.5` сохраняется только как зафиксированная база совместимости прежней эмиссии. Подборы равных шагов в Oklab/sRGB/CAM16-J′ и прямой подбор по метрике Уиттла дают большой промах; последний уходит от сингулярности около чёрного вместо объяснения прогрессии. #219/#261 должны вывести зависящую от контекста прогрессию либо удалить gamma-параметризацию. Свип [1.2,1.9] материально меняет эмиссию (max ΔE_ok≈0.0325). | | 24 | `NEUTRAL_DEFAULT_CHROMA_PEAK_T` | `0.35` | `neutral.rs` | SSOT-TRACKED | **КОНТЕКСТ-ФУНКЦИЯ (hue) ПОДТВЕРЖДЕНА** (co-dependency, #95 2026-07-08 — секция внизу): `chroma_peak_t(hue) =` t светлотного cusp'а; на акцентах хорошо поставлена, диапазон [0.225, 0.479], среднее **0.379 ≈ shipped 0.35**. Скаляр = приближение центра hue-закона; нейтраль = вырождение (atan2-шум). Эмиссионно-НЕЙТРАЛЕН → hue-функцию можно ввести БАЙТ-ИДЕНТИЧНО (кандидат в объективизацию, не semver-major). Позиция t пика огибающей хромы; легальный диапазон `(0,0.5]` (клампится). Sensitivity: свип [0.2,0.5] → max ΔE_ok≈**0.0042** (< ½ JND, лок `curve_params_sensitivity_is_bounded`). | | 25 | `SOFT_CLAMP_THRESHOLD` | `0.022` | `lpc.rs` | GROUNDED | APCA SAPC-8 `0.0.98G-4g` published set. | | 26 | `SOFT_CLAMP_EXP` | `1.414` | `lpc.rs` | GROUNDED | APCA SAPC-8 `0.0.98G-4g` published set. | diff --git a/docs/empirical-residue.md b/docs/empirical-residue.md index 91f74ac1..86032fb8 100644 --- a/docs/empirical-residue.md +++ b/docs/empirical-residue.md @@ -68,7 +68,9 @@ дополнительно исключил `#18 LIGHTNESS_SETTLE`: контрпример опроверг его класс (a), а численная legacy-эвристика не является перцептивной policy-константой. Поэтому актуальный итог — 25, а не 26 или 27. -Внутри прежней классификации констант нет безымянных строк, но это не означает, что корневая модель neutral progression найдена: gamma-строки остаются frozen legacy baselines и открыты в #219/#261. +В прежней классификации констант нет безымянных строк, но это не означает, что +корневая модель нейтральной прогрессии найдена: значения gamma остаются +зафиксированной базой совместимости прежней эмиссии, а модель открыта в #219/#261. ## Классификация текущих 25 policy-констант @@ -101,8 +103,8 @@ | 12 | `NEUTRAL_HUE_DEG` | 286.0 | (e) | измер. якорь; Волна 2: байт-инвариант по разбросу [285.78,286.01], ±20°→0.0114 (~1 JND) | `neutral_hue_emits_byte_invariant_across_measured_family_spread`, `custom_tint_overrides_hue_and_ratio` | | 21 | `CHROMA_FRACTION` | 0.88 | (e) | genuine; Волна 2: sensitivity max ΔE_ok 0.0421 по [0.70,1.0]; labui=1.0; диапазон (0,1] | `chroma_fraction_sensitivity_is_bounded`, `chroma_fraction_out_of_bounds_is_rejected` | | ~~20~~ | ~~`DEFAULT_HARDNESS`~~ | — | — | **УДАЛЁН Волной 1** (снесён механизм p-нормы brand-displacement; config-поле `hardness` живо как vestigial, тест `hardness_below_one_is_rejected` держит его границу) | — | -| 22 | `NEUTRAL_DEFAULT_GAMMA_LIGHT` | 1.75 | **OPEN** | frozen legacy baseline; four scalar explanations, including direct Whittle/crispening, are refuted by magnitude/degeneracy; context-conditioned progression or removal of gamma is owned by #219/#261 | `curve_params_sensitivity_is_bounded`, gamma derivation regressions | -| 23 | `NEUTRAL_DEFAULT_GAMMA_DARK` | 1.5 | **OPEN** | frozen legacy baseline; four scalar explanations, including direct Whittle/crispening, are refuted, with especially strong failure near the dark anchor; context-conditioned progression or removal of gamma is owned by #219/#261 | `curve_params_sensitivity_is_bounded`, gamma derivation regressions | +| 22 | `NEUTRAL_DEFAULT_GAMMA_LIGHT` | 1.75 | **ОТКРЫТО** | зафиксированная база совместимости прежней эмиссии; четыре скалярных объяснения, включая прямое прочтение метрики Уиттла, опровергнуты по магнитуде/вырождению; зависящая от контекста прогрессия либо удаление gamma принадлежат #219/#261 | `curve_params_sensitivity_is_bounded`, регрессионные тесты дериваций gamma | +| 23 | `NEUTRAL_DEFAULT_GAMMA_DARK` | 1.5 | **ОТКРЫТО** | зафиксированная база совместимости прежней эмиссии; четыре скалярных объяснения, включая прямое прочтение метрики Уиттла, опровергнуты, особенно сильно около тёмного якоря; зависящая от контекста прогрессия либо удаление gamma принадлежат #219/#261 | `curve_params_sensitivity_is_bounded`, регрессионные тесты дериваций gamma | | 24 | `NEUTRAL_DEFAULT_CHROMA_PEAK_T` | 0.35 | (e) | genuine; Волна 2: sensitivity max ΔE_ok 0.0042 по [0.2,0.5] (низшая); диапазон (0,0.5] | `curve_params_sensitivity_is_bounded` | **(b) после follow-on = 1 (было 0) — честно.** `IC_DECORATIVE_FLOOR_MIN` = 15.0 diff --git a/docs/whitepaper.md b/docs/whitepaper.md index a5d5d151..e36a30c8 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -596,7 +596,7 @@ Figma-парами, но не объявляет любой browser/color-manage | `NEUTRAL_TINT_RATIO` | 0.10 | [`semantic.rs`](../crates/labcolors-core/src/semantic.rs) | ручка нейтрального подтона; терминал **(e)** design-choice (opt-in v1-политика; sensitivity max ΔE_ok 0.0288 по [0,0.2], Волна 2) | | `TINT_TARGET_MP` | 6.1 | [`semantic.rs`](../crates/labcolors-core/src/semantic.rs) | целевая перцептивная красочность подтона | | `TINT_HUE_STIFFNESS` | 9.0 | [`semantic.rs`](../crates/labcolors-core/src/semantic.rs) | жёсткость притяжения оттенка подтона; терминал **(c) interval-insensitive** (Волна 2): выше порога пиннинга ≈0.36 выход байт-инвариантен по [1,100], дефолт 9.0 = 25× порога | -| `gamma_light` / `gamma_dark` | 1.75 / 1.5 | [`neutral.rs`](../crates/labcolors-core/src/neutral.rs) (`CurveParams::default`) | дефолты степенной интерполяции; **РЕОТКРЫТ (#95, 2026-07-08): «закон не найден ЕЩЁ»** — три метрики step-uniformity рефутируют скаляр (Oklab 1.07, sRGB 1.10, surround-CAM16-J' 0.99–1.04; направление право, магнитуда мала), активная гипотеза — crispening-метрика Уиттла (empirical-inventory §co-dependency) | +| `gamma_light` / `gamma_dark` | 1.75 / 1.5 | [`neutral.rs`](../crates/labcolors-core/src/neutral.rs) (`CurveParams::default`) | зафиксированная база совместимости прежней степенной интерполяции; **КОРНЕВАЯ МОДЕЛЬ ОТКРЫТА (#219/#261)** — четыре скалярных объяснения опровергнуты по магнитуде: равные шаги Oklab, sRGB, CAM16 J′ с окружением и прямое прочтение метрики контрастного усиления Уиттла; это не активная гипотеза и не универсальная свободная ручка | | `chroma_peak_t` | 0.35 | [`neutral.rs`](../crates/labcolors-core/src/neutral.rs) (`CurveParams::default`) | позиция пика огибающей хромы; **КОНТЕКСТ-ФУНКЦИЯ (hue) ПОДТВЕРЖДЕНА (#95)** — `f(hue) =` cusp-светлота, диапазон [0.225,0.479] по акцентам, среднее 0.379 ≈ 0.35; эмиссионно-нейтрален (кандидат в байт-идентичную объективизацию) | | доли текста | 0.97335917 / 0.64359014 / 0.47572199 / 0.29335999 | пресет labui | Ys-перенос Figma-якорей (генезис Y_hk: 102.6 / 66.5 / 48.9 / 29.3; инвариант — цвет: Ys принятых `#141414`/`#767676`/`#C2C2C2` ÷ 106.0407, tertiary — инверсия 48.9 → `#9C9C9C`); деривация запечатана гейтом `anchor_transfer_derivation`; «калибруется» до визуальной приёмки ([`README.md`](../README.md)) | @@ -644,9 +644,10 @@ Figma-парами, но не объявляет любой browser/color-manage ФУНКЦИЮ КОНТЕКСТА, и design-choice допустим лишь после исчерпания контекстных гипотез. Прецедент рефута скалярного кандидата — `HUE_DRIFT_PENALTY_SLOPE` (хорда Oklab отклонена замером) — валиден для НЕ-созависимых ручек; у `CurveParams` контекст есть. -Замеры #95: `gamma_light/dark` — три метрики step-uniformity рефутируют скаляр -(surround-aware CAM16-J' даёт верное НАПРАВЛЕНИЕ асимметрии, но малую магнитуду vs -1.75/1.5; активная гипотеза — crispening-метрика Уиттла); `chroma_peak_t` — hue-функция +Замеры #95/#108: `gamma_light/dark` — четыре скалярные метрики рефутированы по +магнитуде (CAM16 J′ с окружением даёт верное НАПРАВЛЕНИЕ асимметрии, но малую +магнитуду относительно 1.75/1.5; прямое прочтение метрики Уиттла вырождается); +корневая контекстная модель остаётся открытой в #219/#261. `chroma_peak_t` — hue-функция подтверждена (cusp-светлота, среднее по акцентам 0.379 ≈ 0.35), эмиссионно-нейтральна. Полный разбор с числами — `docs/empirical-inventory.md` (§co-dependency). From ce356a905f1908b2b59542f8ac4c45b4e26860a9 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 11 Jul 2026 18:47:44 +0300 Subject: [PATCH 08/11] test: make release boundaries portable in CI --- .../ConformanceTests.swift | 14 +++++++++----- .../colors/test/release-provenance.test.mjs | 19 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift b/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift index 686fbe0c..cb565b44 100644 --- a/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift +++ b/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift @@ -69,11 +69,15 @@ final class ConformanceTests: XCTestCase { func screenComposite(tint: String, alpha: Double, background: String) -> String { let glow = channels(tint) let bg = channels(background) - let result = (0..<3).map { channel in - Int(floor( - Double(bg[channel]) - + alpha * Double(glow[channel]) * Double(255 - bg[channel]) / 255.0 - + 0.5)) + var result: [Int] = [] + result.reserveCapacity(3) + for channel in 0..<3 { + let backgroundChannel = Double(bg[channel]) + let glowChannel = Double(glow[channel]) + let backgroundHeadroom = Double(255 - bg[channel]) + let contribution = alpha * glowChannel * backgroundHeadroom / 255.0 + let rounded = Int(floor(backgroundChannel + contribution + 0.5)) + result.append(rounded) } return String(format: "#%02X%02X%02X", result[0], result[1], result[2]) } diff --git a/packages/colors/test/release-provenance.test.mjs b/packages/colors/test/release-provenance.test.mjs index cbe55852..67f5d7a2 100644 --- a/packages/colors/test/release-provenance.test.mjs +++ b/packages/colors/test/release-provenance.test.mjs @@ -39,24 +39,29 @@ test("prepack source guard is clean-tree and exact-SHA executable evidence", asy command("git", ["add", "."], fixture); command("git", ["commit", "--quiet", "-m", "fixture"], fixture); - const module = await import(pathToFileURL(join(scripts, "prepare-npm-package.mjs"))); - const head = command("git", ["rev-parse", "HEAD"], fixture); - assert.equal(module.verifiedSourceSha(), head); - const previousSha = process.env.GITHUB_SHA; try { + delete process.env.GITHUB_SHA; + const module = await import(pathToFileURL(join(scripts, "prepare-npm-package.mjs"))); + const head = command("git", ["rev-parse", "HEAD"], fixture); + assert.equal(module.verifiedSourceSha(), head); + + process.env.GITHUB_SHA = head; + assert.equal(module.verifiedSourceSha(), head); + process.env.GITHUB_SHA = "0".repeat(40); assert.throws( () => module.verifiedSourceSha(), /does not equal checked-out HEAD/, ); + + delete process.env.GITHUB_SHA; + writeFileSync(join(fixture, "untracked.txt"), "must make the guard red\n"); + assert.throws(() => module.verifiedSourceSha(), /source is dirty/); } finally { if (previousSha === undefined) delete process.env.GITHUB_SHA; else process.env.GITHUB_SHA = previousSha; } - - writeFileSync(join(fixture, "untracked.txt"), "must make the guard red\n"); - assert.throws(() => module.verifiedSourceSha(), /source is dirty/); } finally { rmSync(fixture, { recursive: true, force: true }); } From a4792fa5f947e7ae58f25c591e371a23e3f0bdb8 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 11 Jul 2026 18:55:40 +0300 Subject: [PATCH 09/11] test(swift): use a non-vacuous compositor probe --- .../ConformanceTests.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift b/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift index cb565b44..c88bbec7 100644 --- a/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift +++ b/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift @@ -149,6 +149,16 @@ final class ConformanceTests: XCTestCase { let background = "#101012" let targetDj = 2.3006 + // Independent exact-rational anti-vacuum probe: at alpha 1/2 the two + // encoded operators differ by two bytes in R/G. Keeping this fixture + // independent from the solver-selected alpha prevents an accidental + // same-hex quantisation from weakening the oracle. + let screenProbe = screenComposite(tint: tint, alpha: 0.5, background: background) + let sourceOverProbe = try composite(tint: tint, alpha: 0.5, bg: background) + XCTAssertEqual(screenProbe, "#6A6386") + XCTAssertEqual(sourceOverProbe, "#686186") + XCTAssertNotEqual(screenProbe, sourceOverProbe) + let stable = try solveGlowPoint( tint: tint, background: background, @@ -196,10 +206,6 @@ final class ConformanceTests: XCTestCase { recomposite, compositeHex, "bit-exact относится к композитору, не к CAM16 decision") - XCTAssertNotEqual( - try composite(tint: tint, alpha: alpha, bg: background), - compositeHex, - "anti-vacuum: source-over обязан отличаться от screen на этом fixture") case .indeterminate: XCTFail("explicit legacy profile обязан сохранять прежний determinate path") } From de5e86e8f9ead69e7e6ce06a2a8973bdb2b12c7a Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:39:29 +0300 Subject: [PATCH 10/11] fix: close exact runtime release gaps --- .cargo/mutants.toml | 2 +- .github/workflows/ci.yml | 44 + .github/workflows/mutation.yml | 2 + .github/workflows/native-conformance.yml | 4 + .github/workflows/publish.yml | 11 +- CHANGELOG.md | 69 +- bindings/swift/README.md | 50 +- .../ConformanceTests.swift | 140 +- conformance/README.md | 29 +- crates/labcolors-conformance/src/bin/gen.rs | 4 +- crates/labcolors-conformance/src/lib.rs | 201 ++- .../tests/reference_runner.rs | 16 +- crates/labcolors-core/benches/glow_alpha.rs | 8 +- .../examples/unique_yellow_provenance.rs | 44 +- crates/labcolors-core/src/agnostic_gates.rs | 28 +- crates/labcolors-core/src/alpha.rs | 29 +- crates/labcolors-core/src/cleanliness.rs | 519 +++--- crates/labcolors-core/src/config.rs | 43 +- crates/labcolors-core/src/config/preset.rs | 7 + .../labcolors-core/src/config/test_support.rs | 19 + crates/labcolors-core/src/config/tests.rs | 136 +- crates/labcolors-core/src/glow.rs | 836 ++++++---- crates/labcolors-core/src/lib.rs | 13 +- crates/labcolors-core/src/material.rs | 1454 ++++++++++++++--- crates/labcolors-core/src/numerics.rs | 5 +- crates/labcolors-core/src/semantic.rs | 269 ++- crates/labcolors-core/src/solve.rs | 21 +- crates/labcolors-core/src/spaces/vc.rs | 27 +- crates/labcolors-core/src/wcag.rs | 154 +- .../tests/data/labui_emission_golden.txt | 4 +- .../tests/empirical_inventory.rs | 22 +- .../tests/property_invariants.rs | 36 +- crates/labcolors-ffi/src/lib.rs | 596 ++++--- crates/labcolors-wasm/src/dto.rs | 26 +- crates/labcolors-wasm/src/engine.rs | 220 ++- crates/labcolors-wasm/src/error.rs | 10 +- crates/labcolors-wasm/src/lib.rs | 184 ++- crates/labcolors-wasm/src/projection.rs | 801 ++++++++- crates/labcolors-wasm/tests/wasm_parity.rs | 71 +- docs/decisions/0001-config-boundary.md | 20 +- .../0004-finite-alpha-glow-reference.md | 68 +- docs/empirical-inventory.md | 22 +- docs/empirical-residue.md | 7 +- docs/migrations/exact-alpha-glow.md | 122 +- docs/whitepaper.md | 64 +- packages/colors/README.md | 66 +- packages/colors/adapt-theme.d.ts | 8 +- packages/colors/adapt-theme.js | 113 +- packages/colors/effective-bg.d.ts | 5 +- packages/colors/effective-bg.js | 43 +- packages/colors/index.d.ts | 18 + packages/colors/package-lock.json | 2 +- packages/colors/package.json | 2 +- packages/colors/smoke.consumer.ts | 163 +- packages/colors/test/adapt-theme.test.mjs | 225 ++- .../colors/test/chain-invariants.test.mjs | 10 +- packages/colors/test/hotpath-parity.test.mjs | 5 +- .../colors/test/release-contract.test.mjs | 58 +- .../colors/test/release-provenance.test.mjs | 97 ++ packages/colors/test/runtime.test.mjs | 70 +- .../colors/test/wasm-boundary.golden.json | 2 +- scripts/cargo-workspace.mjs | 18 + scripts/docs-drift.test.mjs | 63 +- scripts/prepare-npm-package.mjs | 7 +- scripts/verify-package-release.mjs | 208 ++- 65 files changed, 5729 insertions(+), 1911 deletions(-) create mode 100644 crates/labcolors-core/src/config/test_support.rs create mode 100644 scripts/cargo-workspace.mjs diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index dab9ab1e..325fbf70 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -20,7 +20,7 @@ examine_globs = [ "crates/labcolors-core/src/pair.rs", ] -# Запас времени на мутант: закон грязи считается быстро, но набор в целом не +# Запас времени на мутант: legacy proxy coordinate считается быстро, но набор в целом не # мгновенный; 3× базового прогона + пол в 60с гасит ложные timeout-«убийства». timeout_multiplier = 3.0 minimum_test_timeout = 60 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b4d2163..c577e1c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,12 +20,42 @@ env: # Pins npm 11.9.0 as part of the official Node distribution. npm tar format # is release input, so a floating runner-provided npm is not reproducible. NODE_TOOLCHAIN: 24.14.0 + # Минимальный consumer runtime проверяется отдельно от release packer. + # Node 22 остаётся поддерживаемой upstream LTS-веткой. + NODE_CONSUMER_FLOOR: 22.11.0 # Browser parity is release evidence too: pin both CfT archives byte-for-byte. CHROME_FOR_TESTING_VERSION: 150.0.7871.115 CHROME_FOR_TESTING_SHA256: 1be2db033133c5e2dd1a4e8664bf67b19a61bcf6ed28d2b00f433b3f0b4f9585 CHROMEDRIVER_FOR_TESTING_SHA256: 6ac3919edd107ca13d08cccc118dc83821877e504014233f171bbd94cb01a80e jobs: + node-consumer-floor: + name: Node 22 consumer floor + runs-on: [self-hosted, Linux, X64] + needs: wasm + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version: ${{ env.NODE_CONSUMER_FLOOR }} + - name: download exact artifact built by the canonical WASM job + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: colors-release-${{ github.sha }}-attempt-${{ github.run_attempt }} + path: ${{ runner.temp }}/node-floor-artifact + - name: packed public runtime on declared Node floor + env: + ARTIFACT_DIR: ${{ runner.temp }}/node-floor-artifact + run: | + mapfile -t tarballs < <(find "$ARTIFACT_DIR" -type f -name '*.tgz' -print) + test "${#tarballs[@]}" -eq 1 + node scripts/verify-package-release.mjs --runtime-smoke "${tarballs[0]}" + msrv: name: MSRV workspace check runs-on: [self-hosted, Linux, X64] @@ -34,6 +64,8 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: toolchain env (runner.temp) run: | echo "RUSTUP_HOME=$RUNNER_TEMP/rustup-$GITHUB_JOB" >> "$GITHUB_ENV" @@ -64,6 +96,8 @@ jobs: # filled it in ~12 jobs (ENOSPC cascade, 2026-07-03). steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false # dtolnay/rust-toolchain has no semver tags — pinned by commit SHA. # The toolchain VERSION is pinned via the explicit input below, not @stable, # so a new stable release cannot silently change CI behavior. @@ -99,6 +133,8 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: toolchain env (runner.temp) # runner-контекст недоступен в job-level env — только в шагах; поэтому # RUSTUP_HOME/CARGO_HOME назначаются здесь через GITHUB_ENV. Смысл тот @@ -157,6 +193,8 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: toolchain env (runner.temp) # runner-контекст недоступен в job-level env — только в шагах; поэтому # RUSTUP_HOME/CARGO_HOME назначаются здесь через GITHUB_ENV. Смысл тот @@ -192,6 +230,8 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: toolchain env (runner.temp) # runner-контекст недоступен в job-level env — только в шагах; поэтому # RUSTUP_HOME/CARGO_HOME назначаются здесь через GITHUB_ENV. Смысл тот @@ -240,6 +280,8 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: toolchain env (runner.temp) # runner-контекст недоступен в job-level env — только в шагах; поэтому # RUSTUP_HOME/CARGO_HOME назначаются здесь через GITHUB_ENV. Смысл тот @@ -434,6 +476,8 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false # Гейт «доки == реальность» (порт эталона labui#150 / lab-icons#53 / # lab-motion): docs/NAMING.md сверяется с фактами ФС. Node уже стоит на # self-hosted раннерах; точная версия npm здесь не release input, потому diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index 27cc5c7d..dccde453 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -34,6 +34,8 @@ jobs: timeout-minutes: 120 steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: toolchain env (runner.temp) # Изоляция RUSTUP_HOME/CARGO_HOME/TMPDIR per job под runner.temp — тот же # приём, что во всех джобах ci.yml (класс утечки /tmp закрыт, история #143). diff --git a/.github/workflows/native-conformance.yml b/.github/workflows/native-conformance.yml index 8c5f1d57..ea75e40d 100644 --- a/.github/workflows/native-conformance.yml +++ b/.github/workflows/native-conformance.yml @@ -41,6 +41,8 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false # Официальный Swift 6.1.3 (Ubuntu), pinned по manifest-list digest. # Внутри ставится pinned Rust, ядро собирается @@ -65,6 +67,8 @@ jobs: runs-on: macos-15 steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master @ 2026-03-27 with: toolchain: ${{ env.RUST_TOOLCHAIN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index aa441639..578b991d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,6 +18,10 @@ # репозитория вернуть provenance и короткоживущую аутентификацию. name: publish +concurrency: + group: npm-publish + cancel-in-progress: false + on: push: tags: ["colors-v*"] @@ -38,8 +42,9 @@ jobs: steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - # Full history is required by the anti-rogue-tag ancestry guard. + # Полная история нужна ancestry guard против постороннего тега. fetch-depth: 0 + persist-credentials: false - name: isolated publish env (runner.temp) run: | echo "TMPDIR=$RUNNER_TEMP/tmp-$GITHUB_JOB" >> "$GITHUB_ENV" @@ -81,6 +86,7 @@ jobs: file: "ci.yml", path: ".github/workflows/ci.yml", requiredJobs: [ + "Node 22 consumer floor", "MSRV workspace check", "clippy + rustfmt", "cargo doc (intra-doc links)", @@ -308,4 +314,5 @@ jobs: - name: npm publish verified CI tarball (granular token, no rebuild/repack) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npm publish --ignore-scripts "${{ steps.verified-artifact.outputs.tarball }}" + TARBALL_PATH: ${{ steps.verified-artifact.outputs.tarball }} + run: npm publish --ignore-scripts "$TARBALL_PATH" diff --git a/CHANGELOG.md b/CHANGELOG.md index f1a6fccd..803826d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Rust различаются, потому что это разные delivery su Breaking release относительно `@labpics/colors` 0.9.1 / Rust 0.1.0. Пошаговый переход и rollback: [exact alpha / typed Glow](docs/migrations/exact-alpha-glow.md). +Release packer воспроизводимо закреплён на Node 24.14/npm 11.9; публичный +consumer contract отделён и проверяется с первого Node 22 LTS — `22.11.0`. + ### Breaking - Каждый client-owned Glow-рецепт требует explicit `decision_profile`; default @@ -15,7 +18,8 @@ Breaking release относительно `@labpics/colors` 0.9.1 / Rust 0.1.0. - `GlowRole` стал union из determinate `kind: "glow"` и typed terminal `kind: "glow-indeterminate"`. Indeterminate не эмитит halo/core/alpha CSS vars. - Единый `referenceProfile` из pre-release API заменён раздельными - `compositeProfile` / `compositeGuarantee`, `diagnosticProfile` и + `compositeProfile` / `compositeGuarantee`, `layerRecipeProfile`, + `appearanceDiagnosticProfile`, nullable `selectionDiagnosticProfile` и `decisionProfile` / `decisionGuarantee`. - `solve_screen_alpha_for_dj` принимает `GlowDecisionProfileV1` и возвращает `NumericalDecisionV1`; поля `GlowSolve` доступны через getters. @@ -24,6 +28,12 @@ Breaking release относительно `@labpics/colors` 0.9.1 / Rust 0.1.0. alpha возвращает `Err`, а не клампится. - Публичные continuous/point compositor-границы возвращают `Result` и одинаково отвергают нечисловой или внедиапазонный ввод в debug/release. +- Поля `BackdropBox` закрыты; `try_new` и material-функции возвращают typed + validation/solve errors. `MaterialAlpha` сообщает typed status и numerical + guarantee вместо неявного `Option`/public fields. +- `labcolors-conformance::generate_solve` и `Pack::generate` теперь возвращают + `Result<_, PackGenerationError>`: внутренний/неизвестный core failure нельзя + сериализовать как обычную физическую недостижимость. ### Added @@ -32,8 +42,15 @@ Breaking release относительно `@labpics/colors` 0.9.1 / Rust 0.1.0. - Machine-readable registry branch-sensitive numerical sites и typed `Determinate` / `Indeterminate` с причиной и sound bounds либо честным `bounds: unavailable`. +- Material alpha несёт `BisectionBracketCharacterizedV1`: выбранный после 60 + шагов upper candidate повторно проверен, lower candidate не держит floor, а + numerical profile равен + `encoded-srgb-byte-scale-affine-platform-binary64-powf-v1`. + Transparent/opaque endpoints имеют отдельные typed variants; wire несёт + primary `alphaStatus`. - Determinate Glow сообщает отдельные halo/core point-композиты и diagnostic - `|ΔJ′|`, target status, constraint layer и классы гарантий. + `|ΔJ′|`, provenance recipe/appearance/selection, target status, constraint + layer и классы гарантий. - Conformance pack 2.0.0 с alpha half-tie-вектором. - Публикуемый `labcolors-core` теперь несёт package-local README; CI проверяет реальный `.crate`, распаковывает его и исполняет doctest вне workspace tree. @@ -45,15 +62,47 @@ Breaking release относительно `@labpics/colors` 0.9.1 / Rust 0.1.0. - Source-over half-tie считается в byte-reference порядке: `#C0B2FA @ 0.122` над `#000000` даёт `#17161F`, без потери соседнего LSB при нормализации. +- Glow перечисляет first-passing alpha фактического binary64 screen-композитора: + алгебраически равные rational walls больше не склеивают достижимый ULP-seam + state. Доказанная граница потока — ≤ 766 states; tight maximum не заявлен. +- Material-alpha использует тот же byte-scale affine порядок + `B + α·(T−B)`, что официальный JS-потребитель. На seam fixtures + `#020202/floor=3` и `#000000/floor=7` прежний normalized-expanded upper + candidate не проходил повторную consumer-проверку на один шаг binary64. +- Material all-backdrop recheck больше не полагается на ложную двухугловую + монотонность: conservative binary64 channel envelope включает обе стороны + downward seam frozen WCAG 2.1 (2018) EOTF `0.03928`. Directed-search guard + ограничивает область бисекции; первый passing state и точная минимальная alpha + не заявляются. - Glow alpha больше не округляется независимо от выбранного sRGB8-state: `alphaCss` round-trip восстанавливает ту же binary64 alpha и тот же композит. - Нетривиальный stable CAM16 target/max-site без sound error bound больше не получает правдоподобный platform-selected verdict: результат typed `Indeterminate`. -- Exact stable no-op больше не маркируется как выполненная CAM16-диагностика: - `diagnosticProfile` честно равен `null` на всех delivery boundaries. -- `BackdropBox` с reversed, non-finite или внедиапазонными bounds отклоняется - как недоменный public input (`None`) без swap, clamp или debug-паники. +- Exact stable no-op больше не маркируется как CAM16-selected verdict: + `selectionDiagnosticProfile` равен `null`. Полный semantic result отдельно + сохраняет non-null `appearanceDiagnosticProfile`, потому что сообщает + CAM16-derived `coreAchievedDj`, и versioned `layerRecipeProfile`. +- `BackdropBox::try_new` различает reversed, non-finite и out-of-range bounds + typed ошибками без swap, clamp или debug-паники. +- Core-generated Glow/Material postcondition failures отделены от + `InvalidInput`: WASM возвращает whole-call `internal_error`, UniFFI — + `IncompatibleCoreContract`, conformance generation останавливается до записи + артефакта вместо generic `"unreachable"` fallback. +- CSS namespace preflight теперь защищает основное имя каждого client-owned + токена, включая `Zero` и алиасы на него. Производные ключи Glow (`-core`, + `-alpha`) и Material (`-01`, `-02`) больше не могут молча записать цвет в + `cssVar` роли с `kind: "none"`; конфликтующий конфиг отклоняется до резолва. +- Устаревший wasm32-only muddiness snapshot (`olive > 0.80`) больше не является + мёртвым `#[test]`: реальный headless-browser gate проверяет публичный WASM- + метод по единственному committed conformance corpus без второго semantic + threshold или hand-written score. +- Исторические API `muddiness` / `drab` / `n_pure` и их conformance corpus + сохраняют численную совместимость, но явно помещены в quarantine как frozen + experimental compatibility proxy / research coordinate. Это не + observer-validated human cleanliness verdict и не сигнал базового compiler / + adaptive runtime; отклонённый provenance констант зафиксирован в inventory + под владельцами #231 / #242. ### Compatibility evidence @@ -63,6 +112,9 @@ Breaking release относительно `@labpics/colors` 0.9.1 / Rust 0.1.0. `resolveVars` (4 ключа на 6 записях); 1376 пар `(lc, wcag)` recheck остались байт-идентичны. Это ограниченное свидетельство для pinned corpus, не универсальная гарантия произвольного клиента. +- После перехода с rational-wall midpoint на actual binary64 partition внутри + тех же 24 листов скорректированы 17 alpha-строк; в pinned corpus их point- + композиты и все остальные vars не изменились. ### Known limits @@ -70,3 +122,8 @@ Breaking release относительно `@labpics/colors` 0.9.1 / Rust 0.1.0. установлен; `stable-v1` намеренно возвращает `Indeterminate`. - Exact point-композит не является сертификатом browser color-management, дисплея/HDR или пространственного blur/overlap-эффекта. +- Material-alpha bisection характеризует повторно проверенный fail/pass bracket + в указанном numerical profile; это не proof глобальной монотонности, первого + passing state, точной минимальной alpha, predecessor или sound cross-runtime + bound. Профиль фиксирует original WCAG 2.1 (2018) split `0.03928`; текущая + формула W3C с `0.04045` требует отдельной версионированной миграции (#284). diff --git a/bindings/swift/README.md b/bindings/swift/README.md index 385fc6a9..4e306709 100644 --- a/bindings/swift/README.md +++ b/bindings/swift/README.md @@ -3,8 +3,15 @@ Swift-поверхность динамического Rust-ядра через UniFFI: сгенерированный Swift вызывает `labcolors-core` в runtime, а не сериализует токены на сборке. Экспортируется рантайм-контраст-ядро (см. `crates/labcolors-ffi`): контраст, -резолв, лестницы, подложка→α, мутность и low-level Glow point decision с -обязательным numerical profile. +резолв, лестницы, подложка→α, legacy-координата `muddiness` и low-level Glow +point decision с обязательным numerical profile. + +Исторически названная `muddiness` поверхность — это +`experimental compatibility proxy`: она сохраняет прежний числовой API и его +conformance-векторы, но не +является валидированным на наблюдателях человеческим вердиктом clean/dirty и не +должна использоваться как production decision. Legacy-идентификатор сохранён +только для совместимости. Текущее исполняемое доказательство этой поверхности — pinned Swift-контейнер на Linux x86_64. Оно не является аттестацией Apple ABI, macOS/arm64 или iOS. @@ -39,21 +46,46 @@ docker run --rm -v "$PWD":/src:ro \ Историческая аттестация pack `1.0.0` (числа, версии, платформа): `docs/conformance/local-swift-attestation.md`; она явно superseded для текущего -pack `2.0.0` и не подменяет новый CI-прогон. На нативном macOS (если появится -БЕСПЛАТНЫЙ раннер) — джоба `swift-conformance-macos-reference` (ручной -`workflow_dispatch`) в том же workflow. +pack `2.0.0` и не подменяет новый CI-прогон. Нативный macOS/arm64 path сейчас +представлен только ручной reference-джобой +`swift-conformance-macos-reference` (`workflow_dispatch`): это **не gate** PR, +`main` или release. Полная platform/runtime-матрица остаётся отдельной работой +в [issue #258](https://github.com/Labpics-Team/lab-colors/issues/258). ## Толерантность conformance Числовые поля зафиксированных семейств conformant в пределах `DRIFT_TOL = 1e-6` (SSOT пака: -`crates/labcolors-conformance/src/lib.rs`): байт-точность f64 -кросс-платформенно невозможна (libm-шум `powf`/`atan2`/`ln` ~1e-13). -Композит-hex — чистая IEEE-алгебра, точен; solve-hex — квантование -трансцендентного резолва, ±1 LSB на канал. +`crates/labcolors-conformance/src/lib.rs`): для зависимых от libm путей +(`powf`/`atan2`/`ln`) битовая идентичность f64 между runtime не гарантируется; +реализации могут расходиться на несколько ULP (~1e-13). +Композит-hex точен относительно объявленного encoded-sRGB8 operation profile, +который фиксирует порядок IEEE binary64 операций и квантование; исполняемое +Swift/UniFFI evidence сейчас ограничено описанным выше pinned Linux x86_64 +runtime. Solve-hex — квантование трансцендентного резолва, ±1 LSB на канал. Glow проверяется другим контрактом: `stable-v1` обязан вернуть типизированный `Indeterminate` (`site_id` + неразделимое typed `evidence`), если доказанной границы нет; прежний CAM16/libm-dependent путь доступен только как явный `legacy-platform-dependent-v1`. Нативный гейт не заявляет CAM16 bit parity: `bit-exact` относится только к encoded-sRGB8 screen-композиту и его certificate. +Native output кодирует provenance самим вариантом `GlowPointDecision`: +`stableExactNoop`, `legacyReached`, `legacyUnreachable` либо `indeterminate`. +Отдельных `decisionProfile`, `decisionGuarantee`, `targetStatus` и +`selectionDiagnosticProfile` в output больше нет — поэтому клиент физически не +может собрать «stable + legacy guarantee» или другую невозможную комбинацию. +Requested `GlowDecisionProfile` по-прежнему обязателен как input функции; +`GlowPointValue` хранит только общий exact screen-composite payload и его +composite profile/guarantee. + +`stableExactNoop` сам означает byte-exact недостижимость без appearance +selection; `legacyReached` / `legacyUnreachable` сами несут CAM16/libm legacy +provenance и target outcome. `indeterminate` является stable отказом выбрать +состояние без sound bound. Typed `NumericalIndeterminacy.intervalOverlap` +остаётся законным outward evidence неопределённости. + +Граница самостоятельно валидирует tint, background и конечный `targetDj > 0`: +только эти ошибки становятся `ColorError.InvalidGlowRequest`. Ошибка core после +успешной валидации, неизвестный численный variant или новый `Unreachable` +считаются `ColorError.IncompatibleCoreContract`; adapter не выдаёт им +выдуманный fallback-code. diff --git a/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift b/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift index c88bbec7..1339047b 100644 --- a/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift +++ b/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift @@ -144,7 +144,57 @@ final class ConformanceTests: XCTestCase { // MARK: - Low-level Glow decision contract - func testGlowDecisionProfilesRemainExplicitAndDoNotCollapse() throws { + func testGeneratedGlowSurfaceContainsOnlyAtomicProvenanceVariants() throws { + var packageRoot = URL(fileURLWithPath: #filePath) + for _ in 0..<3 { packageRoot = packageRoot.deletingLastPathComponent() } + let generatedSource = packageRoot + .appendingPathComponent("Sources") + .appendingPathComponent("LabColors") + .appendingPathComponent("labcolors.swift") + let source = try String(contentsOf: generatedSource, encoding: .utf8) + + // Positive cases делают negative API-проверки невакуумными: читается + // именно сгенерированная algebraic Glow surface, а не пустой/чужой файл. + XCTAssertTrue(source.contains("public enum GlowPointDecision")) + XCTAssertTrue(source.contains("case stableExactNoop(")) + XCTAssertTrue(source.contains("case legacyReached(")) + XCTAssertTrue(source.contains("case legacyUnreachable(")) + XCTAssertTrue(source.contains("case indeterminate(")) + XCTAssertTrue(source.contains("public enum GlowDecisionProfile")) + XCTAssertTrue(source.contains("case IncompatibleCoreContract(")) + XCTAssertFalse(source.contains("case determinate(")) + XCTAssertFalse(source.contains("public enum GlowDecisionGuarantee")) + XCTAssertFalse(source.contains("public enum GlowTargetStatus")) + XCTAssertFalse(source.contains("public enum GlowDiagnosticProfile")) + } + + func testInvalidPublicGlowInputsKeepPublicInputErrorTaxonomy() { + let invalidInputs: [(String, String, Double)] = [ + ("not-a-color", "#000000", 2.3006), + ("#C0B2FA", "not-a-color", 2.3006), + ("#C0B2FA", "#000000", 0.0), + ("#C0B2FA", "#000000", -1.0), + ("#C0B2FA", "#000000", .nan), + ("#C0B2FA", "#000000", .infinity), + ] + for (tint, background, targetDj) in invalidInputs { + XCTAssertThrowsError(try solveGlowPoint( + tint: tint, + background: background, + targetDj: targetDj, + theme: .light, + profile: .stableV1 + )) { error in + guard case let ColorError.InvalidGlowRequest(reason) = error else { + return XCTFail( + "public input error не должен становиться adapter incompatibility: \(error)") + } + XCTAssertFalse(reason.isEmpty) + } + } + } + + func testGlowInputProfilesRemainExplicitAndOutputProvenanceIsAtomic() throws { let tint = "#C0B2FA" let background = "#101012" let targetDj = 2.3006 @@ -166,13 +216,12 @@ final class ConformanceTests: XCTestCase { theme: .light, profile: .stableV1) switch stable { - case let .indeterminate(decisionProfile, siteId, evidence): - XCTAssertEqual(decisionProfile, .stableV1) + case let .indeterminate(siteId, evidence): XCTAssertEqual(siteId, .glowTargetOrMaximumV1) guard case .soundBoundUnavailable = evidence else { return XCTFail("stable-v1 обязан вернуть typed unavailable-bound evidence") } - case .determinate: + case .stableExactNoop, .legacyReached, .legacyUnreachable: XCTFail("stable-v1 не должен выбирать состояние без sound bound") } @@ -183,31 +232,18 @@ final class ConformanceTests: XCTestCase { theme: .light, profile: .legacyPlatformDependentV1) switch legacy { - case let .determinate( - decisionProfile, - decisionGuarantee, - compositeProfile, - compositeGuarantee, - diagnosticProfile, - alpha, - _, - _, - _, - _, - compositeHex - ): - XCTAssertEqual(decisionProfile, .legacyPlatformDependentV1) - XCTAssertEqual(decisionGuarantee, .legacyPlatformDependentV1) - XCTAssertEqual(compositeProfile, .encodedSrgb8ScreenV1) - XCTAssertEqual(compositeGuarantee, .bitExact) - XCTAssertEqual(diagnosticProfile, Optional(.cam16UcsJPrimeLi2017V1)) - let recomposite = screenComposite(tint: tint, alpha: alpha, background: background) + case let .legacyReached(value): + XCTAssertEqual(value.compositeProfile, .encodedSrgb8ScreenV1) + XCTAssertEqual(value.compositeGuarantee, .bitExact) + XCTAssertEqual(value.targetDj, targetDj) + let recomposite = screenComposite( + tint: tint, alpha: value.alpha, background: background) XCTAssertEqual( recomposite, - compositeHex, + value.compositeHex, "bit-exact относится к композитору, не к CAM16 decision") - case .indeterminate: - XCTFail("explicit legacy profile обязан сохранять прежний determinate path") + case .stableExactNoop, .legacyUnreachable, .indeterminate: + XCTFail("explicit legacy reached fixture обязан вернуть atomic legacyReached") } func assertStableNoop(tint: String, background: String, composite expected: String) throws { @@ -218,35 +254,34 @@ final class ConformanceTests: XCTestCase { theme: .light, profile: .stableV1) switch decision { - case let .determinate( - decisionProfile, - decisionGuarantee, - compositeProfile, - compositeGuarantee, - diagnosticProfile, - _, - _, - _, - targetStatus, - achievedDj, - compositeHex - ): - XCTAssertEqual(decisionProfile, .stableV1) - XCTAssertEqual(decisionGuarantee, .bitExact) - XCTAssertEqual(compositeProfile, .encodedSrgb8ScreenV1) - XCTAssertEqual(compositeGuarantee, .bitExact) - XCTAssertNil(diagnosticProfile) - XCTAssertEqual(targetStatus, .unreachable) - XCTAssertEqual(achievedDj, 0.0) - XCTAssertEqual(compositeHex, expected) - case .indeterminate: - XCTFail("exact screen no-op обязан быть determinate без CAM16") + case let .stableExactNoop(value): + XCTAssertEqual(value.compositeProfile, .encodedSrgb8ScreenV1) + XCTAssertEqual(value.compositeGuarantee, .bitExact) + XCTAssertEqual(value.achievedDj, 0.0) + XCTAssertEqual(value.compositeHex, expected) + case .legacyReached, .legacyUnreachable, .indeterminate: + XCTFail("exact screen no-op обязан вернуть atomic stableExactNoop") } } try assertStableNoop(tint: tint, background: "#FFFFFF", composite: "#FFFFFF") try assertStableNoop(tint: "#010000", background: "#FE0000", composite: "#FE0000") + let legacyUnreachable = try solveGlowPoint( + tint: tint, + background: "#FFFFFF", + targetDj: targetDj, + theme: .light, + profile: .legacyPlatformDependentV1) + switch legacyUnreachable { + case let .legacyUnreachable(value): + XCTAssertEqual(value.compositeProfile, .encodedSrgb8ScreenV1) + XCTAssertEqual(value.compositeGuarantee, .bitExact) + XCTAssertEqual(value.compositeHex, "#FFFFFF") + case .stableExactNoop, .legacyReached, .indeterminate: + XCTFail("explicit legacy no-op обязан вернуть atomic legacyUnreachable") + } + let crossing = try solveGlowPoint( tint: "#800000", background: "#FE0000", @@ -254,13 +289,12 @@ final class ConformanceTests: XCTestCase { theme: .light, profile: .stableV1) switch crossing { - case let .indeterminate(decisionProfile, siteId, evidence): - XCTAssertEqual(decisionProfile, .stableV1) + case let .indeterminate(siteId, evidence): XCTAssertEqual(siteId, .glowTargetOrMaximumV1) guard case .soundBoundUnavailable = evidence else { return XCTFail("first crossing обязан сохранить typed unavailable-bound evidence") } - case .determinate: + case .stableExactNoop, .legacyReached, .legacyUnreachable: XCTFail("#800000 над #FE0000 пересекает первый half-LSB wall") } } @@ -295,7 +329,7 @@ final class ConformanceTests: XCTestCase { } } - // MARK: - Семейство: мутность + // MARK: - Семейство: legacy proxy coordinate func testMuddiness() throws { let vectors = try load("muddiness.json", as: [MuddinessVec].self) diff --git a/conformance/README.md b/conformance/README.md index 130cfdd9..1a80b809 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -34,9 +34,14 @@ | `ladders.json` | альфы позиции лестницы | `{position, alphaLight, alphaDark}` | | `alpha.json` | подложка→α | `{tint, alpha, bg, composite, minAlpha}` | | `solve.json` | резолв контракта | `{bg, contract, theme, outcome}` | -| `muddiness.json` | мутность цвета | `{hex, score}` | +| `muddiness.json` | замороженная legacy-координата `muddiness` | `{hex, score}` | | `manifest.json` | метаданные и реестр мигрированных numerical sites | `{packVersion, coreVersion, packDigest, counts, numericalSites}` | +`muddiness.json` — это `experimental compatibility proxy`: corpus доказывает +воспроизводимость исторического числового API, но не валидированный на +наблюдателях человеческий вердикт clean/dirty и не пригодность для production +decision. Legacy-идентификаторы сохранены только для совместимости. + - `theme` — kebab-ключ: `light` \| `dark` \| `light-ic` \| `dark-ic`. - `contract` (в `solve`): `{kind:"text", lc}` \| `{kind:"ui", lc}` \| `{kind:"range", floor, ceiling}`. @@ -65,17 +70,25 @@ labels (канон labui): роли `icon` в словаре нет. совпадает с каноном по этим правилам: - **Числовые поля** (`lc`, `wcagRatio`, `score`, `alpha`, `minAlpha`, `alpha*`) — - в пределах `DRIFT_TOL = 1e-6` (абсолютная). Байт-точность f64 кросс-платформенно - НЕВОЗМОЖНА: `powf`/`atan2`/`ln` в libm разных ОС/архитектур расходятся на - несколько ULP (~1e-13); реальный дрейф (не тот surround, опечатка в матрице, - путаница единиц) сдвигает значения на целые единицы — на порядки выше - толерантности. Единственный источник этого значения для пака — + в пределах `DRIFT_TOL = 1e-6` (абсолютная). Для зависимых от libm путей + (`powf`/`atan2`/`ln`) битовая идентичность f64 между платформами не + гарантируется: реализации разных ОС/архитектур могут расходиться на несколько + ULP (~1e-13); реальный дрейф (не тот surround, опечатка в матрице, путаница + единиц) сдвигает значения на целые единицы — на порядки выше толерантности. + Единственный источник этого значения для пака — `crates/labcolors-conformance/src/lib.rs` (`DRIFT_TOL`). -- **`composite` (hex)** — ТОЧНО. Композит — чистая IEEE-алгебра кодированного - sRGB (умножение/сложение) плюс квантование: бит-идентичен на всех платформах. +- **`composite` (hex)** — ТОЧНО относительно объявленного encoded-sRGB8 + operation profile: фиксированный порядок IEEE binary64 + умножения/сложения и квантования задаёт byte-reference. Это контракт профиля, + а не заявление о доказанной идентичности произвольной платформы; исполняемое + evidence ограничено фактически аттестованной runtime-матрицей ниже. - **`solve.outcome.hex`** — в пределах **±1 LSB на канал**. Это квантование трансцендентного резолва: у границы 8-бит-ячейки libm-шум может качнуть результат на один шаг. + +Внутренняя ошибка core и неизвестный forward-вариант `Unreachable` не являются +solve-векторами: `Pack::generate()` возвращает `PackGenerationError` и не пишет +правдоподобный `{kind:"unreachable"}` fallback в сертификационный артефакт. - **Строки/enum/bool** (`theme`, `position`, `code`, `floorOverride`, `kind`) — ТОЧНО. diff --git a/crates/labcolors-conformance/src/bin/gen.rs b/crates/labcolors-conformance/src/bin/gen.rs index bb8d9703..b86b50b7 100644 --- a/crates/labcolors-conformance/src/bin/gen.rs +++ b/crates/labcolors-conformance/src/bin/gen.rs @@ -24,11 +24,11 @@ fn vectors_dir() -> PathBuf { .join("vectors") } -fn main() -> std::io::Result<()> { +fn main() -> Result<(), Box> { let dir = vectors_dir(); std::fs::create_dir_all(&dir)?; - let pack = Pack::generate(); + let pack = Pack::generate()?; // Семейства. for (name, bytes) in pack.families() { diff --git a/crates/labcolors-conformance/src/lib.rs b/crates/labcolors-conformance/src/lib.rs index df796bb0..17516943 100644 --- a/crates/labcolors-conformance/src/lib.rs +++ b/crates/labcolors-conformance/src/lib.rs @@ -26,9 +26,14 @@ //! | `ladders.json` | позиция лестницы → (α_light, α_dark) | `LadderPosition::alpha_pair` | //! | `alpha.json` | подложка→α: композит и α_min | `alpha::composite_hex` / `alpha::min_alpha_hex` | //! | `solve.json` | (bg, контракт, тема) → резолв или честный отказ | `solve` | -//! | `muddiness.json` | hex → мутность | `cleanliness::muddiness_from_hex` | +//! | `muddiness.json` | hex → замороженная legacy-координата | `cleanliness::muddiness_from_hex` | //! | `manifest.json` | версии, дайджест, счётчики, migrated numerical sites | `numerical_registry_v1` | //! +//! `muddiness.json` фиксирует только воспроизводимость исторического числового +//! API. Это `experimental compatibility proxy`, а не валидированный на +//! наблюдателях человеческий вердикт clean/dirty и не production decision; +//! legacy-идентификаторы сохранены для совместимости. +//! //! Версия пака ([`PACK_VERSION`]) привязана к версии ядра ([`core_version`]): //! при легитимной смене канона генератор перегенерирует векторы, а //! раннер-референс ловит любой дрейф. @@ -291,22 +296,54 @@ pub enum SolveOutcome { }, } -/// Стабильный код недостижимости. Тождественен маппингу WASM-границы -/// (`labcolors-wasm/src/engine.rs`) — контракт имён общий для всех биндингов. -#[must_use] -pub fn unreachable_code(err: &labcolors_core::Unreachable) -> &'static str { +/// Ошибка построения conformance-пака. Генератор не имеет права превращать +/// внутренний/неизвестный вариант ядра в физически правдоподобный solve outcome. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum PackGenerationError { + /// Core-generated data violated a core postcondition. + InternalCoreInvariant { reason: String }, + /// The core introduced an `Unreachable` variant this pack does not know how + /// to encode. The adapter must be upgraded before regenerating artifacts. + IncompatibleCoreContract { reason: String }, +} + +impl core::fmt::Display for PackGenerationError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::InternalCoreInvariant { reason } => { + write!(f, "internal core invariant failure: {reason}") + } + Self::IncompatibleCoreContract { reason } => { + write!(f, "incompatible core contract: {reason}") + } + } + } +} + +impl std::error::Error for PackGenerationError {} + +/// Стабильный код физической недостижимости. Тождественен маппингу WASM- +/// границы (`labcolors-wasm/src/engine.rs`). Внутренняя ошибка или неизвестный +/// forward-вариант возвращаются как [`PackGenerationError`] и не сериализуются. +pub fn unreachable_code( + err: &labcolors_core::Unreachable, +) -> Result<&'static str, PackGenerationError> { use labcolors_core::Unreachable as U; match err { - U::BelowContrastFloor { .. } => "below_contrast_floor", - U::ExceedsRange { .. } => "exceeds_range", - U::QuantizationGap { .. } => "quantization_gap", - U::FloorUnreachable { .. } => "floor_unreachable", - U::PolarityMismatch { .. } => "polarity_mismatch", - U::GamutUnsupported => "gamut_unsupported", - U::InvalidInput(_) => "invalid_input", - // `Unreachable` помечен `#[non_exhaustive]`; forward-compat-арм - // тождествен WASM-границе (`labcolors-wasm/src/engine.rs`). - _ => "unreachable", + U::BelowContrastFloor { .. } => Ok("below_contrast_floor"), + U::ExceedsRange { .. } => Ok("exceeds_range"), + U::QuantizationGap { .. } => Ok("quantization_gap"), + U::FloorUnreachable { .. } => Ok("floor_unreachable"), + U::PolarityMismatch { .. } => Ok("polarity_mismatch"), + U::GamutUnsupported => Ok("gamut_unsupported"), + U::InvalidInput(_) => Ok("invalid_input"), + U::InternalInvariant(reason) => Err(PackGenerationError::InternalCoreInvariant { + reason: reason.clone(), + }), + _ => Err(PackGenerationError::IncompatibleCoreContract { + reason: err.to_string(), + }), } } @@ -348,59 +385,64 @@ const SOLVE_CASES: [(&str, ContractSpec, &str); 6] = [ /// Дериватор резолв-векторов. Нейтральная (серая) хрома — резолв /// хью-независим, вектор детерминирован; хью/хрома — естественное расширение /// среза (см. PR). -#[must_use] -pub fn generate_solve() -> Vec { +pub fn generate_solve() -> Result, PackGenerationError> { SOLVE_CASES .iter() - .map(|&(bg, contract, theme)| { - let vc = vc_for_theme(theme); - let bg_input = BgInput::solid(bg).expect("валидный hex фона"); - let outcome = match solve( - bg_input, - contract.to_core(), - Hue::deg(0.0), - ChromaPolicy::Neutral, - &vc, - Gamut::Srgb, - ) { - Ok(s) => SolveOutcome::Solved { - hex: s.hex().to_string(), - lc: s.lc(), - wcag_ratio: s.wcag_ratio(), - floor_override: s.floor_override(), - }, - Err(e) => SolveOutcome::Unreachable { - code: unreachable_code(&e).to_string(), - }, - }; - SolveVector { - bg: bg.to_string(), - contract, - theme: theme.to_string(), - outcome, - } - }) + .map( + |&(bg, contract, theme)| -> Result { + let vc = vc_for_theme(theme); + let bg_input = BgInput::solid(bg).expect("валидный hex фона"); + let outcome = match solve( + bg_input, + contract.to_core(), + Hue::deg(0.0), + ChromaPolicy::Neutral, + &vc, + Gamut::Srgb, + ) { + Ok(s) => SolveOutcome::Solved { + hex: s.hex().to_string(), + lc: s.lc(), + wcag_ratio: s.wcag_ratio(), + floor_override: s.floor_override(), + }, + Err(e) => SolveOutcome::Unreachable { + code: unreachable_code(&e)?.to_string(), + }, + }; + Ok(SolveVector { + bg: bg.to_string(), + contract, + theme: theme.to_string(), + outcome, + }) + }, + ) .collect() } // ───────────────────────────────────────────────────────────────────────────── -// Семейство: мутность +// Семейство: legacy-координата muddiness // ───────────────────────────────────────────────────────────────────────────── -/// Один вектор мутности: hex и его оценка «грязи» `[0,1]`. +/// Один вектор замороженной legacy-координаты `muddiness`. +/// +/// Это `experimental compatibility proxy`: вектор доказывает совпадение +/// численного выхода, но не observer-validated human clean/dirty verdict и не +/// пригодность значения для production decision. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MuddinessVector { /// Цвет, `#RRGGBB`. pub hex: String, - /// Оценка мутности `[0,1]` (0 — чистый, 1 — грязный). + /// Замороженный числовой выход legacy-формулы. pub score: f64, } -/// Цвета для мутности: грязная олива, чистый серый, чистый бренд, грязный хаки. +/// Нейтральный corpus исторических sample inputs; названия не задают семантику. const MUDDINESS_CASES: [&str; 4] = ["#6B6B2E", "#808080", "#007AFF", "#8A7A50"]; -/// Дериватор векторов мутности. +/// Дериватор compatibility-векторов legacy-координаты. #[must_use] pub fn generate_muddiness() -> Vec { MUDDINESS_CASES @@ -429,14 +471,15 @@ pub const FAMILY_FILES: [&str; 5] = [ "muddiness.json", ]; -/// Каноническая кросс-платформенная толерантность сравнения f64 для conformance- +/// Каноническая толерантность сравнения f64 для conformance- /// пака. Эта константа — SSOT правила сравнения. Наблюдаемый libm-шум /// (`powf`/`atan2`/`ln` расходятся на несколько ULP между платформами) — /// порядка `1e-13`; реальный дрейф (не тот surround, опечатка в матрице, /// путаница единиц) сдвигает значения на целые единицы. `1e-6` заведомо выше -/// шума и заведомо ниже любой настоящей регрессии. Байт-точность f64 -/// кросс-платформенно НЕВОЗМОЖНА — поэтому conformant-ность числовых полей -/// определяется этой толерантностью (hex/enum/строки — точно). +/// наблюдавшегося шума и заведомо ниже настоящей регрессии из corpus. Для +/// libm-dependent путей bit identity между runtime не гарантируется; evidence +/// ограничено аттестованной матрицей. Поэтому conformant-ность числовых полей +/// определяется этой толерантностью (hex/enum/строки — по своему профилю). pub const DRIFT_TOL: f64 = 1e-6; /// Счётчики векторов по семействам — для манифеста и отчёта PR. @@ -451,7 +494,7 @@ pub struct Counts { pub alpha: usize, /// Резолв-векторы. pub solve: usize, - /// Векторы мутности. + /// Compatibility-векторы legacy-координаты `muddiness`. pub muddiness: usize, /// Итого. pub total: usize, @@ -539,21 +582,20 @@ pub struct Pack { pub alpha: Vec, /// Резолв-векторы. pub solve: Vec, - /// Векторы мутности. + /// Compatibility-векторы legacy-координаты `muddiness`. pub muddiness: Vec, } impl Pack { /// Сгенерировать весь пак из канона ядра. - #[must_use] - pub fn generate() -> Self { - Pack { + pub fn generate() -> Result { + Ok(Pack { contrasts: generate_contrasts(), ladders: generate_ladders(), alpha: generate_alpha(), - solve: generate_solve(), + solve: generate_solve()?, muddiness: generate_muddiness(), - } + }) } /// Счётчики семейств. @@ -627,19 +669,40 @@ pub fn to_canonical_json(value: &T) -> String { mod tests { use super::*; + #[test] + fn unreachable_code_mapping_is_fallible_without_generic_fallback() { + let error = solve( + BgInput::solid("#FFFFFF").unwrap(), + Contract::text(f64::NAN), + Hue::deg(0.0), + ChromaPolicy::Neutral, + &ViewingConditions::srgb(), + Gamut::Srgb, + ) + .unwrap_err(); + assert_eq!(unreachable_code(&error).unwrap(), "invalid_input"); + + let internal = labcolors_core::Unreachable::InternalInvariant("fixture drift".into()); + assert!(matches!( + unreachable_code(&internal), + Err(PackGenerationError::InternalCoreInvariant { reason }) + if reason == "fixture drift" + )); + } + #[test] fn generation_is_deterministic() { // Дважды сгенерированный пак байт-идентичен — вход всегда даёт тот же // выход (детерминизм канона, перенесённый в пак). - let a = Pack::generate(); - let b = Pack::generate(); + let a = Pack::generate().expect("canonical pack generation"); + let b = Pack::generate().expect("canonical pack generation"); assert_eq!(a, b, "генерация пака недетерминирована"); assert_eq!(a.digest(), b.digest(), "дайджест недетерминирован"); } #[test] fn counts_are_nonempty_and_consistent() { - let pack = Pack::generate(); + let pack = Pack::generate().expect("canonical pack generation"); let c = pack.counts(); assert!(c.total > 0, "пустой пак бессмыслен"); assert_eq!( @@ -653,7 +716,9 @@ mod tests { #[test] fn manifest_numerical_registry_is_generated_from_core_ssot() { - let manifest = Pack::generate().manifest(); + let manifest = Pack::generate() + .expect("canonical pack generation") + .manifest(); assert_eq!(manifest.numerical_sites, generate_numerical_sites()); assert_eq!( manifest.numerical_sites.len(), @@ -674,7 +739,7 @@ mod tests { // нормализованный `(byte/255) * alpha * 255` путь ошибочно отдавал // соседний LSB. Проверка одновременно убивает вакуумные изменения // версии/счётчика без обязательного доказательного вектора. - let pack = Pack::generate(); + let pack = Pack::generate().expect("canonical pack generation"); let manifest = pack.manifest(); assert_eq!(PACK_VERSION, "2.0.0", "half-tie требует pack v2"); assert_eq!(manifest.pack_version, PACK_VERSION); @@ -708,7 +773,7 @@ mod tests { // байт-точного гейта дрейфа и дайджеста (сравнение по СЕРИАЛИЗАЦИИ, не // по parse — парсер serde_json по умолчанию не round-trip-точен для // f64, поэтому опираемся на детерминизм сериализации, а не парсинга). - let pack = Pack::generate(); + let pack = Pack::generate().expect("canonical pack generation"); assert_eq!( to_canonical_json(&pack.contrasts), to_canonical_json(&pack.contrasts) @@ -726,7 +791,7 @@ mod tests { #[test] fn solve_pack_contains_reachable_and_unreachable() { // Пак честен: есть и успешный резолв, и намеренный отказ с кодом. - let solve = generate_solve(); + let solve = generate_solve().expect("canonical solve vectors"); assert!( solve .iter() diff --git a/crates/labcolors-conformance/tests/reference_runner.rs b/crates/labcolors-conformance/tests/reference_runner.rs index 9ad644b0..1b2ca9f3 100644 --- a/crates/labcolors-conformance/tests/reference_runner.rs +++ b/crates/labcolors-conformance/tests/reference_runner.rs @@ -4,10 +4,10 @@ //! //! 1. **Гейт дрейфа (толерантный)** — ядро воспроизводит КАЖДЫЙ закоммиченный //! вектор: числовые поля в пределах [`DRIFT_TOL`] (`1e-6`, SSOT пака — -//! `labcolors-conformance/src/lib.rs`), hex/строки/enum/bool — точно. Байт-точность -//! f64 кросс-платформенно НЕВОЗМОЖНА: `powf`/`atan2`/`ln` расходятся на -//! несколько ULP между libm разных ОС (векторы генерятся на одной, CI бежит -//! на другой). Реальный дрейф (не тот surround, опечатка в матрице) сдвигает +//! `labcolors-conformance/src/lib.rs`), hex/строки/enum/bool — точно. Для +//! libm-dependent путей bit identity f64 между runtime не гарантируется: +//! `powf`/`atan2`/`ln` могут расходиться на несколько ULP между libm разных +//! ОС. Реальный дрейф (не тот surround, опечатка в матрице) сдвигает //! значения на целые единицы — на десять порядков выше толерантности. //! Та же толерантность гасит и неточность парсера f64 у `serde_json`. //! @@ -142,7 +142,7 @@ fn core_reproduces_committed_alpha() { #[test] fn core_reproduces_committed_solve() { let committed: Vec = parse("solve.json"); - let fresh = generate_solve(); + let fresh = generate_solve().expect("canonical solve vectors"); assert_eq!(committed.len(), fresh.len(), "solve.json: изменился состав"); for (c, f) in committed.iter().zip(&fresh) { assert_eq!( @@ -188,7 +188,7 @@ fn core_reproduces_committed_muddiness() { "muddiness.json: изменился состав" ); for (c, f) in committed.iter().zip(&fresh) { - assert_eq!(c.hex, f.hex, "ключ мутности"); + assert_eq!(c.hex, f.hex, "legacy proxy key"); approx(c.score, f.score, &format!("muddiness {}", c.hex)); } } @@ -198,7 +198,9 @@ fn core_reproduces_committed_muddiness() { #[test] fn manifest_metadata_matches_core() { let committed: Manifest = parse(MANIFEST_FILE); - let fresh = Pack::generate().manifest(); + let fresh = Pack::generate() + .expect("canonical pack generation") + .manifest(); assert_eq!(committed.pack_version, fresh.pack_version, "версия пака"); assert_eq!(committed.core_version, fresh.core_version, "версия ядра"); assert_eq!(committed.counts, fresh.counts, "счётчики семейств"); diff --git a/crates/labcolors-core/benches/glow_alpha.rs b/crates/labcolors-core/benches/glow_alpha.rs index 19273eec..97dc7e99 100644 --- a/crates/labcolors-core/benches/glow_alpha.rs +++ b/crates/labcolors-core/benches/glow_alpha.rs @@ -9,7 +9,13 @@ fn bench_glow_alpha(c: &mut Criterion) { for (name, tint, background, target) in [ ("base", "#4A8FFF", "#101012", 2.3006), ("bloom", "#FF3B30", "#101012", 13.3251), - ("unreachable", "#3E87FF", "#FFFFFF", 2.3006), + // Белый фон даёт точный no-op из одного состояния, + // а не наихудший случай перебора. + ("exact-noop", "#3E87FF", "#FFFFFF", 2.3006), + // Длинный поток в доказанной границе ≤ 766 состояний. Соседний модульный + // тест закрепляет недостижимость цели 101, поэтому legacy target/max + // обязан прочитать поток до конца; tight максимума отдельно не заявлен. + ("long-unreachable", "#FFFFFE", "#000101", 101.0), ] { group.bench_with_input( BenchmarkId::new("legacy", name), diff --git a/crates/labcolors-core/examples/unique_yellow_provenance.rs b/crates/labcolors-core/examples/unique_yellow_provenance.rs index 1008975c..cb7641e7 100644 --- a/crates/labcolors-core/examples/unique_yellow_provenance.rs +++ b/crates/labcolors-core/examples/unique_yellow_provenance.rs @@ -1,26 +1,30 @@ -//! Воспроизводимая деривация M-12 `H_Y_DEG` — Oklab hue уникального жёлтого. +//! Воспроизводимая compatibility-арифметика M-12 `H_Y_DEG` для выбранного 578nm reference. //! //! Запуск: `cargo run -p labcolors-core --example unique_yellow_provenance` //! -//! Метод (Zone B slice 5, 2026-07-03): -//! 1. λ = 578nm — инвариантная точка жёлтого Бецольда-Брюкке -//! (Purdy 1937, Am. J. Psychol. 49, 313–315; Jacobs & Wascher 1967, -//! J. Opt. Soc. Am. 57, 1155–1156). +//! Метод (Zone B slice 5, статус уточнён 2026-07-11): +//! 1. λ = 578nm — замороженный spectral reference, а не универсальный +//! unique-yellow наблюдателя. Observer claim отклонён в M-12. //! 2. CMF при 578nm — линейная интерполяция официальной CIE 1931 2° 5нм-таблицы. //! 3. Нормировка к Y=1 (hue инвариантен к радиансности: масштабирование LMS′ //! множит a и b одинаково — отношение в atan2 не меняется; проверяется ниже). -//! 4. XYZ → Oklab НАПРЯМУЮ через M1/M2 (Ottosson 2020) — БЕЗ проекции в sRGB: +//! 4. XYZ → Oklab НАПРЯМУЮ через замороженные original-2020 M1/M2 — БЕЗ +//! проекции в sRGB: //! покомпонентный гамут-клэмп не сохраняет оттенок (демонстрируется ниже). //! 5. h = atan2(b, a). //! -//! Печатает также golden-значения muddiness для characterization-тестов. +//! Это provenance именно frozen compatibility value `90.4011°`. Production sRGB +//! path использует revised-2021 matrices (`spaces/oklab.rs`), поэтому результат +//! version-sensitive. Скрипт не доказывает observer meaning или Hanning-эвристику. // CIE 1931 2° standard observer, 5nm table (x̄, ȳ, z̄) const CMF_575: [f64; 3] = [0.8425, 0.9154, 0.0018]; const CMF_580: [f64; 3] = [0.9163, 0.8700, 0.00165]; -/// XYZ (D65) → LMS, Ottosson 2020 M1. Не входит в продуктивное ядро -/// (ядро работает от sRGB), поэтому определена здесь, в скрипте провенанса. +/// XYZ (D65 reference) → LMS, original Ottosson 2020 M1. +/// +/// Эта version-frozen матрица не является SSOT production sRGB path; она +/// сохранена только чтобы воспроизводить опубликованное compatibility value M-12. #[rustfmt::skip] const M1_XYZ_TO_LMS: [[f64; 3]; 3] = [ [0.8189330101, 0.3618667424, -0.1288597137], @@ -28,7 +32,7 @@ const M1_XYZ_TO_LMS: [[f64; 3]; 3] = [ [0.0482003018, 0.2643662691, 0.6338517070], ]; -/// LMS′ → Oklab, Ottosson 2020 M2 (та же матрица, что в spaces/oklab.rs). +/// LMS′ → Oklab, original Ottosson 2020 M2. #[rustfmt::skip] const M2_LMS_TO_OKLAB: [[f64; 3]; 3] = [ [0.2104542553, 0.7936177850, -0.0040720468], @@ -73,7 +77,7 @@ fn main() { let h = hue_deg(lab); println!("Oklab a={:+.10} b={:+.10}", lab[1], lab[2]); println!( - "H_Y = {h:.4}° (константа M-12: {})", + "selected-578nm hue = {h:.4}° (frozen M-12: {})", labcolors_core::cleanliness::H_Y_DEG ); @@ -93,22 +97,4 @@ fn main() { "slice-4 XYZ=(1.207,1,0) без клэмпа: {:.4}° (после sRGB-клэмпа было 96.9°)", hue_deg(lab_wrong) ); - - // Golden-значения muddiness для characterization-тестов (Fowler class B) - println!("\n— mud goldens (muddiness_from_hex) —"); - for (label, hex) in [ - ("olive", "#6B6B2E"), - ("babypoop", "#937C00"), - ("puke", "#9AAE07"), - ("gold1", "#9e6c00"), - ("gold2", "#8f6424"), - ("grey", "#808080"), - ("teal", "#008080"), - ("navy", "#000080"), - ("red", "#FF0000"), - ("blue", "#0000FF"), - ] { - let mud = labcolors_core::cleanliness::muddiness_from_hex(hex).unwrap(); - println!("{label:9} {hex} mud={mud:.8}"); - } } diff --git a/crates/labcolors-core/src/agnostic_gates.rs b/crates/labcolors-core/src/agnostic_gates.rs index 765d3c0d..c4d21d39 100644 --- a/crates/labcolors-core/src/agnostic_gates.rs +++ b/crates/labcolors-core/src/agnostic_gates.rs @@ -11,6 +11,7 @@ //! changes: the proof that any company plugs in its own design system. use crate::config::fixture::labui_reference; +use crate::config::test_support::resolved_repr as repr; use crate::ladder::{LadderPosition, ThemeAnchors}; use crate::solve::Floor; use crate::{ @@ -36,19 +37,6 @@ fn grid() -> ([(ViewingConditions, &'static str); 2], [&'static str; 6]) { ) } -/// Canonical stable representation of a resolved role. -fn repr(res: &Resolved) -> String { - match res { - Resolved::Color { solved, .. } => solved.hex().to_string(), - Resolved::Translucent(r) => format!("rgba({},{})", r.tint_hex(), r.alpha()), - Resolved::Glow(g) => format!("glow({},{},{:.4})", g.core_hex(), g.halo_hex(), g.alpha()), - Resolved::GlowIndeterminate(_) => "GLOW_INDETERMINATE".to_string(), - Resolved::Material(m) => format!("material({},{:.4})", m.tint_hex(), m.alpha()), - Resolved::None => "none".to_string(), - Resolved::Unreachable(_) => "UNREACHABLE".to_string(), - } -} - // ───────────────────────────────────────────────────────────────────────────── // 1. Frozen golden byte-identity gate // ───────────────────────────────────────────────────────────────────────────── @@ -106,10 +94,18 @@ fn labui_fixture_emission_is_byte_identical_to_frozen_golden() { golden.lines().count(), "labui emission line count drifted — a role/alias/grid point changed" ); - assert_eq!( - got, golden, + let changed: Vec = got + .lines() + .zip(golden.lines()) + .enumerate() + .filter(|(_, (actual, expected))| actual != expected) + .map(|(index, (actual, expected))| format!("line {}\n- {expected}\n+ {actual}", index + 1)) + .collect(); + assert!( + changed.is_empty(), "labui fixture emission drifted from the frozen golden \ - (regenerate with BLESS_LABUI_GOLDEN=1 only for a reviewed change)" + (regenerate only for a reviewed change):\n{}", + changed.join("\n") ); } diff --git a/crates/labcolors-core/src/alpha.rs b/crates/labcolors-core/src/alpha.rs index 6238ba52..c137ab88 100644 --- a/crates/labcolors-core/src/alpha.rs +++ b/crates/labcolors-core/src/alpha.rs @@ -273,7 +273,13 @@ pub fn min_alpha_encoded(solid: [f64; 3], bg: [f64; 3]) -> Option { return Some(0.0); } let analytic = analytic_alpha_candidate(solid, bg); - debug_assert!(analytic > 0.0); + // Для двух разных цветов хотя бы один канал различается. В валидном + // `[0,1]`-домене соответствующий знаменатель строго положителен, поэтому и + // его поканальная граница, и максимум границ обязаны быть больше нуля. + debug_assert!( + analytic > 0.0, + "solid != bg обязан давать положительную analytic alpha: solid={solid:?}, bg={bg:?}, analytic={analytic}" + ); // Положительные finite f64 упорядочены unsigned-битами. Сначала // экспоненциально находим ближайшую пару «не проходит / проходит» вокруг @@ -459,8 +465,17 @@ fn first_srgb8_alpha(solid: [u8; 3], bg: [u8; 3]) -> f64 { } let mut failing = 0.0_f64.to_bits(); let mut passing = 1.0_f64.to_bits(); - debug_assert!(!srgb8_target_is_feasible(solid, 0.0, bg)); - debug_assert!(srgb8_target_is_feasible(solid, 1.0, bg)); + // При alpha=0 композит равен bg при любом тинте, а при alpha=1 выбор + // tint=solid всегда достигает цели. После раннего `solid == bg` это строгая + // пара FAIL/PASS, необходимая двоичному lower-bound. + debug_assert!( + !srgb8_target_is_feasible(solid, 0.0, bg), + "разные solid/bg не могут быть достижимы при alpha=0: solid={solid:?}, bg={bg:?}" + ); + debug_assert!( + srgb8_target_is_feasible(solid, 1.0, bg), + "alpha=1 обязана достигать любой sRGB8-цели через tint=solid: solid={solid:?}, bg={bg:?}" + ); while passing - failing > 1 { let middle = failing + (passing - failing) / 2; @@ -577,7 +592,13 @@ pub(crate) fn resolve_alpha_analog_srgb8( } let floor = first_srgb8_alpha(solid_srgb8, bg_srgb8); - debug_assert!(floor > requested_alpha); + // Запрошенная alpha уже не дала byte-тинт; тотальность канального поиска и + // монотонность reference-предиката означают, что первый PASS обязан быть + // строго правее неё. Равенство здесь выявляет рассогласование двух путей. + debug_assert!( + floor > requested_alpha, + "отвергнутая requested alpha обязана лежать ниже первого sRGB8 PASS: solid={solid_srgb8:?}, bg={bg_srgb8:?}, requested={requested_alpha}, floor={floor}" + ); let tint = srgb8_tint_at_alpha(solid_srgb8, floor, bg_srgb8) .ok_or_else(|| "первая sRGB8-alpha не дала допустимый byte-тинт".to_string())?; verify_srgb8_alpha_analog(solid_srgb8, tint, floor, bg_srgb8)?; diff --git a/crates/labcolors-core/src/cleanliness.rs b/crates/labcolors-core/src/cleanliness.rs index b3dedce3..83b7f931 100644 --- a/crates/labcolors-core/src/cleanliness.rs +++ b/crates/labcolors-core/src/cleanliness.rs @@ -1,42 +1,59 @@ -//! Порт финального «Закона Грязи» (Muddiness Law) системы `labcolors`. +//! Замороженная legacy research-coordinate системы `labcolors`. +//! +//! Исторические имена модуля и функций (`cleanliness`, `muddiness`, `drab`, +//! `n_pure`) — только идентификаторы API. Результат — **experimental compatibility proxy**, +//! а не observer-validated cleanliness/murkiness, +//! эстетическая оценка, human verdict или product contract. Компилятор и +//! runtime-resolver токенов эту координату не используют. +//! +//! Формула и frozen conformance corpus воспроизводимы; это не валидирует +//! человеческий смысл названий. `C0`, `JND`, `B0` и `BW` остаются legacy +//! compatibility constants: опубликованный вывод их точных Oklab-значений и +//! универсальная применимость не установлены. Hanning-форма `hue_weight` — +//! эвристика; эффект Бецольда—Брюкке её не выводит. `H_Y_DEG` воспроизводит +//! арифметическое преобразование выбранного 578 nm reference в Oklab, но не +//! является универсальным unique-yellow наблюдателя. +//! +//! Полный эпистемический статус и владельцы миграции: `docs/empirical-inventory.md` +//! (M-01…M-14) и Issue #231. //! //! # Уравнения //! //! ```text //! raw = neutral_gate(C) * hue_weight(h) * depth_mod(L, C, h) -//! mud = raw ← параметр-свободная монотонная нормировка (Zone B, 2026-06-30) +//! mud = raw ← замороженный legacy-идентификатор результата //! ``` //! //! Platt-звено (логарифм raw + eps → sigmoid с двумя подогнанными скалярами) **удалено полностью** //! в Zone B slice 3: оба скаляра (M-07/M-08) подогнаны на авторском датасете 738 меток //! (наблюдатель-фит) и нарушали инвариант «ZERO observer-fit» (North). mud = raw_chromatic напрямую: //! - параметр-свободно (нет новых констант); -//! - монотонно (произведение монотонных сигмоидных факторов ∈ \[0,1\]); //! - ограничено \[0,1\] точно (каждый множитель ∈ \[0,1\]); -//! - JND-относительно по конструкции (gate N(C) выровнен по M-02 JND). +//! - масштаб logistic gate задаётся замороженной шириной M-02; это не заявление +//! о perceptual threshold или универсальном JND. //! //! Бывший light-escape порог M-03 (DECLARED-CALIBRATION скаляр) и его escape-член //! в `neutral_gate` **удалены целиком** (Zone B, 2026-07-01): escape-член протекал //! ось L в функцию, документированную как чисто-хроматический hue-agnostic гейт — //! нарушение независимости осей. `neutral_gate(c, c0, jnd) = sigmoid((c - c0) / jnd)`. //! -//! # Инвентарь параметров (раздел Muddiness Law) +//! # Инвентарь legacy-параметров //! //! | const | mud-id | статус | значение | //! |-----------------|--------|--------------------------------|-----------------------------| -//! | `C0` | M-01 | cited-and-kept | 0.0395 (граница серого sRGB; Evans/Xie-Fairchild yellow zero-grayness frontier) | -//! | `JND` | M-02 | cited-and-kept | 0.01228 (Oklab chroma JND; Oklab perceptual measurement) | -//! | `B0` | M-04 | cited-measured | 0.036 (центр диапазона [0.030, 0.044]; Newhall-Nickerson-Judd 1943; Lindsey-Brown 2014 PNAS; Boynton 1975) | -//! | `BW` | M-05 | cited-measured | 0.017 (центр диапазона [0.013, 0.020]; Newhall-Nickerson-Judd 1943; Lindsey-Brown 2014 PNAS; Boynton 1975) | +//! | `C0` | M-01 | Indeterminate provenance | 0.0395 frozen compatibility value; exact Oklab derivation not established | +//! | `JND` | M-02 | universal JND Rejected; value Indeterminate | 0.01228 frozen gate width; reference point/domain not specified | +//! | `B0` | M-04 | Rejected provenance; Indeterminate value | 0.036 frozen positive-b gate centre; cited Oklab range was not derived by the named sources | +//! | `BW` | M-05 | Rejected provenance; Indeterminate value | 0.017 frozen positive-b gate width; cited Oklab range was not derived by the named sources | //! | ~~`CAL_EPS`~~ | M-06 | УДАЛЁН (Zone B slice 3) | был 0.01; log-регуляризатор для Platt — более не нужен | //! | ~~`CAL_T`~~ | M-07 | УДАЛЁН (Zone B slice 3) | был 2.356978; Platt-скаляр, подогнанный на 738 авторских метках — нарушал ZERO observer-fit | //! | ~~`CAL_B`~~ | M-08 | УДАЛЁН (Zone B slice 3) | был 6.445168; Platt-смещение — нарушало ZERO observer-fit | //! | ~~`M_W`~~ | M-09 | УДАЛЁН (Front B, 2026-07-04) | был 0.181527; полуширина доверительного поля confidence-слоя — субъективная калибровка владельца без модели надёжности, вне пути эмиссии — удалён целиком | //! | ~~`KAPPA_CORE`~~ | M-10 | УДАЛЁН (Front B, 2026-07-04) | был 0.34; потолок уверенности ядра — та же субъективная калибровка — удалён вместе с confidence-слоем | //! | ~~`KAPPA_INTERIOR`~~ | M-11 | УДАЛЁН (Front B, 2026-07-04) | был 0.10; пол уверенности спорной полосы — удалён вместе с confidence-слоем | -//! | `H_Y_DEG` | M-12 | cited-derived (Zone B slice 5) | 90.4011° — Oklab hue уникального жёлтого (λ=578nm, CIE 1931 2° D65, XYZ→Oklab напрямую); hue_weight = (1+cos(h−H_Y))/2 | +//! | `H_Y_DEG` | M-12 | arithmetic admitted; observer claim Rejected | 90.4011° — выбранный 578 nm reference → Oklab; не universal unique yellow и не вывод Hanning-семантики | //! | ~~`W_HUE[8]`~~ | M-12 | УДАЛЁН (Zone B slice 4) | был: подогнанный K=3 Fourier-вектор логистической регрессии — нарушал ZERO observer-fit | -//! | `CUSP_L_TABLE` | M-13 | cited-and-kept | (чистая геометрия гамута Oklab, верифицирована до f64 — kept as-is) | +//! | `CUSP_L_TABLE` | M-13 | geometry admitted | геометрия гамута Oklab; не человеческая граница cleanliness | //! | ~~`CEIL_N_TABLE`~~ | M-14 | УДАЛЕНА (Zone B slice 4) | была: Fourier CEIL_N, подогнана на датасете v3 — более не нужна после BB-замены | #![allow(clippy::excessive_precision)] @@ -92,25 +109,28 @@ pub(crate) static CUSP_L_TABLE: [f64; 361] = [ // CEIL_N_TABLE удалена (Zone B slice 4, 2026-06-30): // K=3 Fourier-базис CEIL_N использовался в hue_basis() для dot-product c W_HUE. -// После замены hue_weight на Hanning-окно BB ни hue_basis, ни CEIL_N_TABLE +// После замены hue_weight на замороженное Hanning-окно ни hue_basis, ни CEIL_N_TABLE // не нужны в продуктивном коде. // High-precision frozen parameter constants pub const C0: f64 = 0.0395000000000000; -/// JND reuse rationale: this constant is the cited Oklab chroma just-noticeable-difference -/// (Oklab perceptual measurement, M-02 cited-and-kept). It is used as the sigmoid gate width -/// throughout the module because JND is the natural perceptual scale for a logistic boundary — -/// a ±1 JND band captures the transition from sub-threshold to supra-threshold chroma presence. -/// No new fit is performed; this is the same cited value reused as a scale parameter. -/// The applicability of a chroma JND as the gate width is a design assumption; if a future -/// study finds a different gate width is warranted, M-02 should be split into separate cited -/// values (one for detection threshold, one for gate width). Flagged OPEN as an assumption. +/// Frozen width of the legacy sigmoid gate (M-02). +/// +/// The historical name is retained for API compatibility. A universal Oklab +/// just-noticeable difference is rejected: no reference point, direction, +/// viewing protocol, or observer population establishes this exact value. +/// Therefore this constant must not be interpreted as a detection threshold. +/// See `docs/empirical-inventory.md` and Issue #242. pub const JND: f64 = 0.0122779190541810; // M-03 (former light-escape calibration threshold, DECLARED-CALIBRATION) removed // entirely (Zone B, 2026-07-01): its escape term leaked the lightness axis into // what is documented as a hue-agnostic, chroma-only gate (see `neutral_gate` above). -pub const B0: f64 = 0.036; // cited-measured central (Newhall-Nickerson-Judd 1943; Lindsey-Brown 2014 PNAS; Boynton 1975); range [0.030, 0.044] -pub const BW: f64 = 0.017; // cited-measured central (Newhall-Nickerson-Judd 1943; Lindsey-Brown 2014 PNAS; Boynton 1975); range [0.013, 0.020] +/// Frozen centre of the legacy positive-b gate (M-04). +/// The former cited-measured provenance claim is rejected; this is compatibility state. +pub const B0: f64 = 0.036; +/// Frozen width of the legacy positive-b gate (M-05). +/// The former cited-measured provenance claim is rejected; this is compatibility state. +pub const BW: f64 = 0.017; // CAL_EPS / CAL_T / CAL_B удалены (Zone B slice 3, 2026-06-30): // Platt-звено sigmoid(CAL_T*ln(raw+eps)+CAL_B) было observer-fit на 738 авторских метках // (нарушение ZERO observer-fit, North). Заменено параметр-свободным mud = raw_chromatic. @@ -121,14 +141,15 @@ pub const BW: f64 = 0.017; // cited-measured central (Newhall-Nickerson-Judd 194 // W_HUE[8] удалён (Zone B slice 4, 2026-06-30): // Подогнанный вектор K=3 Фурье-регрессии (логистическая регрессия на 738 авторских -// метках, M-12) заменён выведенным Hanning-окном Бецольда-Брюкке. +// метках, M-12) заменён замороженной параметр-свободной Hanning-эвристикой. // Формула: hue_weight(h) = (1 + cos(h − H_Y_DEG)) / 2 -// Провенанс: инвариантная точка жёлтого BB 578nm (Purdy 1937; Jacobs & Wascher 1967, -// J. Opt. Soc. Am. 57, 1155–1156) × Oklab (Ottosson 2020). +// H_Y_DEG воспроизводит арифметическое преобразование выбранного 578nm reference +// в Oklab. Эффект Бецольда—Брюкке не выводит эту Hanning-форму и не придаёт ей +// универсальный observer meaning. // Инварианты: hue_weight(H_Y_DEG) = 1.0 точно; hue_weight(H_Y_DEG ± 180°) = 0.0 точно; // строго монотонное убывание при |h − H_Y_DEG| растёт от 0 до 180°. -/// Oklab hue уникального жёлтого (λ=578nm, CIE 1931 2° наблюдатель, D65). +/// Oklab hue выбранного спектрального reference λ=578nm (CIE 1931 2° CMF). /// /// Вывод (Zone B slice 5, 2026-07-03 — исправление ошибочной деривации slice 4): /// 1. CMF при 578nm — линейная интерп. официальной CIE 1931 2° 5нм-таблицы: @@ -150,13 +171,13 @@ pub const BW: f64 = 0.017; // cited-measured central (Newhall-Nickerson-Judd 194 /// сохраняет оттенок: без клэмпа тот же XYZ давал h=69.7°, после клэмпа 96.9°. /// Значение 96.9172° было артефактом клэмпа поверх неверного XYZ. /// -/// Выбор λ=578nm: инвариантная точка жёлтого Бецольда-Брюкке (Purdy 1937, -/// Am. J. Psychol. 49, 313–315; подтверждено Jacobs & Wascher 1967, -/// J. Opt. Soc. Am. 57, 1155–1156). Прежняя цитата «Parry (1967) JOSA 57, +/// Арифметика выбранного λ=578nm допускается и воспроизводима. Заявление, что +/// это универсальный unique-yellow наблюдателя, отвергнуто: результат зависит +/// от наблюдателя, яркости и метода. Прежняя цитата «Parry (1967) JOSA 57, /// 1130–1134» не существует в литературе и удалена. /// /// Использование: центр Hanning-окна `hue_weight(h)` (M-12). -/// Константа выводная, не подогнана; воспроизводится скриптом деривации из CMF. +/// Число не подогнано; воспроизводится скриптом арифметического преобразования CMF. pub const H_Y_DEG: f64 = 90.4011; #[inline] @@ -181,18 +202,19 @@ pub fn cusp_l_of(h_deg: f64) -> f64 { y0 + fract * (y1 - y0) } -/// Depth term: how far below the hue's clean cusp the colour sits, normalized by cusp L. -/// Ranges from 0.0 (at/above cusp) to 1.0 (deep in the basement). +/// Geometric depth below the hue-dependent sRGB/Oklab gamut cusp, normalized by cusp L. +/// Ranges from 0.0 (at/above cusp) to 1.0 (furthest below it); no human meaning follows. pub fn depth_term(l: f64, h_deg: f64) -> f64 { let cusp_l = cusp_l_of(h_deg); let below = (cusp_l - l) / cusp_l.max(1e-6); below.clamp(0.0, 1.0) } -/// Hue-agnostic chroma-confidence gate: checks if the color is chromatic at all, or reads grey/beige. +/// Frozen hue-agnostic logistic coordinate over Oklab chroma. /// -/// `neutral_gate(c, c0, jnd) = sigmoid((c - c0) / jnd)` — a pure JND-relative -/// chroma-presence gate. The former light-escape term (a product of two +/// `neutral_gate(c, c0, jnd) = sigmoid((c - c0) / jnd)`. The parameter named +/// `jnd` is a compatibility width, not a detection probability or universal +/// perceptual threshold. The former light-escape term (a product of two /// sigmoids gated on lightness and chroma) depended on a DECLARED-CALIBRATION /// threshold (M-03, a Platt-fit scalar on the v3 dataset) and leaked the /// lightness axis into a function documented as hue-agnostic and chroma-only — @@ -201,44 +223,34 @@ pub fn neutral_gate(c: f64, c0: f64, jnd: f64) -> f64 { sigmoid((c - c0) / jnd) } -/// Opponent b-gate (warm/clean filter): gates the geometric depth term. Warm -> 1.0, cool -> 0.0. +/// Frozen positive-b logistic gate multiplied by the geometric depth term. +/// Its historical parameter values have no admitted human clean/dirty semantics. pub fn depth_mod(l: f64, c: f64, h_deg: f64, b0: f64, bw: f64) -> f64 { let dp = depth_term(l, h_deg); let s = sigmoid((b_of(c, h_deg) - b0) / bw); dp * s } -/// Весовой множитель оттенка — Hanning-окно Бецольда-Брюкке (Zone B slice 4, 2026-06-30). +/// Замороженная Hanning-эвристика над Oklab hue (Zone B slice 4, 2026-06-30). /// -/// Заменяет подогнанный вектор W_HUE\[8\] (M-12, удалён в Zone B slice 4) на выводную формулу. +/// Заменяет подогнанный вектор W_HUE\[8\] (M-12, удалён в Zone B slice 4) +/// параметр-свободной формулой. /// -/// # Вывод формулы +/// # Эпистемический статус /// -/// Эффект Бецольда-Брюкке — реальный психофизический феномен (уникальный жёлтый и его -/// инвариантная точка заземлены в этом же файле через Purdy 1937, см. [`H_Y_DEG`]): при -/// росте яркости оттенки смещаются к уникальному жёлтому (h_Y) и уникальному синему. -/// Косинусная модель плотности конвергенции ниже — ЭВРИСТИКА (параметрическая форма -/// Hanning-окна); железный первоисточник ИМЕННО ЭТОЙ формы не установлен. Прежняя ссылка -/// «Parry (1967) JOSA 57, 1130–1134» не существует в литературе (отозвана у [`H_Y_DEG`]) -/// и удалена. Производная смещения по Oklab-оттенку: -/// -/// dΔH_BB/dh = A_BB · cos(h − h_Y) -/// -/// — плотность конвергенции оттенков к h_Y. Максимальна при h = h_Y (все ближние оттенки -/// сходятся к уникальному жёлтому, зона максимальной «грязи»); обнуляется при |h − h_Y| = 90°; -/// минимальна при |h − h_Y| = 180° (противоположное направление). -/// -/// Нормируем в [0, 1]: +/// Эффект Бецольда—Брюкке реален, но не выводит эту косинусную функцию, +/// её центр или human cleanliness semantics. Формула ниже — frozen heuristic; +/// доказаны только её математические свойства. `H_Y_DEG` — арифметика выбранного +/// 578nm reference, не универсальный unique-yellow наблюдателя. /// /// hue_weight(h) = (1 + cos(h − H_Y_DEG)) / 2 [Hanning-окно] /// /// # Провенанс констант /// -/// - `H_Y_DEG = 90.4011°` — Oklab hue уникального жёлтого (λ=578nm, CIE 1931 2° D65). +/// - `H_Y_DEG = 90.4011°` — Oklab hue выбранного λ=578nm reference по CIE 1931 2° CMF. /// Вывод: CMF → XYZ → Oklab напрямую (Ottosson 2020) → atan2(b, a); без sRGB-клэмпа /// (клэмп не сохраняет hue — см. историю slice 4 в доке H_Y_DEG). -/// Цитата: Purdy (1937) — инвариантная точка жёлтого 578nm; Jacobs & Wascher (1967) -/// J. Opt. Soc. Am. 57, 1155–1156. +/// Это не observer-validated invariant; см. M-12 в empirical inventory. /// /// # Инварианты (проверены тестами) /// @@ -270,7 +282,7 @@ pub fn hue_weight(h_deg: f64) -> f64 { } } -/// Сырой хроматический счёт грязи (без масштабирования). +/// Сырая experimental compatibility proxy coordinate (legacy API identifier). /// /// raw = N(C) × hue_weight_BB(h) × depth_mod(L, C, h) /// @@ -282,29 +294,31 @@ pub fn raw_chromatic(l: f64, c: f64, h_deg: f64) -> f64 { cc * hw * dpm } -/// Параметр-свободная монотонная цена грязи (Zone B slices 3+4, 2026-06-30). +/// Frozen experimental compatibility proxy (Zone B slices 3+4, 2026-06-30). +/// +/// `mud = raw_chromatic(l, c, h)` is a deterministic product of three legacy factors: +/// `N(C) = sigmoid((C - C0) / JND)` — frozen logistic gate (M-01/M-02) +/// `hue_weight(h) = (1+cos(h-H_Y_DEG))/2` — Hanning heuristic (M-12) +/// `depth_mod(L, C, h)` — gamut geometry × positive-b gate (M-04/M-05) /// -/// mud = raw_chromatic(l, c, h) — прямое произведение трёх перцептивно-обоснованных -/// факторов из геометрии Oklab и поворота Бецольда-Брюкке: -/// N(C) = sigmoid((C - C0) / JND) — JND-взвешенное присутствие хромы (M-01/M-02) -/// hue_weight(h) = (1+cos(h−H_Y_DEG))/2 — Hanning-окно BB (M-12, Zone B slice 4) -/// depth_mod(L, C, h) — глубина под cusp-L, взвешенная b-гейтом (M-04/M-05) +/// The historical name does not make the output an observer-validated cleanliness, +/// murkiness, aesthetic, or production decision. The compiler/resolver does not use it. /// /// Свойства: -/// - Выводных констант: только H_Y_DEG = 90.4° (CIE D65 unique yellow 578nm, Purdy 1937) +/// - `H_Y_DEG` reproduces the selected 578nm-to-Oklab arithmetic /// - Нет подогнанных скаляров: W_HUE/CEIL_N_TABLE/CAL_T/CAL_B — все удалены /// - raw ∈ \[0,1\] точно (произведение ∈ \[0,1\] множителей) /// - Монотонно в C только на ТЁПЛОЙ полуплоскости (h∈[0°,180°], sin h ≥ 0): /// N(C) растёт, b-гейт depth_mod не убывает. На холодных (sin h < 0) b-гейт /// `sigmoid((c·sin h − B0)/BW)` убывает в C → НЕ глобально монотонно /// (запинено `property_invariants`: warm-инвариант + cool-characterization) -/// - JND-нормировано по конструкции через M-02 +/// - logistic width is the frozen M-02 compatibility value /// - .clamp(0.0, 1.0) — safety no-op при f64-округлении pub fn muddiness_oklch(l: f64, c: f64, h_deg: f64) -> f64 { raw_chromatic(l, c, h_deg).clamp(0.0, 1.0) } -/// Compute muddiness from linear sRGB values [r, g, b] in [0, 1]. +/// Compute the legacy experimental compatibility proxy from linear sRGB in [0, 1]. pub fn muddiness_from_linear_srgb(rgb: [f64; 3]) -> f64 { let lab = crate::spaces::oklab::srgb_linear_to_oklab(rgb); let l = lab[0]; @@ -313,17 +327,17 @@ pub fn muddiness_from_linear_srgb(rgb: [f64; 3]) -> f64 { muddiness_oklch(l, c, h) } -/// Compute muddiness from an sRGB hex color string like "#6B6B2E". +/// Compute the legacy experimental compatibility proxy from an sRGB hex string. pub fn muddiness_from_hex(hex: &str) -> Result { let rgb = crate::spaces::srgb::srgb_from_hex(hex)?; Ok(muddiness_from_linear_srgb(rgb)) } // ───────────────────────────────────────────────────────────────────────────── -// Drab defect head (Zone B slice 2 — second INDEPENDENT output) +// Legacy `drab` coordinate (Zone B slice 2 — second INDEPENDENT output) // // `n_pure` and `drab` are a SEPARATE defect axis from mud/muddiness_oklch. -// They share ONLY the cited gate constants C0 / JND as INPUTS. +// They share ONLY the frozen compatibility constants C0 / JND as INPUTS. // They are NEVER called inside `raw_chromatic`, `muddiness_oklch`, or any // other mud code path — the two axes are provably independent (the curl / // integrability two-axes result from the paradigm North). @@ -332,32 +346,34 @@ pub fn muddiness_from_hex(hex: &str) -> Result { // = 1 - sigmoid((C - C0) / JND) [= 1 - n_pure(C)] // // Constants reused (zero new parameters): -// C0 = 0.0395 (cited-and-kept, M-01, Evans/Xie-Fairchild yellow zero-grayness frontier) -// JND = 0.0122779190541810 (cited-and-kept, M-02, Oklab chroma JND) +// C0 = 0.0395 (M-01, provenance Indeterminate) +// JND = 0.0122779190541810 (M-02, universal JND Rejected) // -// Статус (актуально на 2026-07-01): W_HUE[8] удалён и заменён выведенным Hanning-окном -// Бецольда-Брюкке (Zone B slice 4, M-12 cited-derived, см. таблицу инвентаря выше); +// Статус (актуально на 2026-07-11): W_HUE[8] удалён и заменён замороженной +// Hanning-эвристикой (Zone B slice 4, M-12, см. таблицу инвентаря выше); // Platt CAL_T/CAL_B удалены (Zone B slice 3). Открытых наблюдатель-фит параметров // в этом блоке не осталось. // ───────────────────────────────────────────────────────────────────────────── -/// Pure chroma-presence gate: N_pure(C) = sigmoid((C - C0) / JND). +/// Historical `n_pure` coordinate: `sigmoid((C - C0) / JND)`. /// /// This is the shared gate that enters both mud (as `neutral_gate`) and drab /// (as 1 - N_pure). It is kept as a standalone `pub fn` so callers can /// compose the two heads independently without re-implementing the gate. /// -/// Parameters: cited-and-kept C0 (M-01) and JND (M-02). Zero new parameters. +/// The name is a compatibility identifier, not an observer estimate of purity. +/// Parameters are frozen C0 (M-01) and JND (M-02); zero new parameters. #[inline] pub fn n_pure(c: f64) -> f64 { sigmoid((c - C0) / JND) } -/// Drab defect head: D(C) = 1 − N_pure(C). +/// Historical `drab` coordinate: `D(C) = 1 - N_pure(C)`. /// -/// Measures chroma ABSENCE — the complement of the cited chroma-presence gate. +/// This is the arithmetic complement of the frozen `n_pure` logistic coordinate. /// Returns 1.0 for achromatic colours (C ≈ 0, deeply grey/beige) and 0.0 for -/// strongly chromatic colours (C >> C0). +/// strongly chromatic colours (C >> C0), but it is not an observer-validated +/// estimate of dullness, drabness, purity, or any other human judgement. /// /// Implementation: `1.0 - n_pure(c)` — exact f64 arithmetic complement. /// This guarantees `drab(C) + n_pure(C) == 1.0` exactly in IEEE 754, which @@ -379,7 +395,7 @@ pub fn drab(c: f64) -> f64 { } // ───────────────────────────────────────────────────────────────────────────── -// Surround-aware оценка дефектов (Zone G) +// Context-parameterized legacy coordinates (Zone G) // // Тема снаружи (light / dark / light-ic / dark-ic) маппируется на ViewingConditions // по таблице CIECAM16 (Li et al. 2017, Table 1): @@ -390,21 +406,22 @@ pub fn drab(c: f64) -> f64 { // // Фон bg_hex даёт яркость Yb (Y-компонент фона в % от D65-белого), которая // подставляется в ViewingConditions::srgb_with_yb / dim_surround_with_yb. -// Это единственный канал влияния фона на дефект — через CAM16 J (apparent lightness) -// цвета под данным Yb+surround. +// Это единственный канал влияния фона на legacy coordinate — через CAM16 J +// цвета под данным Yb+surround. `bg_hex + theme` не описывают полный visual context: +// отсутствуют spatial distribution, surround variance, geometry, observer и adaptation history. // -// mud и drab считаются на (l_app=J/100, C_oklab, h_oklab): +// legacy `mud` и `drab` считаются на (l_app=J/100, C_oklab, h_oklab): // - C_oklab, h_oklab — Oklab-координаты исходного цвета (не меняются от surround) // - l_app = J/100 — CAM16 apparent lightness под фоном+темой, нормирован в [0,1] // // Это заменяет Oklab L в depth_term(l, h) (определяет положение цвета под cusp), -// делая оценку surround-aware: цвет на тёмном фоне «воспринимается светлее», -// лежит ближе к cusp-L и получает меньший depth_term → меньшую грязь. +// делая координату чувствительной к локальным Yb+theme inputs. Это не полная +// appearance model и не observer-validated оценка; полный context owner — Issue #230. // // Ноль новых параметров: все VC-параметры — таблица CIECAM16 (Li et al. 2017). // ───────────────────────────────────────────────────────────────────────────── -/// Тема просмотра — определяет условия окружения (surround) для оценки дефектов. +/// Compatibility theme input used to select the frozen CAM16 viewing conditions. /// /// Соответствие CIECAM16 (Li et al. 2017, Table 1): /// - `Light` → average surround (F=1.0, c=0.69, Nc=1.0) @@ -412,13 +429,13 @@ pub fn drab(c: f64) -> f64 { /// - `LightIc` / `DarkIc` — то же, но с флагом повышенного контраста #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Theme { - /// Светлая тема: average surround (CIECAM16 Table 1). + /// Light compatibility context: average surround (CIECAM16 Table 1). Light, - /// Тёмная тема: dim surround (CIECAM16 Table 1). + /// Dark compatibility context: dim surround (CIECAM16 Table 1). Dark, - /// Светлая тема с повышенным контрастом (IC). + /// Light compatibility context with the increased-contrast flag (IC). LightIc, - /// Тёмная тема с повышенным контрастом (IC). + /// Dark compatibility context with the increased-contrast flag (IC). DarkIc, } @@ -466,7 +483,8 @@ impl Theme { /// dark-темы — dim, IC-темы поднимают флаг повышенного контраста. Обе /// точки входа (дефолтный Yb на границе, Yb-от-фона в контексте дефектов) /// обязаны выбирать surround здесь — вторая копия карты в цветовом коде - /// расходилась бы тихо при добавлении темы. + /// расходилась бы тихо при добавлении темы. Эта карта не утверждает полноту + /// perceptual context; она только сохраняет legacy boundary contract. fn vc_by( self, srgb: impl FnOnce() -> crate::spaces::vc::ViewingConditions, @@ -483,9 +501,11 @@ impl Theme { } } -/// Контекст просмотра для surround-aware оценки дефектов. +/// Partial context input for the legacy experimental compatibility proxy. /// -/// Сочетает фон (hex-строка, задаёт яркость Yb) и тему (определяет surround). +/// Сочетает один фон (hex-строка, задаёт mean Yb) и тему (выбирает CAM16 surround). +/// Это не полный appearance context: variance, geometry, adaptation history and +/// observer state отсутствуют. Не использовать как human cleanliness verdict. /// Передаётся в `muddiness_in_context` / `drab_in_context`. #[derive(Debug, Clone, Copy)] pub struct DefectContext<'a> { @@ -521,7 +541,10 @@ fn vc_for_context(theme: Theme, y_b_pct: f64) -> crate::spaces::vc::ViewingCondi ) } -/// Surround-aware оценка грязи цвета в заданном контексте просмотра. +/// Compute the legacy experimental compatibility proxy with local Yb+theme inputs. +/// +/// This is context-parameterized, not fully surround-aware: it does not model +/// spatial distribution, surround variance, geometry, adaptation history, or observers. /// /// # Алгоритм /// @@ -530,13 +553,13 @@ fn vc_for_context(theme: Theme, y_b_pct: f64) -> crate::spaces::vc::ViewingCondi /// 3. Строим `ViewingConditions` для темы `ctx.theme` с данным Yb (CIECAM16 Table 1). /// 4. Из `hex` → XYZ → CAM16 `J` (apparent lightness под фоном+temой). /// 5. `l_app = J / 100` — нормированный apparent lightness ∈ [0, 1]. -/// 6. mud = `raw_chromatic(l_app, C_oklab, h_oklab)` — формула без изменений, +/// 6. legacy proxy = `raw_chromatic(l_app, C_oklab, h_oklab)` — формула без изменений, /// но `l_app` учитывает surround и фон вместо Oklab L. /// /// # Провенанс /// /// VC-параметры: CIECAM16, Li et al. 2017, DOI 10.1002/col.22131, Table 1. -/// Формула mud — без изменений (Zone B slices 3+4; H_Y_DEG, B0, BW цитированы). +/// Legacy formula is unchanged; provenance statuses are recorded in the empirical inventory. pub fn muddiness_in_context(hex: &str, ctx: DefectContext<'_>) -> Result { // Oklab-координаты: C и h не зависят от viewing conditions let rgb = crate::spaces::srgb::srgb_from_hex(hex)?; @@ -554,16 +577,14 @@ pub fn muddiness_in_context(hex: &str, ctx: DefectContext<'_>) -> Result) -> Result { - // drab зависит только от C_oklab — surround не меняет chroma в Oklab + // Historical formula depends only on C_oklab; context is not part of this coordinate. let _ = ctx; // принимается для симметрии API let rgb = crate::spaces::srgb::srgb_from_hex(hex)?; let lab = crate::spaces::oklab::srgb_linear_to_oklab(rgb); @@ -575,182 +596,64 @@ pub fn drab_in_context(hex: &str, ctx: DefectContext<'_>) -> Result mod tests { use super::*; - // ─── Smoke / class-membership reference values ──────────────────────────── - // - // Обновлено в Zone B slice 4 (2026-06-30): W_HUE заменён выведенным Hanning-окном - // Бецольда-Брюкке. Диапазон mud значительно расширился: [0, ~0.41] вместо [0.00..0.14]. - // Это ожидаемо — BB-формула даёт hue_weight ≈ 1.0 в широком тёплом секторе вместо - // узкого sigmoid-пика из логистической регрессии W_HUE. - // - // Ранговый порядок грязности: olive > babypoop > gold2 > gold1 > puke - // (прим.: gold2 > gold1 — артефакт подогнанного W_HUE исчез; теперь глубина - // под cusp-L побеждает, что физически более верно). - // - // Класс теста: class A (unit + reference), Фаулер. - // TDD: RED до коммита 2 (W_HUE → BB), GREEN после. - #[test] - fn test_muddiness_v3_reference_values() { - // Olive #6B6B2E (Наиболее грязный — максимальный raw) - // Провенанс golden: mud = N(C) * hue_weight_BB(h) * depth_mod(L, C, h), - // hue_weight_BB(109°) = (1 + cos(109° - 90.4°)) / 2 ≈ 0.974. - let olive_mud = muddiness_from_hex("#6B6B2E").unwrap(); - assert!( - olive_mud > 0.30, - "olive mud is {olive_mud:.6}, expected > 0.30 (BB-диапазон)" - ); - assert!( - (olive_mud - 0.40640072).abs() < 1e-5, - "olive mud {olive_mud:.8} != 0.40640072 (Zone B slice 5 H_Y golden)" - ); - - // Babypoop #937C00 (Очень грязный) - // hue_weight_BB(96°) = (1 + cos(96° - 90.4°)) / 2 ≈ 0.998. - let babypoop_mud = muddiness_from_hex("#937C00").unwrap(); - assert!( - babypoop_mud > 0.20, - "babypoop mud is {babypoop_mud:.6}, expected > 0.20" - ); - assert!( - (babypoop_mud - 0.33220155).abs() < 1e-5, - "babypoop mud {babypoop_mud:.8} != 0.33220155" - ); - - // Puke #9AAE07 (Умеренно грязный) - let puke_mud = muddiness_from_hex("#9AAE07").unwrap(); - assert!( - (puke_mud - 0.23449461).abs() < 1e-5, - "puke mud {puke_mud:.8} != 0.23449461" - ); - - // Золотые (средний диапазон) - let gold1_mud = muddiness_from_hex("#9e6c00").unwrap(); - assert!( - (gold1_mud - 0.29296999).abs() < 1e-5, - "gold1 mud {gold1_mud:.8} != 0.29296999" - ); - - let gold2_mud = muddiness_from_hex("#8f6424").unwrap(); - assert!( - (gold2_mud - 0.30971375).abs() < 1e-5, - "gold2 mud {gold2_mud:.8} != 0.30971375" - ); - - // Ранговый порядок: olive > babypoop > gold2 > gold1 > puke >> achromatic - // Примечание: gold2 > gold1 (BB-формула; прежний порядок gold1 > gold2 был - // артефактом подогнанного W_HUE, не геометрическим свойством Oklab). - assert!( - olive_mud > babypoop_mud, - "olive должен быть грязнее babypoop: {olive_mud:.6} vs {babypoop_mud:.6}" - ); - assert!( - babypoop_mud > gold1_mud && babypoop_mud > gold2_mud, - "babypoop должен быть грязнее золотых: {babypoop_mud:.6} vs gold1={gold1_mud:.6} gold2={gold2_mud:.6}" - ); - assert!( - gold2_mud > gold1_mud, - "gold2 должен быть грязнее gold1 по BB-формуле: gold2={gold2_mud:.8} vs gold1={gold1_mud:.8}" - ); - assert!( - gold1_mud > puke_mud && gold2_mud > puke_mud, - "золотые должны быть грязнее puke: gold1={gold1_mud:.6} gold2={gold2_mud:.6} vs puke={puke_mud:.6}" - ); - - // Pure grey #808080 (Чистый — achromatic, N(C) → 0) - let grey_mud = muddiness_from_hex("#808080").unwrap(); - assert!( - grey_mud < 0.005, - "grey mud is {grey_mud:.6}, expected < 0.005 (N(C) подавляет)" - ); - - // Teal #008080 (прохладный — b < B0, depth_mod → почти 0) - let teal_mud = muddiness_from_hex("#008080").unwrap(); - assert!( - teal_mud < 0.01, - "teal mud is {teal_mud:.6}, expected < 0.01 (depth_mod b-гейт)" - ); - assert!( - (teal_mud - 0.00430967).abs() < 1e-5, - "teal mud {teal_mud:.8} != 0.00430967 (Zone B slice 5 H_Y golden)" - ); - - // Navy Blue #000080 (Чистый — синий, depth_mod → 0 через b-гейт) - let navy_mud = muddiness_from_hex("#000080").unwrap(); - assert!( - navy_mud < 0.001, - "navy mud is {navy_mud:.8}, expected < 0.001" - ); - } - // ─── Characterization golden test (Zone B slice 4, Fowler class B) ───────── // - // Фиксирует таблицу mud(L,C,h) после замены W_HUE на Hanning-окно BB. - // mud = N(C) * hue_weight_BB(h) * depth_mod(L, C, h) - // где hue_weight_BB(h) = (1 + cos(h − H_Y_DEG)) / 2 + // Фиксирует таблицу legacy coordinate после замены W_HUE на Hanning-эвристику. + // mud = N(C) * hue_weight(h) * depth_mod(L, C, h) + // где hue_weight(h) = (1 + cos(h - H_Y_DEG)) / 2 // // Golden-значения получены из фактического кода после коммита 2 (Zone B slice 4) // и зафиксированы как regression anchor. Tolerance: ±5e-6 (точность f64). // // Класс теста: B (characterization/golden, Фаулер) — якорь регрессии. // История: Zone B slice 3 (Platt-удаление) имела диапазон [0.00..0.14]; - // Zone B slice 4 (BB-замена) расширила до [0.00..0.41] — ожидаемо - // (hue_weight_BB ≈ 1.0 шире, чем узкий sigmoid-пик W_HUE). + // Zone B slice 4 (Hanning-замена) расширила до [0.00..0.41]. #[test] - fn characterization_mud_golden_cited() { + fn characterization_proxy_golden() { struct Case { hex: &'static str, - label: &'static str, expected: f64, } - // Значения = muddiness_oklch(l,c,h) (BB-путь, Zone B slice 4) + // Значения = muddiness_oklch(l,c,h) (frozen compatibility path). let cases = [ Case { hex: "#6B6B2E", - label: "olive", expected: 0.40640072, }, Case { hex: "#937C00", - label: "babypoop", expected: 0.33220155, }, Case { hex: "#9AAE07", - label: "puke", expected: 0.23449461, }, Case { hex: "#9e6c00", - label: "gold1", expected: 0.29296999, }, Case { hex: "#8f6424", - label: "gold2", expected: 0.30971375, }, Case { hex: "#808080", - label: "grey", expected: 0.00125988, }, Case { hex: "#008080", - label: "teal", expected: 0.00430967, }, Case { hex: "#000080", - label: "navy", expected: 0.00000000, }, Case { hex: "#FF0000", - label: "red", expected: 0.00133634, }, Case { hex: "#0000FF", - label: "blue", expected: 0.00000000, }, ]; @@ -758,8 +661,7 @@ mod tests { let got = muddiness_from_hex(c.hex).unwrap(); assert!( (got - c.expected).abs() < 5e-6, - "characterization_mud_golden_cited: {} {}: got={:.8} expected={:.8} delta={:.2e}", - c.label, + "characterization_proxy_golden: {}: got={:.8} expected={:.8} delta={:.2e}", c.hex, got, c.expected, @@ -912,71 +814,59 @@ mod tests { ); } - // ─── Zone B slice 4: W_HUE absent + Бецольд-Брюкке инварианты (TDD RED-first) ── + // ─── Zone B slice 4: W_HUE absent + Hanning invariants (TDD RED-first) ── // - // Три теста закрывают КЛАСС дефектов: «подогнанный вектор вместо выведенной формулы». + // Тесты закрывают класс дефектов: fitted vector вместо frozen parameter-free formula. // // (1) w_hue_fitted_vector_absent_from_shipping — RED: `pub static W_HUE` ещё // существует в продуктивном коде (и hue_basis, hue_weight ещё опираются на него). // GREEN после коммита 2: W_HUE убран, hue_weight переписан. // - // (2) bezold_brucke_at_unique_yellow_is_one — RED: текущий hue_weight(97°) ≈ 0.2 - // (sigmoid(dot(W_HUE, basis(97°))) ≠ 1.0). GREEN после коммита 2: - // выведенная формула (1 + cos(0)) / 2 == 1.0 точно. + // (2) hanning_at_reference_hue_is_one — formula equals 1 at its frozen centre. // - // (3) bezold_brucke_at_opposite_is_zero — RED: текущий hue_weight(277°) ≈ 0.015 - // (sigmoid(dot) ≠ 0.0). GREEN после коммита 2: - // (1 + cos(π)) / 2 == 0.0 точно. + // (3) hanning_at_opposite_hue_is_zero — formula equals 0 at the opposite hue. // - // (4) bezold_brucke_monotone_from_unique_yellow — RED: текущая формула не является - // Hanning-окном и нарушает монотонное убывание от h_Y по обе стороны. - // GREEN после коммита 2: cos(δ) монотонно убывает при |δ| растёт от 0 до π. + // (4) hanning_monotone_from_reference_hue — cosine decreases as |delta| grows. // - // Класс багов: fitted-вектор не имеет гарантированных инвариантов hue_weight(h_Y) = 1, - // hue_weight(h_Y + 180°) = 0 и монотонности; выведенная формула доказывает их аналитически. + // The invariants are mathematical only; they do not validate observer semantics. // - // Провенанс: - // H_Y_DEG = 90.4011° — Oklab hue уникального жёлтого (λ=578nm, CIE 1931 2° наблюдатель, - // D65). Derivation (slice 5): CMF при 578nm — линейная интерполяция CIE 5нм-таблицы + // Arithmetic provenance: + // H_Y_DEG = 90.4011° — Oklab hue of the selected λ=578nm reference using + // CIE 1931 2° CMFs. Derivation (slice 5): linear interpolation of the 5nm table // (575nm: 0.8425/0.9154/0.0018; 580nm: 0.9163/0.8700/0.00165) → (0.886780, 0.888160, // 0.001710), → XYZ/Y = (0.998446, 1, 0.001925), → Oklab напрямую (Ottosson 2020, M1/M2; // hue инвариантен к радиансности, sRGB-клэмп не нужен и не hue-сохраняющ), // → atan2(b, a) = 90.4011°. Неопределённость 5нм-интерполяции: ±0.01° // (1нм-таблица CIE даёт 90.3925° — расхождение 0.009°, на 4 порядка ниже // ширины Hanning-окна; центр объявлен по официальной 5нм-таблице). - // Формула (1 + cos(h − h_Y)) / 2 — Hanning-окно, выведенное из производной - // поворота Бецольда-Брюкке: dΔH_BB/dh = A_BB · cos(h − h_Y), нормированная - // в [0, 1]. Цитаты: Purdy (1937) Am. J. Psychol. 49, 313–315 (инвариант 578nm); - // Jacobs & Wascher (1967) J. Opt. Soc. Am. 57, 1155–1156. + // This arithmetic does not establish a universal unique-yellow observer point. + // The Hanning formula is a heuristic and is not derived from Bezold-Brucke. #[test] fn w_hue_fitted_vector_absent_from_shipping() { // RED пока pub static W_HUE присутствует в продуктивном коде. - // GREEN после замены на выведенную BB-формулу. + // GREEN после замены на frozen Hanning formula. let source = include_str!("cleanliness.rs"); let prod_code = source.split("#[cfg(test)]").next().unwrap_or(source); assert!( !prod_code.contains("pub static W_HUE"), "pub static W_HUE найден в продуктивном коде — подогнанный вектор (M-12) не удалён. \ - Ожидается выведенная формула Бецольда-Брюкке." + Ожидается frozen Hanning formula." ); // Дополнительно: hue_basis (вспомогательная функция для dot-product) тоже должна уйти assert!( !prod_code.contains("fn hue_basis("), "fn hue_basis( найдена в продуктивном коде — K=3 Фурье-базис больше не нужен \ - после перехода на BB-формулу." + после перехода на Hanning formula." ); } #[test] - fn bezold_brucke_at_unique_yellow_is_one() { + fn hanning_at_reference_hue_is_one() { // Hanning-окно: (1 + cos(h − h_Y)) / 2 == 1.0 точно при h == h_Y. - // RED: текущий sigmoid(dot(W_HUE, basis(H_Y_DEG))) ≠ 1.0. - // GREEN: выведенная BB-формула гарантирует (1 + cos(0)) / 2 == 1.0. + // The frozen formula guarantees (1 + cos(0)) / 2 == 1.0. // - // Значение H_Y_DEG определяется из CIE 1931 2° (D65) → derivation в - // комментарии к тест-блоку выше. Мы тестируем через публичный `hue_weight`: - // если формула выведена верно, hue_weight(H_Y_DEG) == 1.0 ТОЧНО (не ≈). + // H_Y_DEG is the selected arithmetic reference documented above. let hw = hue_weight(H_Y_DEG); assert_eq!( hw, 1.0, @@ -986,10 +876,9 @@ mod tests { } #[test] - fn bezold_brucke_at_opposite_is_zero() { + fn hanning_at_opposite_hue_is_zero() { // Hanning-окно: (1 + cos(π)) / 2 == 0.0 ТОЧНО при h == h_Y + 180°. - // RED: текущий sigmoid(dot) ≈ 0.015, не равен 0.0. - // GREEN: выведенная формула гарантирует нулевой вес на противоположном оттенке. + // The frozen formula guarantees zero at the opposite hue. let hw = hue_weight(H_Y_DEG + 180.0); assert_eq!( hw, @@ -1001,10 +890,9 @@ mod tests { } #[test] - fn bezold_brucke_monotone_from_unique_yellow() { + fn hanning_monotone_from_reference_hue() { // Hanning-окно строго монотонно убывает при |δ| растёт от 0 до π. - // RED: текущая sigmoid(dot(W_HUE,...)) — не монотонное Hanning-окно. - // GREEN: (1 + cos(δ)) / 2 монотонно убывает по обе стороны от h_Y. + // (1 + cos(delta)) / 2 decreases on both sides of its reference hue. // // Тест: для 360 равномерных шагов δ от 0 до 180° включительно, // hue_weight(h_Y + δ) должен строго убывать. @@ -1038,69 +926,34 @@ mod tests { } } - // ─── Provenance-range guard (Zone B, Fowler class A — property test) ───── - // - // Asserts B0 lies in [0.030, 0.044] and BW lies in [0.013, 0.020] — the - // cited-measured ranges (Newhall-Nickerson-Judd 1943; Lindsey-Brown 2014 - // PNAS; Boynton 1975). This test was RED against the pre-swap fitted - // values (B0=0.02869 < 0.030, BW=0.02024 > 0.020) and turned GREEN after - // the swap — proving it bites (TDD RED-first; verified in git history as - // the compile-failing state at commit 1 HEAD). - // - // Uses `const { assert!(..) }` so the range check is enforced at COMPILE - // TIME (clippy::assertions_on_constants, Rust ≥1.96). A future constant - // edit outside the cited range will be caught at `cargo build`, not just - // at test time. - #[test] - fn provenance_range_guard_b0_bw() { - const { - assert!( - B0 >= 0.030 && B0 <= 0.044, - "B0 is outside the cited range [0.030, 0.044] \ - (Newhall-Nickerson-Judd 1943; Lindsey-Brown 2014 PNAS; Boynton 1975). \ - M-04 must be cited-measured." - ) - }; - const { - assert!( - BW >= 0.013 && BW <= 0.020, - "BW is outside the cited range [0.013, 0.020] \ - (Newhall-Nickerson-Judd 1943; Lindsey-Brown 2014 PNAS; Boynton 1975). \ - M-05 must be cited-measured." - ) - }; - } - - // ─── Zone G: surround-aware кейс-тесты (Fowler class A, TDD RED-first) ──── + // ─── Zone G: local-context sensitivity tests (Fowler class A) ───────────── // - // Условие успеха (Зона G exit-criteria): - // (1) Серый (#808080) на пастельном фоне (#FFE4E1) даёт более высокую грязь, - // чем тот же серый на нейтральном (#808080) — разница строго по знаку. - // (2) Тёмный розовый (#C2185B) на чёрном фоне (#000000) даёт более низкую грязь, - // чем тот же цвет на белом фоне (#FFFFFF) — направление дельты строго. + // Characterization contract: changing the supplied Yb+theme inputs changes + // CAM16 J and therefore the frozen proxy in the recorded directions. + // This proves wiring and regression sensitivity, not human cleanliness truth + // or completeness of the visual context model. // // Почему тест кусается (mutation-bite): - // Если заменить l_app = J/100 на l_app = Oklab-L (т.е. убрать surround-aware путь), + // Если заменить l_app = J/100 на l_app = Oklab-L (убрать local-context path), // оба теста провалятся: без учёта Yb фона CAM16 J не меняется при смене bg_hex, // поэтому muddiness_in_context вернёт одинаковое значение для обоих фонов. // // TDD RED-first: до добавления `muddiness_in_context` в файл эти тесты не // компилировались (функция не существовала) — RED доказан структурно. // - // Провенанс: параметры CAM16 — Li et al. 2017 Table 1 (cited); mud-формула — - // Zone B slices 3+4 (BB Hanning, B0/BW, C0/JND — все cited). + // CAM16 viewing-condition parameters: Li et al. 2017 Table 1. Legacy proxy + // parameter statuses are separate and recorded in the empirical inventory. use super::{DefectContext, Theme, drab_in_context, muddiness_in_context}; - /// Серый на пастельном фоне грязнее, чем тот же серый на нейтральном. + /// The frozen proxy is higher for #808080 with the supplied pastel background. /// - /// Физика: пастельный фон (#FFE4E1, Yb≈82%) повышает адаптацию → сдвигает - /// CAM16 J серого (#808080, Yb≈22%) вниз от cusp-L → depth_term растёт → - /// mud растёт. На нейтральном фоне (#808080, Yb≈22%) адаптация стандартная. + /// In this partial model, #FFE4E1 supplies a higher Yb than #808080, changing + /// CAM16 J and the geometric depth input. No observer judgement is asserted. /// /// Направление дельты: mud_on_pastel > mud_on_neutral — строго. #[test] - fn grey_on_pastel_is_muddier_than_grey_on_neutral() { + fn proxy_for_grey_is_higher_with_pastel_yb_than_neutral_yb() { let grey = "#808080"; let pastel_bg = "#FFE4E1"; // розово-белёсый, Yb≈82% let neutral_bg = "#808080"; // нейтральный серый, Yb≈22% @@ -1125,22 +978,19 @@ mod tests { assert!( mud_on_pastel > mud_on_neutral, - "Серый на пастельном фоне должен быть грязнее, чем на нейтральном: \ - mud_on_pastel={mud_on_pastel:.6} должно быть > mud_on_neutral={mud_on_neutral:.6}. \ - Если равны — surround-aware путь не работает (J не учитывает Yb фона)." + "local-context proxy ordering changed: pastel={mud_on_pastel:.6} \ + must be > neutral={mud_on_neutral:.6}; equality means Yb no longer reaches CAM16 J" ); } - /// Тёмный розовый чист (низкая грязь) на чёрном и грязнее на белом. + /// The frozen proxy is lower for #C2185B with black than with white inputs. /// - /// Физика: на белом фоне (#FFFFFF, Yb≈100%) CAM16 J тёмного розового (#C2185B) - /// ниже, чем на чёрном (#000000, Yb≈0.5%), — он выглядит тёмным относительно - /// светлого окружения, глубже под cusp-L → depth_term растёт → mud растёт. - /// На чёрном фоне адаптация к темноте, J цвета выше → depth_term меньше → mud меньше. + /// In this partial model the two Yb+theme inputs produce different CAM16 J + /// and geometric depth values. No human clean/dirty direction is asserted. /// /// Направление дельты: mud_on_black < mud_on_white — строго. #[test] - fn dark_pink_is_cleaner_on_black_than_on_white() { + fn proxy_for_dark_pink_is_lower_with_black_input_than_white_input() { let dark_pink = "#C2185B"; // тёмно-розовый (Material Design Pink 800) let black_bg = "#000000"; let white_bg = "#FFFFFF"; @@ -1165,19 +1015,18 @@ mod tests { assert!( mud_on_black < mud_on_white, - "Тёмный розовый должен быть чище (меньше грязи) на чёрном фоне, чем на белом: \ - mud_on_black={mud_on_black:.6} должно быть < mud_on_white={mud_on_white:.6}. \ - Если равны — surround-aware путь не работает." + "local-context proxy ordering changed: black={mud_on_black:.6} \ + must be < white={mud_on_white:.6}; equality means context no longer reaches the formula" ); } - /// Mutation-bite: подмена l_app на фиксированное значение (убираем surround-aware путь) + /// Mutation-bite: replacing l_app with a fixed value removes local-context sensitivity /// должна РОНЯТЬ первый кейс-тест. Проверяем здесь что тест различает два значения. /// /// Реализация: вычисляем mud дважды — с пастельным и нейтральным фоном. /// Разница строго ненулевая → epsilon-тест подтверждает укус. #[test] - fn surround_aware_mud_differs_across_backgrounds_epsilon() { + fn compatibility_proxy_differs_across_local_background_inputs() { let grey = "#808080"; let pastel_bg = "#FFE4E1"; let neutral_bg = "#808080"; @@ -1202,19 +1051,20 @@ mod tests { // Разница должна быть не менее 1e-4 — иначе тест не кусается assert!( (mud_pastel - mud_neutral).abs() > 1e-4, - "surround-aware mud разница < 1e-4: pastel={mud_pastel:.8} neutral={mud_neutral:.8} \ + "local-context proxy delta < 1e-4: pastel={mud_pastel:.8} neutral={mud_neutral:.8} \ delta={:.2e} — тест не кусается (mutation-bite провален).", (mud_pastel - mud_neutral).abs() ); } - /// API-test: drab_in_context принимает контекст и возвращает то же значение, что drab(C). + /// API-test: `drab_in_context` preserves the context-independent legacy coordinate. /// - /// Тест закрывает класс: drab не должен молча игнорировать контекст и возвращать ноль. + /// The boundary accepts context for compatibility, ignores it explicitly, and must not + /// fabricate zero instead of returning the historical arithmetic result. #[test] fn drab_in_context_matches_bare_drab() { use super::n_pure; - let hex = "#937C00"; // babypoop — умеренная хрома + let hex = "#937C00"; // frozen conformance fixture let ctx_light = DefectContext { bg_hex: "#FFFFFF", @@ -1243,21 +1093,20 @@ mod tests { assert_eq!( d_light + n_pure(c_oklab), 1.0, - "drab_in_context(babypoop) + n_pure должно быть ровно 1.0 (D+N=1 инвариант)" + "drab_in_context({hex}) + n_pure должно быть ровно 1.0 (D+N=1 инвариант)" ); - // babypoop (#937C00) — сильно хроматический цвет: C >> C0=0.0395, - // поэтому drab(C) = 1 - N_pure(C) ≈ 0 (т.е. < 0.1 — почти нет тусклости). + // For this fixture C >> C0, so the frozen arithmetic complement is < 0.1. assert!( d_light < 0.1, - "drab_in_context(babypoop) = {d_light:.6} ожидается < 0.1 \ - (babypoop сильно хроматичен, drab → 0)" + "drab_in_context({hex}) = {d_light:.6} ожидается < 0.1 \ + (fixture C >> C0, so the compatibility coordinate approaches 0)" ); } /// IC-тема компилируется и возвращает корректный результат. #[test] fn ic_themes_compile_and_return_finite_value() { - let hex = "#6B6B2E"; // olive + let hex = "#6B6B2E"; // frozen conformance fixture let ctx_lic = DefectContext { bg_hex: "#FFFFFF", theme: Theme::LightIc, diff --git a/crates/labcolors-core/src/config.rs b/crates/labcolors-core/src/config.rs index c543c8b5..4a6a705f 100644 --- a/crates/labcolors-core/src/config.rs +++ b/crates/labcolors-core/src/config.rs @@ -601,22 +601,31 @@ pub enum RoleRecipe { Zero, } -/// Суффиксы CSS-переменных, которые один успешно решённый рецепт резервирует. +/// Суффиксы CSS-переменных, которые один объявленный рецепт резервирует. /// -/// Namespace выводится из типа outcome до резолва: иначе коллизия могла бы -/// зависеть от фона (на одном роль unreachable и «всё работает», на другом два -/// писателя молча делят один `--lab-*`). `Zero` переменных не эмитит никогда. -fn emitted_css_suffixes(recipe: &RoleRecipe) -> &'static [&'static str] { +/// Основное имя принадлежит клиентскому токену даже тогда, когда `Zero` не +/// эмитит значения: projection всё равно публикует его `cssVar`, и сателлит +/// другой роли не вправе занять это имя. Остальной shape выводится из рецепта +/// до резолва, иначе коллизия могла бы зависеть от фона (на одном роль +/// unreachable и «всё работает», на другом два писателя молча делят один +/// `--lab-*`). Исчерпывающий match заставляет каждый новый рецепт явно выбрать +/// свой namespace shape. +fn reserved_css_suffixes(recipe: &RoleRecipe) -> &'static [&'static str] { const PRIMARY: &[&str] = &[""]; const GLOW: &[&str] = &["", "-core", "-alpha"]; const MATERIAL: &[&str] = &["", "-01", "-02"]; - const NONE: &[&str] = &[]; match recipe { RoleRecipe::Glow { .. } => GLOW, RoleRecipe::Material { .. } => MATERIAL, - RoleRecipe::Zero => NONE, - _ => PRIMARY, + RoleRecipe::TextAnchor { .. } + | RoleRecipe::DjAnchor { .. } + | RoleRecipe::DecorativeLc { .. } + | RoleRecipe::Ladder { .. } + | RoleRecipe::PairFill { .. } + | RoleRecipe::PairLabel { .. } + | RoleRecipe::AlphaAnalog { .. } + | RoleRecipe::Zero => PRIMARY, } } @@ -625,16 +634,16 @@ fn emitted_css_suffixes(recipe: &RoleRecipe) -> &'static [&'static str] { /// Отдельный примитив не знает сегодняшних суффиксов и потому не опирается на /// случайное свойство, что `-core/-alpha/-01/-02` пока не пересекаются друг с /// другом: будущая derived↔derived коллизия попадёт в тот же гард. -fn reserve_emitted_css_names( - emitted: &mut std::collections::BTreeSet, +fn reserve_css_names( + reserved: &mut std::collections::BTreeSet, name: &str, suffixes: &[&str], ) -> Result<(), ConfigError> { for suffix in suffixes { let key = format!("--lab-{name}{suffix}"); - if !emitted.insert(key.clone()) { + if !reserved.insert(key.clone()) { return Err(ConfigError::DuplicateKey { - dictionary: "emitted CSS variables", + dictionary: "reserved CSS namespace", key, }); } @@ -1069,10 +1078,10 @@ impl ThemeConfig { // резолва/JSON, чтобы порядок писателей никогда не решал, чьё значение // молча победит. Один общий set ловит role↔satellite, alias↔satellite и // любые будущие satellite↔satellite пересечения без списка частных пар. - let mut emitted = std::collections::BTreeSet::new(); + let mut reserved = std::collections::BTreeSet::new(); for (name, recipe) in &self.roles { - reserve_emitted_css_names(&mut emitted, name, emitted_css_suffixes(recipe))?; + reserve_css_names(&mut reserved, name, reserved_css_suffixes(recipe))?; } for (alias, target) in &self.aliases { let target_recipe = self @@ -1084,7 +1093,7 @@ impl ThemeConfig { referenced_by: format!("aliases.{alias}"), role: target.clone(), })?; - reserve_emitted_css_names(&mut emitted, alias, emitted_css_suffixes(target_recipe))?; + reserve_css_names(&mut reserved, alias, reserved_css_suffixes(target_recipe))?; } Ok(()) @@ -1676,5 +1685,9 @@ pub(crate) mod preset; #[cfg(test)] pub(crate) mod fixture; +/// Общая строковая форма `Resolved` для in-crate characterization/golden-тестов. +#[cfg(test)] +pub(crate) mod test_support; + #[cfg(test)] mod tests; diff --git a/crates/labcolors-core/src/config/preset.rs b/crates/labcolors-core/src/config/preset.rs index 00785b88..e523004c 100644 --- a/crates/labcolors-core/src/config/preset.rs +++ b/crates/labcolors-core/src/config/preset.rs @@ -251,6 +251,13 @@ pub fn labui_preset_roles() -> Vec<(String, RoleRecipe)> { // (зеркало fx-shadow-ambient) на фактическом фоне. Прежние Ladder@52 // (фикс-альфа, нормальная композиция) вырождались на одноимённых фонах; // physics свечения — добавление света, не наложение краски. + // Этот characterization-пресет явно выбирает legacy-профиль только ради + // совместимости с frozen byte-контрактом прежней glow-эмиссии; сам legacy- + // профиль не превращается от этого в cross-runtime гарантию. `StableV1` без + // sound CAM16 bound честно вернул бы `GlowIndeterminate` для каждого + // нетривиального свечения и тем самым намеренно изменил бы frozen output; + // implicit fallback здесь отсутствует, а новые клиентские схемы обязаны + // выбрать профиль сами. let glow = |source: LadderSource| RoleRecipe::Glow { source, step: crate::glow::GlowStep::Base, diff --git a/crates/labcolors-core/src/config/test_support.rs b/crates/labcolors-core/src/config/test_support.rs new file mode 100644 index 00000000..2b9b1237 --- /dev/null +++ b/crates/labcolors-core/src/config/test_support.rs @@ -0,0 +1,19 @@ +//! Общие helpers тестовой конфигурационной поверхности. +//! +//! Модуль существует только под `cfg(test)`: строковая форма ниже — оракул +//! characterization/golden-тестов, а не второй публичный формат эмиссии. + +use crate::Resolved; + +/// Каноническая строковая форма решённой роли для in-crate golden-гейтов. +pub(crate) fn resolved_repr(res: &Resolved) -> String { + match res { + Resolved::Color { solved, .. } => solved.hex().to_string(), + Resolved::Translucent(r) => format!("rgba({},{})", r.tint_hex(), r.alpha()), + Resolved::Glow(g) => format!("glow({},{},{:.4})", g.core_hex(), g.halo_hex(), g.alpha()), + Resolved::GlowIndeterminate(_) => "GLOW_INDETERMINATE".to_string(), + Resolved::Material(m) => format!("material({},{:.4})", m.tint_hex(), m.alpha()), + Resolved::None => "none".to_string(), + Resolved::Unreachable(_) => "UNREACHABLE".to_string(), + } +} diff --git a/crates/labcolors-core/src/config/tests.rs b/crates/labcolors-core/src/config/tests.rs index 19a96803..4e5ffb9d 100644 --- a/crates/labcolors-core/src/config/tests.rs +++ b/crates/labcolors-core/src/config/tests.rs @@ -9,6 +9,7 @@ //! сверка со стабом labui (light+dark) + RED-proof мутаций. use super::fixture::labui_reference; +use super::test_support::resolved_repr as repr; use super::*; use crate::ladder::LadderPosition; use crate::solve::Floor; @@ -30,22 +31,6 @@ fn grid() -> ([(ViewingConditions, &'static str); 2], [&'static str; 6]) { ) } -/// Hex/none/UNREACHABLE-представление резолва — как в golden-тесте ядра. -fn repr(res: &Resolved) -> String { - match res { - Resolved::Color { solved, .. } => solved.hex().to_string(), - // полупрозрачная роль: тинт + фактическая альфа — то, что эмитится `--lab-*`. - Resolved::Translucent(r) => format!("rgba({},{})", r.tint_hex(), r.alpha()), - // Свечение: слои + α — стабильное представление для голденов. - Resolved::Glow(g) => format!("glow({},{},{:.4})", g.core_hex(), g.halo_hex(), g.alpha()), - Resolved::GlowIndeterminate(_) => "GLOW_INDETERMINATE".to_string(), - // Материал: тон + выведенная α — стабильное представление. - Resolved::Material(m) => format!("material({},{:.4})", m.tint_hex(), m.alpha()), - Resolved::None => "none".to_string(), - Resolved::Unreachable(_) => "UNREACHABLE".to_string(), - } -} - /// Собрать карту `role.key() -> hex` из дефолтной таблицы для (bg, vc). fn default_by_key(bg: &BgInput, vc: &ViewingConditions) -> Vec<(&'static str, String)> { resolve_set(bg, &RoleTable::default(), vc) @@ -1590,7 +1575,7 @@ fn validator_rejects_role_and_alias_collisions_with_emitted_satellites() { matches!( cfg.validate(), Err(ConfigError::DuplicateKey { - dictionary: "emitted CSS variables", + dictionary: "reserved CSS namespace", key, }) if key == expected ), @@ -1686,22 +1671,74 @@ fn validator_rejects_role_and_alias_collisions_with_emitted_satellites() { } } +/// `Zero` не эмитит значение, но его клиентское имя всё равно занято: иначе +/// сателлит другой роли мог бы записать цвет в `cssVar` токена с `kind: "none"`. +/// Закон одинаков для явной zero-роли и алиаса на неё, а также для каждого +/// многоключевого shape, известного core. +#[test] +fn validator_reserves_zero_role_and_alias_primary_names() { + let assert_collision = |cfg: ThemeConfig, expected_stem: &str| { + let expected = format!("--lab-{expected_stem}"); + assert!( + matches!( + cfg.validate(), + Err(ConfigError::DuplicateKey { + dictionary: "reserved CSS namespace", + key, + }) if key == expected + ), + "zero-токен обязан защищать зарезервированный CSS key {expected}" + ); + }; + + let glow_recipe = labui_reference() + .roles + .into_iter() + .find(|(name, _)| name == "fx-glow-brand") + .expect("фикстура несёт fx-glow-brand") + .1; + + for (owner, recipe, suffixes) in [ + ("probe-glow", glow_recipe, &["-core", "-alpha"][..]), + ( + "probe-material", + neutral_material(10.0, Floor::AaText), + &["-01", "-02"][..], + ), + ] { + for suffix in suffixes { + let zero_name = format!("{owner}{suffix}"); + + let mut role_cfg = labui_reference(); + role_cfg.roles.push((owner.to_string(), recipe.clone())); + role_cfg.roles.push((zero_name.clone(), RoleRecipe::Zero)); + assert_collision(role_cfg, &zero_name); + + let mut alias_cfg = labui_reference(); + alias_cfg.roles.push((owner.to_string(), recipe.clone())); + alias_cfg + .aliases + .push((zero_name.clone(), "none".to_string())); + assert_collision(alias_cfg, &zero_name); + } + } +} + /// Сегодняшние суффиксы не пересекаются друг с другом, но сам примитив /// namespace не должен полагаться на это случайное свойство. Синтетические /// shapes доказывают derived↔derived ветвь: два разных владельца строят один /// итоговый ключ, и второй резерв немедленно падает. #[test] fn emitted_namespace_primitive_rejects_satellite_to_satellite_collision() { - let mut emitted = std::collections::BTreeSet::new(); - reserve_emitted_css_names(&mut emitted, "probe", &["-outer-inner"]) - .expect("первый сателлит свободен"); - let error = reserve_emitted_css_names(&mut emitted, "probe-outer", &["-inner"]) + let mut reserved = std::collections::BTreeSet::new(); + reserve_css_names(&mut reserved, "probe", &["-outer-inner"]).expect("первый сателлит свободен"); + let error = reserve_css_names(&mut reserved, "probe-outer", &["-inner"]) .expect_err("derived↔derived коллизия обязана быть отвергнута"); assert!(matches!( error, ConfigError::DuplicateKey { - dictionary: "emitted CSS variables", + dictionary: "reserved CSS namespace", key, } if key == "--lab-probe-outer-inner" )); @@ -2383,8 +2420,9 @@ fn material_two_layer_solid_canon_byte_exact_and_guaranteed() { assert!((m.floor() - 4.5).abs() < 1e-12, "AA-text пол = 4.5"); } -/// Гарантия читаемости пересчитываема потребителем из эмитированных `01`/`02`: -/// композит квантованного тинта над чёрным/белым точно даёт худший контраст. +/// Гарантия читаемости пересчитываема из эмитированных `01`/`02`: public core +/// recheck побитно совпадает с сохранённым conservative verdict, а независимые +/// byte-scale consumer probes не опускаются ниже него. #[test] fn material_guarantee_recomputable_over_worst_backdrop() { let res = material_on_white(15.0); @@ -2392,29 +2430,47 @@ fn material_guarantee_recomputable_over_worst_backdrop() { panic!("ожидался Material"); }; let tint = crate::spaces::srgb::srgb_encoded_from_hex(m.tint_hex()).unwrap(); - // ЭКСАКТНЫЙ композит квантованного тинта (как ядро и потребитель), без - // переквантования — над двумя углами полного коридора. - let over_black = crate::alpha::composite_over_encoded(tint, m.alpha(), [0.0; 3]) - .expect("эмитированный материал лежит в домене композитора"); - let over_white = crate::alpha::composite_over_encoded(tint, m.alpha(), [1.0; 3]) - .expect("эмитированный материал лежит в домене композитора"); + let recomputed = crate::material::worst_contrast_encoded( + tint, + m.alpha(), + &crate::material::BackdropBox::FULL, + m.pole(), + ) + .unwrap(); + assert_eq!(recomputed.to_bits(), m.worst_contrast().to_bits()); + + // Independent official scalar order. The old version of this test called + // alpha::composite_over_encoded, which is the normalized-expanded profile + // and therefore could not prove material consumer parity. let pole_lum = if matches!(m.pole(), crate::material::Pole::White) { 1.0 } else { 0.0 }; - let recomputed = - crate::wcag::ratio_from_luminances(pole_lum, crate::wcag::relative_luminance(over_black)) - .min(crate::wcag::ratio_from_luminances( - pole_lum, - crate::wcag::relative_luminance(over_white), - )); + let probes = [0.0, 0.039_28, 0.039_280_000_000_000_01, 0.5, 1.0]; + let mut measured_min = f64::INFINITY; + for red in probes { + for green in probes { + for blue in probes { + let background = [red, green, blue]; + let composite = core::array::from_fn(|channel| { + let tint_byte = (tint[channel] * 255.0).round(); + let background_byte_scale = background[channel] * 255.0; + (background_byte_scale + m.alpha() * (tint_byte - background_byte_scale)) + / 255.0 + }); + measured_min = measured_min.min(crate::wcag::ratio_from_luminances( + pole_lum, + crate::wcag::relative_luminance(composite), + )); + } + } + } + assert!(m.worst_contrast() <= measured_min); assert!( - (recomputed - m.worst_contrast()).abs() < 1e-9, - "пересчёт {recomputed} != вердикту {}", - m.worst_contrast() + m.worst_contrast() >= 4.5, + "conservative verdict ниже AA-пола" ); - assert!(recomputed >= 4.5 - 1e-9, "пересчёт ниже AA-пола"); } /// Нейтральный материал БАЙТ-в-байт переиспользует тон dj-anchor (та же физика diff --git a/crates/labcolors-core/src/glow.rs b/crates/labcolors-core/src/glow.rs index 792c355f..8990cccc 100644 --- a/crates/labcolors-core/src/glow.rs +++ b/crates/labcolors-core/src/glow.rs @@ -1,8 +1,8 @@ -//! Конечный point-reference примитив для screen-рецепта glow. +//! Конечный точечный reference-примитив для screen-рецепта glow. //! -//! Модуль рассчитывает encoded-композит двух слоёв. Он не моделирует -//! физическое излучение, blur-поле или восприятие полного пространственного -//! эффекта: эти утверждения требуют render/context-контракта #221. +//! Модуль рассчитывает encoded-композит двух слоёв. Он не моделирует физическое +//! излучение, поле размытия или восприятие полного пространственного эффекта: +//! эти утверждения требуют контракта рендеринга и контекста #221. //! //! # Модель //! @@ -22,30 +22,31 @@ //! - **монотонно и ЛИНЕЙНО по α** на канал. Из этого НЕ следует монотонность //! CAM16 J': солвер поэтому перебирает конечное множество реально эмитируемых //! sRGB8-композитов, а не предполагает форму перцептивной функции; -//! - на белом (1−bg)=0 слой является point-no-op именно по формуле screen; +//! - на белом (1−bg)=0 слой является точечным no-op именно по формуле screen; //! это численное свойство reference-профиля, не утверждение о физическом -//! свете или неизвестном renderer. +//! свете или неизвестном рендерере. //! -//! # Lab UI preset ступеней +//! # Ступени пресета Lab UI //! -//! Текущие |ΔJ′| взяты из point-композитов клиентского стека теней Lab UI: +//! Текущие |ΔJ′| взяты из точечных композитов клиентского стека теней Lab UI: //! `subtle:=minor`, `base:=ambient`, `bloom:=major`. Это воспроизводимая -//! история preset, но не универсальный psychophysical law и не taxonomy -//! generic core; перенос в client profile отслеживается #221. +//! история пресета, но не универсальный психофизический закон и не таксономия +//! обобщённого core; перенос в клиентский профиль отслеживается #221. //! //! # Анатомия (двухслойный bloom) //! //! В текущем клиентском recipe core — цвет с поднятой к белому светлотой, halo -//! — исходный цвет. Радиусы и blur здесь отсутствуют: названия обозначают +//! — исходный цвет. Радиусы и размытие здесь отсутствуют: названия обозначают //! назначение слоёв у потребителя, а не измеренную геометрию. //! -//! Для хроматического источника core строится существующей policy -//! [`crate::accent_balance`]: midpoint J′ задаёт seed Oklab-светлоты, chroma -//! берётся на sRGB-границе данного hue. Точный sRGB8-нейтраль остаётся -//! нейтральным: у него нет hue, который можно было бы честно «усилить». +//! Для хроматического источника core строится существующей политикой +//! [`crate::accent_balance`]: midpoint J′ задаёт начальную Oklab-светлоту, chroma +//! берётся на sRGB-границе данного hue. Точная sRGB8-нейтраль остаётся +//! нейтральной: у неё нет hue, который можно было бы честно «усилить». //! После хроматического преобразования и sRGB8-квантования итоговый J′ не -//! объявляется равным seed — фактическая point-метрика core возвращается -//! отдельно. Это versioned recipe, а не оптимум, проверенный на наблюдателях. +//! объявляется равным начальному значению — фактическая точечная метрика core +//! возвращается отдельно. Это версионированный recipe, а не оптимум, +//! проверенный на наблюдателях. use crate::lcs::LcsColor; use crate::numerics::{ @@ -56,7 +57,6 @@ use crate::spaces::srgb::{ decode_8bit, hex_from_srgb, hex_from_srgb_encoded, srgb_encoded_from_hex, srgb_to_xyz, }; use crate::spaces::vc::ViewingConditions; -use std::cmp::Ordering; /// Контрактный шаг glow-subtle (|ΔJ'| композита от фона): зеркало /// стек-композита fx-shadow-minor на светлом якоре labui. @@ -69,26 +69,34 @@ pub const GLOW_BASE_DJ: f64 = 2.3006; // SSOT-TRACKED — зеркальная деривация от владельческих альф теней. pub const GLOW_BLOOM_DJ: f64 = 13.3251; -/// Версия конечного reference-домена exact point-композита glow. +/// Версия конечного reference-домена точного точечного композита glow. /// -/// Это не сертификат конкретного браузера, дисплея, CAM16 decision или +/// Это не сертификат конкретного браузера, дисплея или решения CAM16 и не /// пространственного поля: профиль фиксирует только encoded sRGB8 + screen. pub const GLOW_COMPOSITE_PROFILE: &str = "encoded-srgb8-screen-v1"; -/// Diagnostic appearance model для legacy target/max path. Он не является -/// numerical decision guarantee и не повышает composite certificate. +/// Диагностическая appearance-модель, используемая legacy-выбором и замерами +/// полного двухслойного результата Glow. Она не является гарантией численного +/// решения и не усиливает сертификат композита. pub const GLOW_DIAGNOSTIC_PROFILE: &str = "cam16-ucs-jprime-li2017-v1"; -/// Typed identity exact compositing profile. +/// Версия recipe, строящего core-слой из начального CAM16-UCS J′ и Oklab cusp. +/// +/// Профиль идентифицирует только алгоритм построения слоя. Он не является +/// гарантией выбора, appearance-сертификатом или утверждением о пространстве +/// либо браузере. +pub const GLOW_LAYER_RECIPE_PROFILE: &str = "cam16-jprime-oklab-cusp-v1"; + +/// Типизированный идентификатор профиля точного композитинга. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum GlowCompositeProfileV1 { - /// Encoded sRGB8 point screen-composite. + /// Точечный screen-композит в encoded sRGB8. EncodedSrgb8ScreenV1, } impl GlowCompositeProfileV1 { - /// Стабильный wire/certificate key. + /// Стабильный wire-ключ сертификата. pub fn key(self) -> &'static str { match self { Self::EncodedSrgb8ScreenV1 => GLOW_COMPOSITE_PROFILE, @@ -96,16 +104,16 @@ impl GlowCompositeProfileV1 { } } -/// Guarantee exact compositing certificate, независимо от appearance decision. +/// Гарантия точного сертификата композитинга, независимая от appearance-решения. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum GlowCompositeGuaranteeV1 { - /// Integer/rational boundary + emitted binary64 alpha identity. + /// Фактический encoded-sRGB8 state и идентичность эмитированной binary64 alpha. BitExact, } impl GlowCompositeGuaranteeV1 { - /// Стабильный wire key. + /// Стабильный wire-ключ. pub fn key(self) -> &'static str { match self { Self::BitExact => "bit-exact", @@ -113,7 +121,7 @@ impl GlowCompositeGuaranteeV1 { } } -/// Appearance diagnostic identity. Значение не является decision guarantee. +/// Идентификатор appearance-диагностики. Значение не является гарантией решения. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum GlowDiagnosticProfileV1 { @@ -122,7 +130,7 @@ pub enum GlowDiagnosticProfileV1 { } impl GlowDiagnosticProfileV1 { - /// Стабильный wire key. + /// Стабильный wire-ключ. pub fn key(self) -> &'static str { match self { Self::Cam16UcsJPrimeLi2017V1 => GLOW_DIAGNOSTIC_PROFILE, @@ -130,27 +138,46 @@ impl GlowDiagnosticProfileV1 { } } -/// Явно выбранный профиль numerical decision. `Default` намеренно отсутствует: -/// legacy path никогда не должен появляться через fallback. +/// Типизированный идентификатор текущего двухслойного recipe Glow. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum GlowLayerRecipeProfileV1 { + /// Начальный midpoint CAM16-UCS J′, переведённый в Oklab lightness; + /// хроматический core берёт ограниченную cusp chroma текущего Oklab hue, + /// halo равен источнику. + Cam16JPrimeOklabCuspV1, +} + +impl GlowLayerRecipeProfileV1 { + /// Стабильный wire-ключ recipe. + pub const fn key(self) -> &'static str { + match self { + Self::Cam16JPrimeOklabCuspV1 => GLOW_LAYER_RECIPE_PROFILE, + } + } +} + +/// Явно выбранный профиль численного решения. `Default` намеренно отсутствует: +/// legacy-путь никогда не должен появляться через fallback. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum GlowDecisionProfileV1 { - /// Stable contract: без sound bound semantic branch не выбирается. + /// Стабильный контракт: без sound bound семантическая ветвь не выбирается. StableV1, - /// Прежний CAM16/libm-dependent selection, сохранённый только явно. + /// Прежний зависящий от CAM16/libm выбор, сохранённый только явно. LegacyPlatformDependentV1, } impl GlowDecisionProfileV1 { - /// Стабильный config/wire key. - pub fn key(self) -> &'static str { + /// Стабильный config/wire-ключ. + pub const fn key(self) -> &'static str { match self { Self::StableV1 => "stable-v1", Self::LegacyPlatformDependentV1 => "legacy-platform-dependent-v1", } } - /// Разбирает обязательный config key; неизвестный профиль не нормализуется. + /// Разбирает обязательный config-ключ; неизвестный профиль не нормализуется. pub fn parse(raw: &str) -> Result { match raw { "stable-v1" => Ok(Self::StableV1), @@ -160,7 +187,7 @@ impl GlowDecisionProfileV1 { } } -/// Слой, по которому point-солвер держит целевой ΔJ′. +/// Слой, по которому точечный солвер держит целевой ΔJ′. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum GlowConstraintLayer { @@ -177,23 +204,27 @@ impl GlowConstraintLayer { } } -/// Итог проверки target по конечному reference-домену. +/// Итог проверки цели по конечному reference-домену. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum GlowTargetStatus { - /// Найден первый по интенсивности sRGB8-state, держащий цель. - Reached, - /// Цель не держит ни один state; возвращён глобальный максимум |ΔJ′|, а - /// при точном равенстве максимумов — первый state по alpha. - Unreachable, + /// Стабильный точный no-op: все alpha дают байтовое состояние фона, поэтому + /// положительная цель точно недостижима без appearance-выбора. + ExactNoopUnreachable, + /// Явный legacy-выбор CAM16/libm нашёл первое состояние, держащее цель. + LegacyReached, + /// Явный legacy-выбор CAM16/libm не нашёл цель и вернул максимум |ΔJ′|, + /// а при равенстве максимумов — первое состояние по alpha. + LegacyUnreachable, } impl GlowTargetStatus { /// Стабильный wire-ключ. pub fn key(self) -> &'static str { match self { - Self::Reached => "reached", - Self::Unreachable => "unreachable", + Self::ExactNoopUnreachable => "exact-noop-unreachable", + Self::LegacyReached => "legacy-reached", + Self::LegacyUnreachable => "legacy-unreachable", } } } @@ -210,7 +241,7 @@ pub enum GlowStep { } impl GlowStep { - /// Целевой модуль ΔJ′ CAM16-UCS изолированного halo point-композита. + /// Целевой модуль ΔJ′ CAM16-UCS изолированного точечного halo-композита. pub fn target_dj(self) -> f64 { match self { GlowStep::Subtle => GLOW_SUBTLE_DJ, @@ -256,7 +287,7 @@ fn validate_encoded_rgb(label: &str, rgb: [f64; 3]) -> Result<(), String> { fn validate_viewing_numerics(vc: &ViewingConditions) -> Result<(), String> { // Это только числовая защита CAM16-пути. Она не доказывает полноту или - // физическую применимость viewing context: такая граница принадлежит #230. + // физическую применимость контекста просмотра: такая граница принадлежит #230. for (name, value) in [ ("n", vc.n), ("aw", vc.aw), @@ -317,8 +348,8 @@ fn validate_lcs_numerics(label: &str, color: &LcsColor) -> Result<(), String> { } /// Только J′ для горячего пути конечного солвера. Oklab-hue и M′ здесь не -/// участвуют в objective, поэтому полный `LcsColor` был бы лишней работой на -/// каждом из сотен sRGB8-state. Формула J′ остаётся тем же SSOT из `cam16`. +/// участвуют в целевой функции, поэтому полный `LcsColor` был бы лишней работой +/// на каждом из сотен состояний sRGB8. Формула J′ остаётся тем же SSOT из `cam16`. fn jp_from_srgb8(bytes: [u8; 3], vc: &ViewingConditions) -> Result { let rgb = bytes.map(decode_8bit); let (j, _, _) = crate::spaces::cam16::forward(srgb_to_xyz(rgb), vc); @@ -339,7 +370,7 @@ fn jp_from_hex(hex: &str, vc: &ViewingConditions) -> Result { } /// Непрерывный screen-слой над непрозрачным фоном: `bg + α·G·(1−bg)` -/// покомпонентно в encoded sRGB. Это алгебра до финального sRGB8-round; для +/// покомпонентно в encoded sRGB. Это алгебра до финального округления sRGB8; для /// эмитируемого reference-пикселя используй [`screen_layer_over_srgb8`]. /// /// # Errors @@ -364,30 +395,33 @@ pub fn screen_layer_over_encoded( ]) } -/// Screen-слой в конечном encoded-sRGB8 reference-домене. +/// Screen-слой в конечном reference-домене encoded-sRGB8. /// /// Формула вычисляется прямо в шкале байтов и округляется ровно один раз: /// `round(bg + α·glow·(255−bg)/255)`. Указанный слева направо порядок /// binary64-операций является частью reference-профиля и совпадает с JS- /// проверкой официального пакета. Нормализация `byte/255` перед обратным -/// умножением способна сдвинуть half-tie на соседний LSB. +/// умножением способна сдвинуть граничное значение половинного округления на +/// соседний LSB. /// /// # Errors /// /// `Err`, если `alpha` не конечна или лежит вне `[0,1]`. +fn screen_channel_over_srgb8(glow: u8, alpha: f64, bg: u8) -> u8 { + (f64::from(bg) + alpha * f64::from(glow) * f64::from(u8::MAX - bg) / f64::from(u8::MAX)).round() + as u8 +} + pub fn screen_layer_over_srgb8(glow: [u8; 3], alpha: f64, bg: [u8; 3]) -> Result<[u8; 3], String> { if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) { return Err(format!("alpha вне конечного [0,1]: {alpha}")); } - let channel = |index: usize| { - (f64::from(bg[index]) - + alpha * f64::from(glow[index]) * f64::from(u8::MAX - bg[index]) / f64::from(u8::MAX)) - .round() as u8 - }; - Ok([channel(0), channel(1), channel(2)]) + Ok(core::array::from_fn(|channel| { + screen_channel_over_srgb8(glow[channel], alpha, bg[channel]) + })) } -/// Exact certificate одного изолированного encoded-sRGB8 screen-композита. +/// Точный сертификат одного изолированного screen-композита encoded-sRGB8. #[derive(Debug, Clone, PartialEq, Eq)] pub struct GlowCompositeCertificateV1 { tint_srgb8: [u8; 3], @@ -398,27 +432,27 @@ pub struct GlowCompositeCertificateV1 { } impl GlowCompositeCertificateV1 { - /// Reference profile exact арифметики. + /// Reference-профиль точной арифметики. pub fn profile(&self) -> GlowCompositeProfileV1 { GlowCompositeProfileV1::EncodedSrgb8ScreenV1 } - /// Exact guarantee не зависит от CAM16 diagnostic/selection. + /// Точная гарантия не зависит от диагностики или выбора CAM16. pub fn guarantee(&self) -> GlowCompositeGuaranteeV1 { GlowCompositeGuaranteeV1::BitExact } - /// Эмитируемый tint byte tuple. + /// Эмитируемая тройка байтов tint. pub fn tint_srgb8(&self) -> [u8; 3] { self.tint_srgb8 } - /// Background byte tuple reference-запроса. + /// Тройка байтов фона reference-запроса. pub fn background_srgb8(&self) -> [u8; 3] { self.background_srgb8 } - /// Точная binary64 identity alpha. + /// Точная binary64-идентичность alpha. pub fn alpha_bits(&self) -> u64 { self.alpha_bits } @@ -428,7 +462,7 @@ impl GlowCompositeCertificateV1 { &self.alpha_css } - /// Exact composite byte tuple. + /// Точная тройка байтов композита. pub fn composite_srgb8(&self) -> [u8; 3] { self.composite_srgb8 } @@ -450,7 +484,7 @@ fn composite_certificate( } } -/// Изолированный point-замер одного screen-слоя в reference-профиле glow. +/// Изолированный точечный замер одного screen-слоя в reference-профиле glow. /// Пространственное перекрытие core/halo сюда намеренно не входит: без /// геометрии и порядка слоёв его нельзя восстановить честно. pub(crate) struct ScreenLayerMeasurement { @@ -486,55 +520,41 @@ pub(crate) fn measure_screen_layer_at_alpha( }) } -/// Рациональная стенка квантования α. Целые здесь принципиальны: двоичная -/// аппроксимация десятичной границы не должна решать, какой байт существует. +const ALPHA_ZERO_BITS: u64 = 0.0_f64.to_bits(); +const ALPHA_ONE_BITS: u64 = 1.0_f64.to_bits(); +const MAX_QUANTISED_COMPOSITE_STATES: u16 = 766; + #[derive(Debug, Clone, Copy)] -struct AlphaWall { - numerator: u64, - denominator: u64, +struct QuantisedComposite { + bytes: [u8; 3], + lower_bits: u64, + upper_bits: u64, + upper_inclusive: bool, } -impl AlphaWall { - const ZERO: Self = Self { - numerator: 0, - denominator: 1, - }; - const ONE: Self = Self { - numerator: 1, - denominator: 1, - }; - - fn cmp(self, other: Self) -> Ordering { - (self.numerator * other.denominator).cmp(&(other.numerator * self.denominator)) - } - - fn same_value(self, other: Self) -> bool { - self.cmp(other) == Ordering::Equal - } - - fn as_f64(self) -> f64 { - self.numerator as f64 / self.denominator as f64 - } +impl QuantisedComposite { + /// Выбирает внутреннюю representable alpha около численного центра state. + /// Если midpoint округлился на исключённую верхнюю границу, берётся её + /// непосредственный predecessor; singleton при alpha=1 остаётся допустим. + fn canonical_alpha(self) -> f64 { + let lower = f64::from_bits(self.lower_bits); + let upper = f64::from_bits(self.upper_bits); + if self.lower_bits == self.upper_bits { + debug_assert!(self.upper_inclusive); + return lower; + } - /// Центр интервала максимизирует запас до обеих стенок. Это канонизация - /// представления, а не утверждение о перцептивно «лучшей» непрозрачности. - fn midpoint(self, other: Self) -> f64 { - if self.same_value(other) { - return self.as_f64(); + let midpoint = lower + (upper - lower) * 0.5; + let mut bits = midpoint.to_bits().max(self.lower_bits); + if self.upper_inclusive { + bits = bits.min(self.upper_bits); + } else if bits >= self.upper_bits { + bits = self.upper_bits - 1; } - let numerator = self.numerator * other.denominator + other.numerator * self.denominator; - let denominator = 2 * self.denominator * other.denominator; - numerator as f64 / denominator as f64 + f64::from_bits(bits) } } -#[derive(Debug, Clone, Copy)] -struct QuantisedComposite { - bytes: [u8; 3], - lower: AlphaWall, - upper: AlphaWall, -} - fn encoded_bytes(rgb: [f64; 3]) -> [u8; 3] { rgb.map(|channel| (channel * 255.0).round() as u8) } @@ -543,106 +563,185 @@ fn composite_hex(bytes: [u8; 3]) -> String { format!("#{:02X}{:02X}{:02X}", bytes[0], bytes[1], bytes[2]) } -/// Поток всех достижимых sRGB8-композитов без промежуточных коллекций. +/// Поток всех состояний, достижимых representable binary64-alpha в объявленном +/// operation order [`screen_layer_over_srgb8`]. /// -/// На каждом канале стенки строго возрастают, поэтому три уже упорядоченных -/// потока достаточно слить тремя курсорами. Общая сортировка не добавляет -/// математической информации, зато затягивает в WASM аллокатор и универсальную -/// сортировку. Сравнение и группировка остаются рациональными: решение о байте -/// никогда не зависит от погрешности binary64. +/// Алгебраически равные рациональные стенки нельзя склеивать: разные +/// факторизации `G·(255−B)` могут пересечь half-tie на разных `f64`. Поэтому +/// каждый channel transition находит первый passing binary64 точным lower-bound +/// по упорядоченным положительным битам. Каждый переход увеличивает хотя бы один +/// байт, значит состояний не больше `1 + 3·255 = 766`; память остаётся O(1). struct QuantisedComposites { + glow: [u8; 3], background: [u8; 3], - slopes: [u64; 3], - next_values: [u16; 3], + final_bytes: [u8; 3], bytes: [u8; 3], - lower: AlphaWall, + next_boundaries: [Option; 3], + lower_bits: u64, + emitted_states: u16, finished: bool, } +/// Binary64-округление рациональной half-wall, используемое только как seed +/// экспоненциального поиска. Фактическую границу всегда определяет compositor. +fn boundary_seed_bits(glow: u8, background: u8, next_value: u8) -> u64 { + let background = f64::from(background); + let numerator = (f64::from(next_value) - background - 0.5) * f64::from(u8::MAX); + let denominator = f64::from(glow) * (f64::from(u8::MAX) - background); + debug_assert!(denominator > 0.0); + (numerator / denominator).clamp(0.0, 1.0).to_bits() +} + impl QuantisedComposites { fn new(glow: [u8; 3], background: [u8; 3]) -> Self { - let slopes = core::array::from_fn(|channel| { - u64::from(glow[channel]) * (255 - u64::from(background[channel])) + let final_bytes = core::array::from_fn(|channel| { + screen_channel_over_srgb8(glow[channel], 1.0, background[channel]) }); - let next_values = background.map(|byte| u16::from(byte) + 1); - Self { + let mut stream = Self { + glow, background, - slopes, - next_values, + final_bytes, bytes: background, - lower: AlphaWall::ZERO, + next_boundaries: [None; 3], + lower_bits: ALPHA_ZERO_BITS, + emitted_states: 0, finished: false, + }; + for channel in 0..3 { + stream.next_boundaries[channel] = stream.next_boundary(channel); } + stream } - /// На канале `B + α·G·(255−B)/255` новый байт `v` начинается ровно на - /// `255·(2(v−B)−1) / (2·G·(255−B))`. Все множители ограничены 8 битами, - /// поэтому точное перекрёстное сравнение стенок помещается в `u64`. - fn next_wall(&self, channel: usize) -> Option { - let slope = self.slopes[channel]; - let value = self.next_values[channel]; - if slope == 0 || value > u16::from(u8::MAX) { + fn next_boundary(&self, channel: usize) -> Option { + let next_value = self.bytes[channel].checked_add(1)?; + if self.final_bytes[channel] < next_value { return None; } - let base = u64::from(self.background[channel]); - let delta = u64::from(value) - base; - let wall = AlphaWall { - numerator: 255 * (2 * delta - 1), - denominator: 2 * slope, + debug_assert!( + screen_channel_over_srgb8( + self.glow[channel], + f64::from_bits(self.lower_bits), + self.background[channel], + ) < next_value, + "текущий state уже пересёк искомую channel boundary" + ); + let passes = |bits| { + screen_channel_over_srgb8( + self.glow[channel], + f64::from_bits(bits), + self.background[channel], + ) >= next_value }; - (wall.cmp(AlphaWall::ONE) != Ordering::Greater).then_some(wall) + + // Рациональная half-wall служит только seed, а не определением state: + // фиксированный binary64 operation order может сдвинуть first-passing + // alpha относительно этого seed. Экспоненциальный поиск обязательно находит + // bracket между уже известными failing lower и passing 1, после чего + // обычный lower-bound остаётся точным независимо от качества seed. + let seed = boundary_seed_bits(self.glow[channel], self.background[channel], next_value) + .clamp(self.lower_bits.saturating_add(1), ALPHA_ONE_BITS); + + let (mut failing, mut passing); + if passes(seed) { + passing = seed; + let mut stride = 1_u64; + loop { + let probe = passing.saturating_sub(stride).max(self.lower_bits); + if !passes(probe) { + failing = probe; + break; + } + passing = probe; + stride = stride.saturating_mul(2); + } + } else { + failing = seed; + let mut stride = 1_u64; + loop { + let probe = failing.saturating_add(stride).min(ALPHA_ONE_BITS); + if passes(probe) { + passing = probe; + break; + } + failing = probe; + stride = stride.saturating_mul(2); + } + } + + while passing - failing > 1 { + let middle = failing + (passing - failing) / 2; + if passes(middle) { + passing = middle; + } else { + failing = middle; + } + } + Some(passing) + } + + fn emit(&mut self, state: QuantisedComposite) -> Result, String> { + self.emitted_states += 1; + if self.emitted_states > MAX_QUANTISED_COMPOSITE_STATES { + return Err(format!( + "binary64 Glow stream превысил доказанную границу {MAX_QUANTISED_COMPOSITE_STATES}" + )); + } + Ok(Some(state)) } /// Возвращает очередной интервал `[lower, upper)`; последний интервал - /// замкнут справа и заканчивается на `1`. Стенки с одинаковым рациональным - /// значением поглощаются вместе, как в прежней сортированной реализации. + /// замкнут справа и заканчивается на `1`. Вместе поглощаются только + /// transition с одним и тем же первым passing `f64`, а не одинаковые + /// rational real-number walls. fn next_state(&mut self) -> Result, String> { if self.finished { return Ok(None); } - let upper = (0..3) - .filter_map(|channel| self.next_wall(channel).map(|wall| (wall, channel))) - .min_by(|(left_wall, left_channel), (right_wall, right_channel)| { - left_wall - .cmp(*right_wall) - .then_with(|| left_channel.cmp(right_channel)) - }) - .map(|(wall, _)| wall); + let upper = self.next_boundaries.into_iter().flatten().min(); let Some(upper) = upper else { self.finished = true; - return Ok(Some(QuantisedComposite { + return self.emit(QuantisedComposite { bytes: self.bytes, - lower: self.lower, - upper: AlphaWall::ONE, - })); + lower_bits: self.lower_bits, + upper_bits: ALPHA_ONE_BITS, + upper_inclusive: true, + }); }; let state = QuantisedComposite { bytes: self.bytes, - lower: self.lower, - upper, + lower_bits: self.lower_bits, + upper_bits: upper, + upper_inclusive: false, }; - for channel in 0..3 { - if self - .next_wall(channel) - .is_some_and(|wall| wall.same_value(upper)) - { - self.bytes[channel] = self.bytes[channel].checked_add(1).ok_or_else(|| { - format!("внутренняя стенка переполнила sRGB8-канал {channel}") - })?; - self.next_values[channel] += 1; + let previous = self.bytes; + self.bytes = screen_layer_over_srgb8(self.glow, f64::from_bits(upper), self.background)?; + self.lower_bits = upper; + for (channel, previous_byte) in previous.into_iter().enumerate() { + if self.bytes[channel] < previous_byte { + return Err(format!( + "binary64 screen stream уменьшил канал {channel}: {} -> {}", + previous_byte, self.bytes[channel] + )); + } + if self.bytes[channel] > previous_byte { + self.next_boundaries[channel] = self.next_boundary(channel); + } else if self.next_boundaries[channel] == Some(upper) { + return Err(format!( + "channel {channel} не изменился на собственной first-passing boundary" + )); } } - self.lower = upper; - Ok(Some(state)) + self.emit(state) } } /// Тестовый сборщик сохраняет удобный независимый оракул, но `Vec` и сортировка -/// не входят в production-WASM. +/// не входят в production-сборку WASM. #[cfg(test)] fn quantised_composites( glow: [u8; 3], @@ -664,22 +763,22 @@ pub struct GlowSolve { /// численный запас до ближайшей границы квантования. alpha: f64, /// Каноническая CSS-запись той же alpha; хранится вместе с числом, чтобы - /// downstream не вводил собственную политику округления. + /// нижележащий потребитель не вводил собственную политику округления. alpha_css: String, - /// Запрошенный |ΔJ′| point-композита halo. + /// Запрошенный |ΔJ′| точечного композита halo. target_dj: f64, /// Фактический |ΔJ'| композита от фона, замерен на эмитируемом hex. achieved_dj: f64, /// Композит `screen(tint, α)` над фоном, `#RRGGBB`. composite_hex: String, - /// Exact compositing evidence, независимо от legacy appearance decision. + /// Точное свидетельство композитинга, независимое от legacy appearance-решения. composite_certificate: GlowCompositeCertificateV1, - /// Appearance diagnostic реально выполнялся либо exact decision обошёл его. - diagnostic_profile: Option, - /// Деградация: ни один достижимый sRGB8-композит не держит цель (например, - /// над белым фоном слой является point-no-op в этом reference-профиле); - /// возвращён глобальный максимум |ΔJ'| с честным флагом, НЕ ошибка и НЕ - /// молчание. + /// Appearance-диагностика, реально использованная только для выбора. + /// Точный no-op полностью её обходит. + selection_diagnostic_profile: Option, + /// Исход проверки цели с provenance. Точный no-op объявляет точную + /// недостижимость по одному байтовому состоянию; явный legacy-путь объявляет + /// достижение либо возвращает выбранный CAM16 максимум с legacy-маркером. status: GlowTargetStatus, } @@ -709,19 +808,23 @@ impl GlowSolve { &self.composite_hex } - /// Exact point-composite certificate выбранного state. + /// Точный сертификат точечного композита выбранного состояния. pub fn composite_certificate(&self) -> &GlowCompositeCertificateV1 { &self.composite_certificate } - /// Типизированный результат target-проверки. + /// Типизированный результат проверки цели. pub fn status(&self) -> GlowTargetStatus { self.status } - /// Геттер совместимости прежнего boolean-контракта. + /// Геттер совместимости прежнего булева контракта: цель недостижима как при + /// точном no-op, так и при явном legacy-исходе с максимумом. pub fn degraded(&self) -> bool { - self.status == GlowTargetStatus::Unreachable + matches!( + self.status, + GlowTargetStatus::ExactNoopUnreachable | GlowTargetStatus::LegacyUnreachable + ) } /// Слой, по которому решалась цель. @@ -729,14 +832,15 @@ impl GlowSolve { GlowConstraintLayer::Halo } - /// Exact composite profile; appearance diagnostic identity читается отдельно. + /// Точный профиль композита; идентификатор диагностики выбора читается отдельно. pub fn composite_profile(&self) -> GlowCompositeProfileV1 { self.composite_certificate.profile() } - /// Appearance diagnostic identity, только если этот solve реально его вызвал. - pub fn diagnostic_profile(&self) -> Option { - self.diagnostic_profile + /// Идентификатор диагностики выбора, только если точечный солвер действительно + /// вызвал эту модель для выбора target/max. + pub fn selection_diagnostic_profile(&self) -> Option { + self.selection_diagnostic_profile } } @@ -746,7 +850,7 @@ struct ScreenPointInputs { slopes: [u16; 3], } -/// Parse the exact encoded-sRGB8 point inputs and derive their channel slopes. +/// Разобрать точные точечные входы encoded-sRGB8 и вывести наклоны их каналов. fn screen_point_inputs(glow_tint_hex: &str, bg_hex: &str) -> Result { let glow_bytes = encoded_bytes(srgb_encoded_from_hex(glow_tint_hex)?); let bg_bytes = encoded_bytes(srgb_encoded_from_hex(bg_hex)?); @@ -761,41 +865,43 @@ fn screen_point_inputs(glow_tint_hex: &str, bg_hex: &str) -> Result bool { - // Screen is monotone in alpha. Therefore every alpha quantises to the - // background iff the alpha=1 endpoint remains strictly below the first - // half-LSB wall: G*(255-B)/255 < 1/2. + // Screen монотонен по alpha. Поэтому каждая alpha квантуется в фон тогда и + // только тогда, когда endpoint при alpha=1 остаётся строго ниже первой + // стенки половины LSB: G*(255-B)/255 < 1/2. slopes .into_iter() .all(|slope| 2 * u32::from(slope) < u32::from(u8::MAX)) } -/// Exact recheck predicate for the stable Glow point decision. +/// Предикат точной повторной проверки стабильного точечного решения Glow. /// -/// Returns `true` exactly when the monotone encoded-sRGB8 screen endpoint at -/// alpha=1 stays below the first half-LSB wall in every channel. This includes -/// zero slopes and non-zero sub-LSB slopes. No appearance model or epsilon -/// participates. +/// Возвращает `true` тогда и только тогда, когда монотонный screen-endpoint +/// encoded-sRGB8 при alpha=1 остаётся ниже первой стенки половины LSB в каждом +/// канале. Сюда входят нулевые наклоны и ненулевые наклоны меньше LSB. +/// Appearance-модель и epsilon в проверке не участвуют. /// /// # Errors /// -/// Returns `Err` for an invalid tint/background instead of normalising it. +/// Возвращает `Err` для невалидного tint или фона вместо их нормализации. pub fn screen_point_is_exact_noop(glow_tint_hex: &str, bg_hex: &str) -> Result { let inputs = screen_point_inputs(glow_tint_hex, bg_hex)?; Ok(slopes_are_exact_srgb8_noop(inputs.slopes)) } -/// Решить интенсивность screen-слоя с явным numerical-decision profile. +/// Решить интенсивность screen-слоя с явным профилем численного решения. /// -/// `StableV1` не вызывает CAM16 для non-trivial target/max site, пока для него -/// нет sound bound, и возвращает typed `Indeterminate`. Единственный exact -/// determinate stable case — point-no-op: все alpha дают тот же background, -/// поэтому `ΔJ′ = 0` следует из равенства byte-state без appearance math. -/// `LegacyPlatformDependentV1` сохраняет прежний runtime явно и помечает его -/// guarantee; implicit legacy wrapper отсутствует. +/// `StableV1` не вызывает CAM16 для нетривиального участка target/max, пока для +/// него нет sound bound, и возвращает типизированный `Indeterminate`. +/// Единственный точный определённый стабильный случай — точечный no-op: все +/// alpha дают тот же фон, поэтому `ΔJ′ = 0` следует из равенства байтовых +/// состояний без appearance-математики. `LegacyPlatformDependentV1` явно +/// сохраняет прежний runtime и помечает его гарантию; неявная legacy-обёртка +/// отсутствует. /// /// # Errors /// -/// `Err` — невалидный hex/target либо internal postcondition legacy path. +/// `Err` — невалидный hex или target либо нарушенное внутреннее постусловие +/// legacy-пути. pub fn solve_screen_alpha_for_dj( glow_tint_hex: &str, bg_hex: &str, @@ -822,7 +928,7 @@ pub fn solve_screen_alpha_for_dj( slopes, } = screen_point_inputs(glow_tint_hex, bg_hex)?; if slopes_are_exact_srgb8_noop(slopes) { - let alpha = AlphaWall::ZERO.midpoint(AlphaWall::ONE); + let alpha = 0.5; let alpha_css = crate::css_alpha_value(alpha)?; let composite_srgb8 = bg_bytes; return Ok(NumericalDecisionV1::Determinate { @@ -839,8 +945,8 @@ pub fn solve_screen_alpha_for_dj( alpha_css, composite_srgb8, ), - diagnostic_profile: None, - status: GlowTargetStatus::Unreachable, + selection_diagnostic_profile: None, + status: GlowTargetStatus::ExactNoopUnreachable, }, guarantee: DecisionGuaranteeV1::BitExact, }); @@ -852,13 +958,14 @@ pub fn solve_screen_alpha_for_dj( }) } -/// Explicit legacy CAM16/libm-dependent target/max path. +/// Явный зависящий от CAM16/libm legacy-путь target/max. /// -/// Канальные переходы screen выводятся как рациональные стенки округления до -/// sRGB8. Солвер проверяет все достижимые композиты в порядке α и потому не -/// предполагает монотонность CAM16 J'. Если цель недостижима, возвращается -/// глобальный максимум |ΔJ'| среди достижимых состояний; равные максимумы -/// детерминированно разрешаются первым state по alpha. +/// Канальные переходы screen находятся как first-passing binary64 границы того +/// же operation order, который исполняет публичный compositor. Солвер проверяет +/// все достижимые композиты в порядке α и потому не предполагает монотонность +/// CAM16 J'. Если цель недостижима, возвращается глобальный максимум |ΔJ'| среди +/// достижимых состояний; равные максимумы детерминированно разрешаются первым +/// состоянием по alpha. /// /// # Errors /// @@ -913,18 +1020,18 @@ fn solve_screen_alpha_for_dj_legacy( let (state, achieved_dj, status) = if let Some(selected) = selected { let (state, achieved_dj) = selected; - (state, achieved_dj, GlowTargetStatus::Reached) + (state, achieved_dj, GlowTargetStatus::LegacyReached) } else { let (state, achieved_dj) = best.ok_or_else(|| "конечное множество композитов оказалось пустым".to_string())?; - (state, achieved_dj, GlowTargetStatus::Unreachable) + (state, achieved_dj, GlowTargetStatus::LegacyUnreachable) }; let selected_hex = composite_hex(state.bytes); - let alpha = state.lower.midpoint(state.upper); + let alpha = state.canonical_alpha(); - // Канонический CSS-сериализатор хранит ту же binary64 alpha. Production-проверка - // повторно композитит исходное число; строковый parse-round-trip проверяют - // граничные и межъязыковые тесты, не затягивая dec2flt-парсер в WASM. + // Канонический CSS-сериализатор хранит ту же binary64 alpha. Production- + // проверка повторно композитит исходное число; обратное чтение строки + // проверяют граничные и межъязыковые тесты, не затягивая dec2flt-парсер в WASM. let alpha_css = crate::css_alpha_value(alpha)?; let roundtrip_srgb8 = screen_layer_over_srgb8(glow_bytes, alpha, bg_bytes)?; let roundtrip_hex = composite_hex(roundtrip_srgb8); @@ -947,7 +1054,7 @@ fn solve_screen_alpha_for_dj_legacy( alpha_css, roundtrip_srgb8, ), - diagnostic_profile: Some(GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1), + selection_diagnostic_profile: Some(GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1), status, }) } @@ -955,13 +1062,13 @@ fn solve_screen_alpha_for_dj_legacy( /// Версионированный двухслойный recipe от источника: `(core_hex, halo_hex)`. /// /// В recipe v1 halo буквально равен источнику. Для core арифметическая середина -/// J′ источника и 100 задаёт seed Oklab-светлоты. У хроматического источника +/// J′ источника и 100 задаёт начальную Oklab-светлоту. У хроматического источника /// chroma — sRGB-граница его Oklab hue через /// [`crate::accent_balance::accent_balanced`]; у точного sRGB8-нейтраля chroma -/// остаётся нулевой, потому его численный hue не несёт цветового смысла. Фактический J′ -/// эмитированного core измеряется вызывающим кодом: он не обязан совпасть с seed -/// после смены координат и sRGB8-квантования. Это recipe, не наблюдательная -/// модель красоты или геометрии свечения. +/// остаётся нулевой, потому его численный hue не несёт цветового смысла. +/// Фактический J′ эмитированного core измеряется вызывающим кодом: он не обязан +/// совпасть с начальным значением после смены координат и sRGB8-квантования. +/// Это recipe, а не наблюдательная модель красоты или геометрии свечения. pub fn glow_layers_from_source( source_hex: &str, vc: &ViewingConditions, @@ -1015,6 +1122,23 @@ pub fn glow_layers_from_source( mod tests { use super::*; + #[test] + fn status_and_recipe_keys_preserve_their_provenance() { + assert_eq!( + GlowTargetStatus::ExactNoopUnreachable.key(), + "exact-noop-unreachable" + ); + assert_eq!(GlowTargetStatus::LegacyReached.key(), "legacy-reached"); + assert_eq!( + GlowTargetStatus::LegacyUnreachable.key(), + "legacy-unreachable" + ); + assert_eq!( + GlowLayerRecipeProfileV1::Cam16JPrimeOklabCuspV1.key(), + "cam16-jprime-oklab-cusp-v1" + ); + } + fn solve_legacy( tint: &str, background: &str, @@ -1109,7 +1233,8 @@ mod tests { else { panic!("exact point no-op must be a BitExact determinate decision"); }; - assert_eq!(value.status(), GlowTargetStatus::Unreachable); + assert_eq!(value.status(), GlowTargetStatus::ExactNoopUnreachable); + assert!(value.selection_diagnostic_profile().is_none()); assert_eq!(value.achieved_dj().to_bits(), 0.0_f64.to_bits()); assert_eq!(value.composite_hex(), "#FFFFFF"); assert_eq!(value.composite_certificate().composite_srgb8(), [255; 3]); @@ -1121,10 +1246,10 @@ mod tests { invalid_vc.n = f64::NAN; for (tint, background) in [ ("#000000", "#123456"), - // Every channel has G*(255-B)=0, but neither operand is a - // uniform endpoint: R/B use G=0 and G uses B=255. + // Каждый канал имеет G*(255-B)=0, но ни один операнд не является + // однородным endpoint: R/B используют G=0, а G использует B=255. ("#00FF00", "#12FF34"), - // Non-zero red slope 1 stays below the exact half-LSB wall. + // Ненулевой красный наклон 1 остаётся ниже точной стенки половины LSB. ("#010000", "#FE0000"), ] { assert!(screen_point_is_exact_noop(tint, background).unwrap()); @@ -1141,10 +1266,10 @@ mod tests { NumericalDecisionV1::Determinate { value, guarantee: DecisionGuaranteeV1::BitExact, - } if value.status() == GlowTargetStatus::Unreachable + } if value.status() == GlowTargetStatus::ExactNoopUnreachable && value.achieved_dj().to_bits() == 0.0_f64.to_bits() && value.composite_hex() == background - && value.diagnostic_profile().is_none() + && value.selection_diagnostic_profile().is_none() )); } assert!(screen_point_is_exact_noop("#7F0000", "#FE0000").unwrap()); @@ -1169,9 +1294,10 @@ mod tests { } } - /// Независимый оракул не меняет байты по событиям production-алгоритма: - /// он строит открытые промежутки обычной формулой, пробует их середины через - /// публичный композитор и удаляет только соседние дубликаты hex. + /// Независимый оракул не использует production cursor/boundary helper. Для + /// каждого следующего channel byte он отдельно делает lower-bound по всему + /// диапазону положительных `f64` через публичный композитор, затем объединяет + /// найденные bit-boundaries и удаляет только соседние дубликаты hex. fn oracle_states( tint_hex: &str, bg_hex: &str, @@ -1181,31 +1307,34 @@ mod tests { let bg = srgb_encoded_from_hex(bg_hex).unwrap(); let tint_bytes = encoded_bytes(tint); let bg_bytes = encoded_bytes(bg); - let mut walls = vec![0.0, 1.0]; + let mut boundaries = vec![ALPHA_ZERO_BITS, ALPHA_ONE_BITS]; for channel in 0..3 { - let slope = f64::from(tint_bytes[channel]) * f64::from(u8::MAX - bg_bytes[channel]); - if slope == 0.0 { - continue; - } - for value in (u16::from(bg_bytes[channel]) + 1)..=u16::from(u8::MAX) { - let wall = 255.0 * (f64::from(value) - f64::from(bg_bytes[channel]) - 0.5) / slope; - if wall > 1.0 { - break; + let final_value = screen_layer_over_srgb8(tint_bytes, 1.0, bg_bytes).unwrap()[channel]; + for value in (u16::from(bg_bytes[channel]) + 1)..=u16::from(final_value) { + let mut failing = ALPHA_ZERO_BITS; + let mut passing = ALPHA_ONE_BITS; + while passing - failing > 1 { + let middle = failing + (passing - failing) / 2; + let output = + screen_layer_over_srgb8(tint_bytes, f64::from_bits(middle), bg_bytes) + .unwrap()[channel]; + if output >= value as u8 { + passing = middle; + } else { + failing = middle; + } } - walls.push(wall); + boundaries.push(passing); } } - walls.sort_unstable_by(f64::total_cmp); - walls.dedup(); + boundaries.sort_unstable(); + boundaries.dedup(); let bg_jp = LcsColor::from_hex_with_vc(bg_hex, vc).unwrap().jp; let mut states: Vec<(f64, String, f64)> = Vec::new(); - for window in walls.windows(2) { - if window[0] == window[1] { - continue; - } - let alpha = window[0] + (window[1] - window[0]) * 0.5; + for boundary in boundaries { + let alpha = f64::from_bits(boundary); let hex = composite_hex(screen_layer_over_srgb8(tint_bytes, alpha, bg_bytes).unwrap()); if states .last() @@ -1216,17 +1345,6 @@ mod tests { let jp = LcsColor::from_hex_with_vc(&hex, vc).unwrap().jp; states.push((alpha, hex, (jp - bg_jp).abs())); } - // Отдельный endpoint не даёт оракулу молча потерять состояние, которое - // теоретически могло бы существовать только при полностью непрозрачном слое. - let alpha = 1.0; - let hex = composite_hex(screen_layer_over_srgb8(tint_bytes, alpha, bg_bytes).unwrap()); - if states - .last() - .is_none_or(|(_, previous, _)| previous != &hex) - { - let jp = LcsColor::from_hex_with_vc(&hex, vc).unwrap().jp; - states.push((alpha, hex, (jp - bg_jp).abs())); - } states } @@ -1292,11 +1410,10 @@ mod tests { } } - /// Точка `x.5` принадлежит новому байту. Проверка на представимой стенке - /// отделяет это правило от погрешности binary64, а `1/510` ниже проверяет - /// рациональную группировку непредставимой стенки трёх каналов. + /// Точка `x.5` принадлежит новому байту, а каждый state начинается ровно на + /// первом passing `f64` объявленного operation order. #[test] - fn exact_walls_follow_round_half_up_without_binary_boundary_assumptions() { + fn binary64_boundaries_follow_round_half_up_and_are_reproducible() { let at_half = composite_hex(screen_layer_over_srgb8([1, 0, 0], 0.5, [0; 3]).unwrap()); let just_below_half = f64::from_bits(0.5_f64.to_bits() - 1); let below = @@ -1307,36 +1424,125 @@ mod tests { let states = quantised_composites([1, 0, 0], [0; 3]).unwrap(); assert_eq!(states.len(), 2); assert_eq!(states[0].bytes, [0; 3]); - assert!(states[0].upper.same_value(AlphaWall { - numerator: 1, - denominator: 2, - })); + assert_eq!(states[0].upper_bits, 0.5_f64.to_bits()); + assert!(!states[0].upper_inclusive); assert_eq!(states[1].bytes, [1, 0, 0]); - assert!(states[1].lower.same_value(states[0].upper)); + assert_eq!(states[1].lower_bits, states[0].upper_bits); + assert!(states[1].upper_inclusive); let white_states = quantised_composites([255; 3], [0; 3]).unwrap(); assert_eq!(white_states.len(), 256); - let first_wall = AlphaWall { - numerator: 1, - denominator: 510, - }; - assert!(white_states[0].upper.same_value(first_wall)); - assert!(white_states[1].lower.same_value(first_wall)); + let first_boundary = white_states[0].upper_bits; + assert_eq!(white_states[1].lower_bits, first_boundary); + assert_eq!( + screen_layer_over_srgb8([255; 3], f64::from_bits(first_boundary - 1), [0; 3]).unwrap(), + [0; 3] + ); + assert_eq!( + screen_layer_over_srgb8([255; 3], f64::from_bits(first_boundary), [0; 3]).unwrap(), + [1; 3] + ); assert_eq!(white_states[1].bytes, [1; 3]); } #[test] - fn rational_wall_cross_products_fit_the_declared_u64_domain() { - // The executable checked-arithmetic derivation is the repository-owned - // proof; a widened input domain must update these extrema. - let max_numerator = 255_u64 * (2 * 255 - 1); - let max_denominator = 2_u64 * 255 * 255; - assert_eq!(max_numerator, 129_795); - assert_eq!(max_denominator, 130_050); - assert_eq!( - max_numerator.checked_mul(max_denominator), - Some(16_879_839_750) - ); + fn rationally_equal_walls_keep_distinct_binary64_seam_states() { + // Обе алгебраические стенки равны 255/508, но зафиксированный порядок + // binary64-операций переводит зелёный канал на 127 ULP раньше красного. + // Поэтому промежуточный RGB-state достижим и не может быть поглощён + // группировкой по равенству рациональных дробей. + let states = quantised_composites([1, 2, 0], [1, 128, 0]).unwrap(); + let seam_index = states + .iter() + .position(|state| state.bytes == [1, 129, 0]) + .expect("потерян достижимый binary64 seam-state #018100"); + let seam = states[seam_index]; + assert_eq!(seam.lower_bits, 4_602_696_549_879_841_156); + assert_eq!(seam.upper_bits, 4_602_696_549_879_841_283); + assert_eq!(seam.upper_bits - seam.lower_bits, 127); + assert_eq!(states[seam_index + 1].bytes, [2, 129, 0]); + } + + #[test] + fn rational_seed_is_a_stable_performance_hint_not_a_state_definition() { + for (glow, background, next_value, expected_bits) in [ + (1, 1, 2, 0x3fe0_1020_4081_0204), + (2, 128, 129, 0x3fe0_1020_4081_0204), + (33, 131, 132, 0x3f9f_e7f9_fe7f_9fe8), + (11, 0, 8, 0x3fe5_d174_5d17_45d1), + (255, 0, 255, 0x3fef_efef_efef_eff0), + ] { + assert_eq!( + boundary_seed_bits(glow, background, next_value), + expected_bits, + "glow={glow}, background={background}, next={next_value}", + ); + } + } + + #[test] + fn canonical_alpha_pins_the_midpoint_of_the_actual_binary64_partition() { + let state = quantised_composites([255, 59, 48], [0; 3]) + .unwrap() + .into_iter() + .find(|state| state.bytes == [3, 1, 1]) + .expect("reference state #030101"); + assert_eq!(state.lower_bits, 0x3f85_5555_5555_5555); + assert_eq!(state.upper_bits, 0x3f8c_1c1c_1c1c_1c1c); + assert_eq!(state.canonical_alpha().to_bits(), 0x3f88_b8b8_b8b8_b8b8); + + // Если representable state содержит только lower, численный midpoint + // half-ULP округляется к чётной исключённой upper boundary. Канон обязан + // вернуть её непосредственного predecessor, то есть единственный state. + let lower_bits = 0.5_f64.to_bits() + 1; + let singleton = QuantisedComposite { + bytes: [0; 3], + lower_bits, + upper_bits: lower_bits + 1, + upper_inclusive: false, + }; + assert_eq!(singleton.canonical_alpha().to_bits(), lower_bits); + } + + /// Каждый transition увеличивает хотя бы один из трёх байтов. Поэтому их + /// суммарно не больше `3·255`, а states — не больше 766. Fixtures включают + /// длинный поток и ULP-seam, который ломал прежнюю rational grouping. + #[test] + fn binary64_stream_respects_the_766_state_bound() { + for (glow, background) in [ + ([255, 255, 254], [0, 1, 1]), + ([1, 2, 0], [1, 128, 0]), + ([255; 3], [0; 3]), + ] { + let states = quantised_composites(glow, background).unwrap(); + assert!( + states.len() <= usize::from(MAX_QUANTISED_COMPOSITE_STATES), + "glow={glow:?}, background={background:?}, states={}", + states.len() + ); + for pair in states.windows(2) { + assert_eq!(pair[0].upper_bits, pair[1].lower_bits); + assert!(!pair[0].upper_inclusive); + assert!( + pair[1] + .bytes + .into_iter() + .zip(pair[0].bytes) + .all(|(next, previous)| next >= previous) + ); + } + assert!(states.last().unwrap().upper_inclusive); + } + + let solved = solve_legacy( + "#FFFFFE", + "#000101", + 101.0, + &ViewingConditions::dim_surround(), + ) + .unwrap(); + assert_eq!(solved.status(), GlowTargetStatus::LegacyUnreachable); + assert!(solved.achieved_dj() < 101.0); } /// Известная десятичная alpha проверяется независимой целочисленной @@ -1377,29 +1583,28 @@ mod tests { /// солвер выбирает внутренние точки интервалов и перепроверяет результат. #[test] fn byte_screen_pins_reference_operation_order_at_float_wall() { - let below_wall = 0.501_968_503_937_007_9_f64; - let predecessor = f64::from_bits(below_wall.to_bits() - 1); - let successor = f64::from_bits(below_wall.to_bits() + 1); + let rounded_rational_wall = 0.501_968_503_937_007_9_f64; + let first_passing = f64::from_bits(rounded_rational_wall.to_bits() - 1); + let below_first = f64::from_bits(first_passing.to_bits() - 1); assert_eq!( - screen_layer_over_srgb8([1, 0, 0], predecessor, [1, 0, 0]).unwrap()[0], - 2 + screen_layer_over_srgb8([1, 0, 0], below_first, [1, 0, 0]).unwrap()[0], + 1 ); assert_eq!( - screen_layer_over_srgb8([1, 0, 0], below_wall, [1, 0, 0]).unwrap()[0], + screen_layer_over_srgb8([1, 0, 0], first_passing, [1, 0, 0]).unwrap()[0], 2 ); assert_eq!( - screen_layer_over_srgb8([1, 0, 0], successor, [1, 0, 0]).unwrap()[0], + screen_layer_over_srgb8([1, 0, 0], rounded_rational_wall, [1, 0, 0],).unwrap()[0], 2 ); } - /// Исчерпывающая теорема для одного канала: рациональное разбиение обязано - /// совпадать с публичной формулой для всех 65 536 пар `(background, glow)`. - /// Три канала независимы по определению screen, поэтому этот перебор - /// закрывает полный класс стенок без случайной выборки RGB-троек. + /// Исчерпывающая теорема для одного канала: first-passing binary64 + /// partition совпадает с публичной формулой для всех 65 536 пар + /// `(background, glow)`. #[test] - fn every_rational_channel_partition_matches_the_public_compositor() { + fn every_binary64_channel_partition_matches_the_public_compositor() { for background in 0_u16..=255 { for glow in 0_u16..=255 { let b = background as u8; @@ -1412,13 +1617,34 @@ mod tests { assert_eq!(states.len(), usize::from(final_byte - b) + 1); for (index, state) in states.iter().enumerate() { - assert!(state.lower.cmp(state.upper) != Ordering::Greater); + assert!(state.lower_bits <= state.upper_bits); if let Some(next) = states.get(index + 1) { - assert!(state.upper.same_value(next.lower)); + assert_eq!(state.upper_bits, next.lower_bits); + assert!(!state.upper_inclusive); assert_eq!(next.bytes[0], state.bytes[0] + 1); + assert_eq!( + screen_layer_over_srgb8( + [g, 0, 0], + f64::from_bits(next.lower_bits), + [b, 0, 0], + ) + .unwrap()[0], + next.bytes[0], + "B={b}, G={g}, state={index}: lower boundary must pass", + ); + assert_eq!( + screen_layer_over_srgb8( + [g, 0, 0], + f64::from_bits(next.lower_bits - 1), + [b, 0, 0], + ) + .unwrap()[0], + state.bytes[0], + "B={b}, G={g}, state={index}: predecessor must fail", + ); } - let alpha = state.lower.midpoint(state.upper); + let alpha = state.canonical_alpha(); let out = screen_layer_over_srgb8([g, 0, 0], alpha, [b, 0, 0]).unwrap(); assert_eq!( out[0], state.bytes[0], @@ -1436,6 +1662,11 @@ mod tests { for target in [GLOW_SUBTLE_DJ, GLOW_BASE_DJ, GLOW_BLOOM_DJ] { let g = solve_legacy("#3E87FF", "#101012", target, &vc).unwrap(); assert!(!g.degraded(), "цель {target} недостижима на #101012"); + assert_eq!(g.status(), GlowTargetStatus::LegacyReached); + assert_eq!( + g.selection_diagnostic_profile(), + Some(GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1) + ); // Первый достижимый sRGB8-композит обязан держать цель. Его // минимальность независимо проверяется sweep-оракулом ниже. assert!( @@ -1493,8 +1724,8 @@ mod tests { #[test] fn solver_matches_independent_finite_state_oracle() { let vcs = [ViewingConditions::srgb(), ViewingConditions::dim_surround()]; - for tint_hex in ["#000000", "#4A8FFF", "#FF3B30", "#FFFFFF"] { - for bg_hex in ["#000000", "#101012", "#808080", "#FFFFFF"] { + for tint_hex in ["#000000", "#010200", "#4A8FFF", "#FF3B30", "#FFFFFF"] { + for bg_hex in ["#000000", "#018000", "#101012", "#808080", "#FFFFFF"] { for vc in vcs { let oracle = oracle_states(tint_hex, bg_hex, &vc); assert!(!oracle.is_empty()); @@ -1505,7 +1736,7 @@ mod tests { } } - for target in [0.01, 1.0, 10.0, global_best.2 + 1.0] { + for target in [0.01, 0.32, 1.0, 10.0, global_best.2 + 1.0] { let solved = solve_legacy(tint_hex, bg_hex, target, &vc).unwrap(); let expected = oracle.iter().find(|state| state.2 >= target); let (expected_state, degraded) = match expected { @@ -1606,6 +1837,7 @@ mod tests { let vc = ViewingConditions::srgb(); let g = solve_legacy("#3E87FF", "#FFFFFF", GLOW_BASE_DJ, &vc).unwrap(); assert!(g.degraded(), "над белым screen обязан быть point-no-op"); + assert_eq!(g.status(), GlowTargetStatus::LegacyUnreachable); assert!(g.achieved_dj() < GLOW_BASE_DJ); assert_eq!(g.composite_hex(), "#FFFFFF", "screen над белым — тождество"); assert_eq!( diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 78717eb7..e652b40c 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -84,17 +84,20 @@ pub use config::{ }; pub use curve::ColorCurve; pub use glow::{ - GLOW_BASE_DJ, GLOW_BLOOM_DJ, GLOW_COMPOSITE_PROFILE, GLOW_DIAGNOSTIC_PROFILE, GLOW_SUBTLE_DJ, - GlowCompositeCertificateV1, GlowCompositeGuaranteeV1, GlowCompositeProfileV1, - GlowConstraintLayer, GlowDecisionProfileV1, GlowDiagnosticProfileV1, GlowSolve, - GlowTargetStatus, glow_layers_from_source, screen_layer_over_encoded, screen_layer_over_srgb8, + GLOW_BASE_DJ, GLOW_BLOOM_DJ, GLOW_COMPOSITE_PROFILE, GLOW_DIAGNOSTIC_PROFILE, + GLOW_LAYER_RECIPE_PROFILE, GLOW_SUBTLE_DJ, GlowCompositeCertificateV1, + GlowCompositeGuaranteeV1, GlowCompositeProfileV1, GlowConstraintLayer, GlowDecisionProfileV1, + GlowDiagnosticProfileV1, GlowLayerRecipeProfileV1, GlowSolve, GlowTargetStatus, + glow_layers_from_source, screen_layer_over_encoded, screen_layer_over_srgb8, screen_point_is_exact_noop, solve_screen_alpha_for_dj, }; pub use hash::fnv1a_32; pub use ladder::{LadderPosition, LadderTint, ThemeAnchors}; pub use lcs::LcsColor; pub use material::{ - BackdropBox, MaterialAlpha, Pole, committed_pole_encoded, solve_material_alpha_encoded, + BackdropBoundV1, BackdropBox, BackdropBoxErrorV1, EncodedRgbErrorV1, MaterialAlpha, + MaterialAlphaGuaranteeV1, MaterialAlphaStatusV1, MaterialNumericalProfileV1, + MaterialSolveErrorV1, Pole, RgbChannelV1, committed_pole_encoded, solve_material_alpha_encoded, solve_material_alpha_hex, worst_contrast_encoded, }; pub use numerics::{ diff --git a/crates/labcolors-core/src/material.rs b/crates/labcolors-core/src/material.rs index 9c26a33b..51750146 100644 --- a/crates/labcolors-core/src/material.rs +++ b/crates/labcolors-core/src/material.rs @@ -13,22 +13,28 @@ //! //! # Выведенная альфа (не рукописная) //! -//! `α` — минимальная плотность, при которой тинт над ХУДШИМ разрешённым фоном -//! остаётся в контракте базы: коммит-лейбл поверхности (ахроматический полюс +//! `α` — проходящая плотность, выбранная бисекцией с фиксированным числом шагов +//! в объявленном охарактеризованном для платформы runtime: тинт над ХУДШИМ +//! разрешённым фоном остаётся в +//! контракте базы. Коммит-лейбл поверхности (ахроматический полюс //! максимального контраста на тоне `T` — белый на тёмном `T`, чёрный на светлом) //! держит пол читаемости ПО ВСЕМУ коридору достижимых фонов. //! -//! Композит `α·T + (1−α)·B` афинно-возрастает по каждому каналу фона `B`, а -//! WCAG-светлота монотонно растёт по каждому каналу композита — поэтому по -//! осепараллельному коробу-коридору `[min, max]` светлота композита экстремальна -//! РОВНО на углах `min`/`max` (доказано `band_luminance`+property-тестом). Худший -//! контраст полюса берётся по этой достижимой полосе `[effLow, effHigh]`: -//! эквивалент α-граничной гарантии потребителя (`material-guarantee.ts`), но -//! ИНВЕРТИРОВАННОЙ — не «годен/негоден при данной α», а «минимальная годная α». +//! Композит исполняется в официальном byte-scale affine order +//! `(B_byte + α·(T_byte−B_byte))/255`. Алгебраическая монотонность этой формулы +//! недостаточна для binary64: округление может опустить interior background на +//! один ULP ниже обоих endpoint-значений. Кроме того, frozen legacy WCAG 2.1 +//! (2018) split `0.03928` имеет малый downward seam. Поэтому ядро строит +//! поканальный conservative envelope фактического композитора, включает обе +//! стороны пересечённого EOTF seam и только затем складывает положительно +//! взвешенные channel ranges. Это all-backdrop characterization выбранного +//! state, а не ложная теорема о двух углах. //! -//! Худший контраст монотонно растёт по `α` (полоса стягивается к `T`, полюс на -//! верной стороне), поэтому солвер — бисекция, как остальные полы движка -//! (`glow::solve_screen_alpha_for_dj`). На `α = 1` полоса вырождается в `L(T)`, а +//! На поддерживаемом направленном домене material-солвер применяет детерминированную +//! бисекцию с фиксированным числом шагов и повторно проверяет обе стороны +//! найденного fail/pass bracket. Legacy EOTF seam опровергает глобальную +//! монотонность, поэтому результат не называется первым или минимальным. На +//! `α = 1` полоса вырождается в `L(T)`, а //! полюс максимального контраста на ЛЮБОМ тоне даёт ≥ 4.58:1 (кроссовер чёрного и //! белого полюса при `L ≈ 0.179`), поэтому для пола ≤ AA годная `α ∈ (0, 1]` //! существует всегда; при более высоком поле (напр. AAA на среднем тоне) @@ -37,15 +43,42 @@ //! # Пространство //! //! Композит — гамма-кодированный sRGB reference-профиля [`crate::alpha`], -//! заземлённого 12 Figma-парами, но не выдаваемого за любой browser pipeline. WCAG- +//! заземлённого 12 Figma-парами, но не выдаваемого за любой браузерный pipeline. WCAG- //! светлота меряется на кодированном тоне-тинте (квантованном до 8-битного hex — -//! эмитируемый цвет `01`), композит над фоном берётся ТОЧНО (без переквантования): -//! тем же алгоритмом официальный потребитель пересчитывает вердикт из -//! эмитированных `01`/`02`, поэтому его re-check тождествен ядру. Renderer и -//! color-management применимость принадлежат отдельному conformance-гейту. +//! эмитируемый цвет `01`), композит над фоном берётся без промежуточного +//! переквантования как `(B_byte + α·(T_byte−B_byte))/255`: тем же byte-scale +//! affine порядком binary64-операций, что официальный потребитель. Потребитель +//! может независимо воспроизвести point-композиты; all-backdrop verdict ядра +//! дополнительно использует описанный conservative channel/EOTF envelope и не +//! равен одному raw point-замеру. Применимость к рендереру и управлению цветом +//! принадлежит отдельному conformance-гейту. use crate::spaces::srgb::{hex_from_srgb_encoded, srgb_encoded_from_hex}; -use crate::wcag::{ratio_from_luminances, relative_luminance}; +use crate::wcag::{ratio_from_luminances, relative_luminance, relative_luminance_range}; +use std::fmt; + +const MATERIAL_ALPHA_BISECTION_ITERATIONS: u32 = 60; + +/// Absolute encoded-channel padding around the two actual endpoint evaluations +/// of the byte-scale affine compositor. +/// +/// With all numerator-scale operands bounded by 255, a binary64 forward-error +/// accounting bounds one evaluation by +/// `E = 5·2^-46/255 + 2^-53 < 3.90e-16`. Comparing an interior evaluation with +/// an evaluated endpoint therefore costs `2E`; the outward padding operation +/// can lose at most another `2^-53` to rounding. The combined requirement is +/// `< 8.91e-16`. `8·EPSILON = 2^-49` is exactly `816/409 ≈ 1.995` times that +/// requirement while remaining an absolute bound near zero. This covers +/// compositor rounding only; WCAG `powf` remains platform-characterized. +#[cfg(test)] +const MATERIAL_COMPOSITE_SINGLE_EVALUATION_ERROR_BOUND: f64 = + 5.0 * (64.0 * f64::EPSILON) / 255.0 + 0.5 * f64::EPSILON; +#[cfg(test)] +const MATERIAL_COMPOSITE_PAIRWISE_ERROR_BOUND: f64 = + 2.0 * MATERIAL_COMPOSITE_SINGLE_EVALUATION_ERROR_BOUND; +#[cfg(test)] +const MATERIAL_COMPOSITE_OUTWARD_ROUNDING_BOUND: f64 = 0.5 * f64::EPSILON; +const MATERIAL_COMPOSITE_RANGE_MARGIN: f64 = 8.0 * f64::EPSILON; /// Ахроматический полюс коммит-лейбла поверхности: цвет максимального контраста /// на тоне (белый на тёмном тоне, чёрный на светлом). Механизм полярности @@ -60,6 +93,7 @@ pub enum Pole { impl Pole { /// WCAG-светлота полюса: `0.0` (чёрный) / `1.0` (белый). + #[cfg(test)] fn luminance(self) -> f64 { match self { Pole::Black => 0.0, @@ -68,18 +102,351 @@ impl Pole { } } +/// Канал encoded-sRGB в типизированных ошибках валидации. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum RgbChannelV1 { + /// Красный канал. + Red, + /// Зелёный канал. + Green, + /// Синий канал. + Blue, +} + +impl RgbChannelV1 { + fn from_index(index: usize) -> Self { + match index { + 0 => Self::Red, + 1 => Self::Green, + 2 => Self::Blue, + _ => unreachable!("RGB channel index is internal and bounded by 0..3"), + } + } + + /// Стабильный машинный ключ. + pub fn key(self) -> &'static str { + match self { + Self::Red => "red", + Self::Green => "green", + Self::Blue => "blue", + } + } +} + +/// Какая граница [`BackdropBox`] нарушила инвариант encoded-sRGB. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BackdropBoundV1 { + /// Поканальный минимум. + Min, + /// Поканальный максимум. + Max, +} + +impl BackdropBoundV1 { + /// Стабильный машинный ключ. + pub fn key(self) -> &'static str { + match self { + Self::Min => "min", + Self::Max => "max", + } + } +} + +/// Нарушение домена encoded-sRGB `[0,1]^3`. +#[derive(Debug, Clone, Copy, PartialEq)] +#[non_exhaustive] +pub enum EncodedRgbErrorV1 { + /// Канал равен NaN или бесконечности. + NonFiniteChannel { + /// Канал, нарушивший конечный домен. + channel: RgbChannelV1, + }, + /// Конечный канал лежит вне `[0,1]`. + OutOfRangeChannel { + /// Канал, нарушивший диапазон. + channel: RgbChannelV1, + /// Фактическое значение. + value: f64, + }, +} + +impl fmt::Display for EncodedRgbErrorV1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NonFiniteChannel { channel } => { + write!(f, "encoded-sRGB {} channel is not finite", channel.key()) + } + Self::OutOfRangeChannel { channel, value } => write!( + f, + "encoded-sRGB {} channel is outside [0,1]: {value}", + channel.key() + ), + } + } +} + +impl std::error::Error for EncodedRgbErrorV1 {} + +/// Типизированное нарушение инварианта при создании [`BackdropBox`]. +// Имена публичных вариантов явно несут нарушенный инвариант канала; удаление +// общего суффикса сделало бы сопоставление с образцом менее самодокументируемым. +#[allow(clippy::enum_variant_names)] +#[derive(Debug, Clone, Copy, PartialEq)] +#[non_exhaustive] +pub enum BackdropBoxErrorV1 { + /// Канал одной из границ равен NaN или бесконечности. + NonFiniteChannel { + /// Нарушившая граница. + bound: BackdropBoundV1, + /// Нарушивший канал. + channel: RgbChannelV1, + }, + /// Конечный канал одной из границ лежит вне `[0,1]`. + OutOfRangeChannel { + /// Нарушившая граница. + bound: BackdropBoundV1, + /// Нарушивший канал. + channel: RgbChannelV1, + /// Фактическое значение. + value: f64, + }, + /// `min[channel] > max[channel]`; конструктор не переставляет границы молча. + ReversedChannel { + /// Перепутанный канал. + channel: RgbChannelV1, + /// Переданный минимум. + min: f64, + /// Переданный максимум. + max: f64, + }, +} + +impl fmt::Display for BackdropBoxErrorV1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NonFiniteChannel { bound, channel } => write!( + f, + "backdrop {} {} channel is not finite", + bound.key(), + channel.key() + ), + Self::OutOfRangeChannel { + bound, + channel, + value, + } => write!( + f, + "backdrop {} {} channel is outside [0,1]: {value}", + bound.key(), + channel.key() + ), + Self::ReversedChannel { channel, min, max } => write!( + f, + "backdrop {} channel is reversed: min={min}, max={max}", + channel.key() + ), + } + } +} + +impl std::error::Error for BackdropBoxErrorV1 {} + +/// Класс численного профиля, в котором охарактеризована граница material-alpha. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum MaterialNumericalProfileV1 { + /// Byte-scale affine source-over и frozen legacy WCAG 2.1 (2018) + /// `0.03928`/`powf` в Rust IEEE-754 binary64; `powf` остаётся зависимым от + /// платформы и toolchain. Canonical WCAG 2.2 принадлежит issue #284. + EncodedSrgbByteScaleAffinePlatformBinary64PowfV1, +} + +impl MaterialNumericalProfileV1 { + /// Стабильный wire-ключ. + pub fn key(self) -> &'static str { + match self { + Self::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1 => { + "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1" + } + } + } +} + +/// Честный класс численной гарантии выбранной material alpha. +// Суффикс — часть публичного словаря гарантий: каждый текущий вариант является +// явно охарактеризованным исходом, но не доказательством точности или минимальности. +#[allow(clippy::enum_variant_names)] +#[derive(Debug, Clone, Copy, PartialEq)] +#[non_exhaustive] +pub enum MaterialAlphaGuaranteeV1 { + /// Детерминированная бинарная partition с фиксированным числом шагов; + /// выбранный верхний кандидат повторно прошёл floor, а нижний повторно не + /// прошёл. Глобальная монотонность, первый passing state и sound- + /// доказательство межсредовой минимальности не заявляются. + BisectionBracketCharacterizedV1 { + /// Число исполненных шагов бисекции. + iterations: u32, + /// Последний кандидат, измеренный ниже floor в этом профиле. + lower_alpha: f64, + /// Верхняя граница интервала кандидатов, повторно прошедшая floor. + upper_alpha: f64, + /// Численный профиль чувствительного к ветвлению WCAG-пути `powf`. + numerical_profile: MaterialNumericalProfileV1, + }, + /// Уже прозрачный endpoint держит floor; бисекция не нужна. + TransparentEndpointCharacterizedV1 { + /// Численный профиль повторной проверки возвращённого худшего контраста. + numerical_profile: MaterialNumericalProfileV1, + }, + /// Даже непрозрачный endpoint не держит floor; endpoint повторно измерен + /// в указанном профиле и возвращён как ближайшее достижимое состояние. + OpaqueEndpointCharacterizedV1 { + /// Численный профиль чувствительного к ветвлению WCAG-пути `powf`. + numerical_profile: MaterialNumericalProfileV1, + }, +} + +impl MaterialAlphaGuaranteeV1 { + /// Стабильный wire-ключ. + pub fn key(self) -> &'static str { + match self { + Self::BisectionBracketCharacterizedV1 { .. } => "bisection-bracket-characterized-v1", + Self::TransparentEndpointCharacterizedV1 { .. } => { + "transparent-endpoint-characterized-v1" + } + Self::OpaqueEndpointCharacterizedV1 { .. } => "opaque-endpoint-characterized-v1", + } + } + + /// Runtime-класс, неразделимо связанный с гарантией. + pub fn numerical_profile(self) -> MaterialNumericalProfileV1 { + match self { + Self::BisectionBracketCharacterizedV1 { + numerical_profile, .. + } + | Self::TransparentEndpointCharacterizedV1 { numerical_profile } + | Self::OpaqueEndpointCharacterizedV1 { numerical_profile } => numerical_profile, + } + } + + /// Фиксированное число шагов бинарной partition для bracket-исхода. + pub fn iterations(self) -> Option { + match self { + Self::BisectionBracketCharacterizedV1 { iterations, .. } => Some(iterations), + Self::TransparentEndpointCharacterizedV1 { .. } + | Self::OpaqueEndpointCharacterizedV1 { .. } => None, + } + } + + /// Охарактеризованные нижний и верхний кандидаты только для настоящего + /// порогового интервала. + pub fn bracket(self) -> Option<(f64, f64)> { + match self { + Self::BisectionBracketCharacterizedV1 { + lower_alpha, + upper_alpha, + .. + } => Some((lower_alpha, upper_alpha)), + Self::TransparentEndpointCharacterizedV1 { .. } + | Self::OpaqueEndpointCharacterizedV1 { .. } => None, + } + } +} + +/// Доменный исход решения material-alpha; недостижимость не является ошибкой. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum MaterialAlphaStatusV1 { + /// Выбранная alpha проходит запрошенный floor в объявленном runtime. + Satisfied, + /// Floor недостижим даже при alpha 1; возвращается непрозрачный endpoint. + Degraded, +} + +impl MaterialAlphaStatusV1 { + /// Стабильный wire-ключ. + pub fn key(self) -> &'static str { + match self { + Self::Satisfied => "satisfied", + Self::Degraded => "degraded", + } + } +} + +/// Типизированная публичная ошибка material-вычислений; без clamp, swap и fallback. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum MaterialSolveErrorV1 { + /// Тон нарушает домен encoded-sRGB `[0,1]^3`. + InvalidTone(EncodedRgbErrorV1), + /// Hex-обёртка получила неканоническую или невалидную строку sRGB. + InvalidToneHex { + /// Свидетельство парсера. + reason: String, + }, + /// Alpha не конечна или лежит вне `[0,1]`. + InvalidAlpha { + /// Отклонённая alpha. + value: f64, + }, + /// WCAG floor не конечен или лежит вне `[1,21]`. + InvalidFloorRatio { + /// Отклонённое отношение. + value: f64, + }, + /// Текущий закон бисекции неприменим к этому валидному отношению тона и фона. + UnsupportedDirectedSearchRelation { + /// Коммит-полюс, направленное предусловие которого нарушено. + pole: Pole, + }, + /// Предполагавшийся конечным внутренний численный результат вышел из + /// объявленного домена. + NumericallyIndeterminate, +} + +impl fmt::Display for MaterialSolveErrorV1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidTone(error) => write!(f, "invalid material tone: {error}"), + Self::InvalidToneHex { reason } => write!(f, "invalid material tone hex: {reason}"), + Self::InvalidAlpha { value } => { + write!(f, "material alpha is outside finite [0,1]: {value}") + } + Self::InvalidFloorRatio { value } => { + write!(f, "material floor ratio is outside finite [1,21]: {value}") + } + Self::UnsupportedDirectedSearchRelation { pole } => write!( + f, + "material tone/backdrop relation does not support directed search for {pole:?} pole" + ), + Self::NumericallyIndeterminate => { + f.write_str("material numerical result left its declared finite domain") + } + } + } +} + +impl std::error::Error for MaterialSolveErrorV1 {} + /// Осепараллельный короб достижимых фонов: поканальный минимум и максимум. /// /// `min`/`max` — кодированные углы `[0,1]³`. Материальный дефолт (стекло над /// неизвестным живым фоном) — [`FULL`](Self::FULL) = `[чёрный, белый]`, худший /// возможный коридор. Известная область (изображение/градиент под лейблом) — её /// поканальные экстремумы (обобщение коридора, labui ADR-0004 Решение 2). +/// Прямые литералы структуры намеренно недоступны; все публичные экземпляры +/// проходят через [`try_new`](Self::try_new). +/// +/// ```compile_fail +/// use labcolors_core::BackdropBox; +/// let _ = BackdropBox { min: [0.0; 3], max: [1.0; 3] }; +/// ``` #[derive(Debug, Clone, Copy, PartialEq)] pub struct BackdropBox { - /// Поканально-минимальный (темнейший) угол фона. - pub min: [f64; 3], - /// Поканально-максимальный (светлейший) угол фона. - pub max: [f64; 3], + min: [f64; 3], + max: [f64; 3], } impl BackdropBox { @@ -90,36 +457,107 @@ impl BackdropBox { max: [1.0, 1.0, 1.0], }; - /// Короб пригоден для расчёта: оба угла лежат в encoded-sRGB и минимум - /// поканально не превышает максимум. Перепутанные углы отвергаются, а не - /// нормализуются: swap изменил бы контракт входного множества молча. - pub fn is_valid(&self) -> bool { - is_encoded_rgb(self.min) - && is_encoded_rgb(self.max) - && (0..3).all(|channel| self.min[channel] <= self.max[channel]) + /// Создаёт короб только из конечных границ encoded-sRGB с `min <= max`. + /// Перепутанные границы отвергаются, а не нормализуются молча. + pub fn try_new(min: [f64; 3], max: [f64; 3]) -> Result { + validate_backdrop_bound(BackdropBoundV1::Min, min)?; + validate_backdrop_bound(BackdropBoundV1::Max, max)?; + for channel_index in 0..3 { + if min[channel_index] > max[channel_index] { + return Err(BackdropBoxErrorV1::ReversedChannel { + channel: RgbChannelV1::from_index(channel_index), + min: min[channel_index], + max: max[channel_index], + }); + } + } + Ok(Self { min, max }) + } + + /// Поканально-минимальный (темнейший) угол. + pub fn min(self) -> [f64; 3] { + self.min + } + + /// Поканально-максимальный (светлейший) угол. + pub fn max(self) -> [f64; 3] { + self.max } } /// Результат вывода альфы материала: плотность + вердикт гарантии. #[derive(Debug, Clone, Copy, PartialEq)] pub struct MaterialAlpha { - /// Выведенная альфа тинта `01`, `(0, 1]`. - pub alpha: f64, - /// Худший WCAG-контраст коммит-полюса по коридору при выведенной `alpha` - /// (`[1, 21]`). При не-деградации ≥ запрошенного пола. - pub worst_contrast: f64, - /// Коммит-полюс поверхности (полярность лейбла тона). - pub pole: Pole, - /// Даже при `α = 1` худший контраст ниже пола (напр. AAA-пол на среднем - /// тоне): возвращена `α = 1` как ближайшая достижимая, гарантия НЕ выполнена. - /// Честный флаг деградации контракта (закон 2 ADR-0002), не молчание. - pub degraded: bool, + alpha: f64, + worst_contrast: f64, + pole: Pole, + status: MaterialAlphaStatusV1, + guarantee: MaterialAlphaGuaranteeV1, +} + +impl MaterialAlpha { + /// Выбранная alpha тинта `01`, `[0,1]`. + pub fn alpha(self) -> f64 { + self.alpha + } + + /// Повторно вычисленный худший контраст выбранного состояния. + pub fn worst_contrast(self) -> f64 { + self.worst_contrast + } + + /// Коммит-полюс поверхности. + pub fn pole(self) -> Pole { + self.pole + } + + /// Доменный исход satisfied/degraded. + pub fn status(self) -> MaterialAlphaStatusV1 { + self.status + } + + /// Класс численного свидетельства; не смешивается с утверждением точности + /// композитора. + pub fn guarantee(self) -> MaterialAlphaGuaranteeV1 { + self.guarantee + } + + /// Предикат совместимости поверх типизированного статуса. + pub fn degraded(self) -> bool { + self.status == MaterialAlphaStatusV1::Degraded + } +} + +/// Единственный валидатор массива encoded-sRGB: конечность и `[0,1]^3`. +fn validate_encoded_rgb(v: [f64; 3]) -> Result<(), EncodedRgbErrorV1> { + for (channel_index, value) in v.into_iter().enumerate() { + let channel = RgbChannelV1::from_index(channel_index); + if !value.is_finite() { + return Err(EncodedRgbErrorV1::NonFiniteChannel { channel }); + } + if !(0.0..=1.0).contains(&value) { + return Err(EncodedRgbErrorV1::OutOfRangeChannel { channel, value }); + } + } + Ok(()) } -/// Валидный кодированный цвет: конечный и в `[0,1]³`. -fn is_encoded_rgb(v: [f64; 3]) -> bool { - v.into_iter() - .all(|x| x.is_finite() && (0.0..=1.0).contains(&x)) +fn validate_backdrop_bound( + bound: BackdropBoundV1, + value: [f64; 3], +) -> Result<(), BackdropBoxErrorV1> { + validate_encoded_rgb(value).map_err(|error| match error { + EncodedRgbErrorV1::NonFiniteChannel { channel } => { + BackdropBoxErrorV1::NonFiniteChannel { bound, channel } + } + EncodedRgbErrorV1::OutOfRangeChannel { channel, value } => { + BackdropBoxErrorV1::OutOfRangeChannel { + bound, + channel, + value, + } + } + }) } /// Квантовать кодированный цвет до 8-битной сетки дисплея (round-trip через hex — @@ -133,160 +571,257 @@ fn quantise(v: [f64; 3]) -> [f64; 3] { /// /// Белый полюс на тёмном тоне, чёрный на светлом; граница — кроссовер /// `L ≈ 0.1791` (там оба полюса дают ≈ 4.58:1). Тон квантуется (полюс — свойство -/// ЭМИТИРУЕМОГО тона). Мусор-вход отвергается. -pub fn committed_pole_encoded(tone: [f64; 3]) -> Option { - if !is_encoded_rgb(tone) { - return None; - } - let l = relative_luminance(quantise(tone)); +/// ЭМИТИРУЕМОГО тона). +/// +/// # Errors +/// +/// [`MaterialSolveErrorV1::InvalidTone`] для неконечного канала или канала вне диапазона. +pub fn committed_pole_encoded(tone: [f64; 3]) -> Result { + validate_encoded_rgb(tone).map_err(MaterialSolveErrorV1::InvalidTone)?; + Ok(committed_pole_for_valid_tone(quantise(tone))) +} + +fn committed_pole_for_valid_tone(tone_q: [f64; 3]) -> Pole { + let l = relative_luminance(tone_q); // contrast(белый, L) > contrast(чёрный, L) ⇔ 1.05/(L+0.05) > (L+0.05)/0.05 // ⇔ (L+0.05)² < 0.0525. let s = l + 0.05; - Some(if s * s < 1.05 * 0.05 { + if s * s < 1.05 * 0.05 { Pole::White } else { Pole::Black - }) + } +} + +/// Один канал официального material consumer в зафиксированном byte-scale +/// affine operation order. +fn material_channel_over_byte_scale(tint_byte: f64, alpha: f64, background: f64) -> f64 { + let background_byte_scale = background * 255.0; + (background_byte_scale + alpha * (tint_byte - background_byte_scale)) / 255.0 +} + +/// Conservative encoded-channel range over an ordered background interval. +/// +/// The exact affine reference is monotone in the byte-scale background, but +/// its binary64 evaluation is not. Actual endpoint values are therefore sorted +/// and expanded by [`MATERIAL_COMPOSITE_RANGE_MARGIN`]. At the opaque endpoint, +/// source-over is the tint identity and is represented exactly without a +/// fictitious uncertainty band. +fn material_channel_range( + tint_byte: f64, + alpha: f64, + background_lo: f64, + background_hi: f64, +) -> (f64, f64) { + if alpha == 1.0 { + let tint = tint_byte / 255.0; + return (tint, tint); + } + let at_lo = material_channel_over_byte_scale(tint_byte, alpha, background_lo); + let at_hi = material_channel_over_byte_scale(tint_byte, alpha, background_hi); + let raw_lo = at_lo.min(at_hi); + let raw_hi = at_lo.max(at_hi); + ( + (raw_lo - MATERIAL_COMPOSITE_RANGE_MARGIN).max(0.0), + (raw_hi + MATERIAL_COMPOSITE_RANGE_MARGIN).min(1.0), + ) } -/// Достижимая полоса светлоты композита `[effLow, effHigh]`: тинт над двумя -/// углами короба-коридора. Углы — истинные экстремумы по монотонности (см. -/// шапку); `min`/`max` защищают порядок при перепутанном коробе. +/// Conservative characterized luminance enclosure `[effLow, effHigh]` for the +/// tint over every background in the axis-aligned box. +/// +/// Each actual byte-scale channel is enclosed independently; the legacy WCAG +/// profile then includes both sides of a crossed EOTF seam before combining the +/// positive luminance weights. No RGB-corner or global monotonicity theorem is +/// claimed. fn band_luminance(tint_q: [f64; 3], alpha: f64, backdrop: &BackdropBox) -> (f64, f64) { - debug_assert!( - (0..3).all(|c| backdrop.min[c] <= backdrop.max[c]), - "band_luminance: короб коридора не поканальный (min ≤ max нарушен) — \ - перепутанные углы дали бы неэкстремальную полосу" - ); - let composite = |bg: [f64; 3]| { - [ - alpha * tint_q[0] + (1.0 - alpha) * bg[0], - alpha * tint_q[1] + (1.0 - alpha) * bg[1], - alpha * tint_q[2] + (1.0 - alpha) * bg[2], - ] - }; - let la = relative_luminance(composite(backdrop.min)); - let lb = relative_luminance(composite(backdrop.max)); - (la.min(lb), la.max(lb)) + // Source-over at alpha=1 is exactly the emitted tint for every backdrop in + // this byte-scale profile. Preserve that proven endpoint instead of adding + // an uncertainty band that would falsely degrade #000/#FFF at floor 21. + if alpha == 1.0 { + let luminance = relative_luminance(tint_q); + return (luminance, luminance); + } + let ranges: [(f64, f64); 3] = core::array::from_fn(|channel| { + material_channel_range( + (tint_q[channel] * 255.0).round(), + alpha, + backdrop.min[channel], + backdrop.max[channel], + ) + }); + relative_luminance_range( + core::array::from_fn(|channel| ranges[channel].0), + core::array::from_fn(|channel| ranges[channel].1), + ) } -/// Худший WCAG-контраст полюса по достижимой полосе `[lo, hi]` (честный вердикт, -/// не только на концах): если светлота полюса строго ВНУТРИ полосы — некий фон -/// доводит композит до неё → 1:1; иначе — ближний конец. +/// Худший WCAG-контраст ахроматического полюса по достижимой полосе `[lo, hi]`. +/// Для чёрного полюса ближайший endpoint — `lo`, для белого — `hi`. fn worst_contrast_of_band(pole: Pole, lo: f64, hi: f64) -> f64 { - let p = pole.luminance(); - // Ветка «полюс строго ВНУТРИ полосы → 1:1» недостижима для АХРОМАТИЧЕСКОГО - // полюса (`p ∈ {0, 1}`, полоса ⊆ `[0,1]`, крайние точки не «строго внутри»): - // держится общей на случай будущего не-ахроматического коммит-полюса, а не - // как мёртвый код (тест `committed_pole_maximises_contrast` фиксирует, что - // сегодня полюс всегда ахроматический). - if p > lo && p < hi { - 1.0 - } else { - ratio_from_luminances(p, lo).min(ratio_from_luminances(p, hi)) + match pole { + Pole::Black => ratio_from_luminances(0.0, lo), + Pole::White => ratio_from_luminances(1.0, hi), } } /// Худший WCAG-контраст коммит-полюса тинта `tone` при `alpha` над коридором. /// -/// Тон квантуется (эмитируемый `01`); композит над углами берётся точно. -/// `None` — мусор-вход (не кодированный тон, неупорядоченный короб или `alpha` -/// вне `[0,1]`). +/// Тон квантуется (эмитируемый `01`); композитный диапазон использует объявленный +/// порядок binary64-операций без промежуточного квантования, затем conservative +/// channel/EOTF enclosure. +/// # Errors +/// +/// Типизированная [`MaterialSolveErrorV1`] для невалидных tone/alpha либо +/// внутреннего нарушения численного домена. [`BackdropBox`] уже валиден по типу. pub fn worst_contrast_encoded( tone: [f64; 3], alpha: f64, backdrop: &BackdropBox, pole: Pole, -) -> Option { - if !(is_encoded_rgb(tone) - && backdrop.is_valid() - && alpha.is_finite() - && (0.0..=1.0).contains(&alpha)) - { - return None; +) -> Result { + validate_encoded_rgb(tone).map_err(MaterialSolveErrorV1::InvalidTone)?; + if !(0.0..=1.0).contains(&alpha) { + return Err(MaterialSolveErrorV1::InvalidAlpha { value: alpha }); } let (lo, hi) = band_luminance(quantise(tone), alpha, backdrop); - Some(worst_contrast_of_band(pole, lo, hi)) + let worst = worst_contrast_of_band(pole, lo, hi); + if !(1.0..=21.0).contains(&worst) { + return Err(MaterialSolveErrorV1::NumericallyIndeterminate); + } + Ok(worst) } -/// Вывести минимальную альфу тинта `01`: наименьшую плотность, при которой -/// композит тона над ХУДШИМ фоном коридора держит коммит-полюс на поле -/// `floor_ratio` (WCAG-отношение, напр. 4.5). +fn validate_rechecked_bracket( + lower_contrast: f64, + upper_contrast: f64, + floor_ratio: f64, +) -> Result<(), MaterialSolveErrorV1> { + if !matches!( + lower_contrast.partial_cmp(&floor_ratio), + Some(core::cmp::Ordering::Less) + ) { + return Err(MaterialSolveErrorV1::NumericallyIndeterminate); + } + if !matches!( + upper_contrast.partial_cmp(&floor_ratio), + Some(core::cmp::Ordering::Equal | core::cmp::Ordering::Greater) + ) { + return Err(MaterialSolveErrorV1::NumericallyIndeterminate); + } + Ok(()) +} + +/// Выбрать проходящую альфу тинта `01` бинарной partition с фиксированным числом +/// шагов и повторно проверить найденные fail/pass endpoints. /// -/// Бисекция по `α ∈ (0, 1]` (худший контраст монотонно растёт по `α`), замер на -/// квантованном тоне-тинте. Верхняя сторона брекета гарантирует -/// `worst_contrast ≥ floor_ratio` на возвращённой `α`. При недостижимости пола -/// даже на `α = 1` — честный `degraded` (не ошибка, не молчание). +/// Проверка endpoint, затем детерминированная бинарная partition по +/// `α ∈ [0, 1]` и замер на квантованном тоне-тинте. Каждый шаг сохраняет +/// rechecked fail/pass endpoints; глобальная монотонность или первый passing +/// state не заявляются. Возвращённое состояние +/// несёт [`MaterialAlphaGuaranteeV1`]: зависящая от платформы и toolchain +/// функция `powf` не выдаётся за sound-доказательство точной межсредовой +/// минимальной границы. При недостижимости пола даже на `α = 1` возвращается +/// типизированный degraded-исход, а не ошибка или fallback. /// -/// `None` — вход вне домена: не кодированный `tone`, неупорядоченный короб, -/// `floor_ratio` не в `[1, 21]` (WCAG-отношение); ЛИБО тон лежит ВНЕ короба со стороны -/// полюса, где худший контраст НЕ монотонен по `α` и бисекция неприменима (см. -/// ниже). Материальный путь (полный коридор `[чёрный, белый]`) в этот `None` -/// никогда не попадает — тон всегда в кубе. +/// # Errors +/// +/// Типизированный невалидный tone/floor или +/// [`MaterialSolveErrorV1::UnsupportedDirectedSearchRelation`]. +/// Материальный путь с [`BackdropBox::FULL`] всегда лежит в направленном домене. pub fn solve_material_alpha_encoded( tone: [f64; 3], backdrop: &BackdropBox, floor_ratio: f64, -) -> Option { - if !(is_encoded_rgb(tone) - && backdrop.is_valid() - && floor_ratio.is_finite() - && (1.0..=21.0).contains(&floor_ratio)) - { - return None; +) -> Result { + validate_encoded_rgb(tone).map_err(MaterialSolveErrorV1::InvalidTone)?; + if !(1.0..=21.0).contains(&floor_ratio) { + return Err(MaterialSolveErrorV1::InvalidFloorRatio { value: floor_ratio }); } let tone_q = quantise(tone); - let pole = committed_pole_encoded(tone_q)?; - // Бисекция корректна ЛИШЬ когда худший контраст монотонно растёт по `α`, а это - // держится только если тон лежит в коробе коридора со стороны полюса: у чёрного - // полюса композит над темнейшим углом обязан СВЕТЛЕТЬ с ростом `α` (нужно - // `tone ≥ min` поканально), у белого — композит над светлейшим углом обязан - // ТЕМНЕТЬ (`tone ≤ max`). Полный коридор `[чёрный, белый]` гарантирует это - // всегда (материальный путь); узкий коридор с тоном ВНЕ короба со стороны - // полюса выводит из монотонного домена — тогда бисекция дала бы ложную `α`/ - // деградацию, поэтому честный `None` (дисциплина «мусор-вход → None»), а не - // молчаливо неверный ответ. Обобщённый узкий коридор (ADR-0004) с тоном вне - // короба потребует иного солвера (скан/тернарный поиск), не бисекции. - let monotone = match pole { + let pole = committed_pole_for_valid_tone(tone_q); + let worst_at = |alpha: f64| -> Result { + let (lo, hi) = band_luminance(tone_q, alpha, backdrop); + let worst = worst_contrast_of_band(pole, lo, hi); + if !(1.0..=21.0).contains(&worst) { + return Err(MaterialSolveErrorV1::NumericallyIndeterminate); + } + Ok(worst) + }; + + // Общий закон endpoint: если полностью прозрачный tint уже держит floor, + // порогового поиска не существует и выдумывать неуспешную нижнюю границу нельзя. + let transparent = worst_at(0.0)?; + if transparent >= floor_ratio { + return Ok(MaterialAlpha { + alpha: 0.0, + worst_contrast: transparent, + pole, + status: MaterialAlphaStatusV1::Satisfied, + guarantee: MaterialAlphaGuaranteeV1::TransparentEndpointCharacterizedV1 { + numerical_profile: + MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1, + }, + }); + } + + // Density search is supported only in the direction that contracts the + // backdrop interval toward the committed tone: black requires tone >= min, + // white requires tone <= max. This scopes the selection policy; it is not a + // claim that the branch-sensitive binary64 predicate is globally monotone. + // The transparent endpoint above does not require this precondition. + let directed = match pole { Pole::Black => (0..3).all(|c| tone_q[c] >= backdrop.min[c]), Pole::White => (0..3).all(|c| tone_q[c] <= backdrop.max[c]), }; - if !monotone { - return None; + if !directed { + return Err(MaterialSolveErrorV1::UnsupportedDirectedSearchRelation { pole }); } - let worst_at = |alpha: f64| { - let (lo, hi) = band_luminance(tone_q, alpha, backdrop); - worst_contrast_of_band(pole, lo, hi) - }; // α = 1: полоса вырождается в L(tone) → худший контраст = контраст полюса на // тоне (солид-канон). Если и он ниже пола — честная деградация (ближайшее // достижимое), гарантия не выполнена. - let opaque = worst_at(1.0); + let opaque = worst_at(1.0)?; if opaque < floor_ratio { - return Some(MaterialAlpha { + return Ok(MaterialAlpha { alpha: 1.0, worst_contrast: opaque, pole, - degraded: true, + status: MaterialAlphaStatusV1::Degraded, + guarantee: MaterialAlphaGuaranteeV1::OpaqueEndpointCharacterizedV1 { + numerical_profile: + MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1, + }, }); } - // Бисекция минимальной годной α. lo держит НЕгодную сторону, hi — годную. + // Binary partition with a fixed number of steps: lo is rechecked failing and + // hi rechecked passing after every update. No global-first/minimum claim is + // made for the discontinuous legacy predicate. let (mut lo, mut hi) = (0.0_f64, 1.0_f64); - for _ in 0..60 { + for _ in 0..MATERIAL_ALPHA_BISECTION_ITERATIONS { let mid = 0.5 * (lo + hi); - if worst_at(mid) >= floor_ratio { + if worst_at(mid)? >= floor_ratio { hi = mid; } else { lo = mid; } } - Some(MaterialAlpha { + let worst_contrast = worst_at(hi)?; + let lower_contrast = worst_at(lo)?; + validate_rechecked_bracket(lower_contrast, worst_contrast, floor_ratio)?; + Ok(MaterialAlpha { alpha: hi, - worst_contrast: worst_at(hi), + worst_contrast, pole, - degraded: false, + status: MaterialAlphaStatusV1::Satisfied, + guarantee: MaterialAlphaGuaranteeV1::BisectionBracketCharacterizedV1 { + iterations: MATERIAL_ALPHA_BISECTION_ITERATIONS, + lower_alpha: lo, + upper_alpha: hi, + numerical_profile: + MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1, + }, }) } @@ -295,11 +830,14 @@ pub fn solve_material_alpha_encoded( /// /// # Errors /// -/// `Err` при невалидном hex или `floor_ratio` вне `[1, 21]`. -pub fn solve_material_alpha_hex(tone_hex: &str, floor_ratio: f64) -> Result { - let tone = srgb_encoded_from_hex(tone_hex)?; +/// Типизированная [`MaterialSolveErrorV1`] при невалидном hex/floor. +pub fn solve_material_alpha_hex( + tone_hex: &str, + floor_ratio: f64, +) -> Result { + let tone = srgb_encoded_from_hex(tone_hex) + .map_err(|reason| MaterialSolveErrorV1::InvalidToneHex { reason })?; solve_material_alpha_encoded(tone, &BackdropBox::FULL, floor_ratio) - .ok_or_else(|| format!("floor_ratio вне домена [1,21]: {floor_ratio}")) } #[cfg(test)] @@ -307,6 +845,7 @@ mod tests { use super::*; use crate::alpha::composite_over_encoded; use crate::wcag::contrast_ratio; + use proptest::prelude::*; const AA_TEXT: f64 = 4.5; @@ -333,78 +872,89 @@ mod tests { } } - /// На выведенной α худший фон держит пол, а ЧУТЬ НИЖЕ α — рвёт его: α есть - /// ТОЧНАЯ граница годности (реальный солвер-пол, не подгонка). + /// Выбранный верхний кандидат повторно держит floor в объявленном runtime. + /// Отдельная проба на 2% ниже делает fixture невакуумным, но не выдаётся за + /// доказательство predecessor или минимальности без sound bound для `powf`. #[test] - fn solved_alpha_is_the_exact_floor_boundary() { + fn selected_alpha_rechecks_and_fixture_has_a_non_vacuous_threshold() { for hex in ["#E4E4E6", "#B0B0B8", "#35353A", "#2A2A30", "#5C5C5C"] { let m = solve_material_alpha_hex(hex, AA_TEXT).unwrap(); - assert!(!m.degraded, "{hex}: неожиданная деградация на AA"); - assert!(m.alpha > 0.0 && m.alpha <= 1.0, "{hex}: α вне (0,1]"); - let pole = m.pole; + assert!(!m.degraded(), "{hex}: неожиданная деградация на AA"); + assert!(m.alpha() > 0.0 && m.alpha() <= 1.0, "{hex}: α вне (0,1]"); + let pole = m.pole(); let at = |a: f64| worst_contrast_encoded(enc(hex), a, &BackdropBox::FULL, pole).unwrap(); - // На α держит пол (с крошечным допуском на брекет бисекции). + // Финальная повторная runtime-проверка обязана держать floor. assert!( - at(m.alpha) >= AA_TEXT - 1e-9, + at(m.alpha()) >= AA_TEXT - 1e-9, "{hex}: на α={} худший контраст {} < пола", - m.alpha, - at(m.alpha) + m.alpha(), + at(m.alpha()) ); - // Чуть ниже α — рвёт (если α ещё не у самого пола нуля). - if m.alpha > 1e-3 { + // Грубая anti-vacuum характеризация, а не сертификат минимальности. + if m.alpha() > 1e-3 { assert!( - at(m.alpha * 0.98) < AA_TEXT, - "{hex}: годно НИЖЕ α={} — граница не минимальна", - m.alpha + at(m.alpha() * 0.98) < AA_TEXT, + "{hex}: 2%-lower anti-vacuum probe unexpectedly passes for α={}", + m.alpha() ); } } } - /// Худший контраст монотонно НЕ убывает по α (полоса стягивается к тону) — - /// инвариант, на котором стоит бисекция. Свип на тонах обеих полярностей. + /// Frozen legacy EOTF имеет downward seam, поэтому даже направленный + /// material path нельзя объявлять глобально монотонным по alpha. #[test] - fn worst_contrast_is_monotone_in_alpha() { - for hex in ["#EDEDEF", "#C8C8CE", "#9A9AA2", "#40404A", "#1C1C1F"] { - let tone = enc(hex); - let pole = committed_pole_encoded(tone).unwrap(); - let mut prev = 0.0; - for i in 1..=100 { - let a = f64::from(i) / 100.0; - let w = worst_contrast_encoded(tone, a, &BackdropBox::FULL, pole).unwrap(); - assert!( - w >= prev - 1e-9, - "{hex}: худший контраст упал на α={a}: {w} < {prev}" - ); - prev = w; - } - } + fn legacy_eotf_seam_rejects_global_alpha_monotonicity() { + let alpha = 0.039_28_f64; + let next_alpha = f64::from_bits(alpha.to_bits() + 1); + let at = material_channel_over_byte_scale(255.0, alpha, 0.0); + let after = material_channel_over_byte_scale(255.0, next_alpha, 0.0); + assert_eq!(at.to_bits(), alpha.to_bits()); + assert_eq!(after.to_bits(), next_alpha.to_bits()); + + let before_luminance = relative_luminance([at; 3]); + let after_luminance = relative_luminance([after; 3]); + assert!( + after_luminance < before_luminance, + "legacy seam witness stopped rejecting global monotonicity" + ); } - /// Гарантия воспроизводима из эмитированных значений (гарантия (c) контракта): - /// композит квантованного тинта над чёрным/белым точно даёт худший контраст, - /// совпадающий с вердиктом солвера — потребитель пересчитает то же число. + /// Гарантия проверяема из эмитированных значений: dense independent backdrop + /// probes используют официальный scalar compositor, а conservative verdict + /// ядра не превышает ни один измеренный probe. #[test] fn guarantee_recomputable_from_emitted_tint() { for hex in ["#E9E9EB", "#A0A0A8", "#313135"] { let m = solve_material_alpha_hex(hex, AA_TEXT).unwrap(); let tint_q = quantise(enc(hex)); - // Пересчёт «в лоб» как у потребителя: композит над двумя углами. - let over_black = composite_over_encoded(tint_q, m.alpha, [0.0; 3]) - .expect("решённый материал лежит в домене композитора"); - let over_white = composite_over_encoded(tint_q, m.alpha, [1.0; 3]) - .expect("решённый материал лежит в домене композитора"); - let pole_lum = m.pole.luminance(); - let recomputed = ratio_from_luminances(pole_lum, relative_luminance(over_black)).min( - ratio_from_luminances(pole_lum, relative_luminance(over_white)), - ); + let pole_lum = m.pole().luminance(); + let probes = [0.0, 0.039_28, 0.039_280_000_000_000_01, 0.5, 1.0]; + let mut measured_min = f64::INFINITY; + for red in probes { + for green in probes { + for blue in probes { + let background = [red, green, blue]; + let composite = core::array::from_fn(|channel| { + material_channel_over_byte_scale( + (tint_q[channel] * 255.0).round(), + m.alpha(), + background[channel], + ) + }); + let measured = + ratio_from_luminances(pole_lum, relative_luminance(composite)); + measured_min = measured_min.min(measured); + } + } + } assert!( - (recomputed - m.worst_contrast).abs() < 1e-9, - "{hex}: пересчёт {recomputed} != вердикту {}", - m.worst_contrast + m.worst_contrast() <= measured_min, + "{hex}: conservative verdict {} exceeded measured probe {measured_min}", + m.worst_contrast() ); - assert!(recomputed >= AA_TEXT - 1e-9, "{hex}: пересчёт ниже пола"); + assert!(m.worst_contrast() >= AA_TEXT, "{hex}: verdict ниже пола"); } } @@ -441,49 +991,54 @@ mod tests { let g = f64::from(i) / 255.0; let m = solve_material_alpha_encoded([g, g, g], &BackdropBox::FULL, AA_TEXT).unwrap(); assert!( - !m.degraded, + !m.degraded(), "серый {i}: AA обязан быть разрешим без деградации" ); - assert!(m.alpha > 0.0 && m.alpha <= 1.0); + assert!(m.alpha() > 0.0 && m.alpha() <= 1.0); } // Насыщенные тоны разных светлот/оттенков — не только серые. for hex in [ "#3E87FF", "#FF3B30", "#34C759", "#FFCC00", "#AF52DE", "#007AFF", "#B03030", "#0A3A6B", ] { let m = solve_material_alpha_hex(hex, AA_TEXT).unwrap(); - assert!(!m.degraded, "{hex}: AA обязан быть разрешим без деградации"); - assert!(m.alpha > 0.0 && m.alpha <= 1.0); + assert!( + !m.degraded(), + "{hex}: AA обязан быть разрешим без деградации" + ); + assert!(m.alpha() > 0.0 && m.alpha() <= 1.0); } } - /// Guard монотонности: узкий коридор с тоном ВНЕ короба со стороны полюса - /// (где худший контраст НЕ монотонен по α) честно отвергается солвером — а не - /// возвращает ложную α/деградацию. Контрпример из независимой верификации: + /// Directed-search guard: узкий коридор с тоном ВНЕ короба со стороны полюса + /// честно отвергается, а не получает произвольную alpha/degradation. + /// Контрпример из независимой верификации: /// тон `#8A8A8A` (Lum≈0.25, чёрный полюс) над коридором `[#B3B3B3, белый]` /// (все фоны СВЕТЛЕЕ тона) — `tone < min`, немонотонно. #[test] fn guard_rejects_non_monotone_narrow_corridor() { let tone = enc("#8A8A8A"); - assert_eq!(committed_pole_encoded(tone), Some(Pole::Black)); - let bad = BackdropBox { - min: enc("#B3B3B3"), - max: [1.0; 3], - }; - // Немонотонный домен → честный None, не ложная деградация. + assert_eq!(committed_pole_encoded(tone), Ok(Pole::Black)); + let bad = BackdropBox::try_new(enc("#B3B3B3"), [1.0; 3]).unwrap(); + // Неподдерживаемое направление → typed error, не ложная деградация. + assert!(matches!( + solve_material_alpha_encoded(tone, &bad, 15.0), + Err(MaterialSolveErrorV1::UnsupportedDirectedSearchRelation { pole: Pole::Black }) + )); + let transparent = solve_material_alpha_encoded(tone, &bad, 1.0) + .expect("transparent endpoint does not require monotone threshold search"); + assert_eq!(transparent.alpha().to_bits(), 0.0_f64.to_bits()); + assert_eq!(transparent.status(), MaterialAlphaStatusV1::Satisfied); + assert!(matches!( + transparent.guarantee(), + MaterialAlphaGuaranteeV1::TransparentEndpointCharacterizedV1 { .. } + )); + // worst_contrast_encoded сам не применяет selection guard. + assert!(worst_contrast_encoded(tone, 0.5, &bad, Pole::Black).is_ok()); + // Поддерживаемое направление (тон ВНУТРИ короба, tone ≥ min) — решается. + let good = BackdropBox::try_new(enc("#202020"), [1.0; 3]).unwrap(); assert!( - solve_material_alpha_encoded(tone, &bad, 7.0).is_none(), - "тон вне короба со стороны полюса обязан быть отвергнут (немонотонно)" - ); - // Но worst_contrast_encoded НЕ предполагает монотонность — считает верно. - assert!(worst_contrast_encoded(tone, 0.5, &bad, Pole::Black).is_some()); - // Монотонный узкий коридор (тон ВНУТРИ короба, tone ≥ min) — решается. - let good = BackdropBox { - min: enc("#202020"), - max: [1.0; 3], - }; - assert!( - solve_material_alpha_encoded(tone, &good, AA_TEXT).is_some(), - "тон в коробе — монотонный домен, обязан решиться" + solve_material_alpha_encoded(tone, &good, AA_TEXT).is_ok(), + "тон в коробе — поддерживаемое направление, обязан решиться" ); } @@ -512,14 +1067,23 @@ mod tests { // Средний тон: полюс максимального контраста ≈ 4.6, ниже 7:1. let m = solve_material_alpha_encoded([0.42, 0.42, 0.42], &BackdropBox::FULL, 7.0).unwrap(); assert!( - m.degraded, + m.degraded(), "средний тон обязан деградировать на AAA-поле 7:1" ); assert_eq!( - m.alpha, 1.0, + m.alpha(), + 1.0, "деградация возвращает ближайшую достижимую α=1" ); - assert!(m.worst_contrast < 7.0); + assert!(m.worst_contrast() < 7.0); + assert_eq!(m.status(), MaterialAlphaStatusV1::Degraded); + assert!(matches!( + m.guarantee(), + MaterialAlphaGuaranteeV1::OpaqueEndpointCharacterizedV1 { + numerical_profile: + MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1, + } + )); } /// Узкий коридор требует МЕНЬШЕ плотности, чем полный [чёрный, белый]: @@ -529,16 +1093,13 @@ mod tests { let tone = enc("#C8C8CE"); let full = solve_material_alpha_encoded(tone, &BackdropBox::FULL, AA_TEXT).unwrap(); // Узкий светлый коридор: фон только в [#C0.., #FF..]. - let narrow = BackdropBox { - min: enc("#C0C0C0"), - max: [1.0; 3], - }; + let narrow = BackdropBox::try_new(enc("#C0C0C0"), [1.0; 3]).unwrap(); let m = solve_material_alpha_encoded(tone, &narrow, AA_TEXT).unwrap(); assert!( - m.alpha <= full.alpha + 1e-12, + m.alpha() <= full.alpha() + 1e-12, "узкий коридор потребовал не меньше полного: {} vs {}", - m.alpha, - full.alpha + m.alpha(), + full.alpha() ); } @@ -546,56 +1107,415 @@ mod tests { /// обещанием разрешимости). #[test] fn out_of_domain_is_rejected() { - let reversed = BackdropBox { - min: [1.0, 0.0, 0.0], - max: [0.0, 1.0, 1.0], + let point = BackdropBox::try_new([0.25, 0.5, 0.75], [0.25, 0.5, 0.75]) + .expect("degenerate box remains valid"); + assert!(worst_contrast_encoded([1.0; 3], 0.5, &point, Pole::Black).is_ok()); + + assert!(matches!( + solve_material_alpha_encoded([1.5, 0.0, 0.0], &BackdropBox::FULL, AA_TEXT), + Err(MaterialSolveErrorV1::InvalidTone( + EncodedRgbErrorV1::OutOfRangeChannel { + channel: RgbChannelV1::Red, + value: 1.5, + } + )) + )); + assert!(matches!( + solve_material_alpha_encoded([f64::NAN, 0.5, 0.5], &BackdropBox::FULL, AA_TEXT), + Err(MaterialSolveErrorV1::InvalidTone( + EncodedRgbErrorV1::NonFiniteChannel { + channel: RgbChannelV1::Red, + } + )) + )); + for floor in [0.5, 25.0, f64::NAN] { + assert!(matches!( + solve_material_alpha_encoded([0.5; 3], &BackdropBox::FULL, floor), + Err(MaterialSolveErrorV1::InvalidFloorRatio { value }) + if value.to_bits() == floor.to_bits() + )); + } + assert!(matches!( + committed_pole_encoded([2.0, 0.0, 0.0]), + Err(MaterialSolveErrorV1::InvalidTone(_)) + )); + assert!(matches!( + worst_contrast_encoded([0.5; 3], 1.5, &BackdropBox::FULL, Pole::Black), + Err(MaterialSolveErrorV1::InvalidAlpha { value: 1.5 }) + )); + assert!(matches!( + solve_material_alpha_hex("нет", AA_TEXT), + Err(MaterialSolveErrorV1::InvalidToneHex { .. }) + )); + } + + #[test] + fn backdrop_box_constructor_reports_distinct_invariant_failures() { + assert!(matches!( + BackdropBox::try_new([1.0, 0.0, 0.0], [0.0, 1.0, 1.0]), + Err(BackdropBoxErrorV1::ReversedChannel { + channel: RgbChannelV1::Red, + min: 1.0, + max: 0.0, + }) + )); + assert!(matches!( + BackdropBox::try_new([0.0; 3], [f64::NAN, 1.0, 1.0]), + Err(BackdropBoxErrorV1::NonFiniteChannel { + bound: BackdropBoundV1::Max, + channel: RgbChannelV1::Red, + }) + )); + assert!(matches!( + BackdropBox::try_new([-0.01, 0.0, 0.0], [1.0; 3]), + Err(BackdropBoxErrorV1::OutOfRangeChannel { + bound: BackdropBoundV1::Min, + channel: RgbChannelV1::Red, + value: -0.01, + }) + )); + + let point = BackdropBox::try_new([0.25, 0.5, 0.75], [0.25, 0.5, 0.75]) + .expect("degenerate ordered box is a valid reachable set"); + assert_eq!(point.min(), [0.25, 0.5, 0.75]); + assert_eq!(point.max(), [0.25, 0.5, 0.75]); + } + + #[test] + fn rechecked_bracket_postcondition_rejects_each_invalid_side_independently() { + assert!(validate_rechecked_bracket(2.99, 3.0, 3.0).is_ok()); + for (lower, upper) in [ + (3.0, 3.0), + (3.01, 3.0), + (f64::NAN, 3.0), + (2.99, 2.99), + (2.99, f64::NAN), + ] { + assert_eq!( + validate_rechecked_bracket(lower, upper, 3.0), + Err(MaterialSolveErrorV1::NumericallyIndeterminate), + "lower={lower}, upper={upper}", + ); + } + } + + #[test] + fn material_public_api_has_typed_failures_and_explicit_numerical_guarantee() { + assert!(matches!( + worst_contrast_encoded([0.5; 3], f64::NAN, &BackdropBox::FULL, Pole::Black), + Err(MaterialSolveErrorV1::InvalidAlpha { .. }) + )); + assert!(matches!( + solve_material_alpha_encoded([f64::NAN, 0.5, 0.5], &BackdropBox::FULL, AA_TEXT), + Err(MaterialSolveErrorV1::InvalidTone( + EncodedRgbErrorV1::NonFiniteChannel { + channel: RgbChannelV1::Red, + } + )) + )); + assert!(matches!( + solve_material_alpha_encoded([0.5; 3], &BackdropBox::FULL, 25.0), + Err(MaterialSolveErrorV1::InvalidFloorRatio { value: 25.0 }) + )); + + let non_monotone = BackdropBox::try_new(enc("#B3B3B3"), [1.0; 3]).unwrap(); + assert!(matches!( + solve_material_alpha_encoded(enc("#8A8A8A"), &non_monotone, 15.0), + Err(MaterialSolveErrorV1::UnsupportedDirectedSearchRelation { pole: Pole::Black }) + )); + + let solved = solve_material_alpha_hex("#E4E4E6", AA_TEXT).unwrap(); + assert_eq!(solved.status(), MaterialAlphaStatusV1::Satisfied); + let (lower_alpha, upper_alpha) = match solved.guarantee() { + MaterialAlphaGuaranteeV1::BisectionBracketCharacterizedV1 { + iterations: 60, + lower_alpha, + upper_alpha, + numerical_profile: + MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1, + } => (lower_alpha, upper_alpha), + other => panic!("expected characterized threshold bracket, got {other:?}"), }; - assert!(worst_contrast_encoded([1.0; 3], 0.5, &reversed, Pole::Black).is_none()); - assert!(solve_material_alpha_encoded([1.0; 3], &reversed, AA_TEXT).is_none()); + assert_eq!(solved.alpha().to_bits(), upper_alpha.to_bits()); + assert!( + worst_contrast_encoded( + enc("#E4E4E6"), + lower_alpha, + &BackdropBox::FULL, + solved.pole(), + ) + .unwrap() + < AA_TEXT + ); + let recomputed = worst_contrast_encoded( + enc("#E4E4E6"), + solved.alpha(), + &BackdropBox::FULL, + solved.pole(), + ) + .unwrap(); + assert_eq!(recomputed.to_bits(), solved.worst_contrast().to_bits()); + assert!(recomputed >= AA_TEXT); - for invalid_endpoint in [ - BackdropBox { - min: [0.0; 3], - max: [f64::NAN, 1.0, 1.0], - }, - BackdropBox { - min: [0.0; 3], - max: [1.01, 1.0, 1.0], - }, - BackdropBox { - min: [f64::NAN, 0.0, 0.0], - max: [1.0; 3], - }, - BackdropBox { - min: [-0.01, 0.0, 0.0], - max: [1.0; 3], - }, + let transparent = solve_material_alpha_hex("#E4E4E6", 1.0).unwrap(); + assert_eq!(transparent.alpha().to_bits(), 0.0_f64.to_bits()); + assert_eq!(transparent.status(), MaterialAlphaStatusV1::Satisfied); + assert!(matches!( + transparent.guarantee(), + MaterialAlphaGuaranteeV1::TransparentEndpointCharacterizedV1 { + numerical_profile: + MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1, + } + )); + assert!(transparent.worst_contrast() >= 1.0); + } + + #[test] + fn selected_alpha_rechecks_in_the_official_byte_scale_affine_order() { + for (tone_hex, floor, alpha_bits) in [ + ("#020202", 3.0, 0x3fda_d867_f596_0b7c), + ("#000000", 7.0, 0x3fe4_d36f_0c15_b3eb), + ("#000000", 19.7963, 0x3fee_be38_c41e_493b), ] { - assert!(!invalid_endpoint.is_valid()); + let solved = solve_material_alpha_hex(tone_hex, floor).unwrap(); + assert_eq!(solved.alpha().to_bits(), alpha_bits); + let tint = quantise(enc(tone_hex)); + let composite = |background: [f64; 3]| { + core::array::from_fn(|channel| { + let tint_byte = (tint[channel] * 255.0).round(); + let background_byte_scale = background[channel] * 255.0; + (background_byte_scale + solved.alpha() * (tint_byte - background_byte_scale)) + / 255.0 + }) + }; + let lo = relative_luminance(composite([0.0; 3])); + let hi = relative_luminance(composite([1.0; 3])); + let consumer_worst = worst_contrast_of_band(solved.pole(), lo, hi); assert!( - worst_contrast_encoded([0.5; 3], 0.5, &invalid_endpoint, Pole::Black).is_none() + consumer_worst >= solved.worst_contrast(), + "{tone_hex}: conservative core bound {} exceeded endpoint consumer {consumer_worst}", + solved.worst_contrast() + ); + assert!( + consumer_worst >= floor, + "{tone_hex}: reported Satisfied state misses requested floor in the official consumer" ); - assert!(solve_material_alpha_encoded([0.5; 3], &invalid_endpoint, AA_TEXT).is_none()); } + } - let point = BackdropBox { - min: [0.25, 0.5, 0.75], - max: [0.25, 0.5, 0.75], - }; - assert!(worst_contrast_encoded([1.0; 3], 0.5, &point, Pole::Black).is_some()); + #[test] + fn valid_material_fixture_bits_are_characterized_across_the_typed_migration() { + let fixtures = [ + ( + "#E4E4E6", + 0x3fe0_4922_26de_cb0a, + 0x4012_0000_0000_0000, + Pole::Black, + ), + ( + "#B0B0B8", + 0x3fe5_0a37_d1ac_0292, + 0x4012_0000_0000_0000, + Pole::Black, + ), + ( + "#35353A", + 0x3fe5_a367_f8ad_9e98, + 0x4012_0000_0000_0001, + Pole::White, + ), + ( + "#2A2A30", + 0x3fe4_86ad_a430_0884, + 0x4012_0000_0000_0000, + Pole::White, + ), + ( + "#5C5C5C", + 0x3fea_c450_4ad4_a737, + 0x4012_0000_0000_0000, + Pole::White, + ), + ]; + for (hex, alpha_bits, worst_bits, pole) in fixtures { + let solved = solve_material_alpha_hex(hex, AA_TEXT).unwrap(); + assert_eq!(solved.alpha().to_bits(), alpha_bits, "{hex}: alpha drift"); + assert_eq!( + solved.worst_contrast().to_bits(), + worst_bits, + "{hex}: worst contrast drift" + ); + assert_eq!(solved.pole(), pole, "{hex}: pole drift"); + assert_eq!(solved.status(), MaterialAlphaStatusV1::Satisfied); + } + } + + #[test] + fn interior_wcag_eotf_seam_cannot_undercut_a_satisfied_full_box_result() { + let floor = 19.7963; + let solved = solve_material_alpha_hex("#000000", floor).unwrap(); + assert_eq!(solved.status(), MaterialAlphaStatusV1::Satisfied); + assert!(solved.worst_contrast() >= floor); + + // Допустимый interior backdrop переводит byte-scale composite ровно к + // discontinuity frozen legacy WCAG 2.1 (2018) split 0.03928. Старый + // corner-only band пропускал эту точку и завышал all-backdrops verdict. + let gray = 0.999_762_480_394_283_1; + let point = BackdropBox::try_new([gray; 3], [gray; 3]).unwrap(); + let actual = + worst_contrast_encoded(enc("#000000"), solved.alpha(), &point, solved.pole()).unwrap(); + assert!( + actual >= floor, + "full-box Satisfied undercut at interior EOTF seam: {actual} < {floor}" + ); + } + #[test] + fn channel_range_encloses_known_binary64_interior_jitter() { + let alpha = f64::from_bits(1.0_f64.to_bits() - 1); + let tint_byte = 2.0; + let interior_byte_scale = 0.137_643_568_434_543_95; + let interior_background = interior_byte_scale / 255.0; + let actual = material_channel_over_byte_scale(tint_byte, alpha, interior_background); + let endpoint_min = material_channel_over_byte_scale(tint_byte, alpha, 0.0) + .min(material_channel_over_byte_scale(tint_byte, alpha, 1.0)); assert!( - solve_material_alpha_encoded([1.5, 0.0, 0.0], &BackdropBox::FULL, AA_TEXT).is_none() + actual < endpoint_min, + "fixture must reject endpoint-only affine monotonicity" ); + + let (lo, hi) = material_channel_range(tint_byte, alpha, 0.0, 1.0); assert!( - solve_material_alpha_encoded([f64::NAN, 0.5, 0.5], &BackdropBox::FULL, AA_TEXT) - .is_none() + lo <= actual && actual <= hi, + "{actual} outside [{lo}, {hi}]" ); - assert!(solve_material_alpha_encoded([0.5; 3], &BackdropBox::FULL, 0.5).is_none()); - assert!(solve_material_alpha_encoded([0.5; 3], &BackdropBox::FULL, 25.0).is_none()); - assert!(solve_material_alpha_encoded([0.5; 3], &BackdropBox::FULL, f64::NAN).is_none()); - assert!(committed_pole_encoded([2.0, 0.0, 0.0]).is_none()); - assert!(worst_contrast_encoded([0.5; 3], 1.5, &BackdropBox::FULL, Pole::Black).is_none()); - assert!(solve_material_alpha_hex("нет", AA_TEXT).is_err()); + } + + #[test] + fn composite_range_margin_dominates_pairwise_error_and_outward_rounding() { + let single = std::hint::black_box(MATERIAL_COMPOSITE_SINGLE_EVALUATION_ERROR_BOUND); + let pairwise = std::hint::black_box(MATERIAL_COMPOSITE_PAIRWISE_ERROR_BOUND); + let outward = std::hint::black_box(MATERIAL_COMPOSITE_OUTWARD_ROUNDING_BOUND); + let margin = std::hint::black_box(MATERIAL_COMPOSITE_RANGE_MARGIN); + assert!(single < 3.90e-16); + assert_eq!(pairwise, 2.0 * single); + let required = pairwise + outward; + assert!(margin > required); + assert!(margin > 1.9 * required); + } + + #[test] + fn scalar_compositor_can_decrease_across_adjacent_backgrounds_but_range_encloses_both() { + let tint_byte = 228.0; + let alpha = 0.618_654_751_765_414_3; + let byte_scale = 73.103_401_561_508_89_f64; + let next_byte_scale = f64::from_bits(byte_scale.to_bits() + 1); + let background = byte_scale / 255.0; + let next_background = next_byte_scale / 255.0; + let at = material_channel_over_byte_scale(tint_byte, alpha, background); + let next = material_channel_over_byte_scale(tint_byte, alpha, next_background); + assert!( + next < at, + "fixture must reject binary64 background monotonicity" + ); + + let (lo, hi) = material_channel_range(tint_byte, alpha, background, next_background); + assert!(lo <= at && at <= hi); + assert!(lo <= next && next <= hi); + } + + #[test] + fn opaque_endpoint_is_exact_tint_identity() { + for tint_byte in [0.0, 1.0, 2.0, 127.0, 228.0, 254.0, 255.0] { + let expected = tint_byte / 255.0; + let (lo, hi) = material_channel_range(tint_byte, 1.0, 0.0, 1.0); + assert_eq!(lo.to_bits(), expected.to_bits()); + assert_eq!(hi.to_bits(), expected.to_bits()); + for background in [0.0, f64::EPSILON, 0.137_643_568_434_543_95, 0.5, 1.0] { + assert_eq!( + material_channel_over_byte_scale(tint_byte, 1.0, background).to_bits(), + expected.to_bits(), + "tint={tint_byte}, background={background}" + ); + } + } + } + + #[test] + fn extreme_opaque_endpoints_hold_exact_twentyone_to_one() { + for hex in ["#000000", "#FFFFFF"] { + let solved = solve_material_alpha_hex(hex, 21.0).unwrap(); + assert_eq!(solved.status(), MaterialAlphaStatusV1::Satisfied, "{hex}"); + assert_eq!(solved.alpha().to_bits(), 1.0_f64.to_bits(), "{hex}"); + assert_eq!( + solved.worst_contrast().to_bits(), + 21.0_f64.to_bits(), + "{hex}" + ); + } + } + + #[test] + fn point_and_ordered_boxes_preserve_characterized_material_result_bits() { + let tone = enc("#C8C8CE"); + let fixtures = [ + ( + BackdropBox::try_new(enc("#202020"), enc("#202020")).unwrap(), + 0x3fdf_f53f_9eff_0967, + 0x4012_0000_0000_0000, + ), + ( + BackdropBox::try_new(enc("#303030"), enc("#A0A0A0")).unwrap(), + 0x3fdc_9853_0c20_50b1, + 0x4012_0000_0000_0000, + ), + ]; + + for (backdrop, alpha_bits, worst_bits) in fixtures { + let solved = solve_material_alpha_encoded(tone, &backdrop, AA_TEXT).unwrap(); + assert_eq!(solved.alpha().to_bits(), alpha_bits); + assert_eq!(solved.worst_contrast().to_bits(), worst_bits); + assert_eq!(solved.pole(), Pole::Black); + assert_eq!(solved.status(), MaterialAlphaStatusV1::Satisfied); + } + } + + proptest! { + #[test] + fn backdrop_constructor_accepts_exactly_finite_ordered_encoded_boxes( + min in proptest::array::uniform3(any::()), + max in proptest::array::uniform3(any::()), + ) { + let expected = min.into_iter().chain(max).all(|value| { + value.is_finite() && (0.0..=1.0).contains(&value) + }) && (0..3).all(|channel| min[channel] <= max[channel]); + prop_assert_eq!(BackdropBox::try_new(min, max).is_ok(), expected); + } + + #[test] + fn material_channel_range_encloses_official_scalar_compositor( + tint_byte in any::(), + alpha in 0.0_f64..1.0_f64, + background_a in 0.0_f64..1.0_f64, + background_b in 0.0_f64..1.0_f64, + position in 0.0_f64..1.0_f64, + ) { + let background_lo = background_a.min(background_b); + let background_hi = background_a.max(background_b); + let background = background_lo + position * (background_hi - background_lo); + let tint_byte = f64::from(tint_byte); + let actual = material_channel_over_byte_scale(tint_byte, alpha, background); + let (lo, hi) = material_channel_range( + tint_byte, + alpha, + background_lo, + background_hi, + ); + prop_assert!(lo.is_finite() && hi.is_finite()); + prop_assert!((0.0..=1.0).contains(&lo)); + prop_assert!((0.0..=1.0).contains(&hi)); + prop_assert!(lo <= actual, "{actual} below {lo}"); + prop_assert!(actual <= hi, "{actual} above {hi}"); + } } } diff --git a/crates/labcolors-core/src/numerics.rs b/crates/labcolors-core/src/numerics.rs index 06ac92b7..22504572 100644 --- a/crates/labcolors-core/src/numerics.rs +++ b/crates/labcolors-core/src/numerics.rs @@ -150,7 +150,7 @@ define_numerical_registry_v1! { boundary_corpus: "glow stable-indeterminate; exact no-op; finite-state compositor; half-tie alpha", runtime_matrix: "active: native x86_64 + wasm32; native arm64 required before any cross-runtime CAM16 decision claim; exact bytes only for compositor", fallback_status: NumericalFallbackStatusV1::None, - legacy_profile: Some("legacy-platform-dependent-v1"), + legacy_profile: Some(crate::glow::GlowDecisionProfileV1::LegacyPlatformDependentV1.key()), }, } @@ -372,7 +372,8 @@ mod tests { ] && row.bound_status == NumericalBoundStatusV1::Unavailable && row.fallback_status == NumericalFallbackStatusV1::None - && row.legacy_profile == Some("legacy-platform-dependent-v1") + && row.legacy_profile + == Some(crate::glow::GlowDecisionProfileV1::LegacyPlatformDependentV1.key()) })); for (index, row) in rows.iter().enumerate() { assert!(!row.operations.is_empty()); diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index 45ede901..e4cce41f 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -190,15 +190,16 @@ const DECORATIVE_FLOOR_MIN: f64 = 7.5; /// меняет ни одного реального эмитируемого контраста. Ре-аудит /// `science/reclassify-e-buckets` 2026-07-07 — реестр /// docs/empirical-inventory.md. -// Rust 1.85 не считает использование только в const-assert/test runtime-use; -// это provenance decomposition, а не отдельная production policy. +// Rust 1.85 не считает использованием обращение только из const-assert и тестов; +// это разложение provenance, а не отдельная production-политика. #[allow(dead_code)] // SSOT-TRACKED — квант-guard декоративного пола (issue #44), терминал (c) interval-insensitive, см. docs/empirical-inventory.md. const QUANT_GUARD: f64 = 0.2; // Компайл-тайм пиннинг деривации: DECORATIVE_FLOOR_MIN == MODEL_LC_FLOOR + -// QUANT_GUARD в пределах f64-шума суммирования. Это compile-time provenance -// lock; сам shipping-литерал 7.5 при этом НЕ меняется (байт-идентичность). +// QUANT_GUARD в пределах f64-шума суммирования. Это compile-time-фиксация +// provenance; сам поставляемый литерал 7.5 при этом НЕ меняется +// (байт-идентичность). const _: () = { let derived = crate::lpc::MODEL_LC_FLOOR + QUANT_GUARD; let d = DECORATIVE_FLOOR_MIN - derived; @@ -670,7 +671,7 @@ pub enum RoleSpec { tint: crate::ladder::LadderTint, /// Контрактная ступень стека. step: crate::glow::GlowStep, - /// Explicit numerical-decision profile из client contract. + /// Явный профиль численного решения из клиентского контракта. decision_profile: crate::glow::GlowDecisionProfileV1, }, /// Заливка пары ([`crate::pair`]): якорь источника, сдвинутый до победы @@ -740,8 +741,9 @@ pub enum RoleSpec { /// Резолв — [`Resolved::Material`] ([`crate::material`], #89). /// /// Тон строится dj-anchor-солвером на светлоте `tone` от фона резолва в - /// оттенке семьи; α выводится как минимальная плотность, при которой композит - /// над худшим фоном коридора держит `floor` (читаемость коммит-полюса). + /// оттенке семьи; α выбирается охарактеризованным для платформы поиском + /// с фиксированным числом шагов и повторно проверяется как проходящее + /// состояние для `floor`. Material { /// Оттенок семьи тона. `None` — нейтральный материал (подтон таблицы, то /// же 286°, что и остальные нейтральные эмиссии). `Some(tint)` — @@ -1565,12 +1567,14 @@ impl Default for RoleTable { } } -/// The outcome of resolving one role: a solved colour, an honest zero, or a -/// principled reason it is unreachable on this background. +/// The outcome of resolving one role: a solved colour, an honest zero, a typed +/// numerical indeterminacy, or a failure reason. /// -/// Unreachability is surfaced per role, never masked — a role on an extreme +/// Physical unreachability is surfaced per role, never masked — a role on an extreme /// background (e.g. muted text on a mid-grey that cannot supply enough contrast) /// returns [`Unreachable`], it is not silently clipped to a wrong colour. +/// [`Unreachable::InternalInvariant`] has different provenance: bindings must +/// turn it into a whole-call internal/incompatible-contract error. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum Resolved { @@ -1603,8 +1607,8 @@ pub enum Resolved { /// Свечение: screen-слои (core, halo) + решённая интенсивность /// (labui ADR-0002 §5). Потребитель красит слои с `mix-blend-mode: screen`. Glow(GlowResolved), - /// Stable Glow request, для которого sound numerical bound отсутствует: - /// semantic winner не выбран и CSS emission отсутствует. + /// Стабильный запрос Glow, для которого отсутствует sound численная граница: + /// семантический победитель не выбран, CSS-эмиссия отсутствует. GlowIndeterminate(GlowIndeterminateResolved), /// Двухслойный материал (стекло/акрил): полупрозрачный тинт `01` + опаковая /// база `02`, обе — один тон, с ВЫВЕДЕННОЙ альфой (композит-гарантия над @@ -1612,7 +1616,9 @@ pub enum Resolved { Material(MaterialResolved), /// The honest zero of the `Role::None` token: no colour, no contrast. None, - /// No colour can satisfy this role against this background, with the reason. + /// A physical/domain failure, or an internal core invariant failure. Bindings + /// project only physical variants as per-role outcomes and fail the whole + /// call closed for [`Unreachable::InternalInvariant`]. Unreachable(Unreachable), } @@ -1622,8 +1628,8 @@ pub enum Resolved { /// Потребитель красит `--lab-{role}: rgba(tint, α)` — браузер композитит на /// фактической подложке. `composite` — то, во что этот rgba складывается на /// ФОНЕ РЕЗОЛВА (`α·tint + (1−α)·bg`) в объявленном encoded-sRGB8 reference- -/// профиле, заземлённом Figma-якорями, но не выдаваемом за универсальный browser -/// pipeline; его контраст ([`TranslucentResolved::composite_lc`], +/// профиле, заземлённом Figma-якорями, но не выдаваемом за универсальный +/// браузерный pipeline; его контраст ([`TranslucentResolved::composite_lc`], /// [`composite_wcag`](TranslucentResolved::composite_wcag)) — то, что фаза 1 AA меряет /// (контраст полупрозрачной роли определён её композитом, не тинтом). На ином /// фоне композит другой — это и есть смысл альфы; гарантия сформулирована для @@ -1632,7 +1638,7 @@ pub enum Resolved { pub struct TranslucentResolved { /// Тинт `#RRGGBB` — цвет, эмитируемый как `rgba(tint, α)` (без учёта α). tint_hex: String, - /// Фактическая α `(0, 1]` — запрошенная, если существует точный byte-тинт, + /// Фактическая α `(0, 1]` — запрошенная, если существует точный байтовый тинт, /// иначе первый проходящий `binary64` (у прямой лестницы = альфа позиции). alpha: f64, /// Солид-композит `rgba(tint, α)` над фоном резолва, `#RRGGBB`. @@ -1650,7 +1656,8 @@ pub struct TranslucentResolved { /// молча). Параметр-свободный замер: сетка дисплея, не политика. composite_distinct: bool, /// Запрошенная α была поднята до первого разрешимого sRGB8-значения, потому - /// что ни один byte-тинт не воспроизводит цель — честный флаг деградации КОНТРАКТА + /// что ни один байтовый тинт не воспроизводит цель — честный флаг деградации + /// КОНТРАКТА /// РОЛИ (симметрия с `compressed`/`degraded`). Ставится только на пути /// альфа-аналога ([`resolve_rgba_inverted`]), где солид-цель фиксирована, а /// α выводится; у прямой лестницы ([`resolve_rgba_direct`]) всегда `false`. @@ -1725,9 +1732,11 @@ impl TranslucentResolved { /// Слои — [`crate::glow::glow_layers_from_source`] (halo = источник, core = /// пересвет); α — [`crate::glow::solve_screen_alpha_for_dj`] под контрактную /// ступень на фактическом фоне; `degraded` — честный флаг закона 2 ADR-0002: -/// цель не держит ни один достижимый sRGB8-state, поэтому возвращён глобально -/// лучший state. На белом screen является point-no-op только в объявленном -/// reference-профиле — это не утверждение о физическом свечении. +/// цель не держит ни одно достижимое sRGB8-состояние, поэтому возвращено глобально +/// лучшее состояние. На белом screen является точечным no-op только в объявленном +/// reference-профиле — это не утверждение о физическом свечении. Recipe, +/// appearance-диагностика, диагностика выбора и точный сертификат композита +/// возвращаются раздельно: ни одно из них не повышает силу другого. #[derive(Debug, Clone, PartialEq)] pub struct GlowResolved { core_hex: String, @@ -1740,7 +1749,9 @@ pub struct GlowResolved { core_composite_hex: String, core_achieved_dj: f64, target_status: crate::glow::GlowTargetStatus, - diagnostic_profile: Option, + layer_recipe_profile: crate::glow::GlowLayerRecipeProfileV1, + appearance_diagnostic_profile: crate::glow::GlowDiagnosticProfileV1, + selection_diagnostic_profile: Option, decision_profile: crate::glow::GlowDecisionProfileV1, decision_guarantee: crate::numerics::DecisionGuaranteeV1, halo_composite_certificate: crate::glow::GlowCompositeCertificateV1, @@ -1768,19 +1779,29 @@ impl GlowResolved { pub fn target_dj(&self) -> f64 { self.target_dj } - /// Exact composite profile, отдельно от diagnostic/decision guarantee. + /// Точный профиль композита, отдельный от гарантии диагностики или решения. pub fn composite_profile(&self) -> crate::glow::GlowCompositeProfileV1 { self.halo_composite_certificate.profile() } - /// Diagnostic appearance profile, только если solve реально вызвал модель. - pub fn diagnostic_profile(&self) -> Option { - self.diagnostic_profile + /// Версионированный профиль, построивший анатомию core/halo. + pub fn layer_recipe_profile(&self) -> crate::glow::GlowLayerRecipeProfileV1 { + self.layer_recipe_profile } - /// Явно выбранный client numerical profile. + /// Appearance-профиль полного результата Glow. Он всегда присутствует: даже + /// выбор точного no-op вычисляет `core_achieved_dj` через CAM16-UCS J′. + pub fn appearance_diagnostic_profile(&self) -> crate::glow::GlowDiagnosticProfileV1 { + self.appearance_diagnostic_profile + } + /// Диагностический профиль, участвовавший именно в выборе target/max. + /// У стабильного точного no-op его нет. + pub fn selection_diagnostic_profile(&self) -> Option { + self.selection_diagnostic_profile + } + /// Явно выбранный клиентский численный профиль. pub fn decision_profile(&self) -> crate::glow::GlowDecisionProfileV1 { self.decision_profile } - /// Guarantee semantic target/max decision. + /// Гарантия семантического решения target/max. pub fn decision_guarantee(&self) -> crate::numerics::DecisionGuaranteeV1 { self.decision_guarantee } @@ -1808,25 +1829,29 @@ impl GlowResolved { pub fn core_achieved_dj(&self) -> f64 { self.core_achieved_dj } - /// Exact halo-composite evidence. + /// Точное свидетельство halo-композита. pub fn halo_composite_certificate(&self) -> &crate::glow::GlowCompositeCertificateV1 { &self.halo_composite_certificate } - /// Exact core-composite evidence. + /// Точное свидетельство core-композита. pub fn core_composite_certificate(&self) -> &crate::glow::GlowCompositeCertificateV1 { &self.core_composite_certificate } - /// Alias совместимости: прежнее поле измеряло именно halo. + /// Алиас совместимости: прежнее поле измеряло именно halo. pub fn achieved_dj(&self) -> f64 { self.halo_achieved_dj } - /// Alias совместимости прежнего boolean-контракта. + /// Алиас совместимости: `true` для точного и legacy-исходов недостижимости. pub fn degraded(&self) -> bool { - self.target_status == crate::glow::GlowTargetStatus::Unreachable + matches!( + self.target_status, + crate::glow::GlowTargetStatus::ExactNoopUnreachable + | crate::glow::GlowTargetStatus::LegacyUnreachable + ) } } -/// Stable Glow terminal outcome when no sound target/max bound exists. +/// Стабильный терминальный исход Glow при отсутствии sound-границы target/max. #[derive(Debug, Clone, PartialEq)] pub struct GlowIndeterminateResolved { source_hex: String, @@ -1837,27 +1862,27 @@ pub struct GlowIndeterminateResolved { } impl GlowIndeterminateResolved { - /// Канонический source/halo anchor; он не эмитится без decision. + /// Канонический source/halo-якорь; он не эмитится без решения. pub fn source_hex(&self) -> &str { &self.source_hex } - /// Запрошенная diagnostic target magnitude. + /// Запрошенный модуль диагностической цели. pub fn target_dj(&self) -> f64 { self.target_dj } - /// Explicit stable profile из client contract. + /// Явный стабильный профиль из клиентского контракта. pub fn decision_profile(&self) -> crate::glow::GlowDecisionProfileV1 { self.decision_profile } - /// Registered branch-sensitive site. + /// Зарегистрированный чувствительный к ветвлению участок. pub fn site_id(&self) -> crate::numerics::NumericalSiteIdV1 { self.site_id } - /// Неразделимая причина вместе с её sound interval, если он существует. + /// Неразделимая причина вместе с её sound-интервалом, если он существует. pub fn evidence(&self) -> crate::numerics::NumericalIndeterminacyV1 { self.evidence } - /// Target belongs to the halo point layer. + /// Цель относится к точечному слою halo. pub fn constraint_layer(&self) -> crate::glow::GlowConstraintLayer { crate::glow::GlowConstraintLayer::Halo } @@ -1873,19 +1898,21 @@ impl GlowIndeterminateResolved { /// [`base_hex`](Self::base_hex) и [`solid_hex`](Self::solid_hex) равны по /// построению (единственная решаемая величина — α). /// -/// Гарантия читаемости — свойство GLASS-режима (тинт над живым фоном): α выведена -/// как минимальная плотность, при которой коммит-полюс поверхности +/// Гарантия читаемости — свойство GLASS-режима (тинт над живым фоном): α — +/// повторно проверенный верхний кандидат, при котором коммит-полюс поверхности /// ([`pole`](Self::pole)) держит [`floor`](Self::floor) по всему коридору /// `[чёрный, белый]` ([`crate::material`]). [`worst_contrast`](Self::worst_contrast) /// и [`guaranteed`](Self::guaranteed) пересчитываемы потребителем из эмитированных -/// `01`/`02` (та же α-граничная математика, `material-guarantee.ts`). +/// `01`/`02`: ядро и официальный `packages/colors/effective-bg.js::compositeOver` +/// используют один byte-scale affine order `B + α·(T−B)`. #[derive(Debug, Clone, PartialEq)] pub struct MaterialResolved { tone_hex: String, alpha: f64, worst_contrast: f64, + alpha_guarantee: crate::material::MaterialAlphaGuaranteeV1, + alpha_status: crate::material::MaterialAlphaStatusV1, floor: f64, - guaranteed: bool, pole: crate::material::Pole, achieved_dj: f64, tone_compressed: bool, @@ -1912,7 +1939,7 @@ impl MaterialResolved { &self.tone_hex } - /// Выведенная альфа тинта `01`, `(0, 1]`. + /// Выбранная альфа тинта `01`, `[0, 1]`. pub fn alpha(&self) -> f64 { self.alpha } @@ -1923,16 +1950,26 @@ impl MaterialResolved { self.worst_contrast } - /// WCAG-пол читаемости, который держит выведенная α (напр. 4.5 / 3.0). + /// Численный класс выбора alpha-границы; точные утверждения композитора + /// намеренно отделены от этого охарактеризованного для платформы свидетельства. + pub fn alpha_guarantee(&self) -> crate::material::MaterialAlphaGuaranteeV1 { + self.alpha_guarantee + } + + /// Типизированный исход floor; недостижимость отделена от невалидного ввода. + pub fn alpha_status(&self) -> crate::material::MaterialAlphaStatusV1 { + self.alpha_status + } + + /// Запрошенный WCAG-пол (например 4.5 / 3.0). Он выполнен только при + /// [`MaterialAlphaStatusV1::Satisfied`](crate::material::MaterialAlphaStatusV1::Satisfied). pub fn floor(&self) -> f64 { self.floor } - /// Гарантия выполнена: `worst_contrast ≥ floor` (α нашлась в `(0, 1]`). - /// `false` — пол недостижим даже при α = 1 (тогда α = 1 как ближайшая - /// достижимая, честная деградация — не молчание). + /// Предикат совместимости поверх [`Self::alpha_status`]. pub fn guaranteed(&self) -> bool { - self.guaranteed + self.alpha_status == crate::material::MaterialAlphaStatusV1::Satisfied } /// Коммит-полюс поверхности: полюс максимального контраста на тоне (белый на @@ -2346,7 +2383,9 @@ fn resolve_spec_in( match crate::glow::glow_layers_from_source(&halo_hex, vc) { Ok(pair) => pair, Err(e) => { - return Resolved::Unreachable(Unreachable::InvalidInput(e)); + return Resolved::Unreachable(Unreachable::InternalInvariant( + format!("generated Glow layer recipe was rejected: {e}"), + )); } }; let core_measurement = match crate::glow::measure_screen_layer_at_alpha( @@ -2357,7 +2396,9 @@ fn resolve_spec_in( ) { Ok(measurement) => measurement, Err(e) => { - return Resolved::Unreachable(Unreachable::InvalidInput(e)); + return Resolved::Unreachable(Unreachable::InternalInvariant(format!( + "generated Glow core measurement was rejected: {e}" + ))); } }; Resolved::Glow(GlowResolved { @@ -2371,14 +2412,20 @@ fn resolve_spec_in( core_composite_hex: core_measurement.composite_hex, core_achieved_dj: core_measurement.achieved_dj, target_status: g.status(), - diagnostic_profile: g.diagnostic_profile(), + layer_recipe_profile: + crate::glow::GlowLayerRecipeProfileV1::Cam16JPrimeOklabCuspV1, + appearance_diagnostic_profile: + crate::glow::GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1, + selection_diagnostic_profile: g.selection_diagnostic_profile(), decision_profile, decision_guarantee: guarantee, halo_composite_certificate: g.composite_certificate().clone(), core_composite_certificate: core_measurement.certificate, }) } - Err(e) => Resolved::Unreachable(Unreachable::InvalidInput(e)), + Err(e) => Resolved::Unreachable(Unreachable::InternalInvariant(format!( + "generated Glow solve request was rejected: {e}" + ))), }; } RoleSpec::AlphaAnalog { of, alpha } => { @@ -2452,7 +2499,7 @@ fn resolve_spec_in( /// /// Композитинг straight-alpha живёт в версионированном encoded-sRGB8 reference- /// профиле, который воспроизводит измеренные Figma-якоря ([`crate::alpha`]), но -/// не сертифицирует любой browser/color-management pipeline. Контраст меряется +/// не сертифицирует любой pipeline браузера и управления цветом. Контраст меряется /// на КОМПОЗИТЕ (солид-эквивалент), не на тинте: контраст полупрозрачной роли /// определён тем, во что она складывается на подложке. /// Квантовать кодированный цвет до 8-битной сетки (hex-roundtrip без строки): @@ -2715,7 +2762,7 @@ fn resolve_rgba_inverted( ) -> Resolved { // Тот же домен-гард, что у прямого rgba-пути: RoleSpec публичен. Без него // недоменная спека, собранная в обход валидатора конфига, дошла бы до - // численного пути вместо честного typed-исхода. + // численного пути вместо честного типизированного исхода. if !rgba_input_valid(solid_encoded, requested_alpha) { return Resolved::Unreachable(Unreachable::InvalidInput( "alpha-analog-спека вне домена (солид [0,1], α (0,1]) — сборка в обход валидатора" @@ -2751,8 +2798,9 @@ fn finish_rgba( floor_coerced: bool, ) -> Resolved { use crate::spaces::srgb::{hex_from_srgb_encoded, srgb_encoded_from_hex, srgb_gamma_inv}; - // Единый byte-domain SSOT нужен и для hex, и для обеих метрик: нормализация - // `(byte/255)·255` способна изменить half-tie на один LSB. + // Единый байтовый домен SSOT нужен и для hex, и для обеих метрик: + // нормализация `(byte/255)·255` способна изменить граничное значение + // половинного округления на один LSB. let composite_hex = match crate::alpha::composite_hex_from_encoded(tint_encoded, alpha, bg_encoded) { Ok(hex) => hex, @@ -2778,7 +2826,7 @@ fn finish_rgba( let composite_wcag = crate::wcag::contrast_ratio(composite_q, bg_encoded); // Отличимость в encoded-sRGB8 reference (ADR-0002): сравнение по тем же // 8-битным hex, из которых строится сертификат. Фон квантуется тем же - // форматтером; применимость к renderer проверяется отдельно (#241). + // форматтером; применимость к рендереру проверяется отдельно (#241). let composite_distinct = composite_hex != hex_from_srgb_encoded(bg_encoded); Resolved::Translucent(TranslucentResolved { tint_hex: hex_from_srgb_encoded(tint_encoded), @@ -2797,9 +2845,9 @@ fn finish_rgba( /// /// Тон строится тем же dj-anchor-солвером, что декоративные |ΔJ'|-роли /// ([`resolve_dj`]), поэтому различимость поверхности от фона наследуется его -/// физикой. Альфа тинта выводится [`crate::material::solve_material_alpha_hex`] -/// как минимальная плотность, при которой композит тона над худшим фоном коридора -/// `[чёрный, белый]` держит пол читаемости коммит-полюса. `family_hued` — оттенок +/// физикой. Альфа тинта выбирается [`crate::material::solve_material_alpha_hex`] +/// как проходящий верхний кандидат, при котором композит тона над худшим фоном +/// коридора `[чёрный, белый]` держит пол. `family_hued` — оттенок /// семьи присутствует (флаг вырождения оттенка применим); у нейтрали `false`. fn resolve_material( bg: &BgInput, @@ -2830,10 +2878,15 @@ fn resolve_material( // Вырождение оттенка семьи у края гамута — только у семейных материалов; // нейтраль ахроматична намеренно, не «выродилась». let hue_vanished = family_hued && dj.solved.color().mp() < TINT_PERCEPTIBLE_MP_FLOOR; - // α: минимальная плотность под пол над коридором [чёрный, белый]. + // α: повторно проверенный проходящий верхний кандидат над коридором + // [чёрный, белый]. let m = match crate::material::solve_material_alpha_hex(&tone_hex, floor_ratio) { Ok(m) => m, - Err(e) => return Resolved::Unreachable(Unreachable::InvalidInput(e)), + Err(e) => { + return Resolved::Unreachable(Unreachable::InternalInvariant(format!( + "generated Material solve request was rejected: {e}" + ))); + } }; // Различимость солид-канона (= тона) от фона резолва на 8-битной сетке (тот же // замер, что у полупрозрачных ролей; off-grid фон честно квантуется). @@ -2841,11 +2894,12 @@ fn resolve_material( let distinct = tone_hex != bg_hex; Resolved::Material(MaterialResolved { tone_hex, - alpha: m.alpha, - worst_contrast: m.worst_contrast, + alpha: m.alpha(), + worst_contrast: m.worst_contrast(), + alpha_guarantee: m.guarantee(), + alpha_status: m.status(), floor: floor_ratio, - guaranteed: !m.degraded, - pole: m.pole, + pole: m.pole(), achieved_dj: dj.achieved_dj, tone_compressed: dj.degraded, hue_vanished, @@ -3101,9 +3155,10 @@ impl NamedRoleTable { } } - /// Алиасы `(имя, цель)` — эмитируются потребителем как CSS-ссылка - /// `--lab-{имя}: var(--lab-{цель})` (одна истина значения, ноль копий); - /// без переноса сюда алиасные роли контракта терялись бы при компиляции. + /// Алиасы `(имя, цель)` сохраняются в скомпилированном контракте, чтобы + /// delivery boundary спроецировала resolved outcome цели под client-owned + /// именем алиаса. Алиас не запускает отдельный solve и не меняет физическое + /// значение; без переноса сюда алиасные роли терялись бы при компиляции. pub fn aliases(&self) -> &[(String, String)] { &self.aliases } @@ -3730,6 +3785,78 @@ fn bg_display(bg: &BgInput) -> [f64; 3] { mod tests { use super::*; + fn one_glow_table( + source_hex: &str, + decision_profile: crate::glow::GlowDecisionProfileV1, + ) -> NamedRoleTable { + let source = crate::spaces::srgb::srgb_encoded_from_hex(source_hex).unwrap(); + NamedRoleTable::new( + vec![( + "opaque-client-id".to_string(), + RoleSpec::Glow { + tint: LadderTint::new([source; 4]).unwrap(), + step: crate::glow::GlowStep::Base, + decision_profile, + }, + )], + Vec::new(), + RoleChroma::Neutral, + ) + .unwrap() + } + + #[test] + fn full_glow_separates_recipe_appearance_and_selection_diagnostics() { + let vc = ViewingConditions::srgb(); + + let exact = resolve_named_set( + &BgInput::solid("#FFFFFF").unwrap(), + &one_glow_table("#4A8FFF", crate::glow::GlowDecisionProfileV1::StableV1), + &vc, + ); + let Resolved::Glow(exact) = &exact[0].1 else { + panic!("stable point no-op must resolve as a full Glow result"); + }; + assert_eq!( + exact.target_status(), + crate::glow::GlowTargetStatus::ExactNoopUnreachable + ); + assert_eq!( + exact.layer_recipe_profile().key(), + "cam16-jprime-oklab-cusp-v1" + ); + assert_eq!( + exact.appearance_diagnostic_profile(), + crate::glow::GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1 + ); + assert!(exact.selection_diagnostic_profile().is_none()); + + let legacy = resolve_named_set( + &BgInput::solid("#101012").unwrap(), + &one_glow_table( + "#4A8FFF", + crate::glow::GlowDecisionProfileV1::LegacyPlatformDependentV1, + ), + &vc, + ); + let Resolved::Glow(legacy) = &legacy[0].1 else { + panic!("explicit legacy selection must resolve as a full Glow result"); + }; + assert_eq!( + legacy.selection_diagnostic_profile(), + Some(crate::glow::GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1) + ); + assert_eq!( + legacy.appearance_diagnostic_profile(), + crate::glow::GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1 + ); + assert!(matches!( + legacy.target_status(), + crate::glow::GlowTargetStatus::LegacyReached + | crate::glow::GlowTargetStatus::LegacyUnreachable + )); + } + // ───────────────────────────────────────────────────────────────────────── // DIFFERENTIAL HARNESS (perf/max-chroma-hotpath) — cusp-attracted hue. // @@ -5670,7 +5797,7 @@ mod tests { t_ok.alpha() ); - // Byte-grid разрешимость сильнее continuous-инверсии: белый красный + // Разрешимость байтовой сетки сильнее непрерывной инверсии: белый красный // тинт @ 0.12 уже округляется в #1F0000, поэтому коэрсии быть не должно. let black = BgInput::solid("#000000").unwrap(); let quantised_solid = srgb_encoded_from_hex("#1F0000").unwrap(); diff --git a/crates/labcolors-core/src/solve.rs b/crates/labcolors-core/src/solve.rs index ade36a10..396e3002 100644 --- a/crates/labcolors-core/src/solve.rs +++ b/crates/labcolors-core/src/solve.rs @@ -385,7 +385,9 @@ impl Solved { } } -/// Why a contract cannot be satisfied. Returned instead of silently clipping. +/// Why a solve could not return a colour. Physical/domain variants explain a +/// contract failure; [`Self::InternalInvariant`] reports core drift and must be +/// failed closed by bindings rather than projected as a physical outcome. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum Unreachable { @@ -419,6 +421,11 @@ pub enum Unreachable { GamutUnsupported, /// Malformed input, such as an invalid hex colour or a non-finite target. InvalidInput(String), + /// A value produced and validated by the core later violated an internal + /// postcondition. This is never client-input blame: bindings must fail the + /// enclosing call closed instead of projecting it as a physical role + /// outcome. + InternalInvariant(String), } impl core::fmt::Display for Unreachable { @@ -454,6 +461,7 @@ impl core::fmt::Display for Unreachable { ) } Self::InvalidInput(msg) => write!(f, "invalid input: {msg}"), + Self::InternalInvariant(msg) => write!(f, "internal invariant failure: {msg}"), } } } @@ -1312,6 +1320,13 @@ pub(crate) fn bg_luma(rgb: [f64; 3], vc: &ViewingConditions) -> f64 { #[cfg(test)] mod tests { use super::*; + + #[test] + fn internal_invariant_failure_is_not_reported_as_invalid_client_input() { + let error = Unreachable::InternalInvariant("generated fixture drift".into()); + assert!(error.to_string().starts_with("internal invariant failure:")); + assert!(!error.to_string().starts_with("invalid input:")); + } use crate::lpc::lpc_with_vc; /// D2(a) страж (аудит 2026-07-03): `Unreachable` не несёт never-constructed @@ -1343,6 +1358,7 @@ mod tests { Unreachable::PolarityMismatch { target: 1.0 }, Unreachable::GamutUnsupported, Unreachable::InvalidInput("x".to_string()), + Unreachable::InternalInvariant("x".to_string()), ]; for u in &samples { assert!(!u.to_string().is_empty(), "Display пуст для {u:?}"); @@ -1355,7 +1371,8 @@ mod tests { | Unreachable::FloorUnreachable { .. } | Unreachable::PolarityMismatch { .. } | Unreachable::GamutUnsupported - | Unreachable::InvalidInput(_) => {} + | Unreachable::InvalidInput(_) + | Unreachable::InternalInvariant(_) => {} } } } diff --git a/crates/labcolors-core/src/spaces/vc.rs b/crates/labcolors-core/src/spaces/vc.rs index 2f0603e6..21043236 100644 --- a/crates/labcolors-core/src/spaces/vc.rs +++ b/crates/labcolors-core/src/spaces/vc.rs @@ -1,6 +1,6 @@ //! Viewing conditions (surround parameters) for the CIECAM16 forward pass. //! -//! Holds the per-observer constants — adapting luminance, background factor, +//! Holds model viewing-condition inputs — adapting luminance, background factor, //! and the surround triplet `(F, c, N_c)` — that [`crate::spaces::cam16`] //! consumes. The surround presets are CIECAM16 Table 1 (average //! `[1.0, 0.69, 1.0]`, dim `[0.9, 0.59, 0.9]`), as tabulated in Li et al. @@ -98,21 +98,15 @@ impl ViewingConditions { /// but with reduced surround contrast per CIECAM16 Table 1: /// F = 0.9, c = 0.59, N_c = 0.9. /// - /// Produces lower J' for the same stimulus compared to average surround, - /// which matches human perception in darkened viewing environments. + /// Produces lower model J for the same stimulus than the average-surround preset. /// /// # Why dim (F = 0.9), not dark (F = 0.8), for a dark-theme UI? /// - /// A dark-theme UI (phone at night, monitor in night mode) is formally - /// closer to the *dark* surround, whose triplet (F = 0.8, c = 0.525, - /// N_c = 0.8) models a projector in *complete* darkness, where the stimulus - /// is the only light in the field. A screen in a dim room is different: it is - /// a self-luminous panel plus residual ambient light from the room, so its - /// effective surround never reaches that projector limit. The *dim* working - /// point (F = 0.9) sits between average and dark and is the better match for - /// that reality. This is a deliberate engine choice, not a strict CIE - /// mandate; the test-only `dark_surround` constructor keeps the F = 0.8 - /// endpoint available so it can still be exercised in comparisons and tests. + /// A CSS/product theme label does not determine the user's measured surround. + /// Lab Colors therefore freezes *dim* (F = 0.9) as an engine compatibility + /// preset, not as a universal observer claim or CIE mandate. The test-only + /// `dark_surround` constructor keeps the F = 0.8 endpoint available for + /// comparisons and tests. /// /// The documented dim triplet is asserted here so a silent regression to the /// dark parameters — the choice this rationale rejects — fails the doctest: @@ -149,8 +143,8 @@ impl ViewingConditions { /// чёрный фон (#000000, Y=0.0%) clampится к 0.5 % без значимого влияния на /// итоговый J: разница J(Yb=0.5) − J(Yb≈0) пренебрежимо мала на практике. /// - /// Используется в surround-aware оценке дефектов (`cleanliness`), где фон цвета - /// известен из дизайн-контекста — источник: CIECAM16 (Li et al. 2017). + /// Используется legacy `cleanliness` compatibility proxy как partial input + /// `Yb + theme`. Это не полный visual context и не observer verdict. pub fn srgb_with_yb(y_b_pct: f64) -> Self { // average surround: F=1.0, c=0.69, N_c=1.0 — CIECAM02/16 Table 1 Self::build(64.0, y_b_pct.clamp(0.5, 100.0), 1.0, 0.69, 1.0) @@ -161,7 +155,8 @@ impl ViewingConditions { /// Аналогичен [`dim_surround`](Self::dim_surround), но параметр `Yb` задаётся явно. /// Диапазон ограничен [0.5, 100.0] — нижняя граница по той же причине, что в /// [`srgb_with_yb`](Self::srgb_with_yb): Yb=0 вырождает CIECAM16 (nbb→∞). - /// Используется в surround-aware оценке дефектов. + /// Используется legacy compatibility proxy как partial `Yb + theme` input; + /// это не полная surround/observer model. pub fn dim_surround_with_yb(y_b_pct: f64) -> Self { // dim surround: F=0.9, c=0.59, N_c=0.9 — CIECAM02/16 Table 1 Self::build(64.0, y_b_pct.clamp(0.5, 100.0), 0.9, 0.59, 0.9) diff --git a/crates/labcolors-core/src/wcag.rs b/crates/labcolors-core/src/wcag.rs index 44bff876..912a74a7 100644 --- a/crates/labcolors-core/src/wcag.rs +++ b/crates/labcolors-core/src/wcag.rs @@ -1,20 +1,11 @@ -//! WCAG 2.1 contrast ratio — the legal conformance floor. +//! Legacy WCAG 2.1 (2018) relative-luminance profile. //! -//! The European Accessibility Act (in force 2025-06-28) requires, through -//! EN 301 549, WCAG 2.1 level AA: a contrast ratio of at least 4.5:1 for normal -//! text (success criterion 1.4.3) and 3:1 for user-interface components and -//! graphical objects (1.4.11). This is the *standard relative-luminance* -//! contrast ratio — symmetric and chroma-blind — computed on the output sRGB -//! colour. It is the legal minimum beneath the perceptual LPC target the solver -//! aims for; it never replaces it (see [`crate::solve`]). -//! -//! The implementation below is a faithful, self-contained transcription of the -//! W3C definitions, deliberately independent of the CAM16/LPC pipeline so it can -//! be audited line-by-line against the spec: -//! and -//! . EN 301 549 (the harmonised -//! standard invoking WCAG 2.1 AA): -//! . +//! This module preserves the product's existing continuous `0.03928` profile +//! while the canonical WCAG 2.2 finite-domain profile is implemented under +//! issue #284. The current W3C Recommendation uses `0.04045`; therefore this +//! module must not be described as current normative WCAG, cross-runtime exact, +//! or a legal-applicability decision. It remains independent of CAM16/LPC so the +//! technical formula can be audited and replaced as one unit. /// WCAG 2.1 AA minimum contrast ratio for normal text (success criterion 1.4.3). pub(crate) const AA_TEXT_RATIO: f64 = 4.5; @@ -23,23 +14,91 @@ pub(crate) const AA_TEXT_RATIO: f64 = 4.5; /// (success criterion 1.4.11). pub(crate) const AA_UI_RATIO: f64 = 3.0; -/// Linearise one gamma-encoded sRGB channel in `[0, 1]`, per WCAG 2.1 §1.4.3. +/// Split used by the original WCAG 2.1 (2018) continuous formula. /// -/// The 0.03928 threshold is the value normatively fixed by WCAG; for 8-bit -/// inputs it selects the same piecewise branch as the IEC sRGB transfer -/// function, so quantised colours linearise identically either way. +/// The current WCAG Recommendation uses `0.04045`. Issue #284 owns that +/// versioned migration; changing this constant in place would silently mutate a +/// characterized numerical profile. +const LEGACY_CHANNEL_SPLIT: f64 = 0.039_28; + +/// First binary64 value on the power branch of the legacy split. +const LEGACY_CHANNEL_SPLIT_RIGHT: f64 = f64::from_bits(0x3fa4_1c82_16c6_1524); + +const RED_WEIGHT: f64 = 0.2126; +const GREEN_WEIGHT: f64 = 0.7152; +const BLUE_WEIGHT: f64 = 0.0722; + +/// Absolute headroom for the final three weighted binary64 operations in a +/// luminance interval. The material path is still platform-characterized +/// because `powf` has no repository-owned outward error bound. +const LUMINANCE_RANGE_MARGIN: f64 = 8.0 * f64::EPSILON; + +/// Linearise one gamma-encoded sRGB channel in `[0, 1]` under the frozen legacy +/// WCAG 2.1 (2018) profile. fn linearise(channel: f64) -> f64 { - if channel <= 0.039_28 { + if channel <= LEGACY_CHANNEL_SPLIT { channel / 12.92 } else { ((channel + 0.055) / 1.055).powf(2.4) } } -/// WCAG 2.1 relative luminance of a gamma-encoded sRGB colour `[r, g, b]` in -/// `[0, 1]`: `0.2126·R + 0.7152·G + 0.0722·B` over the linearised channels. +/// Characterized range of the legacy channel transfer over an ordered encoded +/// interval. +/// +/// The legacy constants create a small downward discontinuity immediately to +/// the right of `0.03928`. Endpoint-only evaluation is therefore invalid. When +/// the interval crosses the split, both the linear value at the split and the +/// power-branch value at the first representable input above it participate in +/// the extrema. `powf` remains platform-characterized rather than soundly +/// outward-rounded. +fn linearised_channel_range(encoded_lo: f64, encoded_hi: f64) -> (f64, f64) { + debug_assert!( + encoded_lo.is_finite() + && encoded_hi.is_finite() + && (0.0..=1.0).contains(&encoded_lo) + && (0.0..=1.0).contains(&encoded_hi) + && encoded_lo <= encoded_hi + ); + + let mut lo = linearise(encoded_lo).min(linearise(encoded_hi)); + let mut hi = linearise(encoded_lo).max(linearise(encoded_hi)); + if encoded_lo <= LEGACY_CHANNEL_SPLIT && encoded_hi > LEGACY_CHANNEL_SPLIT { + let at_split = linearise(LEGACY_CHANNEL_SPLIT); + let right_of_split = linearise(LEGACY_CHANNEL_SPLIT_RIGHT); + lo = lo.min(at_split).min(right_of_split); + hi = hi.max(at_split).max(right_of_split); + } + (lo, hi) +} + +/// Separable characterized luminance enclosure for three ordered encoded-sRGB +/// channel intervals. +/// +/// All WCAG weights are positive, so channel minima and maxima combine without +/// enumerating the eight RGB corners. A small final absolute pad covers the +/// fixed binary64 multiply/add sequence; the branch-sensitive `powf` calls keep +/// this a platform characterization rather than a cross-runtime proof. +pub(crate) fn relative_luminance_range(encoded_lo: [f64; 3], encoded_hi: [f64; 3]) -> (f64, f64) { + let channels = core::array::from_fn::<_, 3, _>(|channel| { + linearised_channel_range(encoded_lo[channel], encoded_hi[channel]) + }); + let lower = + (RED_WEIGHT * channels[0].0 + GREEN_WEIGHT * channels[1].0) + BLUE_WEIGHT * channels[2].0; + let upper = + (RED_WEIGHT * channels[0].1 + GREEN_WEIGHT * channels[1].1) + BLUE_WEIGHT * channels[2].1; + ( + (lower - LUMINANCE_RANGE_MARGIN).max(0.0), + (upper + LUMINANCE_RANGE_MARGIN).min(1.0), + ) +} + +/// Frozen legacy WCAG 2.1 (2018) relative luminance of a gamma-encoded sRGB +/// colour `[r, g, b]` in `[0, 1]`. pub(crate) fn relative_luminance(srgb: [f64; 3]) -> f64 { - 0.2126 * linearise(srgb[0]) + 0.7152 * linearise(srgb[1]) + 0.0722 * linearise(srgb[2]) + RED_WEIGHT * linearise(srgb[0]) + + GREEN_WEIGHT * linearise(srgb[1]) + + BLUE_WEIGHT * linearise(srgb[2]) } /// The `[1, 21]` WCAG ratio of two relative luminances: @@ -50,7 +109,8 @@ pub(crate) fn ratio_from_luminances(la: f64, lb: f64) -> f64 { (lighter + 0.05) / (darker + 0.05) } -/// WCAG 2.1 contrast ratio between two gamma-encoded sRGB colours, in `[1, 21]`. +/// Frozen legacy WCAG 2.1 (2018) contrast ratio between two gamma-encoded sRGB +/// colours, in `[1, 21]`. /// /// `(L_lighter + 0.05) / (L_darker + 0.05)`. Symmetric and polarity-agnostic by /// construction — unlike the signed perceptual LPC metric, which is why the two @@ -131,4 +191,48 @@ mod tests { assert_eq!(contrast_ratio(a, b).to_bits(), inlined.to_bits()); } } + + #[test] + fn legacy_channel_range_contains_both_sides_of_the_eotf_seam() { + assert_eq!( + LEGACY_CHANNEL_SPLIT_RIGHT.to_bits(), + LEGACY_CHANNEL_SPLIT.to_bits() + 1 + ); + let linear_side = LEGACY_CHANNEL_SPLIT / 12.92; + let power_side = ((LEGACY_CHANNEL_SPLIT_RIGHT + 0.055) / 1.055).powf(2.4); + assert!( + power_side < linear_side, + "fixture must expose the legacy seam" + ); + + // A wide interval makes the power-side seam value an interior extremum; + // endpoint-only evaluation cannot satisfy this assertion. + let (lo, hi) = linearised_channel_range(LEGACY_CHANNEL_SPLIT, 0.5); + assert!(lo <= power_side, "lower range omitted power side"); + assert!(hi >= linear_side, "upper range omitted linear side"); + } + + #[test] + fn luminance_range_is_separable_and_bounded() { + let (lo, hi) = relative_luminance_range( + [LEGACY_CHANNEL_SPLIT, 0.25, 0.75], + [LEGACY_CHANNEL_SPLIT_RIGHT, 0.5, 1.0], + ); + assert!(lo.is_finite() && hi.is_finite()); + assert!((0.0..=1.0).contains(&lo)); + assert!((0.0..=1.0).contains(&hi)); + assert!(lo <= hi); + + for rgb in [ + [LEGACY_CHANNEL_SPLIT, 0.25, 0.75], + [LEGACY_CHANNEL_SPLIT_RIGHT, 0.5, 1.0], + [LEGACY_CHANNEL_SPLIT_RIGHT, 0.25, 0.75], + ] { + let actual = relative_luminance(rgb); + assert!( + lo <= actual && actual <= hi, + "{actual} outside [{lo}, {hi}]" + ); + } + } } diff --git a/crates/labcolors-core/tests/data/labui_emission_golden.txt b/crates/labcolors-core/tests/data/labui_emission_golden.txt index afd1d758..fba22ce5 100644 --- a/crates/labcolors-core/tests/data/labui_emission_golden.txt +++ b/crates/labcolors-core/tests/data/labui_emission_golden.txt @@ -184,7 +184,7 @@ srgb|#F2F2F7|fx-focus-ring-brand=rgba(#007AFF,1) srgb|#F2F2F7|fx-focus-ring-danger=rgba(#FF3B30,1) srgb|#F2F2F7|fx-focus-ring-warning=rgba(#FFA100,1) srgb|#F2F2F7|fx-focus-ring-neutral=rgba(#101012,1) -srgb|#F2F2F7|fx-glow-brand=glow(#8DBCFF,#007AFF,0.9688) +srgb|#F2F2F7|fx-glow-brand=glow(#8DBCFF,#007AFF,0.9687) srgb|#F2F2F7|fx-glow-danger=glow(#FFAA9D,#FF3B30,0.9980) srgb|#F2F2F7|fx-glow-warning=glow(#FFD19F,#FFA100,0.9376) srgb|#F2F2F7|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.6707) @@ -826,7 +826,7 @@ dim|#F2F2F7|fx-focus-ring-brand=rgba(#4A8FFF,1) dim|#F2F2F7|fx-focus-ring-danger=rgba(#FF3A3A,1) dim|#F2F2F7|fx-focus-ring-warning=rgba(#FF9008,1) dim|#F2F2F7|fx-focus-ring-neutral=rgba(#F6F8FA,1) -dim|#F2F2F7|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.9688) +dim|#F2F2F7|fx-glow-brand=glow(#A0C5FF,#4A8FFF,0.9687) dim|#F2F2F7|fx-glow-danger=glow(#FFA89F,#FF3A3A,0.9808) dim|#F2F2F7|fx-glow-warning=glow(#FFC89D,#FF9008,0.9808) dim|#F2F2F7|fx-glow-neutral=glow(#FFFFFF,#FFFFFF,0.7692) diff --git a/crates/labcolors-core/tests/empirical_inventory.rs b/crates/labcolors-core/tests/empirical_inventory.rs index ba988a39..fc96aabe 100644 --- a/crates/labcolors-core/tests/empirical_inventory.rs +++ b/crates/labcolors-core/tests/empirical_inventory.rs @@ -1051,10 +1051,10 @@ fn read_workspace_doc(rel: &str) -> Option { } // ───────────────────────────────────────────────────────────────────────────── -// Muddiness-Law (M-row) auditor — resurrects the retired external `mud-oracle`. +// Legacy-proxy M-row auditor — resurrects the retired external `mud-oracle`. // // BUG CLASS this closes: the GATE-1/2/3 integer-row parser SKIPS the `M-` rows -// (their first cell `M-01` is not a bare integer), so the Muddiness-Law constants +// (their first cell `M-01` is not a bare integer), so the legacy-proxy constants // of `cleanliness.rs` had NO in-repo gate — the SSOT once deferred them to // `.agents/tools/mud-oracle/verify_inventory.js`, which does not exist on this // tree. This auditor pulls that check IN-TREE: every non-REMOVED `M-` row must @@ -1067,7 +1067,7 @@ fn read_workspace_doc(rel: &str) -> Option { // `cleanliness.rs` (no duplication). Shared verbatim by the gate and the RED-proof. // ───────────────────────────────────────────────────────────────────────────── -/// One parsed Muddiness-Law row from the SSOT (`M-` keyed). +/// One parsed legacy-proxy row from the SSOT (`M-` keyed). #[derive(Debug, Clone)] struct MudRow { /// Const name, markdown decoration stripped (backticks, `~~…~~`). @@ -1087,7 +1087,7 @@ fn decimal_places(s: &str) -> usize { } } -/// Parse the `M-` rows of the SSOT. A mud row is a `|`-row whose first cell starts +/// Parse the `M-` rows of the SSOT. A legacy-proxy row is a `|`-row whose first cell starts /// with `M-`. Columns: `mud-id | name | value | module | status | rationale`. fn parse_mud_rows(text: &str) -> Vec { let mut rows = Vec::new(); @@ -1251,7 +1251,7 @@ fn rewrite_mud_row_value(text: &str, name: &str, new_value: &str) -> String { fn mud_rows_match_cleanliness_source() { let ssot = std::fs::read_to_string(inventory_path()).unwrap_or_else(|e| { panic!( - "mud auditor cannot read SSOT at {} ({e})", + "legacy-proxy M-row auditor cannot read SSOT at {} ({e})", inventory_path().display() ) }); @@ -1259,13 +1259,13 @@ fn mud_rows_match_cleanliness_source() { let active = rows.iter().filter(|r| !r.removed).count(); assert!( active > 0, - "mud auditor parsed zero ACTIVE M-rows — the parser is mis-scoped (a vacuous gate)." + "legacy-proxy M-row auditor parsed zero ACTIVE M-rows — the parser is mis-scoped (a vacuous gate)." ); let src = read_module("cleanliness.rs"); let defects = mud_row_defects(&rows, &src); assert!( defects.is_empty(), - "Muddiness-Law auditor FAILED — {} M-row(s) do not join to cleanliness.rs at the \ + "legacy-proxy M-row auditor FAILED — {} M-row(s) do not join to cleanliness.rs at the \ documented value/precision:\n {}", defects.len(), defects.join("\n ") @@ -1835,7 +1835,7 @@ fn red_proof_audit_probe() { real_citation_defects.join("\n ") ); - // 8. Muddiness-Law auditor must flip RED on an M-row value drift. Mutate ONE + // 8. Legacy-proxy M-row auditor must flip RED on an M-row value drift. Mutate ONE // ACTIVE M-row's value in an IN-MEMORY copy of the SSOT and run the *same* // `mud_row_defects` the live gate runs (INV-4), so a mutation to the auditor // is caught here, not green-from-birth. @@ -1853,12 +1853,12 @@ fn red_proof_audit_probe() { let mud_drifted = rewrite_mud_row_value(&real_ssot, mud_probe, "196.9172"); assert_ne!( mud_drifted, real_ssot, - "RED-proof setup wrong — mud value-drift splice did not change the SSOT (row `{mud_probe}` not found)." + "RED-proof setup wrong — legacy-proxy value-drift splice did not change the SSOT (row `{mud_probe}` not found)." ); let mud_drift = mud_row_defects(&parse_mud_rows(&mud_drifted), &cleanliness_src); assert!( mud_drift.iter().any(|d| d.contains(mud_probe)), - "RED-proof FAILED (mud value-drift path) — drifting M-row `{mud_probe}` value to `196.9172` \ - did NOT flip the auditor RED; the Muddiness-Law audit is green-from-birth. Saw: {mud_drift:?}" + "RED-proof FAILED (legacy-proxy value-drift path) — drifting M-row `{mud_probe}` value to `196.9172` \ + did NOT flip the auditor RED; the legacy-proxy M-row audit is green-from-birth. Saw: {mud_drift:?}" ); } diff --git a/crates/labcolors-core/tests/property_invariants.rs b/crates/labcolors-core/tests/property_invariants.rs index 4381285b..7d102a7c 100644 --- a/crates/labcolors-core/tests/property_invariants.rs +++ b/crates/labcolors-core/tests/property_invariants.rs @@ -19,14 +19,15 @@ //! или на границе вызова) роняет property с МИНИМИЗИРОВАННЫМ (shrunk) контрпримером. //! Зелёный-с-рождения запрещён (был бы театром). Механика инъекции описана в PR. //! -//! # ЗАМЕЧАНИЕ ПО monotonicity (задокументированный vs фактический инвариант) +//! # ЗАМЕЧАНИЕ ПО monotonicity //! -//! `muddiness_oklch` документирован как «строго монотонный в C при фиксированных L,h». -//! Это ВЕРНО только для ТЁПЛЫХ оттенков (`sin(h) ≥ 0`, h∈[0°,180°]): там оба зависящих +//! Historical API identifier `muddiness_oklch` denotes an experimental compatibility +//! proxy, not a human judgement. Its coordinate is nondecreasing in C only for warm +//! hues (`sin(h) ≥ 0`, h∈[0°,180°]): там оба зависящих //! от C множителя — `neutral_gate(C)` и b-гейт `sigmoid((C·sin h − B0)/BW)` — растут в C. //! Для ХОЛОДНЫХ оттенков (`sin(h) < 0`, h∈(180°,360°)) b-гейт УБЫВАЕТ в C (b = C·sin h < 0 -//! уходит от B0), поэтому произведение НЕ монотонно — глобальное утверждение доки -//! переусиливает. Property ниже проверяет ИСТИННЫЙ инвариант (тёплая полуплоскость); +//! уходит от B0), поэтому произведение НЕ монотонно. Property ниже проверяет +//! математический инвариант тёплой полуплоскости; //! холодный контрпример зафиксирован тестом `muddiness_cool_hue_is_not_monotone_finding` //! как characterization реального поведения (не одобрение — фиксация факта для владельца). @@ -138,8 +139,8 @@ fn anchors_ld(light: String, dark: String) -> ThemeAnchors { // ───────────────────────────────────────────────────────────────────────────── // СВОЙСТВО 1 — muddiness_oklch монотонна в C (тёплая полуплоскость) // -// КЛАСС: инвариант монотонности параметр-свободной цены грязи. Закрывает весь -// класс «муддинес немонотонен по хроме для тёплого оттенка», не один вход. +// КЛАСС: инвариант монотонности frozen compatibility coordinate. Закрывает +// весь класс регрессий C-зависимости для тёплого оттенка, не один вход. // БЬЁТ НА МУТАЦИИ: замена `neutral_gate` sigmoid на константу / переворот знака // аргумента / отбрасывание C-зависимости → монотонность рушится → RED. // ───────────────────────────────────────────────────────────────────────────── @@ -192,13 +193,13 @@ fn muddiness_strictly_increases_in_chroma_in_the_steep_warm_band() { } /// Закрытие ДЫРЫ, найденной mutation-прогоном (survivor `cleanliness.rs:541:35 -/// replace + with * in muddiness_in_context`): surround-aware путь обязан считать -/// РЕАЛЬНУЮ Oklab-хрому `√(a²+b²)`. Мутант `a²·b²` схлопывает хрому тёплых муддистых -/// цветов почти в ноль (ниже C0) → mud падает с ~0.65 до ~0.03. Тёплые муддистые -/// оливы обязаны читаться грязными (mud > 0.3) в контексте — это убивает мутанта. +/// replace + with * in muddiness_in_context`): local-context path must calculate +/// Euclidean Oklab chroma `sqrt(a²+b²)`. The `a²*b²` mutant collapses these +/// fixtures below C0 and moves the frozen proxy from roughly 0.5-0.7 to ~0.03. +/// The `> 0.3` boundary is mutation characterization, not a human verdict. /// Заземлено замером: #6B6B2E=0.651, #8A7A2E=0.622, #7A5A20=0.666, #B8860B=0.506. #[test] -fn muddiness_in_context_reflects_real_oklab_chroma() { +fn muddiness_in_context_uses_euclidean_oklab_chroma() { let ctx = DefectContext { bg_hex: "#FFFFFF", theme: Theme::Light, @@ -207,16 +208,15 @@ fn muddiness_in_context_reflects_real_oklab_chroma() { let mud = muddiness_in_context(hex, ctx).unwrap(); assert!( mud > 0.3, - "тёплый мутный {hex} обязан читаться грязным в контексте (√(a²+b²)); \ - схлопнутая хрома дала бы ~0. Получено mud={mud}" + "fixture {hex} proxy must remain > 0.3 with Euclidean sqrt(a²+b²); \ + collapsed chroma would approach zero. Got {mud}" ); } } -/// Characterization реального поведения: для ХОЛОДНОГО оттенка (h=270°, sin=−1) -/// муддинес НЕ монотонен по C — сперва растёт, потом падает. Это фиксирует -/// расхождение между глобальным утверждением доки и кодом (находка для владельца), -/// НЕ одобряя его. Числа заземлены прямым вычислением ядра. +/// Characterization: for a cool hue (h=270°, sin=-1), the compatibility +/// coordinate is not monotone in C: it first rises, then falls. This records +/// current mathematics without assigning perceptual meaning. #[test] fn muddiness_cool_hue_is_not_monotone_finding() { let l = 0.3; diff --git a/crates/labcolors-ffi/src/lib.rs b/crates/labcolors-ffi/src/lib.rs index 2040d393..77febaf7 100644 --- a/crates/labcolors-ffi/src/lib.rs +++ b/crates/labcolors-ffi/src/lib.rs @@ -22,14 +22,17 @@ //! | [`solve_contrast`] | `solve` | `solve` | //! | [`ladder_alpha`] | `ladders` | `LadderPosition::alpha_pair` | //! | [`composite`] / [`min_alpha`] | `alpha` | `alpha::composite_hex` / `alpha::min_alpha_hex` | -//! | [`muddiness`] | `muddiness` | `cleanliness::muddiness_from_hex` | +//! | [`muddiness`] | `muddiness` legacy compatibility vectors | `cleanliness::muddiness_from_hex` | //! | [`core_version`] | `manifest` | версия ядра | //! //! [`solve_glow_point`] — отдельный low-level contract test нативной границы: -//! stable-профиль переносит `Indeterminate` с site + typed evidence без fallback, +//! stable-профиль переносит `Indeterminate` с site + typed evidence без fallback +//! для non-trivial selection, а exact byte-no-op возвращает determinate без CAM16; //! legacy выбирается только явно. Его CAM16-диагностика не объявлена //! bit-exact между платформами; `bit-exact` относится лишь к certificate -//! encoded-sRGB8 screen-композитора. +//! encoded-sRGB8 screen-композитора. Output — algebraic sum +//! `StableExactNoop | LegacyReached | LegacyUnreachable | Indeterminate`, а не +//! независимые provenance-поля с незаконными cross-product. //! //! Резолв полной темы из JSON-конфига (агностичный движок) СОЗНАТЕЛЬНО вне //! среза: он требует serde-границы конфига (живёт в WASM-крейте) — перенос её @@ -47,7 +50,7 @@ use labcolors_core::{ GlowCompositeProfileV1, GlowDecisionProfileV1, GlowDiagnosticProfileV1, GlowTargetStatus as CoreGlowTargetStatus, Hue, LadderPosition, NumericalDecisionV1, NumericalIndeterminacyV1, NumericalSiteIdV1, Theme as CoreTheme, ViewingConditions, - recheck_against, solve, solve_screen_alpha_for_dj, + recheck_against, solve, solve_screen_alpha_for_dj, srgb_encoded_from_hex, }; // Регистрирует UniFFI-scaffolding под namespace = имя крейта (`labcolors`). @@ -160,22 +163,6 @@ impl GlowDecisionProfile { } } -/// Guarantee semantic target/max decision с неразделимым evidence. -#[derive(Debug, Clone, PartialEq, uniffi::Enum)] -pub enum GlowDecisionGuarantee { - /// Decision следует из exact integer/rational state. - BitExact, - /// Decision следует из доказанного непересекающегося outward interval. - OutwardIntervalV1 { - /// Нижняя граница. - lower: f64, - /// Верхняя граница. - upper: f64, - }, - /// Explicit legacy CAM16/libm-dependent path. - LegacyPlatformDependentV1, -} - /// Exact point-composite profile. #[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] pub enum GlowCompositeProfile { @@ -190,22 +177,6 @@ pub enum GlowCompositeGuarantee { BitExact, } -/// Appearance diagnostic identity; не decision guarantee. -#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] -pub enum GlowDiagnosticProfile { - /// CAM16-UCS J-prime transcription from Li et al. 2017. - Cam16UcsJPrimeLi2017V1, -} - -/// Результат проверки target по конечному point-composite домену. -#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] -pub enum GlowTargetStatus { - /// Target достигнут. - Reached, - /// Target недостижим; выбран максимум только в explicit legacy profile. - Unreachable, -} - /// Зарегистрированный branch-sensitive site. #[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] pub enum NumericalSiteId { @@ -227,38 +198,48 @@ pub enum NumericalIndeterminacy { }, } -/// Low-level point Glow decision, одинаково типизированный для Rust/Swift/WASM. +/// Общий exact point-composite payload determinate Glow outcome. +/// +/// Semantic provenance задаёт владеющий вариант [`GlowPointDecision`], а не +/// независимые поля, из которых можно собрать невозможную комбинацию. +#[derive(Debug, Clone, PartialEq, uniffi::Record)] +pub struct GlowPointValue { + /// Exact composite profile. + pub composite_profile: GlowCompositeProfile, + /// Exact composite guarantee. + pub composite_guarantee: GlowCompositeGuarantee, + /// Canonical layer alpha. + pub alpha: f64, + /// Shortest-roundtrip CSS alpha. + pub alpha_css: String, + /// Requested target. + pub target_dj: f64, + /// Diagnostic achieved value. + pub achieved_dj: f64, + /// Exact point composite. + pub composite_hex: String, +} + +/// Low-level point Glow decision как сумма только допустимых provenance-state. #[derive(Debug, Clone, PartialEq, uniffi::Enum)] pub enum GlowPointDecision { - /// Selected state с раздельными decision/composite guarantees. - Determinate { - /// Explicit request profile. - decision_profile: GlowDecisionProfile, - /// Guarantee semantic target/max decision. - decision_guarantee: GlowDecisionGuarantee, - /// Exact composite profile. - composite_profile: GlowCompositeProfile, - /// Exact composite guarantee. - composite_guarantee: GlowCompositeGuarantee, - /// Diagnostic appearance identity. - diagnostic_profile: Option, - /// Canonical layer alpha. - alpha: f64, - /// Shortest-roundtrip CSS alpha. - alpha_css: String, - /// Requested target. - target_dj: f64, - /// Legacy/exact target status. - target_status: GlowTargetStatus, - /// Diagnostic achieved value. - achieved_dj: f64, - /// Exact point composite. - composite_hex: String, + /// Stable byte-exact no-op: target недостижим без appearance selection. + StableExactNoop { + /// Exact point-composite payload. + value: GlowPointValue, + }, + /// Explicit legacy CAM16/libm selection достиг target. + LegacyReached { + /// Exact point-composite payload выбранного состояния. + value: GlowPointValue, }, - /// No selected state; caller gets typed site + evidence and no fallback. + /// Explicit legacy CAM16/libm selection не достиг target и вернул максимум. + LegacyUnreachable { + /// Exact point-composite payload диагностически выбранного максимума. + value: GlowPointValue, + }, + /// Stable path не выбрал состояние без sound bound и не сделал fallback. Indeterminate { - /// Explicit request profile. - decision_profile: GlowDecisionProfile, /// Registered site. site_id: NumericalSiteId, /// Typed reason and its sound interval, if present. @@ -294,29 +275,41 @@ pub enum ColorError { /// Стабильный машинный код причины. code: String, }, - /// Невалидный low-level Glow request или неизвестный forward variant. + /// Невалидный low-level Glow request на публичной границе. #[error("невалидный glow request: {reason}")] InvalidGlowRequest { /// Человекочитаемая причина. reason: String, }, + /// FFI-adapter получил internal postcondition failure либо вариант core- + /// контракта, который эта версия нативной поверхности не умеет представить + /// без потери смысла. Это не невалидный пользовательский request. + #[error("несовместимый контракт ядра: {reason}")] + IncompatibleCoreContract { + /// Человекочитаемая причина internal/core-surface несовместимости. + reason: String, + }, } /// Стабильный код недостижимости — тождественен маппингу WASM-границы /// (`labcolors-wasm/src/engine.rs`) и conformance-пака. Общий контракт имён для -/// ВСЕХ биндингов: одна причина → один код на любой платформе. -fn unreachable_code(err: &labcolors_core::Unreachable) -> &'static str { +/// ВСЕХ биндингов: одна известная причина → один код на любой платформе; +/// неизвестный forward-вариант — несовместимость версии, а не fallback-code. +fn unreachable_code(err: &labcolors_core::Unreachable) -> Result<&'static str, ColorError> { use labcolors_core::Unreachable as U; match err { - U::BelowContrastFloor { .. } => "below_contrast_floor", - U::ExceedsRange { .. } => "exceeds_range", - U::QuantizationGap { .. } => "quantization_gap", - U::FloorUnreachable { .. } => "floor_unreachable", - U::PolarityMismatch { .. } => "polarity_mismatch", - U::GamutUnsupported => "gamut_unsupported", - U::InvalidInput(_) => "invalid_input", - // `Unreachable` помечен `#[non_exhaustive]`; forward-compat-арм. - _ => "unreachable", + U::BelowContrastFloor { .. } => Ok("below_contrast_floor"), + U::ExceedsRange { .. } => Ok("exceeds_range"), + U::QuantizationGap { .. } => Ok("quantization_gap"), + U::FloorUnreachable { .. } => Ok("floor_unreachable"), + U::PolarityMismatch { .. } => Ok("polarity_mismatch"), + U::GamutUnsupported => Ok("gamut_unsupported"), + U::InvalidInput(_) => Ok("invalid_input"), + U::InternalInvariant(reason) => Err(ColorError::IncompatibleCoreContract { + reason: format!("internal core invariant failure: {reason}"), + }), + // Forward core variant — несовместимость adapter, не выдуманный code. + _ => Err(incompatible_core_variant("Unreachable")), } } @@ -375,7 +368,9 @@ pub fn recheck(bg: String, fgs: Vec, theme: Theme) -> Result Err(ColorError::Unreachable { - code: unreachable_code(&e).to_string(), - }), + Err(e) => { + let code = unreachable_code(&e)?; + Err(ColorError::Unreachable { + code: code.to_string(), + }) + } } } @@ -453,33 +451,52 @@ pub fn min_alpha(tint: String, bg: String) -> Result { min_alpha_hex(&tint, &bg).map_err(|reason| ColorError::InvalidColor { reason }) } -/// Point Glow solve с обязательным numerical profile. Stable uncertainty -/// возвращается data-вариантом `Indeterminate`, не ошибкой и не fallback. -/// -/// # Errors -/// -/// [`ColorError::InvalidGlowRequest`] на недоменном input/internal variant. -fn unknown_glow_variant(name: &str) -> ColorError { - ColorError::InvalidGlowRequest { +fn incompatible_core_variant(name: &str) -> ColorError { + ColorError::IncompatibleCoreContract { reason: format!("unknown core {name} variant"), } } -fn decision_guarantee_to_ffi( +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GlowDeterminateKind { + StableExactNoop, + LegacyReached, + LegacyUnreachable, +} + +fn glow_determinate_kind( + requested_profile: GlowDecisionProfile, guarantee: DecisionGuaranteeV1, -) -> Result { - match guarantee { - DecisionGuaranteeV1::BitExact => Ok(GlowDecisionGuarantee::BitExact), - DecisionGuaranteeV1::OutwardIntervalV1(interval) => { - Ok(GlowDecisionGuarantee::OutwardIntervalV1 { - lower: interval.lower(), - upper: interval.upper(), - }) - } - DecisionGuaranteeV1::LegacyPlatformDependentV1 => { - Ok(GlowDecisionGuarantee::LegacyPlatformDependentV1) - } - _ => Err(unknown_glow_variant("DecisionGuaranteeV1")), + status: CoreGlowTargetStatus, + selection_diagnostic_profile: Option, +) -> Result { + match ( + requested_profile, + guarantee, + status, + selection_diagnostic_profile, + ) { + ( + GlowDecisionProfile::StableV1, + DecisionGuaranteeV1::BitExact, + CoreGlowTargetStatus::ExactNoopUnreachable, + None, + ) => Ok(GlowDeterminateKind::StableExactNoop), + ( + GlowDecisionProfile::LegacyPlatformDependentV1, + DecisionGuaranteeV1::LegacyPlatformDependentV1, + CoreGlowTargetStatus::LegacyReached, + Some(GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1), + ) => Ok(GlowDeterminateKind::LegacyReached), + ( + GlowDecisionProfile::LegacyPlatformDependentV1, + DecisionGuaranteeV1::LegacyPlatformDependentV1, + CoreGlowTargetStatus::LegacyUnreachable, + Some(GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1), + ) => Ok(GlowDeterminateKind::LegacyUnreachable), + illegal => Err(ColorError::IncompatibleCoreContract { + reason: format!("illegal Glow determinate provenance from core: {illegal:?}"), + }), } } @@ -490,7 +507,7 @@ fn composite_profile_to_ffi( GlowCompositeProfileV1::EncodedSrgb8ScreenV1 => { Ok(GlowCompositeProfile::EncodedSrgb8ScreenV1) } - _ => Err(unknown_glow_variant("GlowCompositeProfileV1")), + _ => Err(incompatible_core_variant("GlowCompositeProfileV1")), } } @@ -499,34 +516,51 @@ fn composite_guarantee_to_ffi( ) -> Result { match guarantee { GlowCompositeGuaranteeV1::BitExact => Ok(GlowCompositeGuarantee::BitExact), - _ => Err(unknown_glow_variant("GlowCompositeGuaranteeV1")), + _ => Err(incompatible_core_variant("GlowCompositeGuaranteeV1")), } } -fn diagnostic_profile_to_ffi( - profile: GlowDiagnosticProfileV1, -) -> Result { - match profile { - GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1 => { - Ok(GlowDiagnosticProfile::Cam16UcsJPrimeLi2017V1) - } - _ => Err(unknown_glow_variant("GlowDiagnosticProfileV1")), +fn numerical_site_to_ffi(site: NumericalSiteIdV1) -> Result { + match site { + NumericalSiteIdV1::GlowTargetOrMaximumV1 => Ok(NumericalSiteId::GlowTargetOrMaximumV1), + _ => Err(incompatible_core_variant("NumericalSiteIdV1")), } } -fn target_status_to_ffi(status: CoreGlowTargetStatus) -> Result { - match status { - CoreGlowTargetStatus::Reached => Ok(GlowTargetStatus::Reached), - CoreGlowTargetStatus::Unreachable => Ok(GlowTargetStatus::Unreachable), - _ => Err(unknown_glow_variant("GlowTargetStatus")), +fn validate_glow_request(tint: &str, background: &str, target_dj: f64) -> Result<(), ColorError> { + srgb_encoded_from_hex(tint).map_err(|reason| ColorError::InvalidGlowRequest { + reason: format!("invalid tint: {reason}"), + })?; + srgb_encoded_from_hex(background).map_err(|reason| ColorError::InvalidGlowRequest { + reason: format!("invalid background: {reason}"), + })?; + if !target_dj.is_finite() || target_dj <= 0.0 { + return Err(ColorError::InvalidGlowRequest { + reason: format!("target_dj должен быть конечным и > 0, получено {target_dj}"), + }); } + Ok(()) } -fn numerical_site_to_ffi(site: NumericalSiteIdV1) -> Result { - match site { - NumericalSiteIdV1::GlowTargetOrMaximumV1 => Ok(NumericalSiteId::GlowTargetOrMaximumV1), - _ => Err(unknown_glow_variant("NumericalSiteIdV1")), - } +fn map_prevalidated_glow_core_result(result: Result) -> Result { + result.map_err(|reason| ColorError::IncompatibleCoreContract { + reason: format!("core rejected a prevalidated Glow request: {reason}"), + }) +} + +fn glow_point_value_to_ffi( + value: &labcolors_core::GlowSolve, +) -> Result { + let certificate = value.composite_certificate(); + Ok(GlowPointValue { + composite_profile: composite_profile_to_ffi(certificate.profile())?, + composite_guarantee: composite_guarantee_to_ffi(certificate.guarantee())?, + alpha: value.alpha(), + alpha_css: value.alpha_css().to_string(), + target_dj: value.target_dj(), + achieved_dj: value.achieved_dj(), + composite_hex: value.composite_hex().to_string(), + }) } fn indeterminacy_to_ffi( @@ -542,10 +576,19 @@ fn indeterminacy_to_ffi( upper: interval.upper(), }) } - _ => Err(unknown_glow_variant("NumericalIndeterminacyV1")), + _ => Err(incompatible_core_variant("NumericalIndeterminacyV1")), } } +/// Point Glow solve с обязательным numerical profile. Stable uncertainty +/// возвращается data-вариантом `Indeterminate`, не ошибкой и не fallback. +/// +/// # Errors +/// +/// [`ColorError::InvalidGlowRequest`] на недоменном публичном input. +/// [`ColorError::IncompatibleCoreContract`] если core отверг уже проверенный +/// request либо FFI-adapter получил illegal/unsupported/unknown outcome, +/// который нельзя представить этой версией Glow-specific поверхности. #[uniffi::export] pub fn solve_glow_point( tint: String, @@ -554,44 +597,58 @@ pub fn solve_glow_point( theme: Theme, profile: GlowDecisionProfile, ) -> Result { + validate_glow_request(&tint, &background, target_dj)?; let core_profile = profile.to_core(); - let decision = - solve_screen_alpha_for_dj(&tint, &background, target_dj, core_profile, &theme.vc()) - .map_err(|reason| ColorError::InvalidGlowRequest { reason })?; + let decision = map_prevalidated_glow_core_result(solve_screen_alpha_for_dj( + &tint, + &background, + target_dj, + core_profile, + &theme.vc(), + ))?; match decision { NumericalDecisionV1::Determinate { value, guarantee } => { - let certificate = value.composite_certificate(); - Ok(GlowPointDecision::Determinate { - decision_profile: profile, - decision_guarantee: decision_guarantee_to_ffi(guarantee)?, - composite_profile: composite_profile_to_ffi(certificate.profile())?, - composite_guarantee: composite_guarantee_to_ffi(certificate.guarantee())?, - diagnostic_profile: value - .diagnostic_profile() - .map(diagnostic_profile_to_ffi) - .transpose()?, - alpha: value.alpha(), - alpha_css: value.alpha_css().to_string(), - target_dj: value.target_dj(), - target_status: target_status_to_ffi(value.status())?, - achieved_dj: value.achieved_dj(), - composite_hex: value.composite_hex().to_string(), - }) + let kind = glow_determinate_kind( + profile, + guarantee, + value.status(), + value.selection_diagnostic_profile(), + )?; + let value = glow_point_value_to_ffi(&value)?; + match kind { + GlowDeterminateKind::StableExactNoop => { + Ok(GlowPointDecision::StableExactNoop { value }) + } + GlowDeterminateKind::LegacyReached => { + Ok(GlowPointDecision::LegacyReached { value }) + } + GlowDeterminateKind::LegacyUnreachable => { + Ok(GlowPointDecision::LegacyUnreachable { value }) + } + } } NumericalDecisionV1::Indeterminate { site_id, evidence } => { + if profile != GlowDecisionProfile::StableV1 { + return Err(ColorError::IncompatibleCoreContract { + reason: "legacy Glow profile returned an Indeterminate core outcome" + .to_string(), + }); + } Ok(GlowPointDecision::Indeterminate { - decision_profile: profile, site_id: numerical_site_to_ffi(site_id)?, evidence: indeterminacy_to_ffi(evidence)?, }) } - _ => Err(ColorError::InvalidGlowRequest { - reason: "unknown NumericalDecisionV1".to_string(), - }), + _ => Err(incompatible_core_variant("NumericalDecisionV1")), } } -/// Оценка мутности («грязи») цвета `[0,1]`: 0 — чистый, 1 — грязный. +/// Замороженная legacy-координата `muddiness` для цвета. +/// +/// Это `experimental compatibility proxy`: функция воспроизводит исторический +/// числовой API, но не является валидированным на наблюдателях человеческим +/// вердиктом clean/dirty и не должна использоваться как production decision. +/// Legacy-идентификатор сохранён только для совместимости. /// /// # Errors /// @@ -606,15 +663,137 @@ mod tests { use super::*; #[test] - fn outward_interval_evidence_survives_the_ffi_mapping() { - let interval = labcolors_core::OutwardIntervalV1::try_new(0.9, 1.1).unwrap(); - assert_eq!( - decision_guarantee_to_ffi(DecisionGuaranteeV1::OutwardIntervalV1(interval)).unwrap(), - GlowDecisionGuarantee::OutwardIntervalV1 { - lower: 0.9, - upper: 1.1, + fn illegal_glow_provenance_cross_product_is_rejected() { + let error = glow_determinate_kind( + GlowDecisionProfile::StableV1, + DecisionGuaranteeV1::LegacyPlatformDependentV1, + CoreGlowTargetStatus::LegacyReached, + Some(GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1), + ) + .unwrap_err(); + assert!(matches!( + error, + ColorError::IncompatibleCoreContract { ref reason } + if reason.contains("illegal Glow determinate provenance") + )); + } + + #[test] + fn prevalidated_core_glow_error_is_not_reclassified_as_public_input() { + let error = map_prevalidated_glow_core_result::<()>(Err("synthetic core drift".into())) + .unwrap_err(); + assert!(matches!( + error, + ColorError::IncompatibleCoreContract { ref reason } + if reason.contains("synthetic core drift") + )); + } + + #[test] + fn stable_noop_output_is_an_atomic_variant() { + let decision = solve_glow_point( + "#010000".into(), + "#FE0000".into(), + 2.3006, + Theme::Light, + GlowDecisionProfile::StableV1, + ) + .unwrap(); + assert!(matches!( + decision, + GlowPointDecision::StableExactNoop { .. } + )); + } + + #[test] + fn known_unreachable_code_mapping_is_fallible_without_code_drift() { + use labcolors_core::Unreachable as U; + let cases = [ + ( + U::BelowContrastFloor { target: 1.0 }, + "below_contrast_floor", + ), + ( + U::ExceedsRange { + target: 100.0, + max_achievable: 90.0, + }, + "exceeds_range", + ), + ( + U::QuantizationGap { + target: 50.0, + nearest: 49.0, + }, + "quantization_gap", + ), + ( + U::FloorUnreachable { + floor: 4.5, + max_ratio: 4.0, + }, + "floor_unreachable", + ), + (U::PolarityMismatch { target: -60.0 }, "polarity_mismatch"), + (U::GamutUnsupported, "gamut_unsupported"), + (U::InvalidInput("fixture".into()), "invalid_input"), + ]; + for (error, expected) in cases { + assert_eq!(unreachable_code(&error).unwrap(), expected); + } + + let internal = U::InternalInvariant("fixture drift".into()); + assert!(matches!( + unreachable_code(&internal), + Err(ColorError::IncompatibleCoreContract { reason }) + if reason == "internal core invariant failure: fixture drift" + )); + } + + #[test] + fn glow_output_sum_is_exhaustive_over_lawful_provenance_states() { + fn branch_name(decision: &GlowPointDecision) -> &'static str { + match decision { + GlowPointDecision::StableExactNoop { .. } => "stable-exact-noop", + GlowPointDecision::LegacyReached { .. } => "legacy-reached", + GlowPointDecision::LegacyUnreachable { .. } => "legacy-unreachable", + GlowPointDecision::Indeterminate { .. } => "indeterminate", } + } + + let decision = solve_glow_point( + "#010000".into(), + "#FE0000".into(), + 2.3006, + Theme::Light, + GlowDecisionProfile::StableV1, + ) + .unwrap(); + assert_eq!( + branch_name(&decision), + "stable-exact-noop", + "exhaustive match обязан читать реальный public output" ); + } + + #[test] + fn unsupported_outward_decision_guarantee_is_an_incompatible_core_contract() { + let interval = labcolors_core::OutwardIntervalV1::try_new(0.9, 1.1).unwrap(); + let error = glow_determinate_kind( + GlowDecisionProfile::StableV1, + DecisionGuaranteeV1::OutwardIntervalV1(interval), + CoreGlowTargetStatus::ExactNoopUnreachable, + None, + ) + .unwrap_err(); + assert!(matches!( + error, + ColorError::IncompatibleCoreContract { ref reason } + if reason.contains("illegal Glow determinate provenance") + )); + + // Outward evidence остаётся законной частью typed Indeterminate: + // запрещена только ложная Glow-specific determinate guarantee. assert_eq!( indeterminacy_to_ffi(NumericalIndeterminacyV1::IntervalOverlap(interval)).unwrap(), NumericalIndeterminacy::IntervalOverlap { @@ -622,6 +801,35 @@ mod tests { upper: 1.1, } ); + + let unknown = incompatible_core_variant("FutureGlowContractV2"); + assert!(matches!( + unknown, + ColorError::IncompatibleCoreContract { ref reason } + if reason.contains("FutureGlowContractV2") + )); + } + + #[test] + fn invalid_public_glow_inputs_remain_invalid_glow_requests() { + for (tint, background, target_dj) in [ + ("not-a-color", "#000000", 2.3006), + ("#C0B2FA", "not-a-color", 2.3006), + ("#C0B2FA", "#000000", 0.0), + ("#C0B2FA", "#000000", -1.0), + ("#C0B2FA", "#000000", f64::NAN), + ("#C0B2FA", "#000000", f64::INFINITY), + ] { + let error = solve_glow_point( + tint.into(), + background.into(), + target_dj, + Theme::Light, + GlowDecisionProfile::StableV1, + ) + .unwrap_err(); + assert!(matches!(error, ColorError::InvalidGlowRequest { .. })); + } } #[test] @@ -686,18 +894,25 @@ mod tests { } #[test] - fn muddiness_orders_olive_above_clean_gray() { - // Значения — факт ядра (см. conformance/vectors/muddiness.json): серый - // почти чист, олива заметно мутнее. Проверяем ПОРЯДОК и чистоту серого, - // не магическое число. - let olive = muddiness("#6B6B2E".into()).unwrap(); - let gray = muddiness("#808080".into()).unwrap(); - assert!(gray < 0.05, "серый должен быть чистым, получено {gray}"); - assert!(olive > gray, "олива должна быть мутнее серого"); + fn muddiness_boundary_matches_frozen_core_coordinate() { + for hex in ["#6B6B2E", "#808080", "#007AFF", "#8A7A50"] { + let boundary = muddiness(hex.into()).expect("valid public input"); + let core = muddiness_from_hex(hex).expect("valid core input"); + assert_eq!( + boundary.to_bits(), + core.to_bits(), + "FFI boundary drifted for {hex}" + ); + } + + assert!(matches!( + muddiness("not-a-colour".into()), + Err(ColorError::InvalidColor { .. }) + )); } #[test] - fn glow_stable_indeterminate_and_legacy_guarantees_do_not_collapse() { + fn glow_provenance_is_encoded_by_atomic_output_variants() { let stable = solve_glow_point( "#C0B2FA".into(), "#000000".into(), @@ -709,7 +924,6 @@ mod tests { assert!(matches!( stable, GlowPointDecision::Indeterminate { - decision_profile: GlowDecisionProfile::StableV1, site_id: NumericalSiteId::GlowTargetOrMaximumV1, evidence: NumericalIndeterminacy::SoundBoundUnavailable, } @@ -725,14 +939,9 @@ mod tests { .unwrap(); assert!(matches!( legacy, - GlowPointDecision::Determinate { - decision_profile: GlowDecisionProfile::LegacyPlatformDependentV1, - decision_guarantee: GlowDecisionGuarantee::LegacyPlatformDependentV1, - composite_profile: GlowCompositeProfile::EncodedSrgb8ScreenV1, - composite_guarantee: GlowCompositeGuarantee::BitExact, - diagnostic_profile: Some(GlowDiagnosticProfile::Cam16UcsJPrimeLi2017V1), - .. - } + GlowPointDecision::LegacyReached { value } + if value.composite_profile == GlowCompositeProfile::EncodedSrgb8ScreenV1 + && value.composite_guarantee == GlowCompositeGuarantee::BitExact )); let stable_noop = solve_glow_point( @@ -745,13 +954,22 @@ mod tests { .unwrap(); assert!(matches!( stable_noop, - GlowPointDecision::Determinate { - decision_profile: GlowDecisionProfile::StableV1, - decision_guarantee: GlowDecisionGuarantee::BitExact, - diagnostic_profile: None, - ref composite_hex, - .. - } if composite_hex == "#FFFFFF" + GlowPointDecision::StableExactNoop { value } + if value.composite_hex == "#FFFFFF" + )); + + let legacy_unreachable = solve_glow_point( + "#C0B2FA".into(), + "#FFFFFF".into(), + 2.3006, + Theme::Light, + GlowDecisionProfile::LegacyPlatformDependentV1, + ) + .unwrap(); + assert!(matches!( + legacy_unreachable, + GlowPointDecision::LegacyUnreachable { value } + if value.composite_hex == "#FFFFFF" )); } @@ -767,18 +985,11 @@ mod tests { .unwrap(); assert!(matches!( sub_lsb, - GlowPointDecision::Determinate { - decision_profile: GlowDecisionProfile::StableV1, - decision_guarantee: GlowDecisionGuarantee::BitExact, - composite_profile: GlowCompositeProfile::EncodedSrgb8ScreenV1, - composite_guarantee: GlowCompositeGuarantee::BitExact, - diagnostic_profile: None, - target_status: GlowTargetStatus::Unreachable, - achieved_dj, - ref composite_hex, - .. - } if achieved_dj.to_bits() == 0.0_f64.to_bits() - && composite_hex == "#FE0000" + GlowPointDecision::StableExactNoop { value } + if value.composite_profile == GlowCompositeProfile::EncodedSrgb8ScreenV1 + && value.composite_guarantee == GlowCompositeGuarantee::BitExact + && value.achieved_dj.to_bits() == 0.0_f64.to_bits() + && value.composite_hex == "#FE0000" )); let crossing = solve_glow_point( @@ -792,7 +1003,6 @@ mod tests { assert!(matches!( crossing, GlowPointDecision::Indeterminate { - decision_profile: GlowDecisionProfile::StableV1, site_id: NumericalSiteId::GlowTargetOrMaximumV1, evidence: NumericalIndeterminacy::SoundBoundUnavailable, } diff --git a/crates/labcolors-wasm/src/dto.rs b/crates/labcolors-wasm/src/dto.rs index 542db3e2..c9e88620 100644 --- a/crates/labcolors-wasm/src/dto.rs +++ b/crates/labcolors-wasm/src/dto.rs @@ -88,8 +88,14 @@ pub struct GlowColor { pub composite_profile: labcolors_core::GlowCompositeProfileV1, /// Exact point-composite guarantee. pub composite_guarantee: labcolors_core::GlowCompositeGuaranteeV1, - /// Diagnostic appearance model, если solve реально её вызвал. - pub diagnostic_profile: Option, + /// Версионированный алгоритм, построивший анатомию core/halo. + pub layer_recipe_profile: labcolors_core::GlowLayerRecipeProfileV1, + /// Диагностика внешнего вида полного результата Glow. Обязательна, потому что + /// `core_achieved_dj` реально вычисляется через CAM16-UCS J′. + pub appearance_diagnostic_profile: labcolors_core::GlowDiagnosticProfileV1, + /// Диагностическая модель, участвовавшая именно в выборе target/max. `None` у + /// точного no-op профиля stable, который не выполняет выбор по внешнему виду. + pub selection_diagnostic_profile: Option, /// Explicit client-selected decision profile. pub decision_profile: labcolors_core::GlowDecisionProfileV1, /// Guarantee target/max decision. @@ -128,21 +134,23 @@ pub struct GlowIndeterminateColor { /// Двухслойный материал (kind material): тон + выведенная α + вердикт гарантии. /// /// Тинт `01`, база `02` и солид-канон равны тону по построению (композит `T` над -/// `T` есть `T`), поэтому один `tone_hex`. `alpha` выведена как минимальная -/// плотность, при которой композит тона над худшим фоном коридора держит `floor`. +/// `T` есть `T`), поэтому один `tone_hex`. `alpha` — повторно проверенная верхняя +/// граница платформенно охарактеризованного поиска; численное свидетельство +/// лежит рядом. #[derive(Debug, Clone, PartialEq)] pub struct MaterialColor { /// Тон `#RRGGBB`: тинт `01`, база `02` и солид-канон одновременно. pub tone_hex: String, - /// Выведенная альфа тинта `01`, `(0, 1]`. + /// Выбранная альфа тинта `01`, `[0, 1]`. pub alpha: f64, /// Худший WCAG-контраст коммит-полюса по коридору `[чёрный, белый]`. pub worst_contrast: f64, - /// WCAG-пол читаемости, который держит α (4.5 / 3.0). + /// Платформенно охарактеризованное свидетельство выбора границы alpha. + pub alpha_guarantee: labcolors_core::MaterialAlphaGuaranteeV1, + /// Типизированный исход проверки пола; от него выведен булев псевдоним совместимости. + pub alpha_status: labcolors_core::MaterialAlphaStatusV1, + /// Запрошенный WCAG-пол (4.5 / 3.0); держится только при `Satisfied`. pub floor: f64, - /// Гарантия выполнена: `worst_contrast ≥ floor`. `false` — пол недостижим - /// даже при α = 1 (честная деградация, α = 1 как ближайшая достижимая). - pub guaranteed: bool, /// Коммит-полюс поверхности белый (`true`, тёмный тон) или чёрный (`false`). pub pole_white: bool, /// Фактический |ΔJ'| тона-базы от фона резолва — различимость поверхности. diff --git a/crates/labcolors-wasm/src/engine.rs b/crates/labcolors-wasm/src/engine.rs index 046001c0..fb28b388 100644 --- a/crates/labcolors-wasm/src/engine.rs +++ b/crates/labcolors-wasm/src/engine.rs @@ -116,8 +116,9 @@ impl Engine { /// Resolve every role for `bg_hex` under `theme`, returning the shared /// result. Repeated identical calls hit the contract cache. /// - /// Errors (bad hex, unknown theme) are returned, never - /// panicked. Per-role unreachability is part of a *successful* result. + /// Errors (bad hex, unknown theme, or an internal core invariant failure) + /// are returned, never panicked. Physical per-role unreachability is part + /// of a *successful* result; internal provenance fails the whole call. pub fn resolve_theme( &self, bg_hex: &str, @@ -259,7 +260,7 @@ fn map_resolved(resolved: Resolved, legal_floor: Option) -> Result RoleOutcome::None, Resolved::Unreachable(reason) => RoleOutcome::Unreachable { - code: unreachable_code(&reason), + code: unreachable_code(&reason)?, message: reason.to_string(), }, // Полупрозрачная эмиссия лестницы/альфа-аналога (конфиг-путь): @@ -283,7 +284,9 @@ fn map_resolved(resolved: Resolved, legal_floor: Option) -> Result) -> Result &'static str { - match reason { +fn unreachable_code(reason: &Unreachable) -> Result<&'static str, BindingError> { + let code = match reason { Unreachable::BelowContrastFloor { .. } => "below_contrast_floor", Unreachable::ExceedsRange { .. } => "exceeds_range", Unreachable::QuantizationGap { .. } => "quantization_gap", @@ -371,7 +375,19 @@ fn unreachable_code(reason: &Unreachable) -> &'static str { Unreachable::PolarityMismatch { .. } => "polarity_mismatch", Unreachable::GamutUnsupported => "gamut_unsupported", Unreachable::InvalidInput(_) => "invalid_input", - _ => "unreachable", + Unreachable::InternalInvariant(reason) => { + return Err(BindingError::Internal { + reason: reason.clone(), + }); + } + _ => return Err(unmapped_unreachable_reason()), + }; + Ok(code) +} + +fn unmapped_unreachable_reason() -> BindingError { + BindingError::Internal { + reason: "unmapped core Unreachable variant".to_string(), } } @@ -439,6 +455,14 @@ mod tests { if reason == "unmapped core Resolved variant" )); assert_eq!(error.code(), "internal_error"); + + let error = unmapped_unreachable_reason(); + assert!(matches!( + error, + BindingError::Internal { ref reason } + if reason == "unmapped core Unreachable variant" + )); + assert_eq!(error.code(), "internal_error"); } /// An engine with the frozen labui passport loaded — the config the built-in @@ -579,7 +603,7 @@ mod tests { Gamut::DisplayP3, ) .unwrap_err(); - assert_eq!(unreachable_code(&gamut_err), "gamut_unsupported"); + assert_eq!(unreachable_code(&gamut_err).unwrap(), "gamut_unsupported"); // A non-finite target is rejected up front as invalid input. let invalid_err = solve( @@ -591,7 +615,14 @@ mod tests { Gamut::Srgb, ) .unwrap_err(); - assert_eq!(unreachable_code(&invalid_err), "invalid_input"); + assert_eq!(unreachable_code(&invalid_err).unwrap(), "invalid_input"); + + let internal = Unreachable::InternalInvariant("fixture drift".into()); + let error = unreachable_code(&internal).unwrap_err(); + assert!(matches!( + error, + BindingError::Internal { reason } if reason == "fixture drift" + )); } #[test] @@ -1002,7 +1033,15 @@ mod tests { g.halo_composite_certificate().guarantee(), o.composite_guarantee ); - assert_eq!(g.diagnostic_profile(), o.diagnostic_profile); + assert_eq!(g.layer_recipe_profile(), o.layer_recipe_profile); + assert_eq!( + g.appearance_diagnostic_profile(), + o.appearance_diagnostic_profile + ); + assert_eq!( + g.selection_diagnostic_profile(), + o.selection_diagnostic_profile + ); assert_eq!(g.decision_profile(), o.decision_profile); assert_eq!(g.decision_guarantee(), o.decision_guarantee); assert_eq!(g.constraint_layer(), o.constraint_layer); @@ -1013,7 +1052,11 @@ mod tests { assert_eq!(g.core_achieved_dj(), o.core_achieved_dj); assert_eq!( g.degraded(), - o.target_status == labcolors_core::GlowTargetStatus::Unreachable + matches!( + o.target_status, + labcolors_core::GlowTargetStatus::ExactNoopUnreachable + | labcolors_core::GlowTargetStatus::LegacyUnreachable + ) ); assert_eq!(g.achieved_dj().to_bits(), o.halo_achieved_dj.to_bits()); } @@ -1033,7 +1076,16 @@ mod tests { "{name}: material worst_contrast" ); assert_eq!(m.floor(), o.floor, "{name}: material floor"); - assert_eq!(m.guaranteed(), o.guaranteed, "{name}: material guaranteed"); + assert_eq!( + m.alpha_status(), + o.alpha_status, + "{name}: material alpha status" + ); + assert_eq!( + m.guaranteed(), + o.alpha_status == labcolors_core::MaterialAlphaStatusV1::Satisfied, + "{name}: material guaranteed compatibility alias" + ); assert_eq!( matches!(m.pole(), labcolors_core::Pole::White), o.pole_white, @@ -1181,10 +1233,16 @@ mod tests { assert_eq!(glow["kind"], "glow"); assert_eq!(glow["decisionProfile"], "stable-v1"); assert_eq!(glow["decisionGuarantee"]["kind"], "bit-exact"); + assert_eq!(glow["targetStatus"], "exact-noop-unreachable"); + assert_eq!(glow["layerRecipeProfile"], "cam16-jprime-oklab-cusp-v1"); assert_eq!( - glow["diagnosticProfile"], + glow["appearanceDiagnosticProfile"], "cam16-ucs-jprime-li2017-v1", + "full Glow computes coreAchievedDj through CAM16" + ); + assert_eq!( + glow["selectionDiagnosticProfile"], serde_json::Value::Null, - "exact no-op does not execute CAM16 and must not claim its profile" + "exact no-op selection does not execute CAM16" ); assert_eq!(glow["haloCompositeHex"], "#FFFFFF"); for key in [ @@ -1196,6 +1254,128 @@ mod tests { } } + /// `None` остаётся полноценным client-owned токеном в metadata, но ни сама + /// zero-роль, ни алиас на неё не получают CSS-значение. Это anti-vacuum + /// проверка ровно той границы, которую namespace preflight защищает. + #[test] + fn none_roles_and_aliases_publish_metadata_but_never_values() { + let mut engine = Engine::new(); + engine.load_config(&labui_json()).expect("labui валиден"); + let resolved = engine + .resolve_theme("#101012", Theme::Dark) + .expect("валидный контракт резолвится"); + let projected: serde_json::Value = + serde_json::from_str(&crate::projection::resolved_json(&resolved).unwrap()).unwrap(); + + for name in ["none", "border-none", "fill-none", "border-ghost"] { + let css_var = format!("--lab-{name}"); + let role = &projected["roles"][name]; + assert_eq!(role["kind"], "none", "{name} обязан остаться none"); + assert_eq!(role["cssVar"], css_var, "{name} сохраняет своё имя"); + assert!( + projected["vars"].get(&css_var).is_none(), + "{name}: none-токен не имеет права получить CSS-значение" + ); + } + } + + /// Differential contract: берём фактические satellite keys из WASM- + /// проекции валидных Glow/Material outcomes и убеждаемся, что публичный + /// `load_config` не разрешает объявить ни zero-роль, ни zero-alias с любым + /// из этих имён. Тест не дублирует список суффиксов валидатора как oracle. + #[test] + fn load_config_rejects_zero_names_for_every_projected_satellite() { + let cases = [ + ( + "probe-glow", + serde_json::json!({ + "kind": "glow", + "source": {"kind": "brand"}, + "step": "base", + "decision_profile": "legacy-platform-dependent-v1" + }), + ), + ( + "probe-material", + serde_json::json!({ + "kind": "material", + "source": {"kind": "neutral", "pick": "mid"}, + "tone_light": 10.0, + "tone_dark": 10.0, + "floor": "aa-text" + }), + ), + ]; + + for (owner, recipe) in cases { + let mut valid: serde_json::Value = + serde_json::from_str(&labui_json()).expect("паспорт labui — валидный JSON"); + valid["roles"] + .as_array_mut() + .expect("roles — массив") + .push(serde_json::json!({"name": owner, "recipe": recipe})); + + let mut engine = Engine::new(); + engine + .load_config(&valid.to_string()) + .expect("контрольный многоключевой рецепт валиден"); + let resolved = engine + .resolve_theme("#101012", Theme::Dark) + .expect("контрольный рецепт резолвится"); + let projected: serde_json::Value = + serde_json::from_str(&crate::projection::resolved_json(&resolved).unwrap()) + .unwrap(); + let prefix = format!("--lab-{owner}-"); + let satellites: Vec = projected["vars"] + .as_object() + .expect("vars — объект") + .keys() + .filter(|key| key.starts_with(&prefix)) + .cloned() + .collect(); + assert_eq!(satellites.len(), 2, "{owner}: ожидаются два сателлита"); + + for css_key in satellites { + let zero_name = css_key + .strip_prefix("--lab-") + .expect("проекция использует канонический prefix"); + + for as_alias in [false, true] { + let mut colliding = valid.clone(); + if as_alias { + colliding["aliases"] + .as_array_mut() + .expect("aliases — массив") + .push(serde_json::json!({ + "alias": zero_name, + "target": "none" + })); + } else { + colliding["roles"] + .as_array_mut() + .expect("roles — массив") + .push(serde_json::json!({ + "name": zero_name, + "recipe": {"kind": "zero"} + })); + } + + let mut candidate = Engine::new(); + match candidate.load_config(&colliding.to_string()) { + Err(BindingError::InvalidConfig { reason }) => { + assert!(reason.contains(&css_key), "{reason}"); + assert!(reason.contains("reserved CSS namespace"), "{reason}"); + } + other => panic!( + "{css_key}: zero {} обязан быть отвергнут, получено {other:?}", + if as_alias { "alias" } else { "role" } + ), + } + } + } + } + } + /// Публичная JSON-граница обязана применять полный namespace-preflight /// ядра: иначе алиас мог затереть числовой `-alpha` цветовой строкой уже в /// `vars`, хотя оба отдельных имени выглядели валидными. @@ -1215,7 +1395,7 @@ mod tests { match engine.load_config(&value.to_string()) { Err(BindingError::InvalidConfig { reason }) => { assert!(reason.contains("--lab-fx-glow-brand-alpha"), "{reason}"); - assert!(reason.contains("emitted CSS variables"), "{reason}"); + assert!(reason.contains("reserved CSS namespace"), "{reason}"); } other => panic!("коллизия производного имени обязана быть отвергнута: {other:?}"), } diff --git a/crates/labcolors-wasm/src/error.rs b/crates/labcolors-wasm/src/error.rs index 8297bd30..bcb1c37a 100644 --- a/crates/labcolors-wasm/src/error.rs +++ b/crates/labcolors-wasm/src/error.rs @@ -47,13 +47,13 @@ pub enum BindingError { #[error("no config loaded: call load_config before resolve_theme")] ConfigRequired, - /// A resolved colour failed to serialise into the oklch emission form. - /// Unreachable by construction (solver/ladder hexes are valid) — carried - /// as a structured code so JS branching stays uniform even for the - /// impossible branch. + /// A core-generated value violated an internal postcondition or the adapter + /// could not represent a known/forward core variant without losing meaning. + /// Includes projection/oklch serialization failures and stable-Glow recheck + /// failures after public inputs were already validated. Never client blame. #[error("internal error: {reason}")] Internal { - /// What exactly failed to serialise. + /// The internal postcondition, projection, or contract mismatch. reason: String, }, diff --git a/crates/labcolors-wasm/src/lib.rs b/crates/labcolors-wasm/src/lib.rs index 519cc304..67de634c 100644 --- a/crates/labcolors-wasm/src/lib.rs +++ b/crates/labcolors-wasm/src/lib.rs @@ -132,6 +132,12 @@ export interface TranslucentRole { * Потребитель красит слои с mix-blend-mode: screen; `vars` несёт * --lab- (halo, oklch), --lab--core и --lab--alpha. */ export type GlowDecisionProfileV1 = "stable-v1" | "legacy-platform-dependent-v1"; +export type GlowLayerRecipeProfileV1 = "cam16-jprime-oklab-cusp-v1"; +export type GlowDiagnosticProfileV1 = "cam16-ucs-jprime-li2017-v1"; +export type GlowTargetStatusV1 = + | "exact-noop-unreachable" + | "legacy-reached" + | "legacy-unreachable"; export type NumericalIndeterminacyV1 = | { readonly reason: "sound-bound-unavailable"; @@ -145,16 +151,18 @@ export type NumericalIndeterminacyV1 = readonly upper: number; }; }; +export interface GlowBitExactDecisionGuaranteeV1 { + readonly kind: "bit-exact"; +} +export interface GlowLegacyDecisionGuaranteeV1 { + readonly kind: "legacy-platform-dependent-v1"; +} export type GlowDecisionGuaranteeV1 = - | { readonly kind: "bit-exact" } - | { - readonly kind: "outward-interval-v1"; - readonly lower: number; - readonly upper: number; - } - | { readonly kind: "legacy-platform-dependent-v1" }; + | GlowBitExactDecisionGuaranteeV1 + | GlowLegacyDecisionGuaranteeV1; -export interface GlowDeterminateRole { +/** Поля, общие для трёх конструктивно допустимых определённых исходов Glow. */ +export interface GlowDeterminateRoleBase { readonly kind: "glow"; readonly cssVar: string; /** Core, предназначенный потребителем для меньшего blur; геометрия не моделируется. */ @@ -168,17 +176,14 @@ export interface GlowDeterminateRole { /** Exact reference-домен point-композита; не renderer/display certificate. */ readonly compositeProfile: "encoded-srgb8-screen-v1"; readonly compositeGuarantee: "bit-exact"; - /** Diagnostic model, либо null когда exact verdict не вызывал appearance math. */ - readonly diagnosticProfile: "cam16-ucs-jprime-li2017-v1" | null; - readonly decisionProfile: GlowDecisionProfileV1; - /** Tagged certificate; an outward guarantee cannot lose its proven interval. */ - readonly decisionGuarantee: GlowDecisionGuaranteeV1; + /** Версионированный алгоритм, построивший анатомию core/halo. */ + readonly layerRecipeProfile: GlowLayerRecipeProfileV1; + /** Модель внешнего вида полного результата; обязательна, потому coreAchievedDj вычислен через CAM16. */ + readonly appearanceDiagnosticProfile: GlowDiagnosticProfileV1; /** Цель решается только по изолированному halo. */ readonly constraintLayer: "halo"; /** Запрошенный |ΔJ'| halo-композита. */ readonly targetDj: number; - /** Результат проверки цели во всём конечном reference-домене. */ - readonly targetStatus: "reached" | "unreachable"; /** Reference-композит изолированного halo на фоне резолва. */ readonly haloCompositeHex: string; /** Фактический |ΔJ'| изолированного halo-композита. */ @@ -189,12 +194,45 @@ export interface GlowDeterminateRole { readonly coreAchievedDj: number; /** @deprecated Alias `haloAchievedDj` для совместимости. */ readonly achievedDj: number; - /** @deprecated Используйте targetStatus === "unreachable". */ - readonly degraded: boolean; /** CSS-значение halo: `oklch(L% C H)`. */ readonly css: string; } +/** Точный no-op: выбор не вызывает CAM16 и не может достигнуть положительную цель. */ +export interface GlowStableExactNoopRole extends GlowDeterminateRoleBase { + readonly decisionProfile: "stable-v1"; + readonly decisionGuarantee: GlowBitExactDecisionGuaranteeV1; + readonly selectionDiagnosticProfile: null; + readonly targetStatus: "exact-noop-unreachable"; + /** @deprecated Выведено из targetStatus. */ + readonly degraded: true; +} + +/** Legacy-выбор CAM16 достиг целевого |ΔJ'| в охарактеризованной среде исполнения. */ +export interface GlowLegacyReachedRole extends GlowDeterminateRoleBase { + readonly decisionProfile: "legacy-platform-dependent-v1"; + readonly decisionGuarantee: GlowLegacyDecisionGuaranteeV1; + readonly selectionDiagnosticProfile: GlowDiagnosticProfileV1; + readonly targetStatus: "legacy-reached"; + /** @deprecated Выведено из targetStatus. */ + readonly degraded: false; +} + +/** Legacy-выбор CAM16 вернул максимум, не достигший целевого |ΔJ'|. */ +export interface GlowLegacyUnreachableRole extends GlowDeterminateRoleBase { + readonly decisionProfile: "legacy-platform-dependent-v1"; + readonly decisionGuarantee: GlowLegacyDecisionGuaranteeV1; + readonly selectionDiagnosticProfile: GlowDiagnosticProfileV1; + readonly targetStatus: "legacy-unreachable"; + /** @deprecated Выведено из targetStatus. */ + readonly degraded: true; +} + +export type GlowDeterminateRole = + | GlowStableExactNoopRole + | GlowLegacyReachedRole + | GlowLegacyUnreachableRole; + /** Stable terminal outcome: no sound target/max decision, therefore no CSS vars. */ export interface GlowIndeterminateRoleBase { readonly kind: "glow-indeterminate"; @@ -210,24 +248,47 @@ export type GlowIndeterminateRole = GlowIndeterminateRoleBase & NumericalIndeter export type GlowRole = GlowDeterminateRole | GlowIndeterminateRole; +/** Численное свидетельство выбора material-alpha. Оно характеризует повторно + * проверенный результат профиля, а не строгую межплатформенную границу + * минимальной alpha. */ +export interface MaterialAlphaGuaranteeBaseV1 { + /** Byte-scale affine binary64 compositor + original WCAG 2.1 (2018) + * `0.03928` EOTF, with a conservative channel envelope and both crossed seam + * sides. Platform-characterized because `powf` is not outward-bounded. */ + readonly numericalProfile: "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1"; +} +/** Rechecked fail/pass endpoints from a fixed-step binary partition after the + * directed-search guard. No global-monotonicity, first-state or minimum claim. */ +export interface MaterialBisectionBracketGuaranteeV1 extends MaterialAlphaGuaranteeBaseV1 { + readonly kind: "bisection-bracket-characterized-v1"; + readonly iterations: number; + readonly lowerAlpha: number; + readonly upperAlpha: number; +} +export interface MaterialTransparentEndpointGuaranteeV1 extends MaterialAlphaGuaranteeBaseV1 { + readonly kind: "transparent-endpoint-characterized-v1"; +} +export interface MaterialOpaqueEndpointGuaranteeV1 extends MaterialAlphaGuaranteeBaseV1 { + readonly kind: "opaque-endpoint-characterized-v1"; +} +export type MaterialAlphaGuaranteeV1 = + | MaterialBisectionBracketGuaranteeV1 + | MaterialTransparentEndpointGuaranteeV1 + | MaterialOpaqueEndpointGuaranteeV1; + /** Двухслойный материал (kind material, #89): тинт `01` (с выведенной α) над * опаковой базой `02`, обе — один тон. `vars` несёт --lab- (солид-канон, * опаковый), --lab--01 (тинт oklch/α) и --lab--02 (база, опаковая). * Композит-гарантия читаемости пересчитываема из toneHex/alpha (α-граница). */ -export interface MaterialRole { +export interface MaterialRoleBase { readonly kind: "material"; readonly cssVar: string; /** Тон #RRGGBB: тинт 01, база 02 и солид-канон одновременно (равны). */ readonly toneHex: string; - /** Выведенная альфа тинта 01, (0, 1]. */ - readonly alpha: number; /** Худший WCAG-контраст коммит-полюса по коридору [чёрный, белый]. */ readonly worstContrast: number; - /** WCAG-пол читаемости, который держит α (4.5 / 3.0). */ + /** Запрошенный WCAG-пол (4.5 / 3.0); держится только при `alphaStatus: "satisfied"`. */ readonly floor: number; - /** Гарантия выполнена: worstContrast ≥ floor. `false` — пол недостижим даже - * при α = 1 (честная деградация, α = 1 как ближайшая достижимая). */ - readonly guaranteed: boolean; /** Коммит-полюс поверхности белый (true, тёмный тон) или чёрный (false). */ readonly poleWhite: boolean; /** Фактический |ΔJ'| тона-базы от фона — различимость поверхности. */ @@ -242,6 +303,35 @@ export interface MaterialRole { readonly css: string; } +/** Уже прозрачная граничная точка держит запрошенный пол. */ +export interface MaterialSatisfiedTransparentRole extends MaterialRoleBase { + readonly alpha: 0; + readonly alphaGuarantee: MaterialTransparentEndpointGuaranteeV1; + readonly alphaStatus: "satisfied"; + readonly guaranteed: true; +} + +/** Повторно проверенная верхняя граница интервала держит запрошенный пол. */ +export interface MaterialSatisfiedBracketRole extends MaterialRoleBase { + readonly alpha: number; + readonly alphaGuarantee: MaterialBisectionBracketGuaranteeV1; + readonly alphaStatus: "satisfied"; + readonly guaranteed: true; +} + +/** Даже непрозрачная граничная точка не держит запрошенный пол; возвращается alpha 1. */ +export interface MaterialDegradedOpaqueRole extends MaterialRoleBase { + readonly alpha: 1; + readonly alphaGuarantee: MaterialOpaqueEndpointGuaranteeV1; + readonly alphaStatus: "degraded"; + readonly guaranteed: false; +} + +export type MaterialRole = + | MaterialSatisfiedTransparentRole + | MaterialSatisfiedBracketRole + | MaterialDegradedOpaqueRole; + export type RoleResult = | SolvedColor | TranslucentRole @@ -417,9 +507,9 @@ impl LabColors { /// Returns a `ResolvedTheme` object. Per-role unreachability is part of a /// successful result (each role carries its own `kind`); only whole-call /// failures reject (no config loaded yet as `config_required`, invalid hex, - /// unknown theme, and the by-construction-unreachable oklch serialisation - /// failure as `internal_error`) — as a structured `": "` - /// error, never an unwound panic. + /// unknown theme, a core invariant failure, and the by-construction- + /// unreachable oklch serialisation failure as `internal_error`) — as a + /// structured `": "` error, never an unwound panic. #[wasm_bindgen(js_name = resolveTheme)] pub fn resolve_theme(&self, bg_hex: &str, theme: &str) -> Result { let theme = crate::theme::parse_theme(theme).map_err(to_js_error)?; @@ -507,10 +597,15 @@ impl LabColors { })?; let background = hex_for_recheck(bg_hex).map_err(to_js_error)?; labcolors_core::screen_point_is_exact_noop(tint.as_ref(), background.as_ref()) - .map_err(|reason| to_js_error(BindingError::InvalidColor { reason })) + .map_err(|reason| to_js_error(stable_glow_recheck_core_error(reason))) } - /// Calculate the muddiness score (0 to 1) of an sRGB hex colour. + /// Return the frozen legacy `muddiness` coordinate for an sRGB hex colour. + /// + /// This is an experimental compatibility proxy: it reproduces the historic + /// numeric API, but is not an observer-validated human clean/dirty verdict + /// or a production decision. The legacy identifier is retained only for + /// compatibility. #[wasm_bindgen(js_name = muddiness)] pub fn muddiness(&self, hex: &str) -> Result { labcolors_core::cleanliness::muddiness_from_hex(hex) @@ -559,6 +654,14 @@ fn to_js_error(err: BindingError) -> JsError { JsError::new(&format!("{}: {}", err.code(), err)) } +fn stable_glow_recheck_core_error(reason: String) -> BindingError { + BindingError::Internal { + reason: format!( + "stable Glow point recheck rejected core-generated validated hex values: {reason}" + ), + } +} + #[cfg(test)] mod native_contract_tests { use super::*; @@ -618,26 +721,11 @@ mod native_contract_tests { .unwrap() ); } -} - -#[cfg(all(test, target_arch = "wasm32"))] -mod tests { - use super::*; #[test] - fn test_wasm_muddiness() { - let colors = LabColors::new(); - - // Olive is highly muddy - let olive_mud = colors.muddiness("#6B6B2E").unwrap(); - assert!(olive_mud > 0.80); - assert!((olive_mud - 0.8699).abs() < 1e-3); - - // Gray is clean - let gray_mud = colors.muddiness("#808080").unwrap(); - assert!(gray_mud < 0.05); - - // Invalid hex returns an error - assert!(colors.muddiness("not_a_hex").is_err()); + fn stable_glow_core_failure_after_boundary_validation_is_internal() { + let error = stable_glow_recheck_core_error("fixture drift".into()); + assert!(matches!(error, BindingError::Internal { .. })); + assert_eq!(error.code(), "internal_error"); } } diff --git a/crates/labcolors-wasm/src/projection.rs b/crates/labcolors-wasm/src/projection.rs index 425dabd2..db1314a6 100644 --- a/crates/labcolors-wasm/src/projection.rs +++ b/crates/labcolors-wasm/src/projection.rs @@ -25,7 +25,7 @@ use std::fmt::Write as _; -use crate::dto::{ResolvedTheme, RoleOutcome}; +use crate::dto::{GlowColor, GlowIndeterminateColor, MaterialColor, ResolvedTheme, RoleOutcome}; use crate::error::BindingError; /// Сериализовать [`ResolvedTheme`] в JSON, литерально повторяющий форму @@ -77,6 +77,7 @@ pub fn resolved_json(resolved: &ResolvedTheme) -> Result { push_var(&mut vars, &css_var, &css); } RoleOutcome::Glow(g) => { + let degraded = glow_degraded_from_provenance(g)?; // Свечение: слои для screen-наложения потребителем. // --lab- несёт halo (единая oklch-форма), сателлиты // --lab--core / --lab--alpha — анатомия и @@ -100,52 +101,59 @@ pub fn resolved_json(resolved: &ResolvedTheme) -> Result { }); } field_str(&mut roles, "alphaCss", &g.alpha_css); - field_str(&mut roles, "compositeProfile", g.composite_profile.key()); + field_str( + &mut roles, + "compositeProfile", + glow_composite_profile_key(g.composite_profile)?, + ); field_str( &mut roles, "compositeGuarantee", - g.composite_guarantee.key(), + glow_composite_guarantee_key(g.composite_guarantee)?, + ); + field_str( + &mut roles, + "layerRecipeProfile", + glow_layer_recipe_profile_key(g.layer_recipe_profile)?, + ); + field_str( + &mut roles, + "appearanceDiagnosticProfile", + glow_diagnostic_profile_key(g.appearance_diagnostic_profile)?, ); + let selection_diagnostic_profile = match g.selection_diagnostic_profile { + Some(profile) => Some(glow_diagnostic_profile_key(profile)?), + None => None, + }; field_opt_str( &mut roles, - "diagnosticProfile", - g.diagnostic_profile.map(|profile| profile.key()), + "selectionDiagnosticProfile", + selection_diagnostic_profile, + ); + field_str( + &mut roles, + "decisionProfile", + glow_decision_profile_key(g.decision_profile)?, + ); + field_glow_decision_guarantee(&mut roles, g.decision_guarantee)?; + field_str( + &mut roles, + "constraintLayer", + glow_constraint_layer_key(g.constraint_layer)?, ); - field_str(&mut roles, "decisionProfile", g.decision_profile.key()); - roles.push_str(",\"decisionGuarantee\":{\"kind\":"); - match g.decision_guarantee { - labcolors_core::DecisionGuaranteeV1::BitExact => { - push_str_lit(&mut roles, "bit-exact"); - } - labcolors_core::DecisionGuaranteeV1::OutwardIntervalV1(interval) => { - push_str_lit(&mut roles, "outward-interval-v1"); - field_num(&mut roles, "lower", interval.lower())?; - field_num(&mut roles, "upper", interval.upper())?; - } - labcolors_core::DecisionGuaranteeV1::LegacyPlatformDependentV1 => { - push_str_lit(&mut roles, "legacy-platform-dependent-v1"); - } - _ => { - return Err(BindingError::Internal { - reason: "проекция: неизвестный DecisionGuaranteeV1".to_string(), - }); - } - } - roles.push('}'); - field_str(&mut roles, "constraintLayer", g.constraint_layer.key()); field_num(&mut roles, "targetDj", g.target_dj)?; - field_str(&mut roles, "targetStatus", g.target_status.key()); + field_str( + &mut roles, + "targetStatus", + glow_target_status_key(g.target_status)?, + ); field_str(&mut roles, "haloCompositeHex", &g.halo_composite_hex); field_num(&mut roles, "haloAchievedDj", g.halo_achieved_dj)?; field_str(&mut roles, "coreCompositeHex", &g.core_composite_hex); field_num(&mut roles, "coreAchievedDj", g.core_achieved_dj)?; // Aliases совместимости старого неоднозначного контракта. field_num(&mut roles, "achievedDj", g.halo_achieved_dj)?; - field_bool( - &mut roles, - "degraded", - g.target_status == labcolors_core::GlowTargetStatus::Unreachable, - ); + field_bool(&mut roles, "degraded", degraded); let halo_css = oklch_css(&g.halo_hex, None)?; let core_css = oklch_css(&g.core_hex, None)?; field_str(&mut roles, "css", &halo_css); @@ -154,35 +162,32 @@ pub fn resolved_json(resolved: &ResolvedTheme) -> Result { push_var(&mut vars, &format!("{css_var}-alpha"), &g.alpha_css); } RoleOutcome::GlowIndeterminate(g) => { + validate_glow_indeterminate_provenance(g)?; field_str(&mut roles, "kind", "glow-indeterminate"); field_str(&mut roles, "sourceHex", &g.source_hex); field_num(&mut roles, "targetDj", g.target_dj)?; - field_str(&mut roles, "constraintLayer", g.constraint_layer.key()); - field_str(&mut roles, "decisionProfile", g.decision_profile.key()); - field_str(&mut roles, "numericalSiteId", g.site_id.key()); - field_str(&mut roles, "reason", g.evidence.reason_key()); - roles.push_str(",\"bounds\":{\"kind\":"); - match g.evidence { - labcolors_core::NumericalIndeterminacyV1::SoundBoundUnavailable => { - push_str_lit(&mut roles, "unavailable"); - } - labcolors_core::NumericalIndeterminacyV1::IntervalOverlap(interval) => { - push_str_lit(&mut roles, "outward"); - field_num(&mut roles, "lower", interval.lower())?; - field_num(&mut roles, "upper", interval.upper())?; - } - _ => { - return Err(BindingError::Internal { - reason: "проекция: неизвестный NumericalIndeterminacyV1".to_string(), - }); - } - } - roles.push('}'); - // Stable Indeterminate emits no halo/core/alpha vars. The - // caller sees a typed terminal outcome instead of a stale or - // platform-selected fallback colour. + field_str( + &mut roles, + "constraintLayer", + glow_constraint_layer_key(g.constraint_layer)?, + ); + field_str( + &mut roles, + "decisionProfile", + glow_decision_profile_key(g.decision_profile)?, + ); + field_str( + &mut roles, + "numericalSiteId", + numerical_site_id_key(g.site_id)?, + ); + fields_numerical_indeterminacy(&mut roles, g.evidence)?; + // Неопределённая stable-ветвь не эмитит переменные halo/core/alpha: + // вызывающий код получает типизированный терминальный исход, а не + // устаревший или выбранный платформой резервный цвет. } RoleOutcome::Material(m) => { + let guaranteed = material_guaranteed_from_provenance(m)?; // Материал (#89): тинт 01 (oklch/α) над опаковой базой 02 (oklch). // --lab- несёт солид-канон (= тон, опаковый) как SOLID- // фолбэк; --lab--01 — тинт со слэш-альфой; --lab--02 — @@ -191,8 +196,14 @@ pub fn resolved_json(resolved: &ResolvedTheme) -> Result { field_str(&mut roles, "toneHex", &m.tone_hex); field_num(&mut roles, "alpha", m.alpha)?; field_num(&mut roles, "worstContrast", m.worst_contrast)?; + field_material_alpha_guarantee(&mut roles, m.alpha_guarantee)?; + field_str( + &mut roles, + "alphaStatus", + material_alpha_status_key(m.alpha_status)?, + ); field_num(&mut roles, "floor", m.floor)?; - field_bool(&mut roles, "guaranteed", m.guaranteed); + field_bool(&mut roles, "guaranteed", guaranteed); field_bool(&mut roles, "poleWhite", m.pole_white); field_num(&mut roles, "achievedDj", m.achieved_dj)?; field_bool(&mut roles, "toneCompressed", m.tone_compressed); @@ -233,6 +244,294 @@ pub fn resolved_json(resolved: &ResolvedTheme) -> Result { Ok(out) } +fn unknown_output_variant(type_name: &str) -> BindingError { + BindingError::Internal { + reason: format!("проекция: неизвестный вариант {type_name}"), + } +} + +fn glow_degraded_from_provenance(glow: &GlowColor) -> Result { + match ( + glow.decision_profile, + glow.decision_guarantee, + glow.selection_diagnostic_profile, + glow.target_status, + ) { + ( + labcolors_core::GlowDecisionProfileV1::StableV1, + labcolors_core::DecisionGuaranteeV1::BitExact, + None, + labcolors_core::GlowTargetStatus::ExactNoopUnreachable, + ) => Ok(true), + ( + labcolors_core::GlowDecisionProfileV1::LegacyPlatformDependentV1, + labcolors_core::DecisionGuaranteeV1::LegacyPlatformDependentV1, + Some(labcolors_core::GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1), + labcolors_core::GlowTargetStatus::LegacyReached, + ) => Ok(false), + ( + labcolors_core::GlowDecisionProfileV1::LegacyPlatformDependentV1, + labcolors_core::DecisionGuaranteeV1::LegacyPlatformDependentV1, + Some(labcolors_core::GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1), + labcolors_core::GlowTargetStatus::LegacyUnreachable, + ) => Ok(true), + _ => Err(BindingError::Internal { + reason: "проекция: несогласованный Glow provenance".to_string(), + }), + } +} + +fn validate_glow_indeterminate_provenance( + glow: &GlowIndeterminateColor, +) -> Result<(), BindingError> { + match glow.decision_profile { + labcolors_core::GlowDecisionProfileV1::StableV1 => Ok(()), + _ => Err(BindingError::Internal { + reason: "проекция: несогласованный GlowIndeterminate provenance".to_string(), + }), + } +} + +fn material_guaranteed_from_provenance(material: &MaterialColor) -> Result { + match (material.alpha_status, material.alpha_guarantee) { + ( + labcolors_core::MaterialAlphaStatusV1::Satisfied, + labcolors_core::MaterialAlphaGuaranteeV1::TransparentEndpointCharacterizedV1 { .. }, + ) if material.alpha.to_bits() == 0.0_f64.to_bits() => Ok(true), + ( + labcolors_core::MaterialAlphaStatusV1::Satisfied, + labcolors_core::MaterialAlphaGuaranteeV1::BisectionBracketCharacterizedV1 { + upper_alpha, + .. + }, + ) if material.alpha.to_bits() == upper_alpha.to_bits() => Ok(true), + ( + labcolors_core::MaterialAlphaStatusV1::Degraded, + labcolors_core::MaterialAlphaGuaranteeV1::OpaqueEndpointCharacterizedV1 { .. }, + ) if material.alpha.to_bits() == 1.0_f64.to_bits() => Ok(false), + _ => Err(BindingError::Internal { + reason: "проекция: несогласованный Material provenance".to_string(), + }), + } +} + +fn glow_composite_profile_key( + profile: labcolors_core::GlowCompositeProfileV1, +) -> Result<&'static str, BindingError> { + match profile { + labcolors_core::GlowCompositeProfileV1::EncodedSrgb8ScreenV1 => { + Ok("encoded-srgb8-screen-v1") + } + _ => Err(unknown_output_variant("GlowCompositeProfileV1")), + } +} + +fn glow_composite_guarantee_key( + guarantee: labcolors_core::GlowCompositeGuaranteeV1, +) -> Result<&'static str, BindingError> { + match guarantee { + labcolors_core::GlowCompositeGuaranteeV1::BitExact => Ok("bit-exact"), + _ => Err(unknown_output_variant("GlowCompositeGuaranteeV1")), + } +} + +fn glow_layer_recipe_profile_key( + profile: labcolors_core::GlowLayerRecipeProfileV1, +) -> Result<&'static str, BindingError> { + match profile { + labcolors_core::GlowLayerRecipeProfileV1::Cam16JPrimeOklabCuspV1 => { + Ok("cam16-jprime-oklab-cusp-v1") + } + _ => Err(unknown_output_variant("GlowLayerRecipeProfileV1")), + } +} + +fn glow_diagnostic_profile_key( + profile: labcolors_core::GlowDiagnosticProfileV1, +) -> Result<&'static str, BindingError> { + match profile { + labcolors_core::GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1 => { + Ok("cam16-ucs-jprime-li2017-v1") + } + _ => Err(unknown_output_variant("GlowDiagnosticProfileV1")), + } +} + +fn glow_decision_profile_key( + profile: labcolors_core::GlowDecisionProfileV1, +) -> Result<&'static str, BindingError> { + match profile { + labcolors_core::GlowDecisionProfileV1::StableV1 => Ok("stable-v1"), + labcolors_core::GlowDecisionProfileV1::LegacyPlatformDependentV1 => { + Ok("legacy-platform-dependent-v1") + } + _ => Err(unknown_output_variant("GlowDecisionProfileV1")), + } +} + +fn glow_target_status_key( + status: labcolors_core::GlowTargetStatus, +) -> Result<&'static str, BindingError> { + match status { + labcolors_core::GlowTargetStatus::ExactNoopUnreachable => Ok("exact-noop-unreachable"), + labcolors_core::GlowTargetStatus::LegacyReached => Ok("legacy-reached"), + labcolors_core::GlowTargetStatus::LegacyUnreachable => Ok("legacy-unreachable"), + _ => Err(unknown_output_variant("GlowTargetStatus")), + } +} + +fn glow_constraint_layer_key( + layer: labcolors_core::GlowConstraintLayer, +) -> Result<&'static str, BindingError> { + match layer { + labcolors_core::GlowConstraintLayer::Halo => Ok("halo"), + _ => Err(unknown_output_variant("GlowConstraintLayer")), + } +} + +fn numerical_site_id_key( + site_id: labcolors_core::NumericalSiteIdV1, +) -> Result<&'static str, BindingError> { + match site_id { + labcolors_core::NumericalSiteIdV1::GlowTargetOrMaximumV1 => Ok("glow-target-or-maximum-v1"), + _ => Err(unknown_output_variant("NumericalSiteIdV1")), + } +} + +fn material_alpha_status_key( + status: labcolors_core::MaterialAlphaStatusV1, +) -> Result<&'static str, BindingError> { + match status { + labcolors_core::MaterialAlphaStatusV1::Satisfied => Ok("satisfied"), + labcolors_core::MaterialAlphaStatusV1::Degraded => Ok("degraded"), + _ => Err(unknown_output_variant("MaterialAlphaStatusV1")), + } +} + +fn material_numerical_profile_key( + profile: labcolors_core::MaterialNumericalProfileV1, +) -> Result<&'static str, BindingError> { + match profile { + labcolors_core::MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1 => { + Ok("encoded-srgb-byte-scale-affine-platform-binary64-powf-v1") + } + _ => Err(unknown_output_variant("MaterialNumericalProfileV1")), + } +} + +/// Поле сертификата определённого решения. Функция владеет всей вложенной +/// формой контракта, чтобы новый вариант сертификата не усложнял проекцию роли. +fn field_glow_decision_guarantee( + out: &mut String, + guarantee: labcolors_core::DecisionGuaranteeV1, +) -> Result<(), BindingError> { + let key = match guarantee { + labcolors_core::DecisionGuaranteeV1::BitExact => "bit-exact", + labcolors_core::DecisionGuaranteeV1::LegacyPlatformDependentV1 => { + "legacy-platform-dependent-v1" + } + labcolors_core::DecisionGuaranteeV1::OutwardIntervalV1(_) => { + return Err(BindingError::Internal { + reason: "проекция: неподдерживаемый Glow DecisionGuaranteeV1: outward-interval-v1" + .to_string(), + }); + } + _ => { + return Err(BindingError::Internal { + reason: "проекция: неподдерживаемый Glow DecisionGuaranteeV1".to_string(), + }); + } + }; + out.push_str(",\"decisionGuarantee\":{\"kind\":"); + push_str_lit(out, key); + out.push('}'); + Ok(()) +} + +/// Поле свидетельства выбора material-alpha. Функция владеет полной вложенной +/// формой контракта и каноническим порядком полей для интервальной и обеих +/// граничных ветвей. +fn field_material_alpha_guarantee( + out: &mut String, + guarantee: labcolors_core::MaterialAlphaGuaranteeV1, +) -> Result<(), BindingError> { + out.push_str(",\"alphaGuarantee\":{\"kind\":"); + match guarantee { + labcolors_core::MaterialAlphaGuaranteeV1::BisectionBracketCharacterizedV1 { + iterations, + lower_alpha, + upper_alpha, + numerical_profile, + } => { + push_str_lit(out, "bisection-bracket-characterized-v1"); + field_num(out, "iterations", f64::from(iterations))?; + field_num(out, "lowerAlpha", lower_alpha)?; + field_num(out, "upperAlpha", upper_alpha)?; + field_str( + out, + "numericalProfile", + material_numerical_profile_key(numerical_profile)?, + ); + } + labcolors_core::MaterialAlphaGuaranteeV1::TransparentEndpointCharacterizedV1 { + numerical_profile, + } => { + push_str_lit(out, "transparent-endpoint-characterized-v1"); + field_str( + out, + "numericalProfile", + material_numerical_profile_key(numerical_profile)?, + ); + } + labcolors_core::MaterialAlphaGuaranteeV1::OpaqueEndpointCharacterizedV1 { + numerical_profile, + } => { + push_str_lit(out, "opaque-endpoint-characterized-v1"); + field_str( + out, + "numericalProfile", + material_numerical_profile_key(numerical_profile)?, + ); + } + _ => { + return Err(BindingError::Internal { + reason: "проекция: неизвестный MaterialAlphaGuaranteeV1".to_string(), + }); + } + } + out.push('}'); + Ok(()) +} + +/// Поля свидетельства для неопределённого решения. `reason` и связанный с ним +/// `bounds` эмитятся вместе: тип остаётся единственным источником их формы. +fn fields_numerical_indeterminacy( + out: &mut String, + evidence: labcolors_core::NumericalIndeterminacyV1, +) -> Result<(), BindingError> { + match evidence { + labcolors_core::NumericalIndeterminacyV1::SoundBoundUnavailable => { + field_str(out, "reason", "sound-bound-unavailable"); + out.push_str(",\"bounds\":{\"kind\":"); + push_str_lit(out, "unavailable"); + } + labcolors_core::NumericalIndeterminacyV1::IntervalOverlap(interval) => { + field_str(out, "reason", "interval-overlap"); + out.push_str(",\"bounds\":{\"kind\":"); + push_str_lit(out, "outward"); + field_num(out, "lower", interval.lower())?; + field_num(out, "upper", interval.upper())?; + } + _ => { + return Err(BindingError::Internal { + reason: "проекция: неизвестный NumericalIndeterminacyV1".to_string(), + }); + } + } + out.push('}'); + Ok(()) +} + /// Единая CSS-форма эмиссии: `oklch(L% C H)` / `oklch(L% C H / A)`. /// Байт-точность реконструкции доказана round-trip тестом ядра на решётке /// куба. Hex к этому месту валиден по построению (солвер/лестница), но при @@ -335,8 +634,8 @@ fn push_str_lit(out: &mut String, s: &str) { mod tests { use super::*; use crate::dto::{ - GlowColor, GlowIndeterminateColor, ResolvedTheme, RgbaColor, RoleEntry, RoleOutcome, - SolvedColor, + GlowColor, GlowIndeterminateColor, MaterialColor, ResolvedTheme, RgbaColor, RoleEntry, + RoleOutcome, SolvedColor, }; fn color_entry(key: &str) -> RoleEntry { @@ -388,7 +687,11 @@ mod tests { composite_profile: labcolors_core::GlowCompositeProfileV1::EncodedSrgb8ScreenV1, composite_guarantee: labcolors_core::GlowCompositeGuaranteeV1::BitExact, - diagnostic_profile: Some( + layer_recipe_profile: + labcolors_core::GlowLayerRecipeProfileV1::Cam16JPrimeOklabCuspV1, + appearance_diagnostic_profile: + labcolors_core::GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1, + selection_diagnostic_profile: Some( labcolors_core::GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1, ), decision_profile: @@ -396,7 +699,7 @@ mod tests { decision_guarantee: labcolors_core::DecisionGuaranteeV1::LegacyPlatformDependentV1, constraint_layer: labcolors_core::GlowConstraintLayer::Halo, - target_status: labcolors_core::GlowTargetStatus::Reached, + target_status: labcolors_core::GlowTargetStatus::LegacyReached, halo_composite_hex: "#13151B".to_string(), halo_achieved_dj: 2.373_123_785_729_128, core_composite_hex: "#15171B".to_string(), @@ -446,10 +749,12 @@ mod tests { "\"alpha\":0.03604545968562789,\"alphaCss\":\"0.03604545968562789\",", "\"compositeProfile\":\"encoded-srgb8-screen-v1\",", "\"compositeGuarantee\":\"bit-exact\",", - "\"diagnosticProfile\":\"cam16-ucs-jprime-li2017-v1\",", + "\"layerRecipeProfile\":\"cam16-jprime-oklab-cusp-v1\",", + "\"appearanceDiagnosticProfile\":\"cam16-ucs-jprime-li2017-v1\",", + "\"selectionDiagnosticProfile\":\"cam16-ucs-jprime-li2017-v1\",", "\"decisionProfile\":\"legacy-platform-dependent-v1\",", "\"decisionGuarantee\":{{\"kind\":\"legacy-platform-dependent-v1\"}},", - "\"constraintLayer\":\"halo\",\"targetDj\":2.3006,\"targetStatus\":\"reached\",", + "\"constraintLayer\":\"halo\",\"targetDj\":2.3006,\"targetStatus\":\"legacy-reached\",", "\"haloCompositeHex\":\"#13151B\",\"haloAchievedDj\":2.373123785729128,", "\"coreCompositeHex\":\"#15171B\",\"coreAchievedDj\":3.235504076619437,", "\"achievedDj\":2.373123785729128,\"degraded\":false,\"css\":\"{halo}\"}},", @@ -464,16 +769,20 @@ mod tests { core = css_core, ); assert_eq!(json, expected); + + // Anti-vacuum: `kind: none` публикует client-owned `cssVar` как + // метаданные контракта, но никогда не присваивает этому имени значение. + let projected: serde_json::Value = serde_json::from_str(&json).unwrap(); + let none = &projected["roles"]["spacer"]; + assert_eq!(none["kind"], "none"); + assert_eq!(none["cssVar"], "--lab-spacer"); + assert!(projected["vars"].get("--lab-spacer").is_none()); } #[test] - fn outward_interval_certificates_survive_json_projection() { + fn interval_overlap_indeterminacy_survives_json_projection() { let interval = labcolors_core::OutwardIntervalV1::try_new(0.9, 1.1).unwrap(); let mut theme = fixture(); - let RoleOutcome::Glow(glow) = &mut theme.roles[3].outcome else { - panic!("fixture pulse must be Glow"); - }; - glow.decision_guarantee = labcolors_core::DecisionGuaranteeV1::OutwardIntervalV1(interval); theme.roles.push(RoleEntry { role_key: "uncertain-pulse".to_string(), outcome: RoleOutcome::GlowIndeterminate(GlowIndeterminateColor { @@ -488,10 +797,6 @@ mod tests { let value: serde_json::Value = serde_json::from_str(&resolved_json(&theme).unwrap()).unwrap(); - let guarantee = &value["roles"]["pulse"]["decisionGuarantee"]; - assert_eq!(guarantee["kind"], "outward-interval-v1"); - assert_eq!(guarantee["lower"].as_f64(), Some(0.9)); - assert_eq!(guarantee["upper"].as_f64(), Some(1.1)); let uncertain = &value["roles"]["uncertain-pulse"]; assert_eq!(uncertain["reason"], "interval-overlap"); assert_eq!(uncertain["bounds"]["kind"], "outward"); @@ -499,13 +804,194 @@ mod tests { assert_eq!(uncertain["bounds"]["upper"].as_f64(), Some(1.1)); } + #[test] + fn numerical_certificate_serializers_keep_canonical_wire_order() { + let interval = labcolors_core::OutwardIntervalV1::try_new(0.9, 1.1).unwrap(); + let numerical_profile = labcolors_core::MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1; + + let mut exact_decision = String::new(); + field_glow_decision_guarantee( + &mut exact_decision, + labcolors_core::DecisionGuaranteeV1::BitExact, + ) + .unwrap(); + assert_eq!( + exact_decision, + ",\"decisionGuarantee\":{\"kind\":\"bit-exact\"}" + ); + + let mut legacy_decision = String::new(); + field_glow_decision_guarantee( + &mut legacy_decision, + labcolors_core::DecisionGuaranteeV1::LegacyPlatformDependentV1, + ) + .unwrap(); + assert_eq!( + legacy_decision, + ",\"decisionGuarantee\":{\"kind\":\"legacy-platform-dependent-v1\"}" + ); + + let mut unavailable = String::new(); + fields_numerical_indeterminacy( + &mut unavailable, + labcolors_core::NumericalIndeterminacyV1::SoundBoundUnavailable, + ) + .unwrap(); + assert_eq!( + unavailable, + ",\"reason\":\"sound-bound-unavailable\",\"bounds\":{\"kind\":\"unavailable\"}" + ); + + let mut overlap = String::new(); + fields_numerical_indeterminacy( + &mut overlap, + labcolors_core::NumericalIndeterminacyV1::IntervalOverlap(interval), + ) + .unwrap(); + assert_eq!( + overlap, + ",\"reason\":\"interval-overlap\",\"bounds\":{\"kind\":\"outward\",\"lower\":0.9,\"upper\":1.1}" + ); + + let mut bracket = String::new(); + field_material_alpha_guarantee( + &mut bracket, + labcolors_core::MaterialAlphaGuaranteeV1::BisectionBracketCharacterizedV1 { + iterations: 60, + lower_alpha: 0.6374, + upper_alpha: 0.6375, + numerical_profile, + }, + ) + .unwrap(); + assert_eq!( + bracket, + ",\"alphaGuarantee\":{\"kind\":\"bisection-bracket-characterized-v1\",\"iterations\":60,\"lowerAlpha\":0.6374,\"upperAlpha\":0.6375,\"numericalProfile\":\"encoded-srgb-byte-scale-affine-platform-binary64-powf-v1\"}" + ); + + let mut transparent_endpoint = String::new(); + field_material_alpha_guarantee( + &mut transparent_endpoint, + labcolors_core::MaterialAlphaGuaranteeV1::TransparentEndpointCharacterizedV1 { + numerical_profile, + }, + ) + .unwrap(); + assert_eq!( + transparent_endpoint, + ",\"alphaGuarantee\":{\"kind\":\"transparent-endpoint-characterized-v1\",\"numericalProfile\":\"encoded-srgb-byte-scale-affine-platform-binary64-powf-v1\"}" + ); + + let mut opaque_endpoint = String::new(); + field_material_alpha_guarantee( + &mut opaque_endpoint, + labcolors_core::MaterialAlphaGuaranteeV1::OpaqueEndpointCharacterizedV1 { + numerical_profile, + }, + ) + .unwrap(); + assert_eq!( + opaque_endpoint, + ",\"alphaGuarantee\":{\"kind\":\"opaque-endpoint-characterized-v1\",\"numericalProfile\":\"encoded-srgb-byte-scale-affine-platform-binary64-powf-v1\"}" + ); + } + + #[test] + fn adapter_owns_every_closed_output_vocabulary() { + assert_eq!( + glow_composite_profile_key( + labcolors_core::GlowCompositeProfileV1::EncodedSrgb8ScreenV1, + ) + .unwrap(), + "encoded-srgb8-screen-v1" + ); + assert_eq!( + glow_composite_guarantee_key(labcolors_core::GlowCompositeGuaranteeV1::BitExact) + .unwrap(), + "bit-exact" + ); + assert_eq!( + glow_layer_recipe_profile_key( + labcolors_core::GlowLayerRecipeProfileV1::Cam16JPrimeOklabCuspV1, + ) + .unwrap(), + "cam16-jprime-oklab-cusp-v1" + ); + assert_eq!( + glow_diagnostic_profile_key( + labcolors_core::GlowDiagnosticProfileV1::Cam16UcsJPrimeLi2017V1, + ) + .unwrap(), + "cam16-ucs-jprime-li2017-v1" + ); + assert_eq!( + glow_decision_profile_key(labcolors_core::GlowDecisionProfileV1::StableV1).unwrap(), + "stable-v1" + ); + assert_eq!( + glow_decision_profile_key( + labcolors_core::GlowDecisionProfileV1::LegacyPlatformDependentV1, + ) + .unwrap(), + "legacy-platform-dependent-v1" + ); + assert_eq!( + glow_target_status_key(labcolors_core::GlowTargetStatus::ExactNoopUnreachable).unwrap(), + "exact-noop-unreachable" + ); + assert_eq!( + glow_target_status_key(labcolors_core::GlowTargetStatus::LegacyReached).unwrap(), + "legacy-reached" + ); + assert_eq!( + glow_target_status_key(labcolors_core::GlowTargetStatus::LegacyUnreachable).unwrap(), + "legacy-unreachable" + ); + assert_eq!( + glow_constraint_layer_key(labcolors_core::GlowConstraintLayer::Halo).unwrap(), + "halo" + ); + assert_eq!( + numerical_site_id_key(labcolors_core::NumericalSiteIdV1::GlowTargetOrMaximumV1) + .unwrap(), + "glow-target-or-maximum-v1" + ); + assert_eq!( + material_alpha_status_key(labcolors_core::MaterialAlphaStatusV1::Satisfied).unwrap(), + "satisfied" + ); + assert_eq!( + material_alpha_status_key(labcolors_core::MaterialAlphaStatusV1::Degraded).unwrap(), + "degraded" + ); + assert_eq!( + material_numerical_profile_key( + labcolors_core::MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1, + ) + .unwrap(), + "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1" + ); + + let interval = labcolors_core::OutwardIntervalV1::try_new(0.9, 1.1).unwrap(); + let mut unsupported = String::new(); + let error = field_glow_decision_guarantee( + &mut unsupported, + labcolors_core::DecisionGuaranteeV1::OutwardIntervalV1(interval), + ) + .unwrap_err(); + assert!(matches!( + error, + BindingError::Internal { reason } + if reason.contains("неподдерживаемый Glow DecisionGuaranteeV1") + )); + } + /// Материал (#89) проецируется в контрактные CSS-переменные: `--lab-` = /// солид-канон (oklch), `--lab--01` = тинт (oklch со слэш-альфой), /// `--lab--02` = опаковая база (oklch). Плюс полный набор полей исхода. /// Пин ИМЁН переменных — та поверхность, что потребляет labui-material.css. #[test] fn material_projects_two_layer_css_vars() { - use crate::dto::MaterialColor; let theme = ResolvedTheme { theme: "light", background: "#FFFFFF".to_string(), @@ -515,8 +1001,16 @@ mod tests { tone_hex: "#B4B4BC".to_string(), alpha: 0.6375, worst_contrast: 4.61, + alpha_guarantee: + labcolors_core::MaterialAlphaGuaranteeV1::BisectionBracketCharacterizedV1 { + iterations: 60, + lower_alpha: 0.6374, + upper_alpha: 0.6375, + numerical_profile: + labcolors_core::MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1, + }, + alpha_status: labcolors_core::MaterialAlphaStatusV1::Satisfied, floor: 4.5, - guaranteed: true, pole_white: false, achieved_dj: 18.25, tone_compressed: false, @@ -550,6 +1044,26 @@ mod tests { assert_eq!(r["toneHex"], "#B4B4BC"); assert_eq!(r["alpha"].as_f64().unwrap(), 0.6375); assert_eq!(r["worstContrast"].as_f64().unwrap(), 4.61); + assert_eq!( + r["alphaGuarantee"]["kind"], + "bisection-bracket-characterized-v1" + ); + assert_eq!(r["alphaGuarantee"]["iterations"], 60); + assert_eq!(r["alphaGuarantee"]["lowerAlpha"], 0.6374); + assert_eq!(r["alphaGuarantee"]["upperAlpha"], 0.6375); + assert_eq!( + r["alpha"].as_f64().unwrap().to_bits(), + r["alphaGuarantee"]["upperAlpha"] + .as_f64() + .unwrap() + .to_bits(), + "serialized alpha обязан побитно совпадать с проходящей upperAlpha" + ); + assert_eq!( + r["alphaGuarantee"]["numericalProfile"], + "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1" + ); + assert_eq!(r["alphaStatus"], "satisfied"); assert_eq!(r["floor"].as_f64().unwrap(), 4.5); assert_eq!(r["guaranteed"], true); assert_eq!(r["poleWhite"], false); @@ -560,6 +1074,143 @@ mod tests { assert_eq!(r["css"].as_str().unwrap(), solid); } + #[test] + fn material_terminal_variants_keep_status_guarantee_and_boolean_correlated() { + let numerical_profile = labcolors_core::MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1; + let role = |alpha, alpha_guarantee, alpha_status| { + RoleOutcome::Material(MaterialColor { + tone_hex: "#B4B4BC".to_string(), + alpha, + worst_contrast: 4.61, + alpha_guarantee, + alpha_status, + floor: 4.5, + pole_white: false, + achieved_dj: 18.25, + tone_compressed: false, + hue_vanished: false, + distinct: true, + }) + }; + let theme = ResolvedTheme { + theme: "light", + background: "#FFFFFF".to_string(), + roles: vec![ + RoleEntry { + role_key: "transparent".to_string(), + outcome: role( + 0.0, + labcolors_core::MaterialAlphaGuaranteeV1::TransparentEndpointCharacterizedV1 { + numerical_profile, + }, + labcolors_core::MaterialAlphaStatusV1::Satisfied, + ), + }, + RoleEntry { + role_key: "opaque".to_string(), + outcome: role( + 1.0, + labcolors_core::MaterialAlphaGuaranteeV1::OpaqueEndpointCharacterizedV1 { + numerical_profile, + }, + labcolors_core::MaterialAlphaStatusV1::Degraded, + ), + }, + ], + }; + + let value: serde_json::Value = + serde_json::from_str(&resolved_json(&theme).unwrap()).unwrap(); + let transparent = &value["roles"]["transparent"]; + assert_eq!(transparent["alpha"], 0.0); + assert_eq!( + transparent["alphaGuarantee"]["kind"], + "transparent-endpoint-characterized-v1" + ); + assert_eq!(transparent["alphaStatus"], "satisfied"); + assert_eq!(transparent["guaranteed"], true); + + let opaque = &value["roles"]["opaque"]; + assert_eq!(opaque["alpha"], 1.0); + assert_eq!( + opaque["alphaGuarantee"]["kind"], + "opaque-endpoint-characterized-v1" + ); + assert_eq!(opaque["alphaStatus"], "degraded"); + assert_eq!(opaque["guaranteed"], false); + } + + #[test] + fn projection_rejects_illegal_output_cross_products() { + let mut glow_theme = fixture(); + let RoleOutcome::Glow(glow) = &mut glow_theme.roles[3].outcome else { + panic!("fixture pulse must be Glow"); + }; + glow.decision_profile = labcolors_core::GlowDecisionProfileV1::StableV1; + let glow_error = resolved_json(&glow_theme).unwrap_err(); + assert!(matches!( + glow_error, + BindingError::Internal { reason } if reason.contains("несогласованный Glow provenance") + )); + + let indeterminate_theme = ResolvedTheme { + theme: "light", + background: "#FFFFFF".to_string(), + roles: vec![RoleEntry { + role_key: "indeterminate".to_string(), + outcome: RoleOutcome::GlowIndeterminate(GlowIndeterminateColor { + source_hex: "#4A8FFF".to_string(), + target_dj: 2.3006, + decision_profile: + labcolors_core::GlowDecisionProfileV1::LegacyPlatformDependentV1, + site_id: labcolors_core::NumericalSiteIdV1::GlowTargetOrMaximumV1, + evidence: labcolors_core::NumericalIndeterminacyV1::SoundBoundUnavailable, + constraint_layer: labcolors_core::GlowConstraintLayer::Halo, + }), + }], + }; + let indeterminate_error = resolved_json(&indeterminate_theme).unwrap_err(); + assert!(matches!( + indeterminate_error, + BindingError::Internal { reason } + if reason.contains("несогласованный GlowIndeterminate provenance") + )); + + let numerical_profile = labcolors_core::MaterialNumericalProfileV1::EncodedSrgbByteScaleAffinePlatformBinary64PowfV1; + let material_theme = ResolvedTheme { + theme: "light", + background: "#FFFFFF".to_string(), + roles: vec![RoleEntry { + role_key: "material".to_string(), + outcome: RoleOutcome::Material(MaterialColor { + tone_hex: "#B4B4BC".to_string(), + alpha: 0.5, + worst_contrast: 4.61, + alpha_guarantee: + labcolors_core::MaterialAlphaGuaranteeV1::BisectionBracketCharacterizedV1 { + iterations: 60, + lower_alpha: 0.49, + upper_alpha: 0.75, + numerical_profile, + }, + alpha_status: labcolors_core::MaterialAlphaStatusV1::Satisfied, + floor: 4.5, + pole_white: false, + achieved_dj: 18.25, + tone_compressed: false, + hue_vanished: false, + distinct: true, + }), + }], + }; + let material_error = resolved_json(&material_theme).unwrap_err(); + assert!(matches!( + material_error, + BindingError::Internal { reason } + if reason.contains("несогласованный Material provenance") + )); + } + /// Числа переживают декаду через кратчайшую десятичную запись: биты double /// после парсинга равны исходным (и JSON синтаксически валиден для serde). #[test] diff --git a/crates/labcolors-wasm/tests/wasm_parity.rs b/crates/labcolors-wasm/tests/wasm_parity.rs index eaf30771..a3c8e669 100644 --- a/crates/labcolors-wasm/tests/wasm_parity.rs +++ b/crates/labcolors-wasm/tests/wasm_parity.rs @@ -128,7 +128,7 @@ fn assert_hex_within_one(actual: &str, expected: &str, context: &str) { /// libm drift, so this anchors wasm32 independently to committed bytes/values. #[wasm_bindgen_test] fn committed_conformance_pack_replays_in_wasm32() { - let fresh = Pack::generate(); + let fresh = Pack::generate().expect("canonical pack generation"); let contrasts: Vec = serde_json::from_str(include_str!("../../../conformance/vectors/contrasts.json")).unwrap(); assert_eq!(contrasts.len(), fresh.contrasts.len()); @@ -219,6 +219,39 @@ fn committed_conformance_pack_replays_in_wasm32() { assert_eq!(committed_manifest.counts.total, 82, "anti-vacuum pack size"); } +/// The public WASM compatibility proxy is wired to the same frozen vectors as +/// every other delivery surface. The committed file is the oracle: this test +/// intentionally carries no second hand-written semantic threshold or score. +#[wasm_bindgen_test] +fn public_muddiness_binding_matches_committed_conformance_vectors() { + let committed: Vec = + serde_json::from_str(include_str!("../../../conformance/vectors/muddiness.json")).unwrap(); + let manifest: Manifest = + serde_json::from_str(include_str!("../../../conformance/vectors/manifest.json")).unwrap(); + assert!( + manifest.counts.muddiness > 0, + "compatibility corpus must be non-vacuous" + ); + assert_eq!(committed.len(), manifest.counts.muddiness); + + let colors = LabColors::new(); + for vector in committed { + let actual = colors + .muddiness(&vector.hex) + .unwrap_or_else(|error| panic!("{} must remain accepted: {error:?}", vector.hex)); + approx_pack_number( + actual, + vector.score, + &format!("public muddiness {}", vector.hex), + ); + } + + assert!( + colors.muddiness("not_a_hex").is_err(), + "invalid public input must reject rather than panic or fabricate a score" + ); +} + /// Shared parity assertion: for a passport, the binding's `resolveTheme` /// must reproduce the core `resolve_named_set`, role for role. Expectations /// come straight from the core inside the same wasm runtime — never hand-typed. @@ -308,8 +341,17 @@ fn assert_parity(passport: &str, bg_hex: &str, theme: &str) { Some(g.halo_composite_certificate().guarantee().key()) ); assert_eq!( - get_str(&entry, "diagnosticProfile").as_deref(), - g.diagnostic_profile().map(|profile| profile.key()) + get_str(&entry, "layerRecipeProfile").as_deref(), + Some(g.layer_recipe_profile().key()) + ); + assert_eq!( + get_str(&entry, "appearanceDiagnosticProfile").as_deref(), + Some(g.appearance_diagnostic_profile().key()) + ); + assert_eq!( + get_str(&entry, "selectionDiagnosticProfile").as_deref(), + g.selection_diagnostic_profile() + .map(|profile| profile.key()) ); assert_eq!( get_str(&entry, "decisionProfile").as_deref(), @@ -364,7 +406,11 @@ fn assert_parity(passport: &str, bg_hex: &str, theme: &str) { ); assert_eq!( get_bool(&entry, "degraded"), - g.target_status() == labcolors_core::GlowTargetStatus::Unreachable + matches!( + g.target_status(), + labcolors_core::GlowTargetStatus::ExactNoopUnreachable + | labcolors_core::GlowTargetStatus::LegacyUnreachable + ) ); } // `Resolved` is `#[non_exhaustive]`: a future core variant must be @@ -542,9 +588,22 @@ fn stable_glow_both_lawful_outcomes_cross_the_wasm_boundary() { Some("stable-v1") ); assert_eq!( - get_str(&glow, "diagnosticProfile"), + get_str(&glow, "layerRecipeProfile").as_deref(), + Some("cam16-jprime-oklab-cusp-v1") + ); + assert_eq!( + get_str(&glow, "appearanceDiagnosticProfile").as_deref(), + Some("cam16-ucs-jprime-li2017-v1"), + "full resolved Glow must identify its core appearance measurement" + ); + assert_eq!( + get_str(&glow, "selectionDiagnosticProfile"), None, - "exact no-op must not claim an appearance diagnostic" + "exact no-op must not claim a selection diagnostic" + ); + assert_eq!( + get_str(&glow, "targetStatus").as_deref(), + Some("exact-noop-unreachable") ); let guarantee = get_obj(&glow, "decisionGuarantee"); assert_eq!(get_str(&guarantee, "kind").as_deref(), Some("bit-exact")); diff --git a/docs/decisions/0001-config-boundary.md b/docs/decisions/0001-config-boundary.md index b1771b89..c51e78d5 100644 --- a/docs/decisions/0001-config-boundary.md +++ b/docs/decisions/0001-config-boundary.md @@ -63,11 +63,13 @@ load-bearing продакшн-hex движка — 10 якорей `Accent::anch - `themes` — словарь `имя → пресет условий просмотра (VC)` из закрытого физического меню (`srgb | dim | srgb-ic | dim-ic`, kebab-контракт JSON); произвольных VC-чисел в конфиге нет. -- `roles` — имя → рецепт из физического меню (6 вариантов; `kind` в JSON — kebab): - `text-anchor(fraction, floor)` · `dj-anchor(light, dark)` · +- `roles` — имя → рецепт из физического меню (10 вариантов; `kind` в JSON — kebab): + `text-anchor(fraction, floor, hue?)` · `dj-anchor(light, dark)` · `decorative-lc(magnitude)` (Lc-величина, стек теней) · - `ladder(brand|family|sentiment|neutral, position)` · - `alpha-analog(of, alpha)` (композит-инверсия, alpha.rs) · `zero`. + `ladder(source, position, floor?)` · `glow(source, step, decision_profile)` · + `pair-fill(source)` · `pair-label(source, fraction, floor)` · + `alpha-analog(of, alpha)` (композит-инверсия, alpha.rs) · + `material(source, tone_light, tone_dark, floor)` · `zero`. `position` — ЗАКРЫТОЕ меню позиций лестницы (label / fill P–Q / border / focus …); перечень зафиксирован приложением A. Рецепт `ladder` реализован (`RoleSpec::Ladder`, `crate::ladder`) и поглотил акцентный GAP #59; как и @@ -75,9 +77,13 @@ load-bearing продакшн-hex движка — 10 якорей `Accent::anch `DjMagnitude`, `alpha.rs`). - `aliases` — компонентные алиасы на существующие роли. -Имена ролей — произвольные строки (`[a-z0-9-]+`), эмиссия `--lab-{имя}` -существующей механикой. Коллапс-контракт исполняется схемой: в конфиге labui -нет `static-*/inverted-*/on-*/mat-*` — роли решаются от фактического фона. +Имена ролей — произвольные строки (`[a-z0-9-]+`). Каждая роль и каждый алиас +резервируют client-owned primary `--lab-{имя}` ещё до resolve; `Zero` сохраняет +это имя в metadata, хотя значения не эмитит. Многоключевые outcomes дополнительно +резервируют свой shape: Glow — `-core/-alpha`, Material — `-01/-02`. Поэтому +порядок JSON-писателей или фон не могут решить коллизию молча. Коллапс-контракт +исполняется схемой: в конфиге labui нет `static-*/inverted-*/on-*/mat-*` — роли +решаются от фактического фона. ## API diff --git a/docs/decisions/0004-finite-alpha-glow-reference.md b/docs/decisions/0004-finite-alpha-glow-reference.md index faf6b132..2afac565 100644 --- a/docs/decisions/0004-finite-alpha-glow-reference.md +++ b/docs/decisions/0004-finite-alpha-glow-reference.md @@ -70,21 +70,36 @@ Source-over affine-форма алгебраически равна операт тип — `Result<(String, f64), String>` без внутреннего `Option`. Некорректная alpha не клампится в другой запрос, а возвращает `Err`. -### 2. Glow перечисляет exact states, а semantic decision профилирован - -Для каждого канала screen переход в новый байт `v` начинается на рациональной -стенке: - -```text -255 · (2(v − B) − 1) / (2 · G · (255 − B)) -``` - -Три уже упорядоченных потока канальных стенок сливаются точным перекрёстным -умножением целых без общей сортировки и промежуточных коллекций. Это даёт exact -множество достижимых RGB-состояний и bit-exact certificate каждого выбранного +### 2. Glow перечисляет actual binary64 states, а semantic decision профилирован + +Для каждого канала production-перечислитель находит первый representable +binary64-alpha, переводящий публичный reference-композитор в следующий байт. +Поиск — точный lower-bound по упорядоченным положительным `f64`-битам с вызовом +того же `bg + alpha · glow · (255−bg) / 255`, который выполняет официальный +потребитель. Аналитическая рациональная half-wall используется только как +ускоряющий seed: экспоненциальный поиск строит bracket между уже известными +failing/passing endpoints, затем bitwise lower-bound находит точную границу; +корректность от качества seed не зависит. Канальные переходы сливаются только +тогда, когда совпадают их фактические first-passing bits. Это даёт полное +множество достижимых в данном численном профиле RGB-состояний и bit-exact +certificate выбранного point-композита. Однако порядок этих states по диагностическому CAM16-UCS J′ — другая задача: она содержит `powf`/libm и влияет на semantic branch. +Ранее рассмотренная рациональная группировка стенок отвергнута для production: +алгебраически равные дроби при разных факторизациях выражения могут разойтись на +несколько ULP. Контрпример `glow=#010200`, `background=#018000` создаёт +достижимый промежуточный state `#018100`, который рациональная группировка +теряла. Поэтому точное для рациональной модели число 763 не является границей +фактического runtime-потока. + +Доказанная безопасная верхняя граница — **не более 766 состояний**: каждый +переход увеличивает хотя бы один из трёх байтов, каждый байт может увеличиться +не более 255 раз, плюс существует начальное состояние. Tightness этой границы +не установлена и не требуется контракту. Рабочий перечислитель хранит текущий +state и по одной следующей границе на канал, поэтому его вспомогательная память +O(1); `Vec` остаётся только в тестовом оракуле под `#[cfg(test)]`. + Compatibility profile `legacy-platform-dependent-v1` проверяет states в порядке alpha и выбирает: @@ -92,11 +107,10 @@ Compatibility profile `legacy-platform-dependent-v1` проверяет states 2. если такого нет — глобальный максимум `|ΔJ′|`; 3. при равном максимуме — первое состояние по alpha. -Рациональные стенки задают алгебраическое разбиение; финальная проверка выполняет -зафиксированное binary64-выражение в том же порядке, что официальный JS- -потребитель. На отдельных ULP-швах эти определения могут классифицировать саму -стенку по-разному, поэтому production-alpha берётся только из внутренности -интервала и перед возвратом обязана воспроизвести выбранный state. +Каждая граница уже определена зафиксированным binary64-выражением в том же +порядке, что официальный JS-потребитель. Production-alpha берётся из +representable внутренности фактического интервала (или из замкнутого singleton +`alpha=1`) и перед возвратом обязана воспроизвести выбранный state. Этот legacy-выбор не зависит от предположения о монотонности J′, но остаётся platform/libm-dependent: точного эталона или sound outward bound для CAM16- @@ -133,11 +147,14 @@ CSS-переменная alpha копирует её буквально. Пов композит и `|ΔJ′|` для halo и core, `targetStatus` и `constraintLayer`. `compositeProfile=encoded-srgb8-screen-v1` вместе с `compositeGuarantee=bit-exact` описывает только конечный point-композитор. -`diagnosticProfile=cam16-ucs-jprime-li2017-v1` идентифицирует реально -выполненную appearance-диагностику; exact no-op возвращает `null`, потому что -CAM16 в этом пути не вызывается. `decisionProfile` / `decisionGuarantee` -отдельно сообщают, на каком основании принят semantic target/max verdict. Один общий profile был бы -ложным повышением диагностической модели до exact-гарантии. Совместный +`layerRecipeProfile=cam16-jprime-oklab-cusp-v1` идентифицирует алгоритм +построения core/halo. `appearanceDiagnosticProfile=cam16-ucs-jprime-li2017-v1` +идентифицирует модель reported `haloAchievedDj`/`coreAchievedDj` и потому +присутствует у полного resolved result. `selectionDiagnosticProfile` называет +модель только если она участвовала именно в target/max выборе; у exact no-op он +равен `null`. `decisionProfile` / `decisionGuarantee` отдельно сообщают, на +каком основании принят semantic verdict. Один общий profile был бы ложным +повышением recipe или диагностической модели до exact-гарантии. Совместный blur/overlap/backdrop/HDR-эффект без геометрии не сертифицируется; это отдельный `SpatialField` из #221. @@ -148,7 +165,9 @@ chroma — как recipe core v1. Эмитированный core отдельн ### 5. Доказательства являются исполняемыми -- все 65 536 пар одного screen-канала проверяют рациональное разбиение; +- все 65 536 пар одного screen-канала исчерпывающе проверяют first-passing + binary64-разбиение против публичного compositor; отдельный ULP-seam тест + запрещает склейку алгебраически равных рациональных стенок; - независимый finite-state oracle проверяет legacy-выбор reached/unreachable; - source-over и screen при `alpha=0.122` сверяются с независимой точной рациональной формулой на всех 65 536 парах байтов каждый; @@ -188,7 +207,8 @@ WASM/TypeScript: неизвестная строка отвергаются при `loadConfig`, а профиль входит в fingerprint; - determinate mocks обязаны добавить `alphaCss`, `constraintLayer`, `targetDj`, `targetStatus`, оба composite, оба `achievedDj`, а также раздельные - `compositeProfile`, `compositeGuarantee`, nullable `diagnosticProfile`, + `compositeProfile`, `compositeGuarantee`, `layerRecipeProfile`, + `appearanceDiagnosticProfile`, nullable `selectionDiagnosticProfile`, `decisionProfile`, `decisionGuarantee`; - exhaustive union обязан обрабатывать `kind: "glow-indeterminate"`; этот terminal outcome несёт site/reason/bounds, но не создаёт halo/core/alpha vars; diff --git a/docs/empirical-inventory.md b/docs/empirical-inventory.md index 2cd43947..8c393e1c 100644 --- a/docs/empirical-inventory.md +++ b/docs/empirical-inventory.md @@ -49,7 +49,7 @@ Marker column: `SSOT-TRACKED` = has a paper trail in this table but no citation- | 53 | `QUANT_GUARD` | `0.2` | `semantic.rs` | SSOT-TRACKED | **(c) INTERVAL-INSENSITIVE** (ре-аудит `science/reclassify-e-buckets`, 2026-07-07; ранее (e)) — квант-guard над `MODEL_LC_FLOOR` (7.3): зазор `DECORATIVE_FLOOR_MIN − MODEL_LC_FLOOR`, держащий декоративный пол строго выше 7.3, чтобы решатель не садился на инфимум клипа и не возвращал нулевой контраст (issue #44). **Доказательство сильнее обычного (c):** `QUANT_GUARD` НЕ используется в продакшене НЕЗАВИСИМО — единственная величина, которую видит решатель, это байт-идентичный литерал `DECORATIVE_FLOOR_MIN` (7.5); guard существует только как провенанс-разложение этого литерала на DERIVED-член (7.3) и задекларированный запас (лок `decorative_floor_is_model_floor_plus_guard`, компайл-тайм `const _`). Замер (`no_pair_emits_contrast_below_model_floor`, скан квантованных пар): фактический минимальный ненулевой контраст = **7.3005** (сидит 0.0005 над модельным инфимумом 7.3, а не у номинальной цели 7.5) — скачок |Lc| одного 8-бит шага у клипа = **7.85** (жёсткий разрыв, guard не покрывает и не должен). Пока guard положителен и цель остаётся достижимой в пределах `QUANT_BUDGET` = 1.0, точная магнитуда 0.2 не меняет ни одного реального эмитируемого контраста; доминантный член 7.3 остаётся DERIVED. | | 54 | `MODEL_LC_FLOOR` | `7.3` | `lpc.rs` | SSOT-TRACKED | **(a) DERIVED** = (LO_CLIP − LO_BOW_OFFSET) × LC_SCALE = (0.1 − 0.027) × 100 = 7.3 — минимальный ненулевой контраст модели (за низкоконтрастным клипом `contrast_core` стартует ровно с него, внутри клипа — ноль; полярности симметричны, `LO_WOB_OFFSET == LO_BOW_OFFSET`). Алгебраическая идентичность из GROUNDED APCA `0.0.98G-4g` набора; хранится литералом 7.3 (чистое число для инвентаря), тождество с формулой пиннится `model_lc_floor_is_the_published_clip_minimum` (дрейф любого из трёх APCA-входов или правка литерала ломает лок). Опора `DECORATIVE_FLOOR_MIN` (строка 1), issue #44. | -## Muddiness Law constants — `cleanliness.rs` +## Замороженный experimental compatibility proxy — исторический API `cleanliness.rs` > **GATE SCOPE NOTE:** `cleanliness.rs` is NOT in the `PERCEPTUAL_MODULES` audit surface of > `tests/empirical_inventory.rs` (that gate covers `semantic.rs`, `scale.rs`, `sentiment.rs`, @@ -64,24 +64,26 @@ Marker column: `SSOT-TRACKED` = has a paper trail in this table but no citation- > gate** and does not exist on this tree. The module header in `cleanliness.rs` cross-references > each constant to its row here. -Science-vs-fit boundary: the **structural form** (warm gate b=C·sin h, neutral gate sigmoid on C, depth term below cusp-L) is derived from Oklab opponent-colour theory and CAM16-UCS. **The confidence layer (M_W / KAPPA_CORE / KAPPA_INTERIOR, M-09/10/11) has been removed entirely** (Front B, 2026-07-04): it was a single-observer DECLARED-CALIBRATION with no reliability model, off the emission path and outside the agnostic contract — see the REMOVED rows below. `muddiness` (distance geometry) remains. The former M-03 light-escape threshold has been **removed entirely** (Zone B, 2026-07-01): its escape term leaked the lightness axis into the neutral gate, which is documented as chroma-only; `neutral_gate` is now `sigmoid((C - C0) / JND)`. **B0 and BW are cited-measured** (Zone B, 2026-06-29). **Platt scalars CAL_T/CAL_B/CAL_EPS (M-06/M-07/M-08) have been removed** (Zone B slice 3, 2026-06-30). **W_HUE[8] and CEIL_N_TABLE have been replaced** (Zone B slice 4, 2026-06-30) by the derived Bezold-Brücke Hanning window `hue_weight(h) = (1 + cos(h − H_Y_DEG)) / 2`; H_Y_DEG = 90.4011° (Oklab hue of unique yellow λ=578nm, CIE 1931 2° D65, XYZ→Oklab direct — corrected in Zone B slice 5, 2026-07-03: the slice-4 value 96.9172° rested on transposed CMF rows and a hue-distorting sRGB clamp, and its citation «Parry (1967) JOSA 57, 1130–1134» does not exist; real sources: Purdy (1937) Am. J. Psychol. 49, 313–315; Jacobs & Wascher (1967) JOSA 57, 1155–1156). Zero fitting. CUSP_L_TABLE is cited-and-kept: pure sRGB-gamut geometry, kept as-is per paradigm North. +Эпистемическая граница: shipped-формула — **замороженный experimental compatibility proxy / research coordinate**. Её арифметика и committed corpus воспроизводимы, но число не является observer-validated оценкой cleanliness/murkiness, вероятностью, эстетическим вердиктом или product contract. Базовый компилятор и adaptive runtime эту координату не используют. Множители (сигмоида Oklab-chroma, positive-`b` gate, hue-окно и глубина ниже sRGB/Oklab cusp) — детерминированные математические признаки; ни один опубликованный источник не выводит их произведение как человеческую перцептивную шкалу. + +Confidence-слой (M_W / KAPPA_CORE / KAPPA_INTERIOR, M-09/10/11), Platt-скаляры (M-06/M-07/M-08), fitted W_HUE и CEIL_N_TABLE удалены. Это удаление не доказывает provenance оставшихся констант. В частности, названная литература не выводит точные Oklab-значения C0/JND/B0/BW; Hanning-окно — явная эвристика, а не следствие эффекта Бецольда–Брюкке; преобразование выбранного 578 nm reference в Oklab допустимо как арифметика, но не устанавливает универсальный observer unique-yellow; sRGB/Oklab cusp — геометрия гамута, а не человеческая clean/dirty-граница. Владелец эпистемической/API-миграции — [#231](https://github.com/Labpics-Team/lab-colors/issues/231); применимость JND и evidence возможной замены принадлежат [#242](https://github.com/Labpics-Team/lab-colors/issues/242). | mud-id | name | value | module | status | rationale | |--------|------|-------|--------|--------|-----------| -| M-01 | `C0` | `0.0395` | `cleanliness.rs` | cited-and-kept | Grey-frontier chroma floor — центр сигмоида перехода ахроматический→хроматический (`sigmoid((C−C0)/JND)`). Единственное честное основание: литература о пороге «нулевой серости» / хроматического порога (Evans, «The Perception of Color»; Xie & Fairchild). **⚠️ Прежняя геометрическая мотивировка** («граница гамута sRGB для серых ≈0.04 при L=0.5») **удалена как некорректная**: у ахроматических серых C=0 по определению, «границы гамута для серых при C≈0.04» не существует. Строгий вывод значения 0.0395 в Oklab из Evans/Xie-Fairchild НЕ установлен — **TODO вывода**. cited-and-kept, значение не меняется (North). Bound: 0.030 ≤ C0 ≤ 0.050 (декларированный диапазон). | -| M-02 | `JND` | `0.012278` | `cleanliness.rs` | cited-and-kept | Chroma just-noticeable difference in Oklab. Oklab is perceptually uniform to first order; one JND on the opponent axes a,b is ≈ 0.012 (derived from the ΔE₂₀₀₀ ≈ 1 JND mapped through the Oklab linearisation). JND = 0.01228 is used as the sigmoid transition width for both the neutral gate and the b-gate, encoding that transitions span ≈ 1 perceptual unit. Cited: Oklab perceptual measurement. Kept as-is per paradigm North. **⚠️ Оговорка:** отображение ΔE₂₀₀₀→Oklab использует линеаризацию (якобиан), но точка пространства, в которой якобиан взят, НЕ зафиксирована в первоисточнике деривации (ΔE₂₀₀₀↔Oklab зависит от точки); значение держится как задекларированное и не округляется. Bound: 0.010 ≤ JND ≤ 0.016. | -| M-04 | `B0` | `0.036` | `cleanliness.rs` | cited-measured | Oklab-b warm-gate centre. Structural form (b = C·sin h threshold) is derived from Oklab opponent-colour theory: the yellow–blue axis b separates warm (b > 0) from cool (b < 0). Value 0.036 is the central of the cited range [0.030, 0.044]: the empirical yellow–brown perceptual boundary from unique-hue experiments (Newhall-Nickerson-Judd 1943; Lindsey-Brown 2014 PNAS; Boynton 1975). Observer-fit replaced by cited-measured central (Zone B). | -| M-05 | `BW` | `0.017` | `cleanliness.rs` | cited-measured | Oklab-b warm-gate transition width. Same structural role as JND but for the b-axis warm gate. Value 0.017 is the central of the cited range [0.013, 0.020]: the perceptual transition width of the warm–cool boundary from the same unique-hue studies (Newhall-Nickerson-Judd 1943; Lindsey-Brown 2014 PNAS; Boynton 1975). Observer-fit replaced by cited-measured central (Zone B). | +| M-01 | `C0` | `0.0395` | `cleanliness.rs` | Indeterminate provenance | Замороженный центр сигмоиды compatibility proxy. Ни литература о порогах, ни геометрия гамута sRGB не выводят точное Oklab-значение `0.0395`; прежнее утверждение об универсальной grey frontier отклонено. Сохранение числа удерживает research coordinate и не является evidence человеческого смысла ([#231](https://github.com/Labpics-Team/lab-colors/issues/231)). | +| M-02 | `JND` | `0.012278` | `cleanliness.rs` | universal JND Rejected; Indeterminate value | Замороженная ширина сигмоиды. Универсальный Oklab JND отклонён: перенос из ΔE₂₀₀₀ требует заданных reference point, направления, observer population и viewing domain; без них не выводится ни это число, ни его применение как logistic width. Evidence и применимость возможной замены принадлежат [#242](https://github.com/Labpics-Team/lab-colors/issues/242). | +| M-04 | `B0` | `0.036` | `cleanliness.rs` | Rejected provenance; Indeterminate value | Замороженный центр positive-`b` gate этого proxy. Newhall–Nickerson–Judd, Lindsey–Brown и Boynton не выводят заявленный Oklab-`b` range или точное значение `0.036`; прежнее literature-grounded утверждение о границе отклонено. Число остаётся только ради совместимости ([#231](https://github.com/Labpics-Team/lab-colors/issues/231)). | +| M-05 | `BW` | `0.017` | `cleanliness.rs` | Rejected provenance; Indeterminate value | Замороженная ширина positive-`b` gate этого proxy. Названные источники не выводят Oklab transition-width range или точное значение `0.017`; прежний provenance отклонён. Число остаётся только ради совместимости ([#231](https://github.com/Labpics-Team/lab-colors/issues/231)). | | M-06 | `CAL_EPS` | `0.01` _(former)_ | `cleanliness.rs` | REMOVED (Zone B slice 3, 2026-06-30) | Platt link log-offset — removed together with the Platt sigmoid chain. `pub const CAL_EPS` no longer exists in `cleanliness.rs`; any reference is test-only (absent-constant guard). | | M-07 | `CAL_T` | `2.356978` _(former)_ | `cleanliness.rs` | REMOVED (Zone B slice 3, 2026-06-30) | Platt link scale — removed. Was a fitted scalar on v3 train split; violated ZERO observer-fit invariant (North). `pub const CAL_T` no longer exists in `cleanliness.rs`. | | M-08 | `CAL_B` | `6.445168` _(former)_ | `cleanliness.rs` | REMOVED (Zone B slice 3, 2026-06-30) | Platt link bias — removed. Was a fitted scalar on v3 train split; violated ZERO observer-fit invariant (North). `pub const CAL_B` no longer exists in `cleanliness.rs`. | | M-09 | `M_W` | `0.181527` _(former)_ | `cleanliness.rs` | REMOVED (Front B, 2026-07-04) | Полуширина доверительного поля confidence-слоя. Была DECLARED-CALIBRATION из однонаблюдательской разметки владельца (N=1, 738 меток, reliability-модели нет) — observer-fit вне агностичного контракта движка и вне пути эмиссии (`confidence` не звался из solve/semantic/resolve). Слой удалён целиком, НЕ ре-калиброван (датасета для честного мультинаблюдательского ре-груна нет — любая новая калибровка была бы театром). `pub const M_W` больше не существует в `cleanliness.rs`; снос байт-идентичен эмиссии. | | M-10 | `KAPPA_CORE` | `0.34` _(former)_ | `cleanliness.rs` | REMOVED (Front B, 2026-07-04) | Потолок уверенности стабильного ядра confidence-слоя — та же однонаблюдательская калибровка (N=1), без модели надёжности. Удалён вместе с confidence-слоем; `pub const KAPPA_CORE` больше не существует в `cleanliness.rs`. | | M-11 | `KAPPA_INTERIOR` | `0.10` _(former)_ | `cleanliness.rs` | REMOVED (Front B, 2026-07-04) | Пол уверенности спорной полосы confidence-слоя — та же однонаблюдательская калибровка (N=1), без модели надёжности. Удалён вместе с confidence-слоем; `pub const KAPPA_INTERIOR` больше не существует в `cleanliness.rs`. | -| M-12 | `H_Y_DEG` | `90.4011` | `cleanliness.rs` | cited-derived (Zone B slice 5, 2026-07-03) | Oklab hue уникального жёлтого (λ=578nm, CIE 1931 2° D65). Вывод: CMF при 578nm — линейная интерп. CIE 5нм-таблицы (575nm: 0.8425/0.9154/0.0018; 580nm: 0.9163/0.8700/0.00165) → (0.886780, 0.888160, 0.001710) → XYZ/Y = (0.998446, 1, 0.001925) → Oklab НАПРЯМУЮ (Ottosson 2020; Oklab hue инвариантен к радиансности, sRGB-проекция не нужна; покомпонентный клэмп НЕ сохраняет hue) → atan2(b,a) = 90.4011° (±0.01° — неопределённость 5нм-интерполяции: 1нм-таблица даёт 90.3925°). Центр Hanning-окна `hue_weight(h) = (1 + cos(h − H_Y_DEG)) / 2`; производная поворота Бецольда-Брюкке: dΔH_BB/dh = A_BB·cos(h−h_Y). История: значение slice 4 (96.9172°) ОТОЗВАНО — CMF-строки были перепутаны (заявленные ȳ=0.7070/0.7570 не существуют на 570/580нм; ложный XYZ=(1.207,1,0)), hue брался после sRGB-клэмпа (артефакт: 69.7°→96.9° от клэмпа), а цитата «Parry (1967) JOSA 57, 1130–1134» не существует. Реальные источники: инвариантная точка жёлтого ≈578nm — измерения BB-сдвига Jacobs & Wascher (1967) J. Opt. Soc. Am. 57, 1155–1156; сама рамка инвариантных оттенков (contours for constant hue) — Purdy (1937) Am. J. Psychol. 49, 313–315 (атрибуция уточнена по ревью 2026-07-03: Purdy — феномен BB/контуры, не прямое измерение 578nm). | -| M-12 | ~~`W_HUE[8]`~~ | _(удалён)_ | `cleanliness.rs` | REMOVED (Zone B slice 4, 2026-06-30) | Был: K=3 Fourier-базис + CEIL_N, подогнанный логистической регрессией на 738 авторских метках v3. Нарушал ZERO observer-fit (North). Заменён Hanning-окном BB (H_Y_DEG). | -| M-13 | `CUSP_L_TABLE[361]` | see code | `cleanliness.rs` | cited-and-kept | Oklab cusp lightness per integer hue degree (0–360). Derived from the sRGB gamut geometry (kept as-is per paradigm North, never refit): for each hue angle h the cusp L is the Oklab L of the maximum-chroma sRGB-boundary point at that hue. This is a deterministic geometric quantity (no fitting). Reproduced via the Oklab gamut-intersection algorithm (find_cusp in the Björn Ottosson reference implementation). Bound: values lie in [0.45, 0.97] as per full sRGB gamut sweep. | -| M-14 | ~~`CEIL_N_TABLE[361]`~~ | _(удалена)_ | `cleanliness.rs` | REMOVED (Zone B slice 4, 2026-06-30) | Была: нормированный CEIL_N-коэффициент (второй Fourier-базис). Подогнана на v3 для захвата несинусоидальной вариации потолка грязи; не выводима из первых принципов. После замены hue_weight на BB-формулу более не нужна. | +| M-12 | `H_Y_DEG` | `90.4011` | `cleanliness.rs` | arithmetic admitted; observer claim Rejected | Арифметика admitted для объявленного frozen reference: интерполяция выбранных CMF при 578 nm, нормировка XYZ/Y и преобразование этого стимула в Oklab дают примерно `90.4011°` с указанными матрицами. Утверждение об универсальном observer unique-yellow отклонено: unique hue зависит от наблюдателя, luminance и протокола. Центрированное здесь Hanning-окно — явная эвристика; измерения Бецольда–Брюкке не выводят это окно или человеческую cleanliness-coordinate ([#231](https://github.com/Labpics-Team/lab-colors/issues/231)). | +| M-12 | ~~`W_HUE[8]`~~ | _(удалён)_ | `cleanliness.rs` | REMOVED (Zone B slice 4, 2026-06-30) | Был K=3 Fourier-базис, подогнанный логистической регрессией на 738 авторских метках v3. Заменён замороженной heuristic Hanning-формой с центром `H_Y_DEG`; это не делает форму выводом из эффекта Бецольда–Брюкке. | +| M-13 | `CUSP_L_TABLE[361]` | see code | `cleanliness.rs` | geometry admitted; observer frontier Rejected | Детерминированная геометрия гамута sRGB/Oklab: для каждого целого hue таблица хранит cusp lightness максимального по chroma граничного sRGB-цвета. Эта воспроизводимая геометрия admitted; её интерпретация как psychophysical clean/dirty frontier отклонена ([#231](https://github.com/Labpics-Team/lab-colors/issues/231)). | +| M-14 | ~~`CEIL_N_TABLE[361]`~~ | _(удалена)_ | `cleanliness.rs` | REMOVED (Zone B slice 4, 2026-06-30) | Была подогнанным Fourier-коэффициентом legacy proxy и не выводилась из первых принципов. После перехода на heuristic Hanning-форму более не используется. | | S-01 | `chroma_fraction` (тинт) | конфиг `(0,1]` | `sentiment.rs` | applied (2026-07-03) | Анти-неоновый потолок продакшн-тинта сентимента: `c = min(c_якоря, f·C_max(L, h_решённый))`. Хрома якоря клиента — авторитет идентичности; потолок кусается только на неоново-насыщенном якоре или суженном гамуте смещённого оттенка. Одновременно закрыт молчаливый канальный клип sRGB (искажал оттенок; усечение теперь по оси хромы). labui-референс ставит `1.0` (чистая стена гамута: Figma-якорь danger #FF3B30 сидит выше 0.88·C_max); реестровый дефолт 0.88 — для клиентов без якорной калибровки. | | S-02 | Warning↔Success light-ic | ab ≈ 0.042 → ВЫЛЕЧЕНО | `sentiment.rs`, `config.rs` | RESOLVED (2026-07-03, тот же день) | Находка попарного аудита: брендоцентричный закон был попарно слеп, а flip-ветка при ДАЛЁКОМ бренде зеркалила Warning через полкруга (IC-якорь #C93400 ниже пола 45° → телепорт в зелень #508300 к Success #248A3D, ab 0.042 < 0.0687). Лечение — многотельная легальность: `NeighborZone` в is_legal/legalize/resolve + двухфазная оккупация `sentiment_solid_for_mode` (покоящиеся сентименты — неподвижные оккупанты, идентичность якорей цела по построению; смещённые держат угловой отступ — инверсия хорды s_perc_min при средней хроме пары). Warning light-ic ложится в янтарную дугу у пола. Пины: `labui_sentiment_solids_keep_pairwise_ab_distance` (все пары ≥ порога, без исключений), `warning_light_ic_heals_into_amber_arc_not_green` (янтарь + RED-proof однотельного закона). | diff --git a/docs/empirical-residue.md b/docs/empirical-residue.md index 3c1f25f5..d368784b 100644 --- a/docs/empirical-residue.md +++ b/docs/empirical-residue.md @@ -116,9 +116,10 @@ OPEN в #219/#261 и не является блокером для generic fact- JND. `DECORATIVE_FLOOR_MIN` (7.5) — не (b), а (a): выводится алгебраически из GROUNDED APCA-клипа, а не из отдельного опубликованного измерения. Прочие калибровочные константы прямого опубликованного измерения ИМЕННО своего значения -по-прежнему не имеют; литературно-заземляемые `B0`/`BW`/`H_Y_DEG` — это M-строки -`cleanliness.rs` (волна Zone B), вне этого набора. Выдумывать цитату под -остальные — подлог. +по-прежнему не имеют. `B0`/`BW`/`H_Y_DEG` — замороженные compatibility values +M-строк `cleanliness.rs` (волна Zone B), вне этого набора: прежние +literature-grounded / universal-observer claims отклонены, а не доказаны. +Выдумывать цитату под остальные — подлог. ## EXPOSURE-анализ diff --git a/docs/migrations/exact-alpha-glow.md b/docs/migrations/exact-alpha-glow.md index 958bc92b..e5c3b0f2 100644 --- a/docs/migrations/exact-alpha-glow.md +++ b/docs/migrations/exact-alpha-glow.md @@ -3,6 +3,10 @@ > Роль: breaking-migration guide для `@labpics/colors` 0.10.0 и Rust workspace > 0.2.0 с предыдущих `@labpics/colors` 0.9.1 / Rust 0.1.0. +Release artifact собирается закреплённым Node 24.14/npm 11.9, но это не +consumer requirement: `@labpics/colors` поддерживает `Node >=22.11.0` (первый +Node 22 LTS), и этот floor проверяется отдельным CI job. + Эта версия разделяет два контракта, которые раньше выглядели одним: 1. exact encoded-sRGB8 point-композит и его воспроизводимые байты; @@ -109,7 +113,7 @@ determinate resolve; собственный DOM-adapter обязан обесп `stable-v1` не означает «всегда Indeterminate». Если point screen-композит bit-exact не может изменить фон при любой alpha, target/max-branch не требует -CAM16: результат determinate, `targetStatus: "unreachable"` и +CAM16: результат determinate, `targetStatus: "exact-noop-unreachable"` и `decisionGuarantee: { kind: "bit-exact" }`. ### Determinate Glow @@ -121,27 +125,30 @@ CAM16: результат determinate, `targetStatus: "unreachable"` и | --- | --- | | `alphaCss` | Каноническая shortest-roundtrip строка той же binary64 alpha; CSS var копирует её буквально. | | `constraintLayer` | Слой цели; сейчас `"halo"`. | -| `targetDj` | Запрошенный point `|ΔJ′|`. | -| `targetStatus` | `"reached"` либо `"unreachable"`. | +| `targetDj` | Запрошенный point `\|ΔJ′\|`. | +| `targetStatus` | `"exact-noop-unreachable"`, `"legacy-reached"` либо `"legacy-unreachable"`; provenance результата не теряется в общем boolean. | | `haloCompositeHex` | Exact reference point-композит halo. | -| `haloAchievedDj` | Diagnostic `|ΔJ′|` этого halo-композита. | +| `haloAchievedDj` | Diagnostic `\|ΔJ′\|` этого halo-композита. | | `coreCompositeHex` | Exact reference point-композит core при той же alpha. | -| `coreAchievedDj` | Diagnostic `|ΔJ′|` этого core-композита. | +| `coreAchievedDj` | Diagnostic `\|ΔJ′\|` этого core-композита. | | `compositeProfile` | `"encoded-srgb8-screen-v1"`. | | `compositeGuarantee` | `"bit-exact"`. | -| `diagnosticProfile` | `"cam16-ucs-jprime-li2017-v1"` либо `null`; `null` у exact no-op, где appearance model не выполнялась. | +| `layerRecipeProfile` | `"cam16-jprime-oklab-cusp-v1"`; identity алгоритма построения core/halo, не numerical guarantee. | +| `appearanceDiagnosticProfile` | `"cam16-ucs-jprime-li2017-v1"`; non-null у полного resolved result, потому что `*AchievedDj` измеряются этой моделью. | +| `selectionDiagnosticProfile` | `"cam16-ucs-jprime-li2017-v1"` только когда модель участвовала в target/max selection; `null` у exact no-op. | | `decisionProfile` | Профиль, явно выбранный конфигом. | -| `decisionGuarantee` | Tagged certificate semantic verdict: `{kind:"bit-exact"}` для exact no-op, `{kind:"legacy-platform-dependent-v1"}` для compatibility path либо `{kind:"outward-interval-v1", lower, upper}` с самим доказанным outward interval. | +| `decisionGuarantee` | Tagged certificate semantic verdict: `{kind:"bit-exact"}` для exact no-op либо `{kind:"legacy-platform-dependent-v1"}` для compatibility path. Outward determinate Glow capability в этом релизе не поддерживается. | `achievedDj` остаётся deprecated alias на `haloAchievedDj`, а `degraded` — -deprecated alias на `targetStatus === "unreachable"`. Новый код должен читать -типизированные поля. +deprecated alias на status `exact-noop-unreachable` или `legacy-unreachable`. +Новый код должен читать типизированные поля. В промежуточном pre-release контракте #269 девятым новым полем был единый -`referenceProfile`. В финальном 0.10.0 его нет: замените его пятью отдельными -полями `compositeProfile`, `compositeGuarantee`, `diagnosticProfile`, -`decisionProfile`, `decisionGuarantee`. Иначе exact-гарантия композитора -ошибочно распространяется на CAM16 decision. +`referenceProfile`. В финальном 0.10.0 его нет: замените его семью отдельными +полями `compositeProfile`, `compositeGuarantee`, `layerRecipeProfile`, +`appearanceDiagnosticProfile`, `selectionDiagnosticProfile`, `decisionProfile`, +`decisionGuarantee`. Иначе exact-гарантия композитора ошибочно распространяется +на recipe, CAM16 diagnostics или semantic decision. ## 3. Обновите Rust alpha API @@ -184,6 +191,42 @@ Continuous `resolve_alpha_analog` сохраняет `Option`, по в debug и release. Не восстанавливайте прежнее поведение через локальный `clamp`. +### Material alpha сообщает typed outcome и численную границу честно + +`BackdropBox` больше нельзя создать struct literal: поля закрыты, а +`BackdropBox::try_new(min, max)` возвращает `BackdropBoxErrorV1` с отдельными +вариантами для reversed, non-finite и out-of-range channel. Границы не +переставляются и не клампятся. `worst_contrast_encoded`, +`solve_material_alpha_encoded` и `solve_material_alpha_hex` возвращают +`Result<_, MaterialSolveErrorV1>`; недостижимый floor остаётся typed domain +outcome `MaterialAlphaStatusV1::Degraded`, а не ошибкой или fallback. + +У `MaterialAlpha` используйте getters `alpha()`, `worst_contrast()`, `pole()`, +`status()` и `guarantee()`. Satisfied-результат несёт +`MaterialAlphaGuaranteeV1::BisectionBracketCharacterizedV1`: после 60 шагов +бисекции `lower_alpha` повторно измерен ниже floor, `upper_alpha` повторно +проходит floor, а `alpha()` побитно равен `upper_alpha`. Перед поиском действует +directed-search guard; неподдерживаемое отношение возвращает +`UnsupportedDirectedSearchRelation`. Numerical profile явно равен +`encoded-srgb-byte-scale-affine-platform-binary64-powf-v1`. Это +platform-characterization, а не заявление глобальной монотонности, первого +passing state, точной минимальной alpha, предшественника или sound cross-runtime +bound. Degraded-результат аналогично +несёт `OpaqueEndpointCharacterizedV1` для повторно проверенного `alpha = 1`. +Если floor уже держится при `alpha = 0`, возвращается отдельный +`TransparentEndpointCharacterizedV1`, а не фиктивный threshold bracket. На wire +primary outcome — `alphaStatus`; прежний `guaranteed` остаётся только derived +compatibility alias. `alphaGuarantee` сохраняет tagged evidence и profile. + +Если consumer независимо перепроверяет material, он обязан повторить именно +byte-scale affine order `(B_byte + alpha·(T_byte−B_byte))/255`. Прежняя +normalized expanded запись `alpha·T + (1−alpha)·B` алгебраически равна, но не +принадлежит этому binary64 profile и расходится на известных WCAG-швах. +Профиль фиксирует original WCAG 2.1 (2018) split `0.03928` (не текущий W3C +`0.04045`). Для all-backdrop результата ядро расширяет фактические endpoint- +композиты conservative binary64 channel envelope и включает обе стороны +пересечённого EOTF seam; перепроверка только двух углов недостаточна. + ## 4. Обновите Rust Glow API `solve_screen_alpha_for_dj` теперь принимает обязательный @@ -233,20 +276,42 @@ tagged object, а не строкой: ```ts type GlowDecisionGuaranteeV1 = | { readonly kind: "bit-exact" } - | { - readonly kind: "outward-interval-v1"; - readonly lower: number; - readonly upper: number; - } | { readonly kind: "legacy-platform-dependent-v1" }; ``` -Нельзя отделять `lower`/`upper` от outward-варианта или принимать неизвестный -`kind` как `bit-exact`. +Generic core сохраняет `DecisionGuaranteeV1::OutwardIntervalV1` для других +численных sites, но Glow-adapter не умеет построить соответствующий determinate +outcome и возвращает структурную несовместимость. Не принимайте неизвестный +`kind` как `bit-exact` или legacy. + +Low-level UniFFI/Swift поверхность не переносит flattened provenance-поля +generic wire-формы. Её `GlowPointDecision` — algebraic sum ровно четырёх +допустимых outcome: `stableExactNoop(value)`, `legacyReached(value)`, +`legacyUnreachable(value)` и `indeterminate(siteId, evidence)`. Общий +`GlowPointValue` хранит только alpha, target/achieved diagnostic, exact +composite hex и composite profile/guarantee. Отдельные native-типы +`GlowDecisionGuarantee`, `GlowTargetStatus`, `GlowDiagnosticProfile` и +одноимённые независимые output-поля удалены: невозможный cross-product нельзя +сконструировать. `GlowDecisionProfile` остаётся обязательным input функции, но +не дублируется в output. + +Native adapter заранее валидирует tint, background и конечный `targetDj > 0`; +только такой public input возвращает `ColorError.InvalidGlowRequest`. Если после +успешной проверки core всё же возвращает `Err`, неизвестный forward variant, +illegal provenance tuple либо generic `DecisionGuaranteeV1::OutwardIntervalV1`, +граница возвращает `ColorError.IncompatibleCoreContract`. Тот же закон действует +для нового неизвестного `Unreachable`: adapter не подменяет его строкой +`"unreachable"`. `NumericalIndeterminacy.intervalOverlap` остаётся в Swift как +законное outward evidence для `indeterminate`. + +В Swift замените прежний `case .determinate(...)` на exhaustive switch по этим +четырём вариантам; `value` одинаков только по форме composite payload, а смысл +ветки берётся из enum-case. Удалите mocks и helper-типы, которые продолжают +принимать независимые native decision/status/diagnostic поля. Поля `GlowSolve` закрыты; используйте getters `alpha()`, `alpha_css()`, `target_dj()`, `achieved_dj()`, `composite_hex()`, `status()` и -`composite_certificate()`. Exact certificate хранит tint/background bytes, +`selection_diagnostic_profile()`, `composite_certificate()`. Exact certificate хранит tint/background bytes, binary64 identity alpha, каноническую CSS-строку и composite bytes. Он не сертифицирует браузер, дисплей, blur или пространственное перекрытие. @@ -256,14 +321,21 @@ binary64 identity alpha, каноническую CSS-строку и composite 1. Добавьте `decision_profile` во все Glow-рецепты JSON и typed builders. 2. Обновите exhaustive `RoleResult` switches для `glow-indeterminate`. -3. Добавьте все 13 determinate-полей из таблицы выше; удалите +3. Добавьте все 15 determinate-полей из таблицы выше; удалите `referenceProfile` из pre-release mocks. 4. Для `stable-v1` проверяйте отсутствие трёх CSS vars, а не snapshot legacy- цвета. 5. Сравнивайте alpha через `alphaCss` или побитный parse round-trip; не округляйте её до фиксированного числа знаков. 6. Используйте conformance pack 2.0.0. Half-tie - `#C0B2FA @ 0.122` над `#000000` обязан дать `#17161F`. + `#C0B2FA @ 0.122` над `#000000` обязан дать `#17161F`. Обрабатывайте + `generate_solve()` / `Pack::generate()` как `Result`: internal core failure + теперь возвращает `PackGenerationError`, а не fake `unreachable` vector. +7. Проверьте имена `Zero`-ролей и алиасов на них: их `cssVar` остаётся + client-owned namespace даже без эмитированного значения и не может совпадать + с производным ключом Glow (`-core`/`-alpha`) или Material (`-01`/`-02`). + Такой конфиг теперь честно отклоняется на preflight вместо зависимости от + порядка JSON-писателей. В закреплённом Lab UI regression corpus exact alpha меняет ровно 24 листа `resolveVars`: четыре `--lab-fx-glow-*-alpha` на шести записях. Все 1376 пар @@ -304,7 +376,9 @@ Rollback выполняется парой runtime + config: ## Границы гарантии - `bit-exact` относится к объявленному encoded-sRGB8 point-композитору. -- `cam16-ucs-jprime-li2017-v1` — identity диагностической модели, не error bound. +- `cam16-jprime-oklab-cusp-v1` — identity layer recipe, не numerical guarantee. +- `cam16-ucs-jprime-li2017-v1` — identity appearance/selection diagnostic, + не error bound; наличие в appearance measurement не доказывает selection. - `legacy-platform-dependent-v1` явно допускает зависимость semantic branch от platform/libm. - `stable-v1` сохраняет неопределённость вместо выдуманного epsilon или diff --git a/docs/whitepaper.md b/docs/whitepaper.md index da40f216..af1d041a 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -522,35 +522,55 @@ GLASS-режиме база отброшена и `01` лежит над ЖИВ **Единственная решаемая величина — альфа**, и она НЕ рукописная. Солид-канон `01`-над-`02` = `α·T + (1−α)·T = T` — байт-точно равен тону при любой α (композит `T` над `T` есть `T`), поэтому солид тривиально верен; альфа выводится из -GLASS-режима. Тинт над фоном `B` даёт композит `α·T + (1−α)·B` в -гамма-кодированном sRGB reference-профиля -[`alpha.rs`](../crates/labcolors-core/src/alpha.rs). Профиль заземлён измеренными -Figma-парами, но не объявляет любой browser/color-management pipeline тождественным. +GLASS-режима. Тинт над фоном `B` считается в объявленном byte-scale affine +порядке binary64-операций +`(B_byte + α·(T_byte−B_byte))/255`. Ядро +[`material.rs`](../crates/labcolors-core/src/material.rs) и официальный +потребитель [`effective-bg.js`](../packages/colors/effective-bg.js) исполняют +этот порядок одинаково. Алгебраически эквивалентная expanded-запись не является +тем же численным профилем: на WCAG-швах она может выбрать соседнюю alpha. +Профиль заземлён измеренными Figma-парами, но не объявляет любой +browser/color-management pipeline тождественным. Контракт: композит над ЛЮБЫМ разрешённым фоном обязан держать читаемость поверхности. Поверхность коммитит **ахроматический полюс** максимального контраста на тоне (белый на тёмном `T`, чёрный на светлом — параметр-свободный механизм -полярности); α — минимальная плотность, при которой этот полюс держит WCAG-пол -`floor` (4.5 / 3.0) по всему коридору достижимых фонов `[чёрный, белый]` (стекло -над неизвестным фоном — худший коридор). - -Композит афинно-возрастает по каждому каналу фона, а WCAG-светлота монотонна по -каналам композита, поэтому светлота композита экстремальна на углах коридора -`min`/`max`; худший контраст полюса берётся по достижимой полосе -`[effLow, effHigh]` — инверсия α-граничной гарантии потребителя -(`material-guarantee.ts`): не «годен при данной α», а «минимальная годная α». -Худший контраст монотонно растёт по α (полоса стягивается к тону), поэтому солвер — -бисекция, как остальные полы движка (`glow::solve_screen_alpha_for_dj`). На α = 1 -полоса вырождается в `L(T)`, а полюс максимального контраста на ЛЮБОМ тоне даёт -≥ 4.58:1 (кроссовер чёрного и белого полюса при `L ≈ 0.179`), поэтому для пола ≤ AA -годная α ∈ (0, 1] существует всегда; при более высоком поле честно возвращается -α = 1 с флагом `guaranteed = false` — не молчание. +полярности). Солвер возвращает проходящий верхний кандидат 60-шаговой бинарной +partition для WCAG-пола `floor` (4.5 / 3.0) по всему коридору достижимых фонов +`[чёрный, белый]` (стекло над неизвестным фоном — худший коридор) и повторно +проверяет обе стороны найденного fail/pass bracket в объявленном runtime. + +Профиль намеренно заморожен на original WCAG 2.1 (2018) EOTF split `0.03928`; +текущий текст W3C использует `0.04045`, а его версионированная миграция +принадлежит issue #284. У legacy-функции есть малый downward seam, а binary64- +округление byte-scale affine композитора может опустить interior background ниже +обоих endpoint-значений. Поэтому ядро строит conservative поканальный envelope +фактических endpoint-вычислений, добавляет доказанный абсолютный запас для +compositor rounding, включает обе стороны пересечённого EOTF seam и только затем +складывает положительно взвешенные channel ranges. Это all-backdrop +characterization, а не двухугловая теорема. + +Fixed-step бисекция разрешена только после directed-search guard: отношение тона +и backdrop обязано стягивать допустимый интервал в нужном направлении полюса. +Полный коридор `[чёрный, белый]` удовлетворяет guard; неподдерживаемое узкое +отношение возвращает typed `UnsupportedDirectedSearchRelation`, а не ложную +alpha. Ни глобальная монотонность branch-sensitive predicate, ни первый passing +state, ни точная минимальная alpha не заявляются. WCAG-светлота содержит +platform/toolchain-sensitive `powf`, поэтому результат остаётся +platform-characterized и не является sound cross-runtime bound. На α = 1 +полоса вырождается в `L(T)`, а полюс максимального контраста на +ЛЮБОМ тоне даёт ≥ 4.58:1 (кроссовер чёрного и белого полюса при `L ≈ 0.179`), +поэтому для пола ≤ AA годная α ∈ (0, 1] существует всегда; при более высоком +поле честно возвращается typed `Degraded` с повторно проверенным opaque endpoint +`α = 1` — не молчание. Порядок тиров **выводится физикой, не подбором**: на светлой теме тон дальше от белого (крупнее |ΔJ'| = base) требует ПЛОТНЕЕ α, чем ближе к фону (subtle) — инвариант запинен тестами. Замер идёт на КВАНТОВАННОМ тоне-тинте (эмитируемый `01`) -с точным (непереквантованным) композитом над углами — ровно как потребитель -пересчитывает вердикт из эмитированных `01`/`02`, поэтому гарантия ядра и re-check -потребителя тождественны (тест `material_guarantee_recomputable_over_worst_backdrop`). +с непереквантованным reference-композитом и conservative channel/EOTF envelope +— ровно в byte-scale affine order официального потребителя. Выбранный upper candidate +повторно проходит floor в том же охарактеризованном numerical profile (тест +`material_guarantee_recomputable_over_worst_backdrop`); это re-check выбранного +state, не proof его точной минимальности или cross-runtime bit identity. --- diff --git a/packages/colors/README.md b/packages/colors/README.md index c10d8fc7..77e9759c 100644 --- a/packages/colors/README.md +++ b/packages/colors/README.md @@ -163,14 +163,33 @@ type RoleResult = `mix-blend-mode: screen`: `cssVar` — halo, `${cssVar}-core` — core, `${cssVar}-alpha` — каноническая `alphaCss`. `compositeProfile` / `compositeGuarantee` описывают exact point-композит отдельно от - `diagnosticProfile` и `decisionGuarantee`. `diagnosticProfile` равен - `"cam16-ucs-jprime-li2017-v1"`, только если solve реально выполнил эту - диагностику; exact no-op возвращает `null`. `kind: "glow-indeterminate"` + `decisionGuarantee`. `layerRecipeProfile` фиксирует общий рецепт слоёв + `"cam16-jprime-oklab-cusp-v1"`, а обязательный + `appearanceDiagnosticProfile` — CAM16 appearance-модель результата. Отдельный + `selectionDiagnosticProfile` сообщает, применялась ли CAM16-диагностика именно + при выборе состояния: для exact no-op он равен `null`, для legacy solve — + `"cam16-ucs-jprime-li2017-v1"`. `targetStatus` различает эти ветви явно: + `"exact-noop-unreachable"`, `"legacy-reached"` или + `"legacy-unreachable"`. Determinate-форма — union из этих трёх согласованных + ветвей: смешать stable/legacy profile, guarantee, selection diagnostic, + status и `degraded` на уровне TypeScript невозможно. Outward interval не + входит в Glow capability этого релиза. `kind: "glow-indeterminate"` означает, что профиль `stable-v1` не выбрал target/max state без sound bound; для такой роли CSS-переменные не эмитятся и legacy fallback не применяется. Цель и все `*CompositeHex` / `*AchievedDj` относятся только к изолированным point-слоям, а не к полному blur/overlap-эффекту, браузеру или дисплею. -- `MaterialRole` — тон двухслойного материала и выведенная alpha: primary — солид-канон, `-01` — тинт, `-02` — опаковая база; поля результата явно сообщают гарантию и её границы. +- `MaterialRole` — union трёх терминальных исходов: satisfied transparent + endpoint (`alpha = 0`), satisfied bisection bracket (`alpha` побитно равна + `upperAlpha`) и degraded opaque endpoint (`alpha = 1`). `alphaStatus`, + `alphaGuarantee` и compatibility-поле `guaranteed` согласованы типом. `floor` + — запрошенный пол; он держится только при `alphaStatus: "satisfied"`. Primary + остаётся солид-каноном, `-01` — тинтом, `-02` — опаковой базой. Численный + профиль явно фиксирует operation order: + `encoded-srgb-byte-scale-affine-platform-binary64-powf-v1`: original WCAG 2.1 + (2018) split `0.03928`, conservative binary64 channel envelope и обе стороны + пересечённого EOTF seam. Bracket доступен только после directed-search guard; + это platform-characterization, а не двухугловая теорема, глобальная + монотонность, первый passing state или точная минимальная alpha. - `NoneRole` — роль намеренно пустая по дизайну (`kind: "none"`), не ошибка. - `UnreachableRole` — ни один цвет не удовлетворяет требованиям для этого фона (`kind: "unreachable"`). @@ -223,7 +242,11 @@ profile, а не как численную гарантию. Полная миг ### `engine.muddiness(hex): number` -`muddiness` — оценка «грязи» цвета `hex` в диапазоне `[0, 1]` (Закон Грязи). +`muddiness` возвращает замороженный числовой выход исторической формулы. Это +`experimental compatibility proxy`: legacy-имя сохранено для совместимости, но +значение не является валидированным на наблюдателях человеческим вердиктом +clean/dirty и не должно использоваться как production decision. Сам диапазон +`[0, 1]` — свойство формулы, а не шкала человеческого восприятия. --- @@ -276,7 +299,7 @@ interface AdaptThemeOptions { sustainMs?: number; // минимальное время удержания нарушения (по умолчанию 120) dwellMs?: number; // минимальный интервал между пересчётами (по умолчанию 250) easeMs?: number; // длительность перехода (по умолчанию 280; уменьшается при reduced-motion) - strict?: boolean; // держать минимальный контраст на каждом кадре перехода (по умолчанию false) + strict?: boolean; // legacy characterized clamp; не universal floor certificate (по умолчанию false) reducedMotion?: boolean; // переопределить системную настройку } @@ -289,20 +312,37 @@ interface AdaptController { } ``` +`strict` сохраняет прежнее runtime-поведение, но не является доказательством +минимального или проходящего состояния на каждом кадре: путь +Oklab→gamut clip→sRGB8 немонотонен. Finite hard-floor-safe replacement ведётся в +#287; до него этот флаг следует считать compatibility characterization. + Управляйте через `start()` (внутренний rAF-цикл) или вызывайте `tick()` из собственного цикла. Смена темы применяется мгновенно — это осознанное намерение, а не дрейф. +Перед применением результата контроллер проверяет provenance как stable-, так и +legacy-ветвей Glow. Нетипизированный внешний mock с невозможным сочетанием +profile/guarantee/diagnostic/status отклоняется `TypeError`; fallback-цвет из +такого объекта не применяется. --- ### `effectiveBackground(element, options?): string` -Возвращает непрозрачный `#RRGGBB` — цвет, который наблюдатель реально видит за содержимым `element`. Обходит цепочку предков и выполняет альфа-композитинг каждого `background-color` до получения непрозрачного результата, поверх `fallback` (по умолчанию белый). +Возвращает непрозрачный reference-`#RRGGBB`, вычисленный для поддерживаемого +подмножества DOM `background-color`. Это не browser pixel capture и не +сертификат цвета, который реально видит наблюдатель. Helper обходит цепочку +предков и композитит распознанные слои поверх `fallback` (по умолчанию белый). ```ts const bg = effectiveBackground(panel); // например "#0F1014" const bg2 = effectiveBackground(panel, { fallback: "#101012" }); ``` -**Честное ограничение:** работает только с сплошными и полупрозрачными `background-color` — не с `background-image`, градиентами, размытым фоном или видео. Для таких поверхностей передайте фон явно. +**Честное ограничение:** работает только с поддерживаемыми сплошными и +полупрозрачными `background-color`; неподдерживаемый CSS, неполная прозрачная +цепочка, `background-image`, градиент, blur и video не дают полного наблюдения. +Текущий compatibility helper ещё может отбросить неподдерживаемый слой или +использовать fallback; для correctness передайте фон явно. Typed `Unknown` +replacement принадлежит #283. Дополнительно экспортируются вспомогательные функции для работы со слоями: `parseCssColor`, `compositeOver`, `compositeStackToHex`, `toHex` и `oklabLerp` (перцептуально равномерная интерполяция между двумя hex-значениями — траектория цвета при переходе). @@ -323,10 +363,12 @@ const bg2 = effectiveBackground(panel, { fallback: "#101012" }); именованные экспорты и допускают tree-shaking, но их размер также следует мерить сборкой, а не описывать приблизительно. -Активно проверенная release-матрица: Node 24.14 с прямой передачей wasm-байтов -в `init({ module_or_path })` и headless Chrome из CI. ESM/URL-обёртка генерируется -wasm-pack, но совместимость с конкретной версией Vite, webpack, Next или другим -браузером не заявляется без отдельного smoke-гейта. +Release packer закреплён на Node 24.14/npm 11.9; это не consumer floor. +Публичный контракт `Node >=22.11.0` независимо прогоняется отдельным CI job. Browser- +матрица использует прямую передачу wasm-байтов в `init({ module_or_path })` и +headless Chrome из CI. ESM/URL-обёртка генерируется wasm-pack, но совместимость +с конкретной версией Vite, webpack, Next или другим браузером не заявляется без +отдельного smoke-гейта. ### Для аудиторов цепочки поставки diff --git a/packages/colors/adapt-theme.d.ts b/packages/colors/adapt-theme.d.ts index c344d7e5..35db47b0 100644 --- a/packages/colors/adapt-theme.d.ts +++ b/packages/colors/adapt-theme.d.ts @@ -36,10 +36,10 @@ export interface AdaptThemeOptions { /** Crossfade duration in ms. Default `280` (capped to a short fade under reduced motion). */ easeMs?: number; /** - * Hold each role's WCAG legal floor every frame of the ease: an interpolated - * colour is only shown while it still clears the role's `legalFloor` against - * the live background (for text on animated content / `prefers-contrast`). - * Default `false` (the free ease — brief surplus dips, destination always legal). + * Enable the legacy characterized per-frame clamp. The current + * Oklab→clip→sRGB8 path is not globally monotone, so this option is not a + * universal floor/least-blend certificate; #287 owns the finite replacement. + * Default `false`. */ strict?: boolean; /** Override reduced-motion detection (default reads `matchMedia`). */ diff --git a/packages/colors/adapt-theme.js b/packages/colors/adapt-theme.js index fef4c0f9..eeb8a8a3 100644 --- a/packages/colors/adapt-theme.js +++ b/packages/colors/adapt-theme.js @@ -39,13 +39,11 @@ // usually sit on a substrate, so a brief dip of the aesthetic *surplus* // during the ease is imperceptible while the freshly-solved destination is // always legal. -// * Strict (`strict: true`): the WCAG legal floor is HELD every frame. For -// text directly on animated content or under `prefers-contrast`, an -// intermediate colour is only shown while it still clears the role's -// `legalFloor` against the live background; a role whose eased intermediate -// would dip below its floor is advanced (monotonically) to the least blend -// that stays legal — never below the line, never a backwards flicker. Roles -// with no legal floor (decorative) ease freely either way. +// * Strict (`strict: true`): requests the legacy per-frame floor clamp. The +// current Oklab→clip→sRGB8 path is characterized, but is not globally +// monotone and therefore does not prove a least/legal blend for every frame; +// #287 owns the finite hard-floor-safe replacement. Roles with no declared +// floor (decorative) ease freely either way. import { applyTheme } from "./apply-theme.js"; import { @@ -67,9 +65,9 @@ function easeOut(t) { return 1 - u * u * u; } -/** WCAG 2.1 relative luminance of `#RRGGBB` — a faithful transcription of the - * normative definition (0.03928 split, 2.4 exponent), so the strict floor-clamp - * agrees byte-for-byte with the core's `legalFloor` semantics. */ +/** Relative luminance of `#RRGGBB` in the frozen original WCAG 2.1 (2018) + * profile (0.03928 split, 2.4 exponent), so the strict floor-clamp agrees + * byte-for-byte with the core's versioned `legalFloor` semantics. */ function relativeLuminanceHex(hex) { const rgb = parseCssColor(hex) ?? [0, 0, 0, 1]; const lin = (c) => { @@ -136,8 +134,9 @@ function segLum(seg, t) { * @param {number} [options.sustainMs=120] breach must persist this long * @param {number} [options.dwellMs=250] minimum between re-solves * @param {number} [options.easeMs=280] crossfade duration - * @param {boolean} [options.strict=false] hold each role's WCAG legal floor - * every frame of the ease (for text on animated content / `prefers-contrast`) + * @param {boolean} [options.strict=false] enable the legacy characterized + * per-frame clamp; the current non-monotone interpolation path is not a + * universal floor certificate (replacement tracked by #287) * @param {boolean} [options.reducedMotion] override; default reads matchMedia * @param {() => number} [options.now] clock (default performance.now/Date.now) * @param {*} [options.win=globalThis] @@ -259,7 +258,23 @@ export function adaptTheme(element, options) { return { breached, worstIdx }; }; - const stableGlowsFrom = (result) => { + const stableVarKeys = (role) => [ + role.cssVar, + `${role.cssVar}-core`, + `${role.cssVar}-alpha`, + ]; + + const hasDeterminateGlowEnvelope = (result, role, emittedKeys) => + role.compositeProfile === "encoded-srgb8-screen-v1" && + role.compositeGuarantee === "bit-exact" && + role.layerRecipeProfile === "cam16-jprime-oklab-cusp-v1" && + role.appearanceDiagnosticProfile === "cam16-ucs-jprime-li2017-v1" && + role.constraintLayer === "halo" && + typeof role.coreHex === "string" && + typeof role.haloHex === "string" && + emittedKeys.every((emittedKey) => typeof result.vars?.[emittedKey] === "string"); + + const validatedStableGlowsFrom = (result) => { const out = []; for (const [key, role] of Object.entries(result.roles ?? {})) { if (!role) continue; @@ -270,31 +285,45 @@ export function adaptTheme(element, options) { } continue; } + const expectedCssVar = `--lab-${key}`; + if (role.cssVar !== expectedCssVar) { + throw new TypeError( + `adaptTheme: Glow '${key}' has non-canonical cssVar`, + ); + } + const emittedKeys = stableVarKeys(role); if (role.decisionProfile === "legacy-platform-dependent-v1") { if (role.kind !== "glow") { throw new TypeError( `adaptTheme: legacy Glow '${key}' cannot be Indeterminate`, ); } + const reached = + role.targetStatus === "legacy-reached" && role.degraded === false; + const unreachable = + role.targetStatus === "legacy-unreachable" && role.degraded === true; + if ( + !hasDeterminateGlowEnvelope(result, role, emittedKeys) || + role.decisionGuarantee?.kind !== "legacy-platform-dependent-v1" || + role.selectionDiagnosticProfile !== "cam16-ucs-jprime-li2017-v1" || + (!reached && !unreachable) + ) { + throw new TypeError( + `adaptTheme: legacy Glow '${key}' lacks lawful legacy evidence`, + ); + } continue; } if (role.decisionProfile !== "stable-v1") { throw new TypeError(`adaptTheme: Glow '${key}' lacks an explicit known decisionProfile`); } - if (typeof role.cssVar !== "string") { - throw new TypeError(`adaptTheme: stable Glow '${key}' lacks cssVar`); - } - const emittedKeys = [role.cssVar, `${role.cssVar}-core`, `${role.cssVar}-alpha`]; if (role.kind === "glow") { if ( role.decisionGuarantee?.kind !== "bit-exact" || - role.compositeProfile !== "encoded-srgb8-screen-v1" || - role.compositeGuarantee !== "bit-exact" || - role.diagnosticProfile !== null || - role.constraintLayer !== "halo" || - role.targetStatus !== "unreachable" || - typeof role.haloHex !== "string" || - emittedKeys.some((emittedKey) => typeof result.vars?.[emittedKey] !== "string") + !hasDeterminateGlowEnvelope(result, role, emittedKeys) || + role.selectionDiagnosticProfile !== null || + role.targetStatus !== "exact-noop-unreachable" || + role.degraded !== true ) { throw new TypeError(`adaptTheme: stable Glow '${key}' lacks BitExact evidence`); } @@ -331,6 +360,7 @@ export function adaptTheme(element, options) { // Adopt one already-resolved set as the current colours (no ease). const adoptResolved = (result, now) => { + const nextStableGlows = validatedStableGlowsFrom(result); // Carry the FULL canonical var set (color + translucent) so no reachable // role is dropped by a subsequent apply; only color roles feed the ease. baseVars = result.vars && typeof result.vars === "object" ? result.vars : {}; @@ -343,7 +373,7 @@ export function adaptTheme(element, options) { hex: r.hex, legalFloor: typeof r.legalFloor === "number" ? r.legalFloor : null, })); - stableGlows = stableGlowsFrom(result); + stableGlows = nextStableGlows; fgsCache = roles.map((r) => r.hex); // The adopt may change the var/role KEY SET — force the next write through // `applyTheme`'s full clear-then-write instead of the mid-ease diff path. @@ -406,12 +436,6 @@ export function adaptTheme(element, options) { // oklch form, translucent roles their tint+alpha. const applyRolesDirect = () => applyHexes({}); - const stableVarKeys = (role) => [ - role.cssVar, - `${role.cssVar}-core`, - `${role.cssVar}-alpha`, - ]; - /** * Recheck the background-dependent stable Glow decision class. This is not a * contrast surplus and therefore never passes through sustain/dwell/easing. @@ -443,7 +467,7 @@ export function adaptTheme(element, options) { // stable Glow satellites are adopted; color/translucent roles remain under // the existing adaptive contrast controller and do not snap. const fresh = colors.resolveTheme(samples[0], theme); - const freshStable = stableGlowsFrom(fresh); + const freshStable = validatedStableGlowsFrom(fresh); const freshByKey = new Map(freshStable.map((role) => [role.key, role])); const nextVars = { ...baseVars }; for (const previous of stableGlows) { @@ -512,15 +536,12 @@ export function adaptTheme(element, options) { if (easing.size === 0) applyRolesDirect(); }; - // Strict mode: the least blend in [e, 1] whose interpolated colour clears - // `floor` against EVERY background sample in `bgLums`. The destination (`to`, - // blend 1) is a freshly-solved legal colour, so it anchors the search; we - // bisect toward it from the natural ease value `e`. Returns `e` unchanged when - // the eased colour is already legal everywhere (the common case — no - // intervention). The returned blend is always floor-legal against the worst - // sample, except in the unavoidable case where even `to` is illegal against a - // sample that drifted further this frame — then it returns 1 (the most-legal - // colour we have) and the recheck loop re-solves. + // Legacy strict clamp: fixed-step bisection from the natural ease value `e` + // toward the freshly-solved destination. Oklab→clip→sRGB8 legality is not + // globally monotone, so this is a characterized compatibility selector, not + // a proof of the least or universally legal blend. #287 owns the finite + // hard-floor-safe replacement. If even `to` fails after background drift, the + // selector returns 1 and the recheck loop requests another solve. const floorBlend = (seg, e, bgLums, floor) => { const legalAt = (blend) => { const lum = segLum(seg, blend); @@ -587,8 +608,9 @@ export function adaptTheme(element, options) { // so on a frame where they drift favourably it could return a *lower* // blend than last frame — a backwards step toward the old colour, the // precise jarring reversal this mode exists to avoid. `held` clamps that - // out: the colour progresses monotonically from→to and never below the - // legal line on any sample. + // out: the scalar blend parameter never retreats. This latch alone is + // not a proof that the quantized colour stays above every floor; #287 + // owns that finite-path guarantee. blend = Math.max(floorBlend(seg, e, bgLums, r.legalFloor), seg.held); seg.held = blend; } @@ -640,8 +662,9 @@ export function adaptTheme(element, options) { const now = nowArg ?? clock(); const samples = readSamples(); const key = samples.join("|"); - // Advance any in-flight ease first (against the live samples, so strict mode - // holds the legal floor every frame as the backdrop keeps drifting under it). + // Advance any in-flight ease first against the live samples. Legacy strict + // mode applies its characterized clamp here; it is not a universal + // per-frame floor certificate (#287). if (easing.size > 0) stepEase(now, samples, key); // Steady state: a static backdrop with no in-flight ease and no pending diff --git a/packages/colors/effective-bg.d.ts b/packages/colors/effective-bg.d.ts index b964728a..fdbe8a27 100644 --- a/packages/colors/effective-bg.d.ts +++ b/packages/colors/effective-bg.d.ts @@ -41,8 +41,9 @@ export declare function oklabLerp(from: string, to: string, t: number): string; export declare function compositeStackToHex(layersFrontToBack: Rgba[], opaqueBase: Rgba): string; /** - * The opaque effective background `#RRGGBB` visible behind `element`'s content, - * by walking ancestors and alpha-compositing their `background-color`s. + * Legacy opaque reference-background estimate for the supported + * `background-color` subset. This is not a browser pixel observation; use an + * explicit sampled background when correctness depends on unsupported layers. */ export declare function effectiveBackground( element: unknown, diff --git a/packages/colors/effective-bg.js b/packages/colors/effective-bg.js index efee31dd..3fff132b 100644 --- a/packages/colors/effective-bg.js +++ b/packages/colors/effective-bg.js @@ -3,13 +3,14 @@ // `labcolors` resolves roles against a *solid* background. A real UI surface is // often translucent (a panel at `rgba(…, .8)` over its parents) or has no // background of its own (inheriting whatever is behind it). To resolve such a -// surface honestly you need the **effective** background: the opaque colour a -// viewer actually sees behind the element's own content. +// surface you need a background observation. This legacy helper produces a +// reference estimate for the subset it understands; it is not a browser pixel +// capture or a claim about what a viewer actually sees. // // This module computes it by walking the ancestor chain and **alpha-compositing** // each element's `background-color` (front-to-back) until the stack is opaque, -// over an opaque fallback (white by default). It is the bridge from the DOM's -// layered, translucent reality to the single solid hex the WASM core consumes. +// over an opaque fallback (white by default), yielding one solid reference hex +// the WASM core can consume. // // HONEST LIMIT: this composites solid/translucent `background-color` layers only. // It does NOT sample `background-image`s, gradients, blurred backdrops, video, or @@ -24,9 +25,10 @@ // and `oklch()` (the engine's OWN emission form since 0.4.0, and what a browser // serialises `background-color` back to for an oklch-painted surface). Other // modern forms — `lab()`, `lch()`, `color(srgb …)`, `color-mix()`, `hsl()`, -// named colours beyond `transparent` — are NOT parsed and return `null` (a -// dropped layer). If a surface's background is authored in one of those, pass the -// effective background explicitly via the `background` option. +// named colours beyond `transparent` — are NOT parsed and currently become a +// dropped layer for compatibility. That is not safe evidence: pass the +// background explicitly. Issue #283 owns the typed `Unknown` replacement for +// unsupported syntax, missing opaque bases and incomplete traversal. /** @typedef {[number, number, number, number]} Rgba r,g,b in 0..255, a in 0..1 */ @@ -183,9 +185,10 @@ function oklchAlpha(tok) { export function compositeOver(top, bottom) { const at = top[3]; const ab = bottom[3]; - // Affine-форма математически равна expanded source-over, но монотонна по - // top alpha в binary64. Expanded `at + ab*(1-at)` и две цветовые ветви могут - // дать PASS→FAIL→PASS на соседних ULP и разрушить конечный alpha lower-bound. + // Affine-форма математически равна expanded source-over и фиксирует + // объявленный byte-scale binary64 operation order. Это numerical profile, + // не утверждение глобальной монотонности: округление может менять локальный + // порядок соседних значений, а legacy WCAG EOTF дополнительно имеет seam. const a = ab + at * (1 - ab); if (a === 0) return [0, 0, 0, 0]; const c = (i) => { @@ -332,8 +335,8 @@ export function parseCssColorCached(css) { return hit; } -/** WCAG 2.1 relative luminance of r,g,b channels (0..255) — the normative - * 0.03928 / 12.92 / 2.4 constants, matching `adapt-theme`'s floor semantics. */ +/** Relative luminance of r,g,b channels (0..255) in the frozen original WCAG + * 2.1 (2018) profile: 0.03928 / 12.92 / 2.4, matching `adapt-theme`. */ function wcagLumChannels(r, g, b) { const lin = (c) => { const s = c / 255; @@ -408,10 +411,10 @@ export function lerpPairHex(pair, t) { return toHex([linearToSrgb(lin[0]) * 255, linearToSrgb(lin[1]) * 255, linearToSrgb(lin[2]) * 255]); } -/** WCAG relative luminance of `lerpPairHex(pair, t)` WITHOUT the `#RRGGBB` - * round-trip: channels are quantised to the exact bytes `toHex` would emit, - * then fed to the normative formula — so strict-mode bisection over `t` is - * ~20 flops instead of serialise + re-parse, at identical results. */ +/** Relative luminance of `lerpPairHex(pair, t)` in the frozen legacy WCAG 2.1 + * (2018) profile, WITHOUT the `#RRGGBB` round-trip: channels are quantised to + * the exact bytes `toHex` would emit, then fed to the same profile formula. + * This preserves value parity; it does not prove bisection monotonicity. */ export function lerpPairLuminance(pair, t) { if (t <= 0) return wcagLumChannels(pair.aBytes[0], pair.aBytes[1], pair.aBytes[2]); if (t >= 1) return wcagLumChannels(pair.bBytes[0], pair.bBytes[1], pair.bBytes[2]); @@ -448,13 +451,13 @@ export function compositeStackToHex(layersFrontToBack, opaqueBase) { } /** - * The opaque effective background `#RRGGBB` visible behind `element`'s own - * content, by walking ancestors and compositing their `background-color`s. + * Legacy opaque reference-background estimate for the supported + * `background-color` subset. This is not a browser pixel observation. * * Walks from `element` upward, collecting each `background-color` layer, and * stops at the first fully-opaque layer (which becomes the base). If the chain - * reaches the root without an opaque layer, `fallback` (default white) is the - * base — matching how a browser shows the page's default canvas. + * reaches the root without an opaque layer, `fallback` (default white) is used + * as an explicit compatibility assumption; it is not evidence of the canvas. * * Pure and injectable: pass `getStyle` and `parentOf` to test without a DOM; in * the browser they default to `getComputedStyle` and `el.parentElement`. diff --git a/packages/colors/index.d.ts b/packages/colors/index.d.ts index 6b35c40f..60aafaee 100644 --- a/packages/colors/index.d.ts +++ b/packages/colors/index.d.ts @@ -20,12 +20,30 @@ export type { UnreachableRole, TranslucentRole, GlowDecisionProfileV1, + GlowLayerRecipeProfileV1, + GlowDiagnosticProfileV1, + GlowTargetStatusV1, NumericalIndeterminacyV1, + GlowBitExactDecisionGuaranteeV1, + GlowLegacyDecisionGuaranteeV1, GlowDecisionGuaranteeV1, + GlowDeterminateRoleBase, + GlowStableExactNoopRole, + GlowLegacyReachedRole, + GlowLegacyUnreachableRole, GlowDeterminateRole, GlowIndeterminateRoleBase, GlowIndeterminateRole, GlowRole, + MaterialAlphaGuaranteeBaseV1, + MaterialBisectionBracketGuaranteeV1, + MaterialTransparentEndpointGuaranteeV1, + MaterialOpaqueEndpointGuaranteeV1, + MaterialAlphaGuaranteeV1, + MaterialRoleBase, + MaterialSatisfiedTransparentRole, + MaterialSatisfiedBracketRole, + MaterialDegradedOpaqueRole, MaterialRole, RoleResult, ThemeAnchors, diff --git a/packages/colors/package-lock.json b/packages/colors/package-lock.json index 385f0676..07491794 100644 --- a/packages/colors/package-lock.json +++ b/packages/colors/package-lock.json @@ -9,7 +9,7 @@ "version": "0.10.0", "license": "MIT", "engines": { - "node": ">=24.14.0" + "node": ">=22.11.0" }, "devDependencies": { "typescript": "^5.5.0" diff --git a/packages/colors/package.json b/packages/colors/package.json index 98905e38..06876103 100644 --- a/packages/colors/package.json +++ b/packages/colors/package.json @@ -82,6 +82,6 @@ "labpics" ], "engines": { - "node": ">=24.14.0" + "node": ">=22.11.0" } } diff --git a/packages/colors/smoke.consumer.ts b/packages/colors/smoke.consumer.ts index b415543e..3cfc325c 100644 --- a/packages/colors/smoke.consumer.ts +++ b/packages/colors/smoke.consumer.ts @@ -10,7 +10,155 @@ import init, { effectiveBackground, oklabLerp, } from "./index.js"; -import type { ResolvedTheme, RoleResult, ThemeName } from "./index.js"; +import type { + GlowDiagnosticProfileV1, + GlowDeterminateRole, + GlowDeterminateRoleBase, + GlowLayerRecipeProfileV1, + GlowTargetStatusV1, + MaterialRole, + MaterialRoleBase, + ResolvedTheme, + RoleResult, + ThemeName, +} from "./index.js"; + +declare const glowDeterminateCommon: GlowDeterminateRoleBase; +const requireGlowDeterminate = (_role: GlowDeterminateRole): void => {}; + +requireGlowDeterminate({ + ...glowDeterminateCommon, + decisionProfile: "stable-v1", + decisionGuarantee: { kind: "bit-exact" }, + selectionDiagnosticProfile: null, + targetStatus: "exact-noop-unreachable", + degraded: true, +}); +requireGlowDeterminate({ + ...glowDeterminateCommon, + decisionProfile: "legacy-platform-dependent-v1", + decisionGuarantee: { kind: "legacy-platform-dependent-v1" }, + selectionDiagnosticProfile: "cam16-ucs-jprime-li2017-v1", + targetStatus: "legacy-reached", + degraded: false, +}); +requireGlowDeterminate({ + ...glowDeterminateCommon, + decisionProfile: "legacy-platform-dependent-v1", + decisionGuarantee: { kind: "legacy-platform-dependent-v1" }, + selectionDiagnosticProfile: "cam16-ucs-jprime-li2017-v1", + targetStatus: "legacy-unreachable", + degraded: true, +}); + +// @ts-expect-error stable-профиль не может нести legacy-status. +requireGlowDeterminate({ ...glowDeterminateCommon, decisionProfile: "stable-v1", decisionGuarantee: { kind: "bit-exact" }, selectionDiagnosticProfile: null, targetStatus: "legacy-reached", degraded: false }); +// @ts-expect-error legacy-профиль не может рекламировать bit-exact decision. +requireGlowDeterminate({ ...glowDeterminateCommon, decisionProfile: "legacy-platform-dependent-v1", decisionGuarantee: { kind: "bit-exact" }, selectionDiagnosticProfile: "cam16-ucs-jprime-li2017-v1", targetStatus: "legacy-reached", degraded: false }); +// @ts-expect-error exact no-op не выполняет CAM16 selection diagnostic. +requireGlowDeterminate({ ...glowDeterminateCommon, decisionProfile: "stable-v1", decisionGuarantee: { kind: "bit-exact" }, selectionDiagnosticProfile: "cam16-ucs-jprime-li2017-v1", targetStatus: "exact-noop-unreachable", degraded: true }); +// @ts-expect-error reached-ветвь не является degraded. +requireGlowDeterminate({ ...glowDeterminateCommon, decisionProfile: "legacy-platform-dependent-v1", decisionGuarantee: { kind: "legacy-platform-dependent-v1" }, selectionDiagnosticProfile: "cam16-ucs-jprime-li2017-v1", targetStatus: "legacy-reached", degraded: true }); +// @ts-expect-error Glow API не поддерживает outward decision guarantee. +requireGlowDeterminate({ ...glowDeterminateCommon, decisionProfile: "stable-v1", decisionGuarantee: { kind: "outward-interval-v1", lower: 0, upper: 1 }, selectionDiagnosticProfile: null, targetStatus: "exact-noop-unreachable", degraded: true }); + +function narrowGlowDeterminate(role: GlowDeterminateRole): void { + if (role.decisionProfile === "stable-v1") { + const status: "exact-noop-unreachable" = role.targetStatus; + const guarantee: "bit-exact" = role.decisionGuarantee.kind; + const diagnostic: null = role.selectionDiagnosticProfile; + const degraded: true = role.degraded; + void status; + void guarantee; + void diagnostic; + void degraded; + } else if (role.targetStatus === "legacy-reached") { + const degraded: false = role.degraded; + void degraded; + } else { + const status: "legacy-unreachable" = role.targetStatus; + const degraded: true = role.degraded; + void status; + void degraded; + } +} + +declare const materialCommon: MaterialRoleBase; +const requireMaterial = (_role: MaterialRole): void => {}; + +requireMaterial({ + ...materialCommon, + alpha: 0, + alphaGuarantee: { + kind: "transparent-endpoint-characterized-v1", + numericalProfile: "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1", + }, + alphaStatus: "satisfied", + guaranteed: true, +}); +requireMaterial({ + ...materialCommon, + alpha: 0.5, + alphaGuarantee: { + kind: "bisection-bracket-characterized-v1", + iterations: 60, + lowerAlpha: 0.49, + upperAlpha: 0.5, + numericalProfile: "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1", + }, + alphaStatus: "satisfied", + guaranteed: true, +}); +requireMaterial({ + ...materialCommon, + alpha: 1, + alphaGuarantee: { + kind: "opaque-endpoint-characterized-v1", + numericalProfile: "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1", + }, + alphaStatus: "degraded", + guaranteed: false, +}); + +// @ts-expect-error degraded несовместим с transparent endpoint. +requireMaterial({ ...materialCommon, alpha: 0, alphaGuarantee: { kind: "transparent-endpoint-characterized-v1", numericalProfile: "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1" }, alphaStatus: "degraded", guaranteed: false }); +// @ts-expect-error satisfied несовместим с opaque endpoint. +requireMaterial({ ...materialCommon, alpha: 1, alphaGuarantee: { kind: "opaque-endpoint-characterized-v1", numericalProfile: "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1" }, alphaStatus: "satisfied", guaranteed: true }); +// @ts-expect-error compatibility boolean выводится из typed status. +requireMaterial({ ...materialCommon, alpha: 0.5, alphaGuarantee: { kind: "bisection-bracket-characterized-v1", iterations: 60, lowerAlpha: 0.49, upperAlpha: 0.5, numericalProfile: "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1" }, alphaStatus: "satisfied", guaranteed: false }); +// @ts-expect-error transparent endpoint конструктивно имеет alpha 0. +requireMaterial({ ...materialCommon, alpha: 0.25, alphaGuarantee: { kind: "transparent-endpoint-characterized-v1", numericalProfile: "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1" }, alphaStatus: "satisfied", guaranteed: true }); +// @ts-expect-error degraded opaque endpoint конструктивно имеет alpha 1. +requireMaterial({ ...materialCommon, alpha: 0.9, alphaGuarantee: { kind: "opaque-endpoint-characterized-v1", numericalProfile: "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1" }, alphaStatus: "degraded", guaranteed: false }); + +function narrowMaterial(role: MaterialRole): void { + if (role.alphaStatus === "degraded") { + const alpha: 1 = role.alpha; + const guarantee: "opaque-endpoint-characterized-v1" = role.alphaGuarantee.kind; + const guaranteed: false = role.guaranteed; + void alpha; + void guarantee; + void guaranteed; + return; + } + + const status: "satisfied" = role.alphaStatus; + const guaranteed: true = role.guaranteed; + void status; + void guaranteed; + if (role.alphaGuarantee.kind === "transparent-endpoint-characterized-v1") { + const guarantee: "transparent-endpoint-characterized-v1" = role.alphaGuarantee.kind; + void guarantee; + } else { + const guarantee: "bisection-bracket-characterized-v1" = role.alphaGuarantee.kind; + const upper: number = role.alphaGuarantee.upperAlpha; + void guarantee; + void upper; + } +} + +void narrowGlowDeterminate; +void narrowMaterial; async function consume(clientConfigJson: string): Promise { await init(); @@ -48,9 +196,16 @@ async function consume(clientConfigJson: string): Promise { const glow: RoleResult = result.roles["fx-glow-brand"]; if (glow.kind === "glow") { - const diagnosticProfile: "cam16-ucs-jprime-li2017-v1" | null = - glow.diagnosticProfile; - void diagnosticProfile; + const layerRecipeProfile: GlowLayerRecipeProfileV1 = glow.layerRecipeProfile; + const appearanceDiagnosticProfile: GlowDiagnosticProfileV1 = + glow.appearanceDiagnosticProfile; + const selectionDiagnosticProfile: GlowDiagnosticProfileV1 | null = + glow.selectionDiagnosticProfile; + const targetStatus: GlowTargetStatusV1 = glow.targetStatus; + void layerRecipeProfile; + void appearanceDiagnosticProfile; + void selectionDiagnosticProfile; + void targetStatus; } applyTheme(document.documentElement, result); diff --git a/packages/colors/test/adapt-theme.test.mjs b/packages/colors/test/adapt-theme.test.mjs index 400646d0..8f5a0a22 100644 --- a/packages/colors/test/adapt-theme.test.mjs +++ b/packages/colors/test/adapt-theme.test.mjs @@ -80,8 +80,8 @@ const floorRole = (hex, lc, legalFloor) => ({ }, }); -// WCAG 2.1 contrast ratio between two #RRGGBB — the exact normative transcription -// (0.03928 split, 2.4 exponent), the yardstick the strict clamp must respect. +// Contrast ratio between two #RRGGBB in the frozen original WCAG 2.1 (2018) +// profile (0.03928 split, 2.4 exponent), matching the versioned core contract. function wcagContrast(fg, bg) { const lum = (hex) => { const n = parseInt(hex.slice(1), 16); @@ -145,14 +145,18 @@ test("stable Glow class changes re-resolve and clear/restore satellites synchron fx: { kind: "glow", cssVar, + coreHex: "#D8CEFF", haloHex: "#C0B2FA", decisionProfile: "stable-v1", decisionGuarantee: { kind: "bit-exact" }, compositeProfile: "encoded-srgb8-screen-v1", compositeGuarantee: "bit-exact", - diagnosticProfile: null, + layerRecipeProfile: "cam16-jprime-oklab-cusp-v1", + appearanceDiagnosticProfile: "cam16-ucs-jprime-li2017-v1", + selectionDiagnosticProfile: null, constraintLayer: "halo", - targetStatus: "unreachable", + targetStatus: "exact-noop-unreachable", + degraded: true, }, }, }; @@ -228,14 +232,18 @@ test("stable Glow rejects a missing exact recheck capability or malformed eviden fx: { kind: "glow", cssVar: "--lab-fx", + coreHex: "#D8CEFF", haloHex: "#C0B2FA", decisionProfile: "stable-v1", decisionGuarantee: { kind: "bit-exact" }, compositeProfile: "encoded-srgb8-screen-v1", compositeGuarantee: "bit-exact", - diagnosticProfile: null, + layerRecipeProfile: "cam16-jprime-oklab-cusp-v1", + appearanceDiagnosticProfile: "cam16-ucs-jprime-li2017-v1", + selectionDiagnosticProfile: null, constraintLayer: "halo", - targetStatus: "unreachable", + targetStatus: "exact-noop-unreachable", + degraded: true, }, }, }; @@ -268,14 +276,68 @@ test("stable Glow rejects a missing exact recheck capability or malformed eviden /lacks BitExact evidence/u, ); - const falseDiagnostic = structuredClone(stable); - falseDiagnostic.roles.fx.diagnosticProfile = "cam16-ucs-jprime-li2017-v1"; + const falseSelectionDiagnostic = structuredClone(stable); + falseSelectionDiagnostic.roles.fx.selectionDiagnosticProfile = + "cam16-ucs-jprime-li2017-v1"; assert.throws( () => adaptTheme(fakeElement(), { ...options, colors: { - resolveTheme: () => falseDiagnostic, + resolveTheme: () => falseSelectionDiagnostic, + recheckContrast: () => [], + isStableGlowPointNoop: () => true, + }, + }), + /lacks BitExact evidence/u, + ); + + for (const field of [ + "layerRecipeProfile", + "appearanceDiagnosticProfile", + "selectionDiagnosticProfile", + ]) { + const missingProfile = structuredClone(stable); + delete missingProfile.roles.fx[field]; + assert.throws( + () => + adaptTheme(fakeElement(), { + ...options, + colors: { + resolveTheme: () => missingProfile, + recheckContrast: () => [], + isStableGlowPointNoop: () => true, + }, + }), + /lacks BitExact evidence/u, + `${field} must not collapse at the adaptive boundary`, + ); + } + + const genericStatus = structuredClone(stable); + genericStatus.roles.fx.targetStatus = "unreachable"; + assert.throws( + () => + adaptTheme(fakeElement(), { + ...options, + colors: { + resolveTheme: () => genericStatus, + recheckContrast: () => [], + isStableGlowPointNoop: () => true, + }, + }), + /lacks BitExact evidence/u, + "generic unreachable must not erase exact provenance", + ); + + const falseDegraded = structuredClone(stable); + falseDegraded.roles.fx.degraded = false; + assert.throws( + () => + adaptTheme(fakeElement(), { + ...options, + colors: { + resolveTheme: () => falseDegraded, recheckContrast: () => [], isStableGlowPointNoop: () => true, }, @@ -326,6 +388,29 @@ test("stable Glow rejects a missing exact recheck capability or malformed eviden /lacks lawful Indeterminate evidence/u, ); + const mismatchedCssVar = structuredClone(wrongSite); + mismatchedCssVar.roles.fx.numericalSiteId = "glow-target-or-maximum-v1"; + mismatchedCssVar.roles.fx.cssVar = "--lab-other"; + mismatchedCssVar.vars = { "--lab-fx": "forbidden-fallback" }; + const mismatchTarget = fakeElement(); + assert.throws( + () => + adaptTheme(mismatchTarget, { + ...options, + colors: { + resolveTheme: () => mismatchedCssVar, + recheckContrast: () => [], + isStableGlowPointNoop: () => false, + }, + }), + /non-canonical cssVar/u, + ); + assert.equal( + mismatchTarget.props.size, + 0, + "mismatched cssVar must not smuggle a fallback var into the target", + ); + for (const decisionProfile of [undefined, "stable-v2", "stable-v1-typo"]) { const unknownProfile = structuredClone(stable); if (decisionProfile === undefined) delete unknownProfile.roles.fx.decisionProfile; @@ -361,6 +446,98 @@ test("stable Glow rejects a missing exact recheck capability or malformed eviden ); }); +test("legacy Glow validates correlated provenance before adopting external results", () => { + const cssVar = "--lab-fx"; + const legacyReached = { + vars: { + [cssVar]: "oklch(70% 0.1 280)", + [`${cssVar}-core`]: "oklch(80% 0.1 280)", + [`${cssVar}-alpha`]: "0.5", + }, + roles: { + fx: { + kind: "glow", + cssVar, + coreHex: "#D8CEFF", + haloHex: "#C0B2FA", + alpha: 0.5, + alphaCss: "0.5", + compositeProfile: "encoded-srgb8-screen-v1", + compositeGuarantee: "bit-exact", + layerRecipeProfile: "cam16-jprime-oklab-cusp-v1", + appearanceDiagnosticProfile: "cam16-ucs-jprime-li2017-v1", + selectionDiagnosticProfile: "cam16-ucs-jprime-li2017-v1", + decisionProfile: "legacy-platform-dependent-v1", + decisionGuarantee: { kind: "legacy-platform-dependent-v1" }, + constraintLayer: "halo", + targetDj: 2.3006, + targetStatus: "legacy-reached", + haloCompositeHex: "#C0B2FA", + haloAchievedDj: 2.4, + coreCompositeHex: "#D8CEFF", + coreAchievedDj: 3.1, + achievedDj: 2.4, + degraded: false, + css: "oklch(70% 0.1 280)", + }, + }, + }; + const options = { + theme: "light", + background: () => "#FFFFFF", + win: {}, + }; + const colorsFor = (result) => ({ + resolveTheme: () => result, + recheckContrast: () => [], + }); + + const accepted = fakeElement(); + const controller = adaptTheme(accepted, { + ...options, + colors: colorsFor(legacyReached), + }); + assert.equal(accepted.props.get(cssVar), legacyReached.vars[cssVar]); + controller.stop(); + + const legacyUnreachable = structuredClone(legacyReached); + legacyUnreachable.roles.fx.targetStatus = "legacy-unreachable"; + legacyUnreachable.roles.fx.degraded = true; + const acceptedUnreachable = fakeElement(); + adaptTheme(acceptedUnreachable, { + ...options, + colors: colorsFor(legacyUnreachable), + }).stop(); + assert.equal( + acceptedUnreachable.props.get(cssVar), + legacyUnreachable.vars[cssVar], + ); + + const malformedCases = [ + ["bit-exact legacy decision", (role) => (role.decisionGuarantee = { kind: "bit-exact" })], + ["null legacy selection", (role) => (role.selectionDiagnosticProfile = null)], + ["generic exact status", (role) => (role.targetStatus = "exact-noop-unreachable")], + ["reached marked degraded", (role) => (role.degraded = true)], + ["unknown recipe", (role) => (role.layerRecipeProfile = "future-recipe-v2")], + ["unknown composite", (role) => (role.compositeProfile = "browser-screen-v1")], + ]; + for (const [name, mutate] of malformedCases) { + const malformed = structuredClone(legacyReached); + mutate(malformed.roles.fx); + const target = fakeElement(); + assert.throws( + () => + adaptTheme(target, { + ...options, + colors: colorsFor(malformed), + }), + /legacy Glow 'fx' lacks lawful legacy evidence/u, + name, + ); + assert.equal(target.props.size, 0, `${name}: invalid result must not be applied`); + } +}); + test("stable Glow invalidation does not snap an in-flight color ease", () => { const el = fakeElement(); let bg = "#FFFFFF"; @@ -393,14 +570,18 @@ test("stable Glow invalidation does not snap an in-flight color ease", () => { ? { kind: "glow", cssVar, + coreHex: "#D8CEFF", haloHex: "#C0B2FA", decisionProfile: "stable-v1", decisionGuarantee: { kind: "bit-exact" }, compositeProfile: "encoded-srgb8-screen-v1", compositeGuarantee: "bit-exact", - diagnosticProfile: null, + layerRecipeProfile: "cam16-jprime-oklab-cusp-v1", + appearanceDiagnosticProfile: "cam16-ucs-jprime-li2017-v1", + selectionDiagnosticProfile: null, constraintLayer: "halo", - targetStatus: "unreachable", + targetStatus: "exact-noop-unreachable", + degraded: true, } : { kind: "glow-indeterminate", @@ -484,14 +665,18 @@ test("a color re-solve cannot reintroduce stable Glow vars unsafe for another sa ? { kind: "glow", cssVar, + coreHex: "#D8CEFF", haloHex: "#C0B2FA", decisionProfile: "stable-v1", decisionGuarantee: { kind: "bit-exact" }, compositeProfile: "encoded-srgb8-screen-v1", compositeGuarantee: "bit-exact", - diagnosticProfile: null, + layerRecipeProfile: "cam16-jprime-oklab-cusp-v1", + appearanceDiagnosticProfile: "cam16-ucs-jprime-li2017-v1", + selectionDiagnosticProfile: null, constraintLayer: "halo", - targetStatus: "unreachable", + targetStatus: "exact-noop-unreachable", + degraded: true, } : { kind: "glow-indeterminate", @@ -681,7 +866,7 @@ function easeContrasts({ strict }) { return { h, out }; } -test("strict floor-clamp holds the WCAG floor every frame of the ease", () => { +test("legacy strict clamp holds the floor on the canonical polarity-crossing fixture", () => { const { h, out } = easeContrasts({ strict: true }); assert.equal(h.colors.resolveCount(), 2); for (const c of out) { @@ -691,7 +876,7 @@ test("strict floor-clamp holds the WCAG floor every frame of the ease", () => { assert.equal(h.el.props.get("--lab-label-primary"), "#FFFFFF"); }); -test("strict floor-clamp is monotone (contrast never steps backwards)", () => { +test("legacy strict fixture has non-regressing sampled contrast", () => { const { out } = easeContrasts({ strict: true }); for (let i = 1; i < out.length; i++) { assert.ok( @@ -701,9 +886,9 @@ test("strict floor-clamp is monotone (contrast never steps backwards)", () => { } }); -test("strict floor-clamp never reverses the colour when the background drifts favourably", () => { - // The structural guarantee: the displayed colour only ever advances from→to, - // never retreats — even when a favourably-drifting (darkening) background would +test("held latch never reverses the scalar blend when the background drifts favourably", () => { + // The structural guarantee is only on the scalar blend parameter: it advances + // from→to even when a favourably-drifting (darkening) background would // let the stateless floor solver pick a LOWER blend frame to frame. Without the // `held` latch the grey value would step back down; with it, it is monotone. const h = harness({ strict: true, easeMs: 400 }); // long ease so bg drift dominates @@ -732,7 +917,7 @@ test("strict floor-clamp never reverses the colour when the background drifts fa } }); -test("the default (free) ease dips below the floor mid-transition — what strict fixes", () => { +test("the default ease dips below the floor on the canonical strict comparison fixture", () => { const { out } = easeContrasts({ strict: false }); assert.ok( out.some((c) => c < 4.5), @@ -809,7 +994,7 @@ test("initial apply re-solves against the worst sample of a varying backdrop", ( assert.equal(colors.lastResolveBg(), "#202020"); }); -test("strict worst-case: the floor is held against the hardest sample every frame", () => { +test("legacy strict clamp holds the canonical hardest-sample fixture", () => { let samples = ["#0A0A0A"]; // passing at construction const h = harness({ strict: true, easeMs: 100, background: () => samples }); h.colors.setResolve(floorRole("#FFFFFF", 100, 4.5)); diff --git a/packages/colors/test/chain-invariants.test.mjs b/packages/colors/test/chain-invariants.test.mjs index 1610f6ec..0b16a31f 100644 --- a/packages/colors/test/chain-invariants.test.mjs +++ b/packages/colors/test/chain-invariants.test.mjs @@ -237,15 +237,17 @@ test("glow roles emit halo primary + -core/-alpha satellites, all well-formed", assert.equal(role.constraintLayer, "halo"); assert.equal(role.compositeProfile, "encoded-srgb8-screen-v1"); assert.equal(role.compositeGuarantee, "bit-exact"); - assert.equal(role.diagnosticProfile, "cam16-ucs-jprime-li2017-v1"); + assert.equal(role.layerRecipeProfile, "cam16-jprime-oklab-cusp-v1"); + assert.equal(role.appearanceDiagnosticProfile, "cam16-ucs-jprime-li2017-v1"); + assert.equal(role.selectionDiagnosticProfile, "cam16-ucs-jprime-li2017-v1"); assert.equal(role.decisionProfile, "legacy-platform-dependent-v1"); assert.deepEqual(role.decisionGuarantee, { kind: "legacy-platform-dependent-v1", }); - assert.ok(["reached", "unreachable"].includes(role.targetStatus)); - assert.equal(role.degraded, role.targetStatus === "unreachable"); + assert.ok(["legacy-reached", "legacy-unreachable"].includes(role.targetStatus)); + assert.equal(role.degraded, role.targetStatus === "legacy-unreachable"); assert.ok(Object.is(role.achievedDj, role.haloAchievedDj)); - if (role.targetStatus === "reached") { + if (role.targetStatus === "legacy-reached") { assert.ok(role.haloAchievedDj >= role.targetDj); } else { assert.ok(role.haloAchievedDj < role.targetDj); diff --git a/packages/colors/test/hotpath-parity.test.mjs b/packages/colors/test/hotpath-parity.test.mjs index be1ebad1..afd4db55 100644 --- a/packages/colors/test/hotpath-parity.test.mjs +++ b/packages/colors/test/hotpath-parity.test.mjs @@ -177,8 +177,9 @@ const hasCompiled = typeof ebg.lerpPairHex === "function" && typeof ebg.lerpPairLuminance === "function"; -/** Reference WCAG relative luminance — a verbatim copy of the string path the - * runtime used pre-optimisation (normative 0.03928/12.92/2.4 constants). */ +/** Reference relative luminance — a verbatim copy of the string path the + * runtime used pre-optimisation (frozen WCAG 2.1 2018 profile: + * 0.03928/12.92/2.4). */ function refLuminance(css) { const rgb = parseCssColor(css) ?? [0, 0, 0, 1]; const lin = (c) => { diff --git a/packages/colors/test/release-contract.test.mjs b/packages/colors/test/release-contract.test.mjs index a4643a19..1a7bcfea 100644 --- a/packages/colors/test/release-contract.test.mjs +++ b/packages/colors/test/release-contract.test.mjs @@ -38,6 +38,29 @@ function workflowNodeScript(workflow, stepName) { .join("\n"); } +function assertCheckoutCredentialsAreEphemeral(workflow, name) { + const lines = workflow.split("\n"); + const checkouts = lines + .map((line, index) => ({ line, index })) + .filter(({ line }) => line.trimStart().startsWith("- uses: actions/checkout@")); + assert.ok(checkouts.length > 0, `${name} has no checkout steps`); + + for (const { line, index } of checkouts) { + const indentation = line.length - line.trimStart().length; + const step = []; + for (let cursor = index + 1; cursor < lines.length; cursor++) { + const candidate = lines[cursor]; + const candidateIndentation = candidate.length - candidate.trimStart().length; + if (candidate.trim().length > 0 && candidateIndentation <= indentation) break; + step.push(candidate); + } + assert.ok( + step.some((candidate) => candidate.trim() === "persist-credentials: false"), + `${name} checkout at line ${index + 1} persists the workflow token`, + ); + } +} + test("breaking release metadata is one explicit 0.2.0/0.10.0 contract", () => { const workspace = read("Cargo.toml"); assert.match(workspace, /\[workspace\.package\][\s\S]*\nversion = "0\.2\.0"/); @@ -53,6 +76,8 @@ test("breaking release metadata is one explicit 0.2.0/0.10.0 contract", () => { assert.equal(packageJson.packageManager, "npm@11.9.0"); assert.equal(packageLock.version, "0.10.0"); assert.equal(packageLock.packages[""].version, "0.10.0"); + assert.equal(packageJson.engines.node, ">=22.11.0"); + assert.equal(packageLock.packages[""].engines.node, ">=22.11.0"); assert.equal( packageJson.scripts.prepack, "npm run build && node ../../scripts/prepare-npm-package.mjs", @@ -100,6 +125,11 @@ test("MSRV and packaged Rust crate gates are executable CI contracts", () => { const ci = read(".github", "workflows", "ci.yml"); assert.match(ci, /^\s*MSRV_TOOLCHAIN: 1\.85\.0$/m); assert.match(ci, /^\s*NODE_TOOLCHAIN: 24\.14\.0$/m); + assert.match(ci, /^\s*NODE_CONSUMER_FLOOR: 22\.11\.0$/m); + assert.match( + ci, + /^\s*node-consumer-floor:[\s\S]*needs: wasm[\s\S]*node-version: \$\{\{ env\.NODE_CONSUMER_FLOOR \}\}[\s\S]*actions\/download-artifact@[0-9a-f]{40}[\s\S]*--runtime-smoke/m, + ); assert.match(ci, /^\s*CHROME_FOR_TESTING_VERSION: 150\.0\.7871\.115$/m); assert.match( ci, @@ -169,12 +199,29 @@ test("MSRV and packaged Rust crate gates are executable CI contracts", () => { ci, /wasm-pack test --headless --chrome --chromedriver "\$CHROMEDRIVER_PATH" crates\/labcolors-wasm --locked/, ); + assertCheckoutCredentialsAreEphemeral(ci, "CI"); + assertCheckoutCredentialsAreEphemeral( + read(".github", "workflows", "native-conformance.yml"), + "native conformance", + ); + assertCheckoutCredentialsAreEphemeral( + read(".github", "workflows", "mutation.yml"), + "scheduled mutation", + ); + assertCheckoutCredentialsAreEphemeral( + read(".github", "workflows", "publish.yml"), + "publish", + ); }); test("publish accepts only canonical exact-SHA workflow runs and their immutable CI artifact", () => { const publish = read(".github", "workflows", "publish.yml"); assert.match(publish, /^\s*NODE_TOOLCHAIN: "24\.14\.0"$/m); assert.match(publish, /^\s*NPM_TOOLCHAIN: "11\.9\.0"$/m); + assert.match( + publish, + /^concurrency:\n group: npm-publish\n cancel-in-progress: false$/m, + ); assert.match(publish, /permissions:\n contents: read\n actions: read\n/); assert.doesNotMatch(publish, /^\s*checks:/m); assert.doesNotMatch(publish, /id-token:/); @@ -194,6 +241,7 @@ test("publish accepts only canonical exact-SHA workflow runs and their immutable assert.doesNotMatch(publish, /^\s*cache: npm$/m); const requiredChecks = [ + "Node 22 consumer floor", "MSRV workspace check", "clippy + rustfmt", "cargo doc (intra-doc links)", @@ -239,7 +287,7 @@ test("publish accepts only canonical exact-SHA workflow runs and their immutable assert.match(publish, /packedPackage\.name !== "@labpics\/colors"/); assert.match( publish, - /run: npm publish --ignore-scripts "\$\{\{ steps\.verified-artifact\.outputs\.tarball \}\}"/, + /TARBALL_PATH: \$\{\{ steps\.verified-artifact\.outputs\.tarball \}\}[\s\S]*run: npm publish --ignore-scripts "\$TARBALL_PATH"/, ); assert.doesNotMatch(publish, /wasm-pack|npm ci|release:verify|actions\/upload-artifact|npm pack/); @@ -265,6 +313,7 @@ test("canonical-run guard executes against workflow-scoped runs and jobs", () => "name: guard — canonical exact-SHA workflow runs and their own jobs", ); const requiredCiJobs = [ + "Node 22 consumer floor", "MSRV workspace check", "clippy + rustfmt", "cargo doc (intra-doc links)", @@ -461,6 +510,11 @@ test("release verifier performs an independent byte-for-byte reproduction pass", assert.match(verifier, /reproducibility/); assert.match(verifier, /byteIdentical: true/); assert.match(verifier, /const npmVersion = lockedNpmVersion\(packageJson\)/); + assert.match(verifier, /consumerRuntime: \{/); + assert.match(verifier, /verifiedFloor: consumerNodeFloor/); + assert.match(verifier, /canonicalGate: "Node 22 consumer floor"/); + assert.match(verifier, /buildToolchain: \{/); + assert.match(verifier, /node: process\.versions\.node/); assert.match(verifier, /GITHUB_OUTPUT/); assert.match(verifier, /familySetSha256: sha256\(Buffer\.concat\(familyBuffers\)\)/); assert.match(verifier, /sha256: sha256\(familyBuffers\[index\]\)/); @@ -482,6 +536,7 @@ test("published build metadata binds source, conformance, and WASM inputs", () = assert.ok(packageJson.files.includes("build-metadata.json")); const prepare = read("scripts", "prepare-npm-package.mjs"); + assert.match(prepare, /import \{ workspaceVersion \} from "\.\/cargo-workspace\.mjs";/); assert.match(prepare, /const BUILD_METADATA = resolve\(PACKAGE_DIR, "build-metadata\.json"\)/); assert.match(prepare, /sourceSha/); assert.ok( @@ -500,6 +555,7 @@ test("published build metadata binds source, conformance, and WASM inputs", () = assert.match(prepare, /wasm: \{ bytes: wasm\.length, sha256: sha256\(wasm\) \}/); const verifier = read("scripts", "verify-package-release.mjs"); + assert.match(verifier, /import \{ workspaceVersion \} from "\.\/cargo-workspace\.mjs";/); assert.match(verifier, /function validateBuildMetadata/); assert.match(verifier, /isDeepStrictEqual\(metadata, expected\)/); assert.match(verifier, /require\.resolve\("@labpics\/colors\/build-metadata\.json"\)/); diff --git a/packages/colors/test/release-provenance.test.mjs b/packages/colors/test/release-provenance.test.mjs index 67f5d7a2..e0fbe77e 100644 --- a/packages/colors/test/release-provenance.test.mjs +++ b/packages/colors/test/release-provenance.test.mjs @@ -2,8 +2,10 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { copyFileSync, + existsSync, mkdirSync, mkdtempSync, + readFileSync, rmSync, writeFileSync, } from "node:fs"; @@ -31,6 +33,10 @@ test("prepack source guard is clean-tree and exact-SHA executable evidence", asy join(root, "scripts", "prepare-npm-package.mjs"), join(scripts, "prepare-npm-package.mjs"), ); + copyFileSync( + join(root, "scripts", "cargo-workspace.mjs"), + join(scripts, "cargo-workspace.mjs"), + ); try { command("git", ["init", "--quiet"], fixture); command("git", ["config", "user.name", "Lab Colors release test"], fixture); @@ -67,6 +73,76 @@ test("prepack source guard is clean-tree and exact-SHA executable evidence", asy } }); +test("workspace version parser is bounded to the workspace.package table", async () => { + const { workspaceVersion } = await import( + pathToFileURL(join(root, "scripts", "cargo-workspace.mjs")) + ); + assert.equal( + workspaceVersion(` +[package] +version = "9.9.9" + +[workspace.package] +edition = "2024" +version = "0.2.0" + +[workspace.metadata.release] +version = "8.8.8" +`), + "0.2.0", + ); + assert.equal( + workspaceVersion("[workspace.package]\r\nedition = \"2024\"\r\nversion = \"0.3.0\"\r\n"), + "0.3.0", + ); + assert.throws( + () => workspaceVersion(` +[workspace.package] +edition = "2024" + +[workspace.metadata.release] +version = "8.8.8" +`), + /workspace core version is absent/u, + ); + assert.throws( + () => workspaceVersion(` +[workspace.package] +edition = "2024" + +[[example]] +version = "7.7.7" +`), + /workspace core version is absent/u, + ); +}); + +test("release command wrapper terminates a child that hangs past its bound", async () => { + const { command, RELEASE_COMMAND_TIMEOUT_MS } = await import( + pathToFileURL(join(root, "scripts", "verify-package-release.mjs")) + ); + assert.equal(RELEASE_COMMAND_TIMEOUT_MS, 5 * 60 * 1_000); + const fixture = mkdtempSync(join(tmpdir(), "labcolors-release-timeout-")); + try { + const marker = join(fixture, "started"); + const hangingFixture = + `require("node:fs").writeFileSync(${JSON.stringify(marker)}, "started\\n");` + + "setInterval(() => {}, 60_000);"; + const startedAt = Date.now(); + assert.throws( + // Node's test runner executes files concurrently. Give the minimum Node + // 22 consumer enough startup headroom under a saturated CI worker; the + // marker still proves the child ran before the bounded timeout fired. + () => command(process.execPath, ["-e", hangingFixture], fixture, { timeoutMs: 2_000 }), + /timed out after 2000 ms/u, + ); + assert.ok(existsSync(marker), "anti-vacuum: the hanging fixture never started"); + assert.ok(Date.now() - startedAt < 8_000, "bounded child exceeded the test deadline"); + } finally { + rmSync(fixture, { recursive: true, force: true }); + } +}); + test("build metadata exact validator rejects one-field tampering", async () => { const { validateBuildMetadata } = await import( pathToFileURL(join(root, "scripts", "verify-package-release.mjs")) @@ -105,3 +181,24 @@ test("build metadata exact validator rejects one-field tampering", async () => { /does not exactly bind the release inputs/, ); }); + +test("clean-consumer smoke закрепляет коррелированный Glow/Material wire", () => { + const verifier = readFileSync( + join(root, "scripts", "verify-package-release.mjs"), + "utf8", + ); + for (const literal of [ + "layerRecipeProfile", + "appearanceDiagnosticProfile", + "selectionDiagnosticProfile", + "exact-noop-unreachable", + "legacy-reached", + "legacy-unreachable", + "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1", + ]) { + assert.match(verifier, new RegExp(literal, "u"), `missing ${literal}`); + } + assert.doesNotMatch(verifier, /\bdiagnosticProfile\b/u); + assert.doesNotMatch(verifier, /targetStatus === "(?:reached|unreachable)"/u); + assert.doesNotMatch(verifier, /outward-interval-v1/u); +}); diff --git a/packages/colors/test/runtime.test.mjs b/packages/colors/test/runtime.test.mjs index 647af2bb..ad5b621f 100644 --- a/packages/colors/test/runtime.test.mjs +++ b/packages/colors/test/runtime.test.mjs @@ -43,7 +43,8 @@ test("compositeOver is true source-over alpha", () => { assert.equal(toHex(compositeOver([0, 5, 5, 0.1], [5, 5, 5, 1])), "#050505"); // Expanded-форма на этих соседних alpha давала 208→207→208. Affine- - // reference обязан быть монотонным, иначе first-passing alpha недоказуема. + // reference фиксирует другой, объявленный operation order; этот конкретный + // seam характеризуется без утверждения глобальной монотонности. const centre = 0.812992125984252; const predecessor = centre - Number.EPSILON / 2; const successor = centre + Number.EPSILON / 2; @@ -53,6 +54,73 @@ test("compositeOver is true source-over alpha", () => { assert.ok(seam[0] <= seam[1] && seam[1] <= seam[2], String(seam)); }); +test("material alpha rechecks in the declared byte-scale affine legacy-WCAG profile", () => { + // Independent consumer oracle for the Rust material solver. The product's + // versioned profile freezes the original WCAG 2.1 (2018) 0.03928 split; it is + // not the current W3C formula and JS `**` is not a cross-runtime powf proof. + // These fixtures verify the emitted alpha in the declared JS operation order. + const srgbToLinear = (byte) => { + const encoded = byte / 255; + return encoded <= 0.03928 + ? encoded / 12.92 + : ((encoded + 0.055) / 1.055) ** 2.4; + }; + const contrastAgainstWhite = (rgb) => { + const luminance = + 0.2126 * srgbToLinear(rgb[0]) + + 0.7152 * srgbToLinear(rgb[1]) + + 0.0722 * srgbToLinear(rgb[2]); + return 1.05 / (luminance + 0.05); + }; + + for (const { tint, oldAlpha, selectedAlpha, floor } of [ + { + tint: [2, 2, 2], + oldAlpha: 0.41945837958353488, + selectedAlpha: 0.41945837958353827, + floor: 3, + }, + { + tint: [0, 0, 0], + oldAlpha: 0.65080978737170625, + selectedAlpha: 0.65080978737171102, + floor: 7, + }, + ]) { + const bottom = [255, 255, 255, 1]; + const oldContrast = contrastAgainstWhite(compositeOver([...tint, oldAlpha], bottom)); + const selectedContrast = contrastAgainstWhite( + compositeOver([...tint, selectedAlpha], bottom), + ); + assert.ok(oldContrast < floor, `old ${oldContrast} must miss ${floor}`); + assert.ok(selectedContrast >= floor, `selected ${selectedContrast} must hold ${floor}`); + } + + // Endpoint-only evaluation used to accept the old alpha exactly at the + // requested floor, while an admissible interior backdrop crossed the + // downward 0.03928 EOTF seam and missed it. The selected alpha comes from the + // conservative channel envelope that includes both seam sides. + const seamFloor = 19.7963; + const oldSeamAlpha = 0.96071066801335769; + const selectedSeamAlpha = 0.96072042755466869; + const interiorByte = 0.9997624803942831 * 255; + const interior = [interiorByte, interiorByte, interiorByte, 1]; + const oldInteriorContrast = contrastAgainstWhite( + compositeOver([0, 0, 0, oldSeamAlpha], interior), + ); + const selectedInteriorContrast = contrastAgainstWhite( + compositeOver([0, 0, 0, selectedSeamAlpha], interior), + ); + assert.ok( + oldInteriorContrast < seamFloor, + `endpoint-only alpha ${oldInteriorContrast} must expose the interior seam miss`, + ); + assert.ok( + selectedInteriorContrast >= seamFloor, + `enveloped alpha ${selectedInteriorContrast} must hold ${seamFloor}`, + ); +}); + test("toHex rounds and clamps", () => { assert.equal(toHex([255, 255, 255, 1]), "#FFFFFF"); assert.equal(toHex([0, 0, 0, 1]), "#000000"); diff --git a/packages/colors/test/wasm-boundary.golden.json b/packages/colors/test/wasm-boundary.golden.json index 63840816..d12dbc3b 100644 --- a/packages/colors/test/wasm-boundary.golden.json +++ b/packages/colors/test/wasm-boundary.golden.json @@ -1 +1 @@ -{"_meta":{"purpose":"Байт-точный golden границы JS↔WASM: recheckContrast/_recheckContrastMulti и полный snapshot resolveTheme.vars.","regenerated":"2026-07-10","regen_reason":"ADR-0004: point-glow решается по конечным encoded-sRGB8 screen-state; alpha эмитируется кратчайшей decimal-записью без повторного округления.","drift_scope":"Относительно origin/main изменены ровно 24 листа resolveVars: только --lab-fx-glow-{brand,danger,warning,neutral}-alpha на 6 фонах. Ни один base/core, label, border, fill, background или другой var не изменён.","recheck_unchanged":"Секция recheck: 2752/2752 числовых значений, то есть 1376/1376 пар (lc, wcag), байт-идентичны origin/main; ни одно значение recheckContrast не изменено.","numeric_provenance":{"glow_alpha":"Старый fixed-48 bisection + 4-decimal alpha заменён перебором конечных sRGB8-state, внутренней alpha выбранного интервала и shortest-roundtrip binary64 CSS.","scope_check":"Фактический структурный diff вычислен по каждому листу resolveVars; 4 alpha-ключа × 6 записей = 24."},"wcag_floors_not_weakened":"Glow — декоративная роль без WCAG-пола. Отдельно подтверждено: все 1376 пар (2752 числовых значения) recheck неизменны, поэтому измеренный контраст ни одной проверяемой роли не ослаблен.","isolated_verification_required":"Регенерация принимается только после независимой проверки scope, recheck-инварианта и полного Rust/npm/browser gate.","history":{"previous_regenerated":"2026-07-10","previous_regen_reason":"Глава #64 (ADR-0003): ось читаемости активирована в домене Ys — реактивный recheck (measure_contrast/recheck_against) и solver-вывод считаются в Ys. recheck flat и resolveVars регенерированы против пересобранного движка; inputs (theme/bg/fgs) сохранены. semver-major по ADR.","previous_drift_scope":"resolveVars: exactly 24 leaves changed, all --lab-fx-glow-{brand,danger,warning,neutral}-core (glow-centre colour, background-independent → 4 distinct values). No other var moved. glow base/alpha, labels, borders, fills, backgrounds all byte-identical.","previous_recheck_unchanged":"Исторический regen также не менял recheck; прежний текст ошибочно называл 2752 числа парами. Фактический объём: 1376 пар (lc, wcag).","previous_numeric_provenance":{"glow-brand-core":"oklch(78.84894% 0.062240 265.472) -> oklch(78.76611% 0.108476 257.256): chroma rises to the blue gamut wall at the functional lightness (fixed-fraction under-saturated it); L ±0.08pp, hue shifts to the cusp.","glow-danger-core":"oklch(81.62208% 0.103545 27.533) -> oklch(81.77103% 0.102764 29.025): near-identical (red already sat near its wall).","glow-warning-core":"oklch(88.80612% 0.081204 68.866) -> oklch(88.88181% 0.082838 68.777): near-identical.","glow-neutral-core":"oklch(99.97226% 0.001314 106.423) -> oklch(100.00000% 0.000000 89.876): neutral hue is achromatic → gamut wall chroma is 0 (was fixed-fraction residue noise); hue_vanished flag fires honestly for the achromatic sentiment."},"previous_wave1_scope":"resolveVars: 69 info-* leaves changed across 6 theme×bg entries. Zero non-info drift (scope-checked). recheck untouched."}},"recheck":[{"theme":"light","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#00030E","#0057BB","#037BFF","#78AFFF","#150000","#BE0005","#FF070E","#FF9A8C","#1B0D00","#A06300","#D28300","#FFAA3A","#000B01","#007C2D","#00A73F","#00D452","#020013","#4D00F9","#6D6EFF","#9FA9FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.59876563358553,20.60103636973117,79.91552226990729,6.816302360996974,63.95198968935827,3.97647793086433,41.89234722606997,2.2400949113548063,105.32470288110206,20.35108757719671,78.97384099389187,6.579727828852003,63.90360859563009,3.970705427078051,37.83221395673913,2.048194921340976,103.87315757816087,19.018778265876662,70.62528347734393,4.90716956443272,53.97808064225952,3.0056474129326713,34.232350404909745,1.8982213760304238,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.76444562516849,20.751302844974063,83.48658341134008,7.825697912025839,63.67798680778349,3.9439477616440737,40.86459333479343,2.1890112231357013]},{"theme":"light","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E6F0FF","#60A1FF","#0074F3","#004CA4","#FFF4F2","#FF8273","#F40009","#A80004","#FFF7EE","#F79C00","#C97E00","#8F5800","#C2FFC9","#00C84D","#00A03C","#006E26","#EEF0FF","#9099FF","#6662FF","#4300DB"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-97.97941453819666,18.27263411531394,-53.55180732861014,8.039306941172763,-34.54692380627581,4.792369408161928,-17.418806999279397,2.56984376276454,-102.41236835704379,19.474434420712562,-56.92703132210305,8.692613650917824,-34.93266406694178,4.850563950189369,-18.308962222075785,2.6667193472488013,-103.5613239139708,19.790915044333452,-61.97487030298502,9.71021814570805,-44.901407836051874,6.467843223430967,-25.783665577412222,3.5638173443979557,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.707239087904505,6.093571182387199,-23.370874926227668,3.2583602660985775,-98.9883689300502,18.54346384276384,-54.32153113615954,8.18635152260471,-34.304902287955215,4.756029101219059,-14.435475219756622,2.261552965854495]},{"theme":"light","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00163A","#003272","#004494","#040000","#3A0000","#760002","#990003","#0B0400","#261300","#512F00","#845100","#000200","#001D05","#004114","#006322","#000002","#160059","#2B0097","#3B00C5"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.89312592554248,4.521969910173767,28.754774555141132,3.1292028976206594,22.090354756391577,2.35851620111828,40.23106479600237,5.289901483963394,36.823385380889306,4.5064610721321205,27.786796188195932,3.002060282138028,21.02583740042693,2.2570951143110336,39.64148780057383,5.154352713354357,36.89828467875601,4.523118840482662,27.981450040156208,3.027209986332121,13.567318350507929,1.6836851754208233,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.3306726916817,5.312553092992316,36.84988795298528,4.512349526507044,30.916517468784825,3.4313626667845316,25.033605894158644,2.6690076358452903]},{"theme":"light","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#E7F1FF","#73ADFF","#2985FF","#0060CB","#FFF5F4","#FF9687","#FF4336","#CD0006","#FFF8F0","#FFA62C","#DC8A00","#AC6B00","#C7FFCC","#00D251","#00B043","#008631","#EFF1FF","#9CA6FF","#767AFF","#532BFF"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.32090210121694,9.956824779623638,-48.0046310941281,4.945145596338778,-30.285195552724065,3.1849116126306156,-14.342034884792934,1.9106934427040745,-91.71639880150975,10.602849175762582,-51.97724928251108,5.385053682831725,-31.570923815646545,3.301015045785777,-14.82740446463819,1.94471301206827,-92.86717481243348,10.774663089384916,-55.64998948221256,5.805759082702303,-40.41406674606042,4.149700467279057,-23.7537806706175,2.6249801387527425,-87.99158651504085,10.054343074329001,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-88.33220875070053,10.104016013712561,-48.80525652513833,5.032519885671211,-30.915189659062865,3.2415642790980215,-11.323318014385267,1.7063261265170042]},{"theme":"light","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000103","#001536","#003374","#0058BC","#070000","#360000","#730002","#BE0005","#0F0500","#241200","#4F2F00","#A16400","#000400","#001B05","#003F13","#007C2D","#000005","#140055","#3300AC","#4E00FA"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.65687576920386,5.198408498908002,36.59714090951085,4.504660010775197,27.83078380746869,3.029349552004155,13.31049770996219,1.6744770124926043,39.57933742278427,5.181007308853048,36.66186584356392,4.51899176883356,27.77510172821011,3.0221297438766705,12.721590912296357,1.6379804580418844,38.8564182220264,5.016999835671664,36.579572970042136,4.500775701626332,27.65113132417139,3.006116231224861,0,1.205042331050678,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.737197591996,5.216381929144518,36.59037871155487,4.5031645842195545,27.68585404738298,3.010592990868115,17.038043883395044,1.933101474636693]},{"theme":"light","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000104","#00204D","#003B83","#005CC4","#080000","#4C0001","#830002","#C70006","#0F0600","#331C00","#5B3600","#A76800","#000400","#002809","#004917","#00822F","#000005","#1F0075","#3A00C3","#511AFF"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.87966672163739,5.884401548408216,38.30767260595269,4.50632138120243,29.16726657041677,3.0308664461202253,15.823096179531001,1.7789458759332724,43.782795788734404,5.8597651770218455,38.402450836988166,4.527103297945568,29.016037258963053,3.011424325654164,14.9864556348382,1.7254955647131072,42.99821792375369,5.657954629703501,38.41622407485322,4.5301350068404425,28.93769973409828,3.0014083479801514,0,1.28054929135364,43.62397616937584,5.819206103341516,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.970091731453365,5.907320242919889,38.28628402517786,4.501651045763603,28.99214635836292,3.0083657301901288,19.11088367642911,2.0125842767555064]},{"theme":"light","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E6F0FF","#64A4FF","#0077F9","#0050AC","#FFF4F3","#FF8777","#F9000A","#B00005","#FFF7EF","#FA9E00","#CD8000","#955C00","#C3FFC9","#00CA4E","#00A33D","#007329","#EEF0FF","#939CFF","#6967FF","#4600E6"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.3221346483494,16.53807546575917,-53.23458352949761,7.5085332653696355,-34.299151794570456,4.531377660721796,-17.34040000814869,2.4827755446650244,-100.79897840003042,17.636700891614694,-56.73966900010428,8.131153426700068,-34.4661942600266,4.554628420782135,-18.26813498707333,2.5778416349364046,-101.94670279716274,17.922901715066683,-61.50032492399525,9.010497025774457,-44.48245067627942,6.049054371037485,-25.983086445749187,3.447217107352778,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-97.33108904020293,16.783196253636362,-53.9780650339559,7.638815670143083,-34.15649094558268,4.511565332868746,-14.178333309149869,2.174938899863266]},{"theme":"light","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#00030D","#0057B9","#0079FD","#74ADFF","#150000","#BC0005","#FD000A","#FF9788","#1B0D00","#9F6200","#CB7F00","#FFA730","#000B01","#007B2C","#00A63E","#00D251","#020013","#4C00F7","#6C6CFF","#9DA6FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.48497819102282,19.361251289328656,75.9364891028185,6.438321694919453,60.636932359482174,3.8278153397206016,38.76699274290234,2.152541489746932,101.19913515757631,19.11640864166299,75.28055256994783,6.281220843840182,60.46094873358925,3.807428346232839,34.70184973776556,1.9658801332164664,99.74758985463511,17.864929125608796,66.9127104243937,4.673002507305443,52.13428535623209,3.0001216660302674,31.16352525448906,1.8227359456035261,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.421867335088024,3.0235758695027473,33.32486172802245,1.9082022515525083,101.63887790164272,19.492343272893066,79.67393134344084,7.442688338927282,60.17031912503092,3.7741072518393395,37.967753096619255,2.113772626279229]},{"theme":"light","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E6F1FF","#67A5FF","#0079FD","#0053B2","#FFF5F3","#FF8A7A","#FD000A","#B60005","#FFF7EF","#FC9F00","#D08200","#9A5F00","#C4FFCA","#00CB4E","#00A53E","#00772A","#EEF1FF","#949EFF","#6B6BFF","#4900EE"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-94.97224440690107,14.900628115594163,-51.99577035710509,6.808135431331698,-33.457001357615106,4.175350120738554,-16.755883343489252,2.3335896778079332,-99.44736446714371,15.886233758087014,-55.869159717403925,7.428487313030341,-33.63398343987836,4.1977071628099685,-17.709012021143003,2.4235530906296177,-100.15616825242344,16.04465607216319,-60.39258141645385,8.181935837052826,-43.77686148964125,5.57032618168592,-25.67861999448382,3.2497240951569792,-95.39392806862337,14.992417327878547,-58.246405974587134,7.820609198173194,-41.3596549106396,5.227261702394524,-23.16505433719768,2.9752806858720673,-95.97861011098902,15.120061263203354,-52.9765437482588,6.963016139185695,-33.57839540296167,4.190679013723035,-13.485138200601865,2.040069034387161]},{"theme":"light","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EBF3FF","#D5E6FF","#92BFFF","#4291FF","#FFF8F7","#FFDDD8","#FFA597","#FF6253","#FFFAF5","#FFE0BE","#FFAA39","#E79100","#D0FFD4","#7DFF93","#00DA54","#00B947","#F1F4FF","#DFE4FF","#AEB8FF","#8087FF"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.43421936609613,5.100687932363686,-64.08586870464498,4.502412953317652,-41.11294430990105,3.017135229848019,-18.888486914529935,1.8251241221961068,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-40.940708789235714,3.0069340578770496,-21.122834074579163,1.9331720160112165,-77.70409895709017,5.493642615376888,-64.34147182620212,4.520282197700913,-40.849183913847796,3.0015190917179395,-28.18413111501551,2.292444746213784,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-41.2870784084647,3.027463512408998,-26.013778316259767,2.1791858289450103,-73.64084437140687,5.1896286230945075,-64.42349258712335,4.5260223420377566,-41.02780633656069,3.0120908713744514,-19.464187683477977,1.8526993919522168]},{"theme":"light","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#E8F1FF","#79B0FF","#368BFF","#0065D6","#FFF6F4","#FF9B8D","#FF5446","#D80007","#FFF8F1","#FFAB3F","#E18E00","#B47000","#C8FFCD","#00D552","#00B545","#008D34","#EFF1FF","#A0AAFF","#7B81FF","#573FFF"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-89.77514075602531,11.177468536043195,-51.89752791553575,5.733156384550998,-34.996215684604906,3.8114980409485155,-18.92394323577027,2.320895932067837,-94.48648764812391,11.954651961610136,-56.02375667309079,6.252768618847473,-36.81873931987068,4.002056146272223,-19.591172380553846,2.375625103310219,-95.24016771148194,12.080915286992608,-59.85145182180837,6.751641124414219,-44.82390104707481,4.887508677146329,-28.683175933367217,3.184369254481423,-90.45906258458693,11.288985892577246,-57.499124974387314,6.443154991463433,-42.83495757582209,4.660276446547384,-26.099142978345885,2.9428672820648862,-90.66219607331998,11.322193559065964,-52.9772743130274,5.867283737103295,-35.76790053375578,3.8916727925995533,-16.863337707955058,2.1560372721390864]},{"theme":"light","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#000209","#00479B","#0065D5","#3289FF","#100000","#9F0004","#D70007","#FF4F41","#170A00","#7A4A00","#9F6200","#DF8C00","#000801","#006322","#00822F","#00B344","#01000D","#3E00CE","#573EFF","#797EFF"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.22533906008685,12.552007092013115,56.99670466281028,5.352680994694784,44.43662180236468,3.3253647200326193,29.122033451491582,2.0641033245604916,76.01516726706762,12.436514137733148,55.97147975339624,5.133613537650522,43.91010121645754,3.2654000659595037,27.461071919951625,1.9716236837030474,74.81611736383091,11.770648316029503,52.816193854903005,4.528105409994289,41.51373908005361,3.011071841176318,19.679891349505557,1.6112041351931181,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.42233250009274,3.001939806510638,21.536768417458934,1.687644517337887,76.3513076320574,12.62087594492386,59.99704797613187,6.063965240585196,46.62694808670478,3.592268428661898,28.6381619838115,2.036506690369533]},{"theme":"light","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#DA7A00","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.42046624663428,20.43859328774187,80.31672424485741,6.9206271592755195,64.53598724808649,4.047139863973963,42.92136358171458,2.2930773168482466,105.12312344447312,20.1664284240786,79.32640956847692,6.666965706057784,64.47466861031803,4.0396342482899685,38.694434835096864,2.0867987099188317,103.94997559222902,19.08898290736351,71.78028267942507,5.099770465607366,55.19920557284124,3.104259347966041,35.17783363962355,1.9359471784913758,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.57179613789485,3.2209892436744982,37.49012931672191,2.03317973864909,105.58294145619523,20.58664787402852,83.26627791326895,7.757864847967472,64.57504887916579,4.051931818616954,42.090577540190225,2.2501559682241092]},{"theme":"dark","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#E3EEFF","#639EFF","#006DFE","#0048AD","#FFF4F3","#FF7F74","#F1001F","#A80012","#FFF7F1","#FF921B","#D17400","#955100","#B8FFBE","#00C649","#009F39","#006E25","#EDEEFF","#9794FF","#725BFF","#4E00D6"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.73192161780433,17.93999294470544,-52.63286661912575,7.865273702162647,-33.381528921151094,4.618607106385301,-17.0501576858861,2.5303719690800346,-102.4562582898777,19.486486384763445,-56.148341062851905,8.53993960492931,-34.330992536827104,4.759940239487347,-18.37234103122508,2.6737006902768288,-103.68997976483836,19.82648045930809,-60.35961607275431,9.379380118591195,-43.39157032733722,6.209218370112115,-24.93643207012505,3.4548840566407524,-97.27916136850891,18.085610527348415,-59.34389647192141,9.173841233004914,-42.25887199411116,6.018336366435839,-23.360562325895305,3.2570864547421428,-97.9872053947921,18.274719246988955,-53.323021296304695,7.99582368531923,-33.63351308594995,4.6559156780457185,-14.771084781300106,2.2949481607054216]},{"theme":"dark","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000104","#00153F","#00317C","#00429F","#070000","#3A0002","#750009","#9B0010","#0C0300","#291200","#562D00","#8B4B00","#000300","#001D05","#004112","#006521","#010005","#1A0057","#350098","#4700C4"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.20955594591163,5.284998875760269,36.83828085440759,4.509769828130583,28.330761142327287,3.0728704517581886,21.819504217645104,2.3321848000622474,40.142120786515875,5.269603471071473,36.80831529061993,4.503115584720142,27.930310922781615,3.0205822411426944,20.559717681283935,2.214401964672886,39.70511745091868,5.169077378914306,36.80795164205375,4.503034882134433,27.78871707797606,3.002307432697674,14.025581970431295,1.7128949100718707,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.795231110735635,4.500212645278562,30.09285126953815,3.3133648365325183,24.185949678819867,2.5748913454036786]},{"theme":"dark","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E4EFFF","#75AAFF","#3482FF","#005AD5","#FFF6F4","#FF9389","#FF3C3B","#CC0019","#FFF8F2","#FFA350","#E58000","#B36300","#BCFFC2","#00D04D","#00AF3F","#00862E","#EEEFFF","#A2A1FF","#7F74FF","#601DFF"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-86.07057705487603,9.776047393674569,-47.005198509651805,4.836992692850337,-29.617188814986996,3.125340410912816,-13.465221764046625,1.8500449052652255,-92.15650032550448,10.66842694645063,-51.122226750198095,5.289032001141636,-30.69564720892069,3.221769980092269,-14.697188723997265,1.9355550836437716,-92.95341233620333,10.787583067801071,-55.01595924446716,5.732183607436098,-39.01368610071904,4.00959458617392,-22.717127042375882,2.5408179478715844,-86.56463892830851,9.847321829129962,-52.766100540364825,5.474290510931042,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.32881842479753,9.957973594755346,-47.6452882768299,4.906141960838558,-29.99057738527554,3.1585747205458907,-10.990631160230787,1.6845762464445797]},{"theme":"dark","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000107","#00133B","#00317C","#0054C7","#0B0000","#360002","#730009","#BF0016","#100500","#261000","#542C00","#A85D00","#000501","#001B04","#004012","#007D2B","#010009","#180052","#3B00A6","#5A00F5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.616537893860624,5.189361828546554,36.671589094740156,4.521147554384925,27.767977778595686,3.0212072610618272,13.560368041515986,1.6902988819780091,39.46146930028483,5.1544680971312,36.646426655727396,4.515570174173857,27.733751845040576,3.016779185756929,12.426352071081123,1.6200937148513173,38.82022467282525,5.008731246970903,36.66757146424993,4.520256699490143,27.6755983348751,3.009270049666561,0,1.2356833222737893,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.666598999480925,5.200587158978297,36.5946359240296,4.504106003407952,27.684199915070796,3.0103795763779906,16.582944142840912,1.898781610023956]},{"theme":"dark","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000107","#001E53","#00398D","#0058CF","#0C0000","#4C0004","#83000C","#C60018","#110500","#371A00","#603300","#AE6000","#000601","#002808","#004916","#00822D","#01000A","#250070","#4400BC","#5E01FF"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.84943203331798,5.876721182230661,38.357196991852035,4.517162998132957,28.969046969390032,3.0054117916551304,16.078397205708793,1.7957132708136676,43.66281217184597,5.829141814712315,38.38176722933373,4.522556047220382,28.96308732518828,3.004650205577566,15.121014183458136,1.7339400568635355,43.01534021323247,5.66238735865634,38.36369120360285,4.518587529333456,28.956857085057415,3.003854272405285,0,1.32359073643957,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.88937786124038,5.8868666342953615,38.32550423955951,4.510220548286027,28.94413025668513,3.002229117722787,19.264807559936084,2.024522036885816]},{"theme":"dark","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E3EEFF","#66A0FF","#0E71FF","#004CB5","#FFF5F3","#FF8378","#F60020","#AF0013","#FFF7F1","#FF962A","#D57700","#9B5500","#B9FFBF","#00C849","#00A23A","#007226","#EDEEFF","#9997FF","#7460FF","#5200DF"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-95.07464172795706,16.23701078357789,-51.883866319617056,7.27432706128743,-33.10437599843017,4.366728841176053,-16.962076325331164,2.444617555127284,-101.23789901188303,17.745933910216884,-55.660852739640006,7.937252290960222,-33.86013630190819,4.470540437936136,-18.127983335051407,2.5633448249431416,-102.03269987499108,17.94442049171427,-60.01549050114797,8.732123022916136,-43.24478677485694,5.853976070416037,-25.11442835418257,3.342537791539289,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.852118066322895,5.637910373772879,-23.122294138224145,3.108849028833141,-96.32992550494482,16.539962663017093,-52.86610846158114,7.444323350911882,-33.336065162067605,4.398429825394501,-14.383943910910213,2.194178799785328]},{"theme":"dark","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#D67800","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.29489852310851,19.198605473400605,76.19115652133168,6.500760037097026,60.41041952456072,3.801604173546957,38.795795858188825,2.153958743950203,100.99755572094737,18.94295256384705,75.20084184495119,6.262487955668726,60.349100886792264,3.7945539156195487,34.56886711157111,1.9601948416949677,99.82440786870325,17.93087452582946,67.65471495589931,4.7903727911067815,52.29898260017819,3.0135204434714913,31.0522659160978,1.8184953129572932,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.44622841436909,3.025575236651027,33.364561593196164,1.9098288766402804,101.45737373266948,19.337677744697757,79.14071018974319,7.287203401685083,60.44948115564005,3.8061054053754897,37.96500981666448,2.1136413881018767]},{"theme":"dark","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E3EEFF","#69A2FF","#1975FF","#004FBB","#FFF5F3","#FF877C","#FA0021","#B50014","#FFF7F1","#FF9833","#D87900","#A05800","#B9FFC0","#00CA4A","#00A43B","#007628","#EDEEFF","#9B99FF","#7664FF","#5500E7"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-93.28410718321774,14.535439506623602,-51.011442228336165,6.654206660130301,-32.65955270222981,4.075313081844305,-16.370466566638967,2.297770413224217,-99.44736446714371,15.886233758087014,-55.07262883280597,7.299026812522581,-33.02825162884881,4.121422426501604,-17.569909707299868,2.4103015359642868,-100.24216533025177,16.063919770413232,-58.9183675206033,7.932997007393653,-42.52780974275247,5.391835753982905,-24.79898612463087,3.1522681596337323,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.905085629416114,5.1638402024196015,-22.782862127638978,2.93464773079297,-94.53939096020551,14.80664328764592,-51.95162400089297,6.801199303451522,-32.707141977036414,4.081250761977603,-13.69412377461175,2.0581050526656677]},{"theme":"dark","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E8F1FF","#D5E6FF","#95BEFF","#498FFF","#FFF9F8","#FFDDD8","#FFA59B","#FF5F57","#FFFAF6","#FFDFC5","#FFA95E","#F18700","#C6FFCB","#7DFF91","#00DA51","#00B943","#F0F1FF","#E1E3FF","#B4B6FF","#8982FF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-71.17583391665679,5.008590480308264,-64.08586870464498,4.502412953317652,-40.969901639859465,3.0086620775128243,-18.492326685553408,1.8062558438836862,-77.39176152066469,5.470028106981068,-64.06334225320066,4.50083949888721,-41.06186552000793,3.0141084284175577,-20.58109958398818,1.9067218663179524,-77.74802214310357,5.496966722090798,-64.16882972322054,4.508209630901898,-40.97688137217631,3.00907529195687,-26.93048349706687,2.2267208299060117,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.24499881268402,3.0249663274797034,-25.961518214862377,2.176489332337808,-72.19208007215988,5.082914152992658,-64.24580556994597,4.513590807561596,-40.91738125342747,3.005553523356943,-18.74578210381434,1.818317299241165]},{"theme":"dark","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E5EFFF","#7AADFF","#3D87FF","#005FDF","#FFF6F5","#FF988E","#FF4D48","#D5001A","#FFF8F3","#FFA75A","#EA8300","#BA6700","#BDFFC3","#00D34E","#00B341","#008C31","#EEEFFF","#A5A5FF","#837AFF","#6433FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.52359832003891,10.974551054175622,-50.82334067294272,5.601029591424501,-33.90019185783247,3.6989242542195706,-17.94937633357589,2.2421371448335408,-94.53061684959366,11.962030249162733,-55.11693338695639,6.136943021617957,-35.65662848796355,3.880065704029081,-19.010500723408704,2.3279588656978594,-95.32685818617963,12.095472485308715,-58.832789738116276,6.617312123916825,-43.099834270009964,4.690265818580579,-27.202021451205145,3.0448373808729228,-89.02366089613628,11.055448870118658,-56.507119659393844,6.314879036055816,-41.91393002442956,4.556652813501584,-25.675484951698337,2.9041378624661793,-89.65880574741696,11.158543725868578,-51.69548126800453,5.708204299589472,-34.40239467659594,3.750316223165759,-16.078998268001875,2.0949612264666513]},{"theme":"dark","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00020F","#0045A5","#0060E2","#3F88FF","#140000","#A10011","#D7001B","#FF4F49","#190900","#814600","#A85D00","#EA8300","#000A01","#006320","#00822D","#00B341","#030014","#4A00CC","#6537FF","#847BFF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.15878035868923,12.51550617725489,56.792209524954714,5.308053794065892,44.82231091099692,3.370285744389143,29.010809014184062,2.057710850183084,75.84645644595844,12.343365988484052,55.500674792931,5.036843578382145,43.81499067450534,3.25473208522667,27.348178994525146,1.9655632934834584,74.80511978941351,11.764533211175078,52.793708756474935,4.52413702940673,41.44647230556496,3.0043476386355414,20.422200521634597,1.6411239547419576,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.19495675605003,12.535354496536943,59.24767070343283,5.876074797996776,46.58341111523108,3.5866744685580465,28.54022915013225,2.030988108287992]},{"theme":"dark","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"light-ic","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#000110","#0042E2","#3B78FF","#85AEFF","#150000","#BD0011","#FF001B","#FF998D","#1A0500","#B04A00","#E96400","#FFA174","#000B01","#007C2D","#00A73F","#00D452","#00040B","#00639C","#0089D6","#4DB3FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.76809306682351,20.754602558566376,81.70242596755779,7.298044196933449,63.72862579130605,3.9499301799077116,41.321899652634976,2.211520376313395,105.32470288110206,20.35108757719671,79.14032751842986,6.6207260543602775,64.03544238362204,3.986463961054262,38.10332212672181,2.0602150617562085,104.62285904737456,19.706640245810163,73.93672186564804,5.489431142391079,57.895853978119064,3.3399091341000195,36.26437665881752,1.9807324696579875,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.53601527934143,20.543945015271454,78.36297683006073,6.432254688155311,62.2832355663668,3.784280156272109,42.68471757482252,2.28072562681704]},{"theme":"light-ic","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E8F0FF","#709FFF","#2D6DFF","#0038C6","#FFF4F3","#FF8275","#F30019","#A7000E","#FFF6F1","#FF8C52","#DF5F00","#9D4100","#C2FFC9","#00C84D","#009F3C","#006E27","#DFF0FF","#10A5FF","#0082CB","#005689"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-98.22794037003116,18.339196272629252,-53.8293996799751,8.092203727319841,-34.21813658812841,4.7430333536498335,-15.622162303180753,2.381112763636304,-102.4562582898777,19.486486384763445,-56.97432424245902,8.701923574139988,-34.76332175899368,4.824974728357804,-18.151652148372637,2.6494391947645526,-103.24826961310278,19.70448161329645,-59.10147589327934,9.125072654774526,-40.787696351505694,5.7744786453226835,-22.81212855995232,3.1897383862050708,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.29572232514961,6.0245037012352585,-23.381466511664765,3.259668821930569,-97.12909045511768,18.04563033112125,-52.54162933208091,7.848085216280267,-36.273216344700636,5.055412083856902,-18.54110013749204,2.69234396770907]},{"theme":"light-ic","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00104D","#002488","#0032B2","#040000","#3A0002","#750007","#98000B","#080100","#2E0E00","#602500","#8F3B00","#000200","#001D05","#004114","#006322","#000102","#00192E","#003A5F","#004D7C"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.799792142283025,4.501224406456354,30.04772679653348,3.3069997994324063,23.797727835383583,2.533076638038498,40.23106479600237,5.289901483963394,36.80831529061993,4.503115584720142,27.941932493369514,3.022087143525339,21.190215752118235,2.2723970292907865,40.01441400964806,5.240352767492206,36.83929336929951,4.509994812829007,27.908315541084118,3.0177360720288915,16.77490328531677,1.9034128352995776,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.22977489962569,5.289607582090012,36.84755783504637,4.511831555904809,27.796281383025352,3.0032808914145437,21.058698283639867,2.260143784790762]},{"theme":"light-ic","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#EAF1FF","#81ABFF","#4982FF","#0048F5","#FFF5F4","#FF9589","#FF403B","#CC0013","#FFF7F3","#FF9E6E","#F46900","#BD5000","#C6FFCC","#00D251","#00B043","#008631","#E1F1FF","#45B0FF","#0090E1","#006CA9"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.69551010135778,10.011246151186945,-48.263751486481056,4.97335266465752,-30.44825366943375,3.199530942583609,-12.043546933756078,1.753943193668956,-91.71639880150975,10.602849175762582,-51.72679347557047,5.3568510977778265,-31.21621892784593,3.268795397886287,-14.661523244642579,1.9330507439017046,-92.55409442667087,10.727809933310253,-53.89547122979923,5.603122076267749,-35.95806640240085,3.7112655792375078,-20.04367187639212,2.329915503818876,-87.89039906251095,10.039605852107476,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-86.58838125370931,9.850752189993974,-46.78872683576137,4.81370216625391,-31.383244337404896,3.2839492580871354,-15.736559790538532,2.009284883552937]},{"theme":"light-ic","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000004","#000E48","#002895","#0042E3","#070000","#360001","#730006","#BE0011","#0C0200","#2B0D00","#5F2400","#B04A00","#000400","#001B05","#003F13","#007C2D","#000103","#00172B","#00395C","#00639D"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.74741500641279,5.218664231374835,36.670023859840576,4.5208004701163915,27.672031623266886,3.008810094200282,15.351341296852958,1.8098737923818007,39.57933742278427,5.181007308853048,36.654144364688555,4.517280323585742,27.751464091733354,3.019069960427185,12.671992881770905,1.6349566993987985,39.23716411822194,5.10371254671437,36.601471319299684,4.505617849545331,27.652865692456807,3.006339685224649,7.684471784052528,1.3665581876464832,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.65687576920386,5.198408498908002,36.65150522712613,4.5166954713046765,27.64129393913477,3.0048491045091428,12.054359140378313,1.5979376774972005]},{"theme":"light-ic","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000004","#001864","#002FA8","#0045ED","#080000","#4C0003","#830008","#C60012","#0D0200","#3E1500","#6C2A00","#B84D00","#000401","#002809","#004917","#00822F","#000103","#00233D","#00416A","#0068A4"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.98030914587015,5.909904848562026,38.293478372508915,4.503221171740978,28.94317626341717,3.002107337321695,17.95394033246701,1.9258322867444517,43.782795788734404,5.8597651770218455,38.38866051296957,4.524070782124866,28.984608537472,3.0074014381148784,15.156957289606964,1.7362054736619685,43.437607341847624,5.771384990646562,38.33399448678559,4.512078832150148,28.981628167432756,3.007020263830111,9.892674202630067,1.4441010369850493,43.614187977218386,5.816700203526642,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.88976990866122,5.886966133555759,38.401524303038336,4.526899458852529,29.03631728859714,3.0140233900654434,14.306925700285792,1.683714708917422]},{"theme":"light-ic","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E9F0FF","#74A2FF","#3371FF","#003CD0","#FFF4F3","#FF8679","#F8001A","#AF000F","#FFF6F2","#FF9058","#E36100","#A44400","#C3FFC9","#00CA4E","#00A33D","#007329","#E0F0FF","#21A8FF","#0085D0","#005B90"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.69585202312288,16.62869936569993,-53.53835262595417,7.561646720391368,-33.90853669724825,4.477228400656394,-15.643424374972522,2.314414627530853,-100.79897840003042,17.636700891614694,-56.5097869254783,8.089667628698802,-34.29633291417122,4.530985783372177,-18.10601775891745,2.56107712884258,-101.63443561648342,17.844851007069916,-58.71630179001587,8.4915976550775,-40.34850051574898,5.4087697654311055,-22.889197327723014,3.0820931228283226,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-95.5914257275877,16.361459308701495,-52.26386479821355,7.339891411251783,-35.96637326948896,4.765958094131511,-18.710865986535588,2.6239504003951875]},{"theme":"light-ic","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#000110","#0041DF","#3876FF","#82ACFF","#140000","#BB0011","#FC001B","#FF968A","#1A0500","#AE4900","#E76300","#FF9F70","#000B01","#007B2C","#00A63F","#00D252","#00040B","#00629B","#0088D4","#47B1FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.64252534329776,19.49544279635548,78.08171358923367,6.990900026896573,60.31711036263554,3.7908833425501425,38.14751646036453,2.122399526754989,101.24542779029852,19.156162076239593,75.44574141529571,6.3202869230281635,60.62673294996029,3.82662941345977,34.94277926170305,1.9762445240799382,100.4972913238488,18.51105924752946,70.36281029021609,5.256397052662421,54.37780741212484,3.1906001595018556,32.840214506002376,1.8885138214456323,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.4093478352768,3.0225491335598114,33.31123457222728,1.9076443919396007,101.41044755581565,19.297565623167603,74.60712675826916,6.125340559948453,58.63177587043786,3.604653750954651,39.55009522975671,2.1915837971666887]},{"theme":"light-ic","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E9F1FF","#76A4FF","#3875FF","#003ED7","#FFF4F3","#FF897C","#FC001B","#B50010","#FFF6F2","#FF935D","#E66300","#A94600","#C4FFCA","#00CB4F","#00A53F","#00772B","#E0F0FF","#2BA9FF","#0087D3","#005E95"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-95.34499825068502,14.98175500729861,-52.621417231568934,6.906762415988252,-33.47849770408375,4.178062617945301,-14.893749254597887,2.163552488849563,-99.00844385529109,15.788447906049281,-55.63494162352738,7.390318911682041,-33.467257033822236,4.176644120449875,-17.547592326855693,2.408179342981014,-99.84390107174409,15.974784754119627,-57.911424929350886,7.764836527735112,-39.59013024218227,4.982347876116714,-22.33006111099907,2.8868875311347497,-95.39392806862337,14.992417327878547,-58.26045217149408,7.822951516486187,-41.372488477704024,5.229057297458628,-23.176065198031452,2.976455642863797,-93.8008911828484,14.646846343308852,-51.05892490110174,6.66159703590636,-35.06283530237813,4.380256350040106,-18.07990151348513,2.4590883821528062]},{"theme":"light-ic","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EDF3FF","#D8E6FF","#9ABDFF","#598FFF","#FFF8F7","#FFDDD8","#FFA59A","#FF6156","#FFF9F7","#FFDECE","#FFA77C","#FF7112","#D0FFD4","#7DFF93","#00D955","#00B947","#E5F3FF","#CEE8FF","#79C3FF","#0099EE"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.68698719753608,5.119268434409956,-64.43727527190525,4.526987198998707,-41.01457995095163,3.0113075347043563,-19.296469937711088,1.8446469211142276,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-41.0312066106476,3.012292267380192,-20.969079339398604,1.925648566411713,-77.34727635116013,5.466668081501757,-64.08510797117285,4.502359813071005,-40.89305028080999,3.004113887137535,-23.84545366268376,2.068528569757355,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-40.827825177303865,3.0002560129496776,-26.013778316259767,2.1791858289450103,-71.69069599862902,5.046190228559761,-64.15987367011111,4.5075837056249375,-40.85863800181002,3.0020782433474276,-19.258832894479415,1.8428420367238694]},{"theme":"light-ic","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#EAF1FF","#86AEFF","#5189FF","#0650FF","#FFF6F4","#FF9B8F","#FF5349","#D70015","#FFF7F3","#FFA376","#FA6C00","#C75400","#C8FFCD","#00D553","#00B445","#008D34","#E2F1FF","#50B4FF","#0094E7","#0072B3"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-90.02549742397721,11.218239018758677,-52.117590678997175,5.760385990810306,-35.453068631322715,3.8588722418561026,-17.16315496591595,2.1796295803105563,-94.48648764812391,11.954651961610136,-56.08049309972586,6.260045669338659,-36.68653001054492,3.9880923379680597,-19.42498133620683,2.3619323117143227,-94.88408174929032,12.021194381023134,-58.024655972390946,6.511549338355004,-40.19653971715428,4.366167533936967,-25.014063203920518,2.8441658803090872,-90.45906258458693,11.288985892577246,-57.51342916294683,6.44501257640493,-42.39985031117064,4.611196053798779,-26.099142978345885,2.9428672820648862,-89.03890865199516,11.05791933389335,-51.142776043878534,5.640183677550314,-35.55629387962385,3.86961291577043,-20.57349953874105,2.4573829383449617]},{"theme":"light-ic","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#00010B","#0035BB","#044EFF","#4E86FF","#0F0000","#9F000C","#D60015","#FF4D45","#150400","#903B00","#BB4F00","#F86B00","#000801","#006322","#008230","#00B344","#000307","#005182","#0071B2","#0093E5"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.30385323493446,12.594966222228084,58.55230095328762,5.70794052541694,46.62204712410211,3.5916381056717506,29.005214449343097,2.0573900879913625,76.05323451380515,12.457481581934719,55.93606418979168,5.126251708978212,44.077718465236906,3.284322001440957,27.730078776096413,1.986177453897667,75.43283879959024,12.113874624281086,52.77586207835023,4.520990578468311,41.44187963136357,3.003889327255304,23.868980516769618,1.7916338607346896,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.41072061989075,3.0007825637465393,21.536768417458934,1.687644517337887,76.14729893560808,12.509202438867769,55.739334564053664,5.085602713702602,43.07798550292857,3.173723442083943,28.271676351762963,2.015968674447834]},{"theme":"light-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark-ic","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#D38200","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E2","#8F5BFF","#B49FFF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.36973966240839,20.39226096069594,79.16304803673168,6.626348275559002,63.912632944603466,3.971781216419874,43.318131952880584,2.3140155027501375,105.16667360757414,20.20636839933495,79.43361118518992,6.693805381831162,64.79709340122825,4.079330310903514,38.90470830731088,2.096381256670562,103.76723276112338,18.92212884965907,70.7453629185683,4.926701923645705,54.109273202798526,3.0160152686700097,34.512224892089854,1.9092708836328856,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.58363991444959,3.2220248289870597,37.502971296697396,2.0337403797475795,105.5639998705614,20.569417077144905,82.76169093861434,7.605398027541734,65.05029978451972,4.11090668779679,42.03743585537794,2.2474520733771812]},{"theme":"dark-ic","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#DFEEFF","#4AA0FF","#007AE0","#005198","#FFF4F3","#FF7D79","#EF0030","#A6001E","#FFF7EF","#F99A00","#CA7D00","#915800","#B8FFBE","#00C648","#009F38","#006E24","#F0EDFF","#A98DFF","#894CFF","#6000C7"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.24720863865953,17.81140932471069,-52.09132740819334,7.763491481432673,-34.99617079306692,4.860177125212619,-17.977340963301042,2.6303715448994067,-102.4562582898777,19.486486384763445,-55.7461567674855,8.461541475204797,-34.016004840410574,4.712824540535038,-18.066789785866806,2.6401456993132153,-103.60398268701002,19.802704734429785,-61.68353878496926,9.650187857341214,-44.76775468944945,6.444757556916112,-26.060791619056427,3.599837857825526,-97.27916136850891,18.085610527348415,-59.33082759509665,9.171209294707863,-42.24705692654848,6.016359585841944,-23.350526713793172,3.2558471191579894,-97.94239544297146,18.262727694350296,-53.03355221198167,7.94095381214551,-32.857248309508414,4.541449578831631,-15.041171256747345,2.322063533056465]},{"theme":"dark-ic","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000103","#001834","#00376B","#004A8C","#070000","#390005","#750012","#99001B","#0D0400","#261300","#522F00","#865100","#000300","#001D05","#004112","#006521","#010005","#22004F","#43008F","#5800B7"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.21965913293546,5.287302224624218,36.81767378273908,4.505192880903515,27.950777846769416,3.023233051937069,21.000772856623527,2.25477321245894,40.142120786515875,5.269603471071473,36.88792093057972,4.5208109211231795,27.866909320024153,3.0123854877509646,20.876190217747006,2.243276608438576,39.57947058767665,5.139984882718895,36.89828467875601,4.523118840482662,27.8635036155662,3.011945822214043,13.292640012092916,1.6665035199669143,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.80600977189143,4.502603953746083,29.651273965169533,3.2515347547731954,23.802145579236715,2.533547958530533]},{"theme":"dark-ic","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E1EFFF","#62ACFF","#0087F7","#0065BB","#FFF5F4","#FF928C","#FF3644","#CA0027","#FFF8F1","#FFA533","#DD8900","#AD6A00","#BCFFC2","#00D04C","#00AF3F","#00862E","#F1EFFF","#B29CFF","#956AFF","#7700F5"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-85.70519954500536,9.72347127561709,-46.57313915269262,4.790554592637207,-29.772868283167547,3.1391774083572677,-14.678197720495364,1.9342213705917548,-91.71639880150975,10.602849175762582,-50.907190680193835,5.264997878479995,-30.103011927821978,3.1686137953283313,-14.381529067368087,1.9134497328938256,-92.9101803888625,10.781105347058572,-55.36632323767275,5.772792653136124,-40.27550194757439,4.135743433221995,-23.629833528615176,2.614848030728214,-86.56463892830851,9.847321829129962,-52.75275099572737,5.472775232316544,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.71836703917482,10.014570561299498,-47.715877818006604,4.91379345402092,-29.619990398118716,3.125589171245837,-10.872649839108671,1.6769004806806627]},{"theme":"dark-ic","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000106","#001632","#00366A","#005EAF","#0B0000","#360004","#730011","#BD0023","#100600","#241200","#502E00","#A36300","#000501","#001B04","#004012","#007D2B","#020009","#20004A","#48009A","#6F00E5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.62660370456575,5.191620542906115,36.592103069401674,4.503545882983851,27.695057695096033,3.0117807086970667,12.482735608358766,1.623488823202914,39.46146930028483,5.1544680971312,36.631002505660305,4.512153756975423,27.677944212691237,3.0095726068531357,12.751768680957815,1.6398240151911772,38.729462058672325,4.987982714010286,36.579572970042136,4.500775701626332,27.747483785710887,3.0185550274007946,0,1.2039987886628472,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.63684952401951,5.1939188146581685,36.601297620325695,4.505579426399357,27.609531401873983,3.0007614645527125,16.085712499624492,1.86220122646402]},{"theme":"dark-ic","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000206","#002245","#003E78","#0062B6","#0B0000","#4C0008","#820015","#C50025","#110600","#341C00","#5B3600","#A96700","#000601","#002808","#004916","#00822D","#02000A","#2E0067","#5300AD","#7400EE"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.7603252928922,5.854038847974277,38.29235457140463,4.502975854531618,29.083956028514045,3.020138635078335,15.054852030501879,1.729780743830801,43.694363439742204,5.837205585263101,38.354206470247746,4.516507237210082,29.087182234169767,3.020553277003921,15.230546565278194,1.740856512459411,42.92491991360228,5.63897133999608,38.33365423714702,4.512004338988724,28.93769973409828,3.0014083479801514,0,1.2794758858630608,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.85966524740055,5.879321607935794,38.335090707081115,4.512318847459295,28.952846517354917,3.003342035233188,18.7969313728254,1.9885281036480773]},{"theme":"dark-ic","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E0EEFF","#4FA3FF","#007DE4","#00559F","#FFF4F3","#FF817D","#F40031","#AD001F","#FFF7EF","#FB9C00","#CE7F00","#965B00","#B9FFBF","#00C849","#00A239","#007226","#F0EEFF","#AA90FF","#8C52FF","#6500D0"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-94.7101738760577,16.149472101206463,-51.76844025191021,7.254462182627787,-34.605057818633654,4.573999892517157,-17.84521283217997,2.534242228411278,-100.79897840003042,17.636700891614694,-55.24750846744172,7.863491890193123,-33.54190885081095,4.42668652015819,-17.815136446883052,2.5311583129351276,-101.94670279716274,17.922901715066683,-61.02785966423507,8.921519321161206,-44.34904450398409,6.02788949767693,-25.86724540779468,3.4331609270477186,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.840254848173814,5.636085625741139,-23.122294138224145,3.108849028833141,-96.71860922522349,16.634224259107555,-52.4354046324141,7.369572153296626,-32.71416296796099,4.3135863728502315,-14.766203746026353,2.2302399315399106]},{"theme":"dark-ic","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#CC7E00","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E1","#8F5BFF","#B49FFF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.24417193888262,19.155084079579783,75.03748031320593,6.224334741672483,59.78706522107772,3.730817455350078,39.19256422935483,2.1736266322828217,101.04110588404839,18.980469423093126,75.30804346166417,6.287699296730837,60.6715256777025,3.8318416601446055,34.77914058378512,1.9691960254812915,99.64166503759763,17.774143327140518,66.61979519504256,4.627804134340593,52.265155427331,3.010761189364384,30.386657168564103,1.79343744066295,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.45807219092383,3.026547994099155,33.37740357317164,1.9103555042369325,101.43843214703563,19.321492322017065,78.73738474892319,7.17242701812758,60.92473206099399,3.861502331684902,37.91186813185219,2.111101535692422]},{"theme":"dark-ic","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E0EEFF","#53A4FF","#007FE8","#0058A5","#FFF4F3","#FF8580","#F80032","#B30021","#FFF7F0","#FD9D00","#D18100","#9B5E00","#B9FFC0","#00CA4A","#00A43A","#007628","#F0EEFF","#AC93FF","#8D57FF","#6800D8"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-92.91963933131836,14.457074514504098,-50.53751898728166,6.580638229837842,-33.762466759799054,4.213972880586457,-17.265259153354464,2.3814244063048684,-99.00844385529109,15.788447906049281,-54.621704682580784,7.2261694596451385,-32.708249696736004,4.081389020198301,-17.26088719592284,2.381011454094406,-100.19905346524207,16.054261379481122,-59.919533209261445,8.101702037099825,-43.64240726631874,5.55098822531801,-25.561703474916055,3.236683846808585,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.893113879964474,5.162174617584753,-22.782862127638978,2.93464773079297,-94.92807468048419,14.891026660055605,-51.83505302329671,6.7828987244211,-32.07221654016657,4.002369709649521,-14.066989001346808,2.090531498955086]},{"theme":"dark-ic","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E5F1FF","#D2E7FF","#8AC0FF","#2091FF","#FFF9F8","#FFDDDA","#FFA49E","#FF5C5D","#FFFAF5","#FFE0C0","#FFAA42","#E99000","#C6FFCB","#7DFF91","#00DA50","#00B943","#F3F1FF","#E6E2FF","#C1B2FF","#9D7AFF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-70.80493468751378,4.981574213000164,-64.1741844582375,4.5085838821814495,-40.86541522684489,3.0024791016401666,-17.915271666932362,1.7789289638170434,-77.39176152066469,5.470028106981068,-64.14420274941838,4.506488572747314,-40.82594869447877,3.0001450551263997,-20.102672577261966,1.8834970356793017,-77.70409895709017,5.493642615376888,-64.40976920815079,4.525061719384059,-40.93902681557826,3.00683450902807,-28.2154352545988,2.2940964528512438,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.23137463469248,3.02415799426954,-25.961518214862377,2.176489332337808,-72.5833510184408,5.111647048756453,-64.45521953810703,4.528243512077896,-40.9348520371047,3.006587427640115,-18.521762858381592,1.807654812879132]},{"theme":"dark-ic","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E1EFFF","#68AEFF","#008BFD","#006AC4","#FFF6F5","#FF9791","#FF494F","#D30029","#FFF8F1","#FFA941","#E28C00","#B46E00","#BEFFC3","#00D34D","#00B340","#008C31","#F1EFFF","#B49FFF","#9971FF","#7D09FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.0351868676248,10.895768939711282,-50.01919534416472,5.502978516969682,-33.92042169291382,3.7009882121251625,-19.118481017052428,2.3367853131295435,-94.53061684959366,11.962030249162733,-54.89593536437227,6.108854061876529,-35.13675470036395,3.8260433618990066,-18.695569008603144,2.3023140000481574,-95.24016771148194,12.080915286992608,-59.18458546592085,6.6635751999987605,-44.37309985295556,4.8355935478331284,-28.125674388660414,3.1315039953558483,-89.11957165172129,11.070992163190612,-56.49372778683512,6.313154752438012,-41.901500594929324,4.555261373045625,-25.675484951698337,2.9041378624661793,-90.04835436179425,11.221964232051386,-51.2922333885473,5.658542947646642,-34.21126239038315,3.7307191421666435,-15.254686874750304,2.03178289437105]},{"theme":"dark-ic","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00030D","#004D92","#006CC7","#008BFE","#130001","#9F001C","#D60029","#FF4B50","#190B00","#7B4A00","#A06100","#E38C00","#000A01","#006320","#00822D","#00B341","#040013","#5B00BE","#7E16FF","#9971FF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.08600684668272,12.475516218126787,55.96473752983635,5.13221099097022,43.3369682989835,3.2018590596167122,29.318326289650187,2.0754572365320043,75.88156299830736,12.362776535253154,55.82217638546274,5.102669267083783,43.91931922104849,3.266436635421996,27.87409429449102,1.9940348577564484,74.63415754799017,11.66954886455628,52.68236753448497,4.504554642142053,41.632593181405234,3.023005829909607,19.021862742942993,1.5853635699211195,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.18160530883091,12.528031661692317,58.96992438550627,5.808209180198148,47.36214126331064,3.68860336114861,29.106756402593504,2.0632235490419992]},{"theme":"dark-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]}],"resolveVars":[{"theme":"light","bg":"#FFFFFF","vars":{"--lab-bg-tone-2":"oklch(97.84197% 0.010603 286.202)","--lab-bg-tone-3":"oklch(97.84197% 0.010603 286.202)","--lab-label-primary":"oklch(19.12257% 0.014100 291.556)","--lab-label-secondary":"oklch(56.54196% 0.013721 285.953)","--lab-label-tertiary":"oklch(66.97448% 0.014606 285.999)","--lab-label-quaternary":"oklch(81.39504% 0.013897 286.087)","--lab-border-strong":"oklch(19.12257% 0.014100 291.556)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(19.39430% 0.069681 257.297)","--lab-label-brand-secondary":"oklch(57.33316% 0.205939 257.291)","--lab-label-brand-tertiary":"oklch(67.12978% 0.175419 257.690)","--lab-label-brand-quaternary":"oklch(81.31388% 0.094313 257.967)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(20.16318% 0.082740 29.234)","--lab-label-danger-secondary":"oklch(59.62536% 0.244212 28.655)","--lab-label-danger-tertiary":"oklch(69.12968% 0.198650 28.574)","--lab-label-danger-quaternary":"oklch(82.55135% 0.097676 29.389)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(19.32631% 0.042728 66.944)","--lab-label-warning-secondary":"oklch(57.27465% 0.125021 68.799)","--lab-label-warning-tertiary":"oklch(67.73500% 0.148484 68.161)","--lab-label-warning-quaternary":"oklch(82.04043% 0.139600 68.374)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(67.73500% 0.148484 68.161 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(18.59864% 0.054816 147.311)","--lab-label-success-secondary":"oklch(54.91588% 0.161394 147.418)","--lab-label-success-tertiary":"oklch(64.88263% 0.190555 147.443)","--lab-label-success-quaternary":"oklch(79.02491% 0.232159 147.432)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(64.96409% 0.172888 147.450 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(19.54489% 0.077696 259.509)","--lab-label-info-secondary":"oklch(57.68997% 0.232691 259.838)","--lab-label-info-tertiary":"oklch(67.26428% 0.173598 259.980)","--lab-label-info-quaternary":"oklch(81.49851% 0.092914 259.576)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.5","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.5","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.5","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.5","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(66.97448% 0.014606 285.999)"}},{"theme":"light","bg":"#000000","vars":{"--lab-bg-tone-2":"oklch(7.11623% 0.013707 282.350)","--lab-bg-tone-3":"oklch(7.11623% 0.013707 282.350)","--lab-label-primary":"oklch(98.65156% 0.006606 286.278)","--lab-label-secondary":"oklch(80.14049% 0.013952 286.081)","--lab-label-tertiary":"oklch(69.23260% 0.013029 286.055)","--lab-label-quaternary":"oklch(55.16643% 0.013808 285.938)","--lab-border-strong":"oklch(98.65156% 0.006606 286.278)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-brand-secondary":"oklch(80.06451% 0.101154 257.822)","--lab-label-brand-tertiary":"oklch(69.35433% 0.162198 257.610)","--lab-label-brand-quaternary":"oklch(55.96293% 0.201872 257.393)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.64532% 0.006567 28.833)","--lab-label-danger-secondary":"oklch(81.38408% 0.105319 28.863)","--lab-label-danger-tertiary":"oklch(71.19739% 0.181099 28.350)","--lab-label-danger-quaternary":"oklch(58.10538% 0.238004 28.677)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.72985% 0.009577 72.664)","--lab-label-warning-secondary":"oklch(80.98123% 0.149940 68.750)","--lab-label-warning-tertiary":"oklch(70.11346% 0.152989 68.857)","--lab-label-warning-quaternary":"oklch(55.86543% 0.122417 68.219)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.31403% 0.027558 147.033)","--lab-label-success-secondary":"oklch(77.69544% 0.228159 147.448)","--lab-label-success-tertiary":"oklch(67.10085% 0.197388 147.383)","--lab-label-success-quaternary":"oklch(53.45797% 0.157009 147.442)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-info-secondary":"oklch(80.30515% 0.099317 259.806)","--lab-label-info-tertiary":"oklch(69.49027% 0.160387 260.058)","--lab-label-info-quaternary":"oklch(56.23879% 0.227606 259.853)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.013010286081645773","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.01207107843137255","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.009560345877481427","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.00784313725490196","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(69.23260% 0.013029 286.055)"}},{"theme":"light","bg":"#808080","vars":{"--lab-bg-tone-2":"oklch(58.21536% 0.012099 286.018)","--lab-bg-tone-3":"oklch(58.21536% 0.012099 286.018)","--lab-label-primary":"oklch(13.77177% 0.013406 284.503)","--lab-label-secondary":"oklch(20.89710% 0.013787 291.711)","--lab-label-tertiary":"oklch(33.70211% 0.013945 291.371)","--lab-label-quaternary":"oklch(49.18380% 0.012632 285.926)","--lab-border-strong":"oklch(13.77177% 0.013406 284.503)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.89968% 0.076454 257.725)","--lab-label-brand-tertiary":"oklch(34.17043% 0.122830 257.309)","--lab-label-brand-quaternary":"oklch(49.70427% 0.178878 257.337)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(21.88171% 0.089792 29.234)","--lab-label-danger-tertiary":"oklch(35.54757% 0.145582 28.627)","--lab-label-danger-quaternary":"oklch(51.73054% 0.211869 28.643)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.90753% 0.046395 66.438)","--lab-label-warning-tertiary":"oklch(34.00266% 0.074904 67.452)","--lab-label-warning-quaternary":"oklch(49.79045% 0.108745 68.715)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(91.20808% 0.064865 69.118 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(20.10741% 0.059351 147.255)","--lab-label-success-tertiary":"oklch(32.71649% 0.095649 147.616)","--lab-label-success-quaternary":"oklch(47.52181% 0.139141 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(88.67895% 0.198079 147.416 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(21.26627% 0.087439 260.146)","--lab-label-info-tertiary":"oklch(34.39884% 0.140838 260.069)","--lab-label-info-quaternary":"oklch(50.11242% 0.202893 259.861)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.11057506131405706","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.11023622047244094","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.07169755954418741","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.047244094488188976","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.70211% 0.013945 291.371)"}},{"theme":"light","bg":"#3A3A3C","vars":{"--lab-bg-tone-2":"oklch(36.65084% 0.011912 285.797)","--lab-bg-tone-3":"oklch(36.65084% 0.011912 285.797)","--lab-label-primary":"oklch(98.92134% 0.005280 286.303)","--lab-label-secondary":"oklch(82.53513% 0.012530 281.039)","--lab-label-tertiary":"oklch(72.82755% 0.014297 286.039)","--lab-label-quaternary":"oklch(61.25907% 0.011943 286.042)","--lab-border-strong":"oklch(98.92134% 0.005280 286.303)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-brand-secondary":"oklch(82.44675% 0.088464 257.008)","--lab-label-brand-tertiary":"oklch(73.08383% 0.140495 257.442)","--lab-label-brand-quaternary":"oklch(61.77245% 0.208353 257.413)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(61.75781% 0.208491 257.338 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.86605% 0.005516 31.054)","--lab-label-danger-secondary":"oklch(83.38591% 0.092088 28.223)","--lab-label-danger-tertiary":"oklch(74.63167% 0.153784 28.842)","--lab-label-danger-quaternary":"oklch(64.10554% 0.244701 28.706)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.75683% 0.008520 67.727)","--lab-label-warning-secondary":"oklch(83.08426% 0.131366 68.787)","--lab-label-warning-tertiary":"oklch(73.86147% 0.161425 68.614)","--lab-label-warning-quaternary":"oklch(61.96370% 0.135496 68.532)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.51065% 0.024195 147.300)","--lab-label-success-secondary":"oklch(80.08887% 0.235054 147.469)","--lab-label-success-tertiary":"oklch(70.67977% 0.207519 147.455)","--lab-label-success-quaternary":"oklch(59.24524% 0.173588 147.533)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-info-secondary":"oklch(82.42821% 0.087905 259.666)","--lab-label-info-tertiary":"oklch(73.17409% 0.139071 259.898)","--lab-label-info-quaternary":"oklch(61.97090% 0.205878 259.740)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.06909778454881225","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.06091370558375635","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.04060913705583756","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.028061954965508264","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(72.82755% 0.014297 286.039)"}},{"theme":"light","bg":"#007AFF","vars":{"--lab-bg-tone-2":"oklch(57.87475% 0.012117 286.015)","--lab-bg-tone-3":"oklch(57.87475% 0.012117 286.015)","--lab-label-primary":"oklch(13.41536% 0.016099 291.497)","--lab-label-secondary":"oklch(20.01464% 0.013939 291.638)","--lab-label-tertiary":"oklch(33.31157% 0.013986 291.359)","--lab-label-quaternary":"oklch(48.82850% 0.012656 285.922)","--lab-border-strong":"oklch(13.41536% 0.016099 291.497)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.23588% 0.071518 256.892)","--lab-label-brand-tertiary":"oklch(33.52307% 0.122654 257.729)","--lab-label-brand-quaternary":"oklch(49.33014% 0.177503 257.333)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(20.90400% 0.085780 29.234)","--lab-label-danger-tertiary":"oklch(34.90075% 0.142917 28.593)","--lab-label-danger-quaternary":"oklch(51.33845% 0.210254 28.629)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.32691% 0.044866 67.175)","--lab-label-warning-tertiary":"oklch(33.70499% 0.073308 69.356)","--lab-label-warning-quaternary":"oklch(49.32505% 0.107532 68.994)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(90.50991% 0.070001 68.839 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(19.36939% 0.056254 147.869)","--lab-label-success-tertiary":"oklch(32.05045% 0.093836 147.561)","--lab-label-success-quaternary":"oklch(47.21271% 0.138736 147.423)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(87.96697% 0.214930 147.350 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(20.33878% 0.084139 260.257)","--lab-label-info-tertiary":"oklch(33.98919% 0.139378 260.097)","--lab-label-info-quaternary":"oklch(49.73960% 0.201561 259.877)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1414396647356095","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.180541833147175","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.09073588245366797","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.0643078283945157","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.31157% 0.013986 291.359)"}},{"theme":"light","bg":"#FF3B30","vars":{"--lab-bg-tone-2":"oklch(63.60048% 0.011829 286.059)","--lab-bg-tone-3":"oklch(63.60048% 0.011829 286.059)","--lab-label-primary":"oklch(13.83495% 0.015576 284.180)","--lab-label-secondary":"oklch(25.12484% 0.015017 285.269)","--lab-label-tertiary":"oklch(36.40556% 0.013673 291.446)","--lab-label-quaternary":"oklch(51.30243% 0.012497 285.951)","--lab-border-strong":"oklch(13.83495% 0.015576 284.180)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(14.05977% 0.051776 257.879)","--lab-label-brand-secondary":"oklch(25.33060% 0.091490 257.423)","--lab-label-brand-tertiary":"oklch(36.69778% 0.133197 257.540)","--lab-label-brand-quaternary":"oklch(51.93456% 0.187074 257.359)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.49010% 0.059461 29.234)","--lab-label-danger-secondary":"oklch(26.17120% 0.107130 28.474)","--lab-label-danger-tertiary":"oklch(38.31782% 0.156989 28.748)","--lab-label-danger-quaternary":"oklch(53.87750% 0.220648 28.624)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(14.09840% 0.030367 70.955)","--lab-label-warning-secondary":"oklch(25.16306% 0.055216 68.014)","--lab-label-warning-tertiary":"oklch(36.95716% 0.080989 68.209)","--lab-label-warning-quaternary":"oklch(51.83499% 0.113346 68.533)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(94.55166% 0.039779 69.746 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.47470% 0.038038 148.984)","--lab-label-success-secondary":"oklch(24.13011% 0.070822 147.468)","--lab-label-success-tertiary":"oklch(35.33425% 0.103721 147.463)","--lab-label-success-quaternary":"oklch(49.62199% 0.145290 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(93.08890% 0.111383 147.112 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(14.07469% 0.058967 260.480)","--lab-label-info-secondary":"oklch(25.53067% 0.104920 260.137)","--lab-label-info-tertiary":"oklch(37.14168% 0.149591 259.762)","--lab-label-info-quaternary":"oklch(52.13660% 0.212021 259.943)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.18357487922705315","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.4395613333149521","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.17777918620864494","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.10167726510894212","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(36.40556% 0.013673 291.446)"}}]} +{"_meta":{"purpose":"Байт-точный golden границы JS↔WASM: recheckContrast/_recheckContrastMulti и полный snapshot resolveTheme.vars.","regenerated":"2026-07-10","regen_reason":"ADR-0004: point-glow решается по конечным encoded-sRGB8 screen-state; alpha эмитируется кратчайшей decimal-записью без повторного округления.","drift_scope":"Относительно origin/main изменены ровно 24 листа resolveVars: только --lab-fx-glow-{brand,danger,warning,neutral}-alpha на 6 фонах. Ни один base/core, label, border, fill, background или другой var не изменён.","recheck_unchanged":"Секция recheck: 2752/2752 числовых значений, то есть 1376/1376 пар (lc, wcag), байт-идентичны origin/main; ни одно значение recheckContrast не изменено.","numeric_provenance":{"glow_alpha":"Старый fixed-48 bisection + 4-decimal alpha заменён перебором конечных sRGB8-state, внутренней alpha выбранного интервала и shortest-roundtrip binary64 CSS.","scope_check":"Фактический структурный diff вычислен по каждому листу resolveVars; 4 alpha-ключа × 6 записей = 24."},"wcag_floors_not_weakened":"Glow — декоративная роль без WCAG-пола. Отдельно подтверждено: все 1376 пар (2752 числовых значения) recheck неизменны, поэтому измеренный контраст ни одной проверяемой роли не ослаблен.","isolated_verification_required":"Регенерация принимается только после независимой проверки scope, recheck-инварианта и полного Rust/npm/browser gate.","partition_correction":{"regenerated":"2026-07-11","reason":"Коррекция канонической binary64 alpha по фактическому midpoint encoded-sRGB8 partition без дополнительного округления.","drift_scope":"Ровно 17 alpha leaves: light/#000000 danger и четыре Glow alpha на каждом из #808080, #3A3A3C, #007AFF, #FF3B30; остальные resolveVars-листья неизменны.","recheck_unchanged":"2752/2752 числовых значений, то есть 1376/1376 пар (lc, wcag), побитно неизменны.","numeric_provenance":{"halo":"#FF3B30","background":"#000000","state":"#030101","lower_bits":"0x3f85555555555555","upper_bits":"0x3f8c1c1c1c1c1c1c","midpoint_bits":"0x3f88b8b8b8b8b8b8","midpoint":"0.012071078431372548","previous_bits":"0x3f88b8b8b8b8b8b9","previous_relation":"1 ULP выше, внутри той же partition"},"exhaustive_proof":"65 536 channel partitions и seam: state/composite outputs неизменны.","performance":"Full-range → bracketed exact search: base 19.7→4.37 µs, bloom 74.1→16.2 µs, long 656→139 µs."},"history":{"previous_regenerated":"2026-07-10","previous_regen_reason":"Глава #64 (ADR-0003): ось читаемости активирована в домене Ys — реактивный recheck (measure_contrast/recheck_against) и solver-вывод считаются в Ys. recheck flat и resolveVars регенерированы против пересобранного движка; inputs (theme/bg/fgs) сохранены. semver-major по ADR.","previous_drift_scope":"resolveVars: exactly 24 leaves changed, all --lab-fx-glow-{brand,danger,warning,neutral}-core (glow-centre colour, background-independent → 4 distinct values). No other var moved. glow base/alpha, labels, borders, fills, backgrounds all byte-identical.","previous_recheck_unchanged":"Исторический regen также не менял recheck; прежний текст ошибочно называл 2752 числа парами. Фактический объём: 1376 пар (lc, wcag).","previous_numeric_provenance":{"glow-brand-core":"oklch(78.84894% 0.062240 265.472) -> oklch(78.76611% 0.108476 257.256): chroma rises to the blue gamut wall at the functional lightness (fixed-fraction under-saturated it); L ±0.08pp, hue shifts to the cusp.","glow-danger-core":"oklch(81.62208% 0.103545 27.533) -> oklch(81.77103% 0.102764 29.025): near-identical (red already sat near its wall).","glow-warning-core":"oklch(88.80612% 0.081204 68.866) -> oklch(88.88181% 0.082838 68.777): near-identical.","glow-neutral-core":"oklch(99.97226% 0.001314 106.423) -> oklch(100.00000% 0.000000 89.876): neutral hue is achromatic → gamut wall chroma is 0 (was fixed-fraction residue noise); hue_vanished flag fires honestly for the achromatic sentiment."},"previous_wave1_scope":"resolveVars: 69 info-* leaves changed across 6 theme×bg entries. Zero non-info drift (scope-checked). recheck untouched."}},"recheck":[{"theme":"light","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#00030E","#0057BB","#037BFF","#78AFFF","#150000","#BE0005","#FF070E","#FF9A8C","#1B0D00","#A06300","#D28300","#FFAA3A","#000B01","#007C2D","#00A73F","#00D452","#020013","#4D00F9","#6D6EFF","#9FA9FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.59876563358553,20.60103636973117,79.91552226990729,6.816302360996974,63.95198968935827,3.97647793086433,41.89234722606997,2.2400949113548063,105.32470288110206,20.35108757719671,78.97384099389187,6.579727828852003,63.90360859563009,3.970705427078051,37.83221395673913,2.048194921340976,103.87315757816087,19.018778265876662,70.62528347734393,4.90716956443272,53.97808064225952,3.0056474129326713,34.232350404909745,1.8982213760304238,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.76444562516849,20.751302844974063,83.48658341134008,7.825697912025839,63.67798680778349,3.9439477616440737,40.86459333479343,2.1890112231357013]},{"theme":"light","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E6F0FF","#60A1FF","#0074F3","#004CA4","#FFF4F2","#FF8273","#F40009","#A80004","#FFF7EE","#F79C00","#C97E00","#8F5800","#C2FFC9","#00C84D","#00A03C","#006E26","#EEF0FF","#9099FF","#6662FF","#4300DB"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-97.97941453819666,18.27263411531394,-53.55180732861014,8.039306941172763,-34.54692380627581,4.792369408161928,-17.418806999279397,2.56984376276454,-102.41236835704379,19.474434420712562,-56.92703132210305,8.692613650917824,-34.93266406694178,4.850563950189369,-18.308962222075785,2.6667193472488013,-103.5613239139708,19.790915044333452,-61.97487030298502,9.71021814570805,-44.901407836051874,6.467843223430967,-25.783665577412222,3.5638173443979557,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.707239087904505,6.093571182387199,-23.370874926227668,3.2583602660985775,-98.9883689300502,18.54346384276384,-54.32153113615954,8.18635152260471,-34.304902287955215,4.756029101219059,-14.435475219756622,2.261552965854495]},{"theme":"light","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00163A","#003272","#004494","#040000","#3A0000","#760002","#990003","#0B0400","#261300","#512F00","#845100","#000200","#001D05","#004114","#006322","#000002","#160059","#2B0097","#3B00C5"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.89312592554248,4.521969910173767,28.754774555141132,3.1292028976206594,22.090354756391577,2.35851620111828,40.23106479600237,5.289901483963394,36.823385380889306,4.5064610721321205,27.786796188195932,3.002060282138028,21.02583740042693,2.2570951143110336,39.64148780057383,5.154352713354357,36.89828467875601,4.523118840482662,27.981450040156208,3.027209986332121,13.567318350507929,1.6836851754208233,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.3306726916817,5.312553092992316,36.84988795298528,4.512349526507044,30.916517468784825,3.4313626667845316,25.033605894158644,2.6690076358452903]},{"theme":"light","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#E7F1FF","#73ADFF","#2985FF","#0060CB","#FFF5F4","#FF9687","#FF4336","#CD0006","#FFF8F0","#FFA62C","#DC8A00","#AC6B00","#C7FFCC","#00D251","#00B043","#008631","#EFF1FF","#9CA6FF","#767AFF","#532BFF"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.32090210121694,9.956824779623638,-48.0046310941281,4.945145596338778,-30.285195552724065,3.1849116126306156,-14.342034884792934,1.9106934427040745,-91.71639880150975,10.602849175762582,-51.97724928251108,5.385053682831725,-31.570923815646545,3.301015045785777,-14.82740446463819,1.94471301206827,-92.86717481243348,10.774663089384916,-55.64998948221256,5.805759082702303,-40.41406674606042,4.149700467279057,-23.7537806706175,2.6249801387527425,-87.99158651504085,10.054343074329001,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-88.33220875070053,10.104016013712561,-48.80525652513833,5.032519885671211,-30.915189659062865,3.2415642790980215,-11.323318014385267,1.7063261265170042]},{"theme":"light","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000103","#001536","#003374","#0058BC","#070000","#360000","#730002","#BE0005","#0F0500","#241200","#4F2F00","#A16400","#000400","#001B05","#003F13","#007C2D","#000005","#140055","#3300AC","#4E00FA"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.65687576920386,5.198408498908002,36.59714090951085,4.504660010775197,27.83078380746869,3.029349552004155,13.31049770996219,1.6744770124926043,39.57933742278427,5.181007308853048,36.66186584356392,4.51899176883356,27.77510172821011,3.0221297438766705,12.721590912296357,1.6379804580418844,38.8564182220264,5.016999835671664,36.579572970042136,4.500775701626332,27.65113132417139,3.006116231224861,0,1.205042331050678,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.737197591996,5.216381929144518,36.59037871155487,4.5031645842195545,27.68585404738298,3.010592990868115,17.038043883395044,1.933101474636693]},{"theme":"light","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000104","#00204D","#003B83","#005CC4","#080000","#4C0001","#830002","#C70006","#0F0600","#331C00","#5B3600","#A76800","#000400","#002809","#004917","#00822F","#000005","#1F0075","#3A00C3","#511AFF"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.87966672163739,5.884401548408216,38.30767260595269,4.50632138120243,29.16726657041677,3.0308664461202253,15.823096179531001,1.7789458759332724,43.782795788734404,5.8597651770218455,38.402450836988166,4.527103297945568,29.016037258963053,3.011424325654164,14.9864556348382,1.7254955647131072,42.99821792375369,5.657954629703501,38.41622407485322,4.5301350068404425,28.93769973409828,3.0014083479801514,0,1.28054929135364,43.62397616937584,5.819206103341516,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.970091731453365,5.907320242919889,38.28628402517786,4.501651045763603,28.99214635836292,3.0083657301901288,19.11088367642911,2.0125842767555064]},{"theme":"light","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E6F0FF","#64A4FF","#0077F9","#0050AC","#FFF4F3","#FF8777","#F9000A","#B00005","#FFF7EF","#FA9E00","#CD8000","#955C00","#C3FFC9","#00CA4E","#00A33D","#007329","#EEF0FF","#939CFF","#6967FF","#4600E6"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.3221346483494,16.53807546575917,-53.23458352949761,7.5085332653696355,-34.299151794570456,4.531377660721796,-17.34040000814869,2.4827755446650244,-100.79897840003042,17.636700891614694,-56.73966900010428,8.131153426700068,-34.4661942600266,4.554628420782135,-18.26813498707333,2.5778416349364046,-101.94670279716274,17.922901715066683,-61.50032492399525,9.010497025774457,-44.48245067627942,6.049054371037485,-25.983086445749187,3.447217107352778,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-97.33108904020293,16.783196253636362,-53.9780650339559,7.638815670143083,-34.15649094558268,4.511565332868746,-14.178333309149869,2.174938899863266]},{"theme":"light","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#00030D","#0057B9","#0079FD","#74ADFF","#150000","#BC0005","#FD000A","#FF9788","#1B0D00","#9F6200","#CB7F00","#FFA730","#000B01","#007B2C","#00A63E","#00D251","#020013","#4C00F7","#6C6CFF","#9DA6FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.48497819102282,19.361251289328656,75.9364891028185,6.438321694919453,60.636932359482174,3.8278153397206016,38.76699274290234,2.152541489746932,101.19913515757631,19.11640864166299,75.28055256994783,6.281220843840182,60.46094873358925,3.807428346232839,34.70184973776556,1.9658801332164664,99.74758985463511,17.864929125608796,66.9127104243937,4.673002507305443,52.13428535623209,3.0001216660302674,31.16352525448906,1.8227359456035261,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.421867335088024,3.0235758695027473,33.32486172802245,1.9082022515525083,101.63887790164272,19.492343272893066,79.67393134344084,7.442688338927282,60.17031912503092,3.7741072518393395,37.967753096619255,2.113772626279229]},{"theme":"light","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E6F1FF","#67A5FF","#0079FD","#0053B2","#FFF5F3","#FF8A7A","#FD000A","#B60005","#FFF7EF","#FC9F00","#D08200","#9A5F00","#C4FFCA","#00CB4E","#00A53E","#00772A","#EEF1FF","#949EFF","#6B6BFF","#4900EE"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-94.97224440690107,14.900628115594163,-51.99577035710509,6.808135431331698,-33.457001357615106,4.175350120738554,-16.755883343489252,2.3335896778079332,-99.44736446714371,15.886233758087014,-55.869159717403925,7.428487313030341,-33.63398343987836,4.1977071628099685,-17.709012021143003,2.4235530906296177,-100.15616825242344,16.04465607216319,-60.39258141645385,8.181935837052826,-43.77686148964125,5.57032618168592,-25.67861999448382,3.2497240951569792,-95.39392806862337,14.992417327878547,-58.246405974587134,7.820609198173194,-41.3596549106396,5.227261702394524,-23.16505433719768,2.9752806858720673,-95.97861011098902,15.120061263203354,-52.9765437482588,6.963016139185695,-33.57839540296167,4.190679013723035,-13.485138200601865,2.040069034387161]},{"theme":"light","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EBF3FF","#D5E6FF","#92BFFF","#4291FF","#FFF8F7","#FFDDD8","#FFA597","#FF6253","#FFFAF5","#FFE0BE","#FFAA39","#E79100","#D0FFD4","#7DFF93","#00DA54","#00B947","#F1F4FF","#DFE4FF","#AEB8FF","#8087FF"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.43421936609613,5.100687932363686,-64.08586870464498,4.502412953317652,-41.11294430990105,3.017135229848019,-18.888486914529935,1.8251241221961068,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-40.940708789235714,3.0069340578770496,-21.122834074579163,1.9331720160112165,-77.70409895709017,5.493642615376888,-64.34147182620212,4.520282197700913,-40.849183913847796,3.0015190917179395,-28.18413111501551,2.292444746213784,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-41.2870784084647,3.027463512408998,-26.013778316259767,2.1791858289450103,-73.64084437140687,5.1896286230945075,-64.42349258712335,4.5260223420377566,-41.02780633656069,3.0120908713744514,-19.464187683477977,1.8526993919522168]},{"theme":"light","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#E8F1FF","#79B0FF","#368BFF","#0065D6","#FFF6F4","#FF9B8D","#FF5446","#D80007","#FFF8F1","#FFAB3F","#E18E00","#B47000","#C8FFCD","#00D552","#00B545","#008D34","#EFF1FF","#A0AAFF","#7B81FF","#573FFF"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-89.77514075602531,11.177468536043195,-51.89752791553575,5.733156384550998,-34.996215684604906,3.8114980409485155,-18.92394323577027,2.320895932067837,-94.48648764812391,11.954651961610136,-56.02375667309079,6.252768618847473,-36.81873931987068,4.002056146272223,-19.591172380553846,2.375625103310219,-95.24016771148194,12.080915286992608,-59.85145182180837,6.751641124414219,-44.82390104707481,4.887508677146329,-28.683175933367217,3.184369254481423,-90.45906258458693,11.288985892577246,-57.499124974387314,6.443154991463433,-42.83495757582209,4.660276446547384,-26.099142978345885,2.9428672820648862,-90.66219607331998,11.322193559065964,-52.9772743130274,5.867283737103295,-35.76790053375578,3.8916727925995533,-16.863337707955058,2.1560372721390864]},{"theme":"light","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#000209","#00479B","#0065D5","#3289FF","#100000","#9F0004","#D70007","#FF4F41","#170A00","#7A4A00","#9F6200","#DF8C00","#000801","#006322","#00822F","#00B344","#01000D","#3E00CE","#573EFF","#797EFF"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.22533906008685,12.552007092013115,56.99670466281028,5.352680994694784,44.43662180236468,3.3253647200326193,29.122033451491582,2.0641033245604916,76.01516726706762,12.436514137733148,55.97147975339624,5.133613537650522,43.91010121645754,3.2654000659595037,27.461071919951625,1.9716236837030474,74.81611736383091,11.770648316029503,52.816193854903005,4.528105409994289,41.51373908005361,3.011071841176318,19.679891349505557,1.6112041351931181,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.42233250009274,3.001939806510638,21.536768417458934,1.687644517337887,76.3513076320574,12.62087594492386,59.99704797613187,6.063965240585196,46.62694808670478,3.592268428661898,28.6381619838115,2.036506690369533]},{"theme":"light","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#DA7A00","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.42046624663428,20.43859328774187,80.31672424485741,6.9206271592755195,64.53598724808649,4.047139863973963,42.92136358171458,2.2930773168482466,105.12312344447312,20.1664284240786,79.32640956847692,6.666965706057784,64.47466861031803,4.0396342482899685,38.694434835096864,2.0867987099188317,103.94997559222902,19.08898290736351,71.78028267942507,5.099770465607366,55.19920557284124,3.104259347966041,35.17783363962355,1.9359471784913758,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.57179613789485,3.2209892436744982,37.49012931672191,2.03317973864909,105.58294145619523,20.58664787402852,83.26627791326895,7.757864847967472,64.57504887916579,4.051931818616954,42.090577540190225,2.2501559682241092]},{"theme":"dark","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#E3EEFF","#639EFF","#006DFE","#0048AD","#FFF4F3","#FF7F74","#F1001F","#A80012","#FFF7F1","#FF921B","#D17400","#955100","#B8FFBE","#00C649","#009F39","#006E25","#EDEEFF","#9794FF","#725BFF","#4E00D6"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.73192161780433,17.93999294470544,-52.63286661912575,7.865273702162647,-33.381528921151094,4.618607106385301,-17.0501576858861,2.5303719690800346,-102.4562582898777,19.486486384763445,-56.148341062851905,8.53993960492931,-34.330992536827104,4.759940239487347,-18.37234103122508,2.6737006902768288,-103.68997976483836,19.82648045930809,-60.35961607275431,9.379380118591195,-43.39157032733722,6.209218370112115,-24.93643207012505,3.4548840566407524,-97.27916136850891,18.085610527348415,-59.34389647192141,9.173841233004914,-42.25887199411116,6.018336366435839,-23.360562325895305,3.2570864547421428,-97.9872053947921,18.274719246988955,-53.323021296304695,7.99582368531923,-33.63351308594995,4.6559156780457185,-14.771084781300106,2.2949481607054216]},{"theme":"dark","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000104","#00153F","#00317C","#00429F","#070000","#3A0002","#750009","#9B0010","#0C0300","#291200","#562D00","#8B4B00","#000300","#001D05","#004112","#006521","#010005","#1A0057","#350098","#4700C4"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.20955594591163,5.284998875760269,36.83828085440759,4.509769828130583,28.330761142327287,3.0728704517581886,21.819504217645104,2.3321848000622474,40.142120786515875,5.269603471071473,36.80831529061993,4.503115584720142,27.930310922781615,3.0205822411426944,20.559717681283935,2.214401964672886,39.70511745091868,5.169077378914306,36.80795164205375,4.503034882134433,27.78871707797606,3.002307432697674,14.025581970431295,1.7128949100718707,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.795231110735635,4.500212645278562,30.09285126953815,3.3133648365325183,24.185949678819867,2.5748913454036786]},{"theme":"dark","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E4EFFF","#75AAFF","#3482FF","#005AD5","#FFF6F4","#FF9389","#FF3C3B","#CC0019","#FFF8F2","#FFA350","#E58000","#B36300","#BCFFC2","#00D04D","#00AF3F","#00862E","#EEEFFF","#A2A1FF","#7F74FF","#601DFF"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-86.07057705487603,9.776047393674569,-47.005198509651805,4.836992692850337,-29.617188814986996,3.125340410912816,-13.465221764046625,1.8500449052652255,-92.15650032550448,10.66842694645063,-51.122226750198095,5.289032001141636,-30.69564720892069,3.221769980092269,-14.697188723997265,1.9355550836437716,-92.95341233620333,10.787583067801071,-55.01595924446716,5.732183607436098,-39.01368610071904,4.00959458617392,-22.717127042375882,2.5408179478715844,-86.56463892830851,9.847321829129962,-52.766100540364825,5.474290510931042,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.32881842479753,9.957973594755346,-47.6452882768299,4.906141960838558,-29.99057738527554,3.1585747205458907,-10.990631160230787,1.6845762464445797]},{"theme":"dark","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000107","#00133B","#00317C","#0054C7","#0B0000","#360002","#730009","#BF0016","#100500","#261000","#542C00","#A85D00","#000501","#001B04","#004012","#007D2B","#010009","#180052","#3B00A6","#5A00F5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.616537893860624,5.189361828546554,36.671589094740156,4.521147554384925,27.767977778595686,3.0212072610618272,13.560368041515986,1.6902988819780091,39.46146930028483,5.1544680971312,36.646426655727396,4.515570174173857,27.733751845040576,3.016779185756929,12.426352071081123,1.6200937148513173,38.82022467282525,5.008731246970903,36.66757146424993,4.520256699490143,27.6755983348751,3.009270049666561,0,1.2356833222737893,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.666598999480925,5.200587158978297,36.5946359240296,4.504106003407952,27.684199915070796,3.0103795763779906,16.582944142840912,1.898781610023956]},{"theme":"dark","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000107","#001E53","#00398D","#0058CF","#0C0000","#4C0004","#83000C","#C60018","#110500","#371A00","#603300","#AE6000","#000601","#002808","#004916","#00822D","#01000A","#250070","#4400BC","#5E01FF"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.84943203331798,5.876721182230661,38.357196991852035,4.517162998132957,28.969046969390032,3.0054117916551304,16.078397205708793,1.7957132708136676,43.66281217184597,5.829141814712315,38.38176722933373,4.522556047220382,28.96308732518828,3.004650205577566,15.121014183458136,1.7339400568635355,43.01534021323247,5.66238735865634,38.36369120360285,4.518587529333456,28.956857085057415,3.003854272405285,0,1.32359073643957,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.88937786124038,5.8868666342953615,38.32550423955951,4.510220548286027,28.94413025668513,3.002229117722787,19.264807559936084,2.024522036885816]},{"theme":"dark","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E3EEFF","#66A0FF","#0E71FF","#004CB5","#FFF5F3","#FF8378","#F60020","#AF0013","#FFF7F1","#FF962A","#D57700","#9B5500","#B9FFBF","#00C849","#00A23A","#007226","#EDEEFF","#9997FF","#7460FF","#5200DF"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-95.07464172795706,16.23701078357789,-51.883866319617056,7.27432706128743,-33.10437599843017,4.366728841176053,-16.962076325331164,2.444617555127284,-101.23789901188303,17.745933910216884,-55.660852739640006,7.937252290960222,-33.86013630190819,4.470540437936136,-18.127983335051407,2.5633448249431416,-102.03269987499108,17.94442049171427,-60.01549050114797,8.732123022916136,-43.24478677485694,5.853976070416037,-25.11442835418257,3.342537791539289,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.852118066322895,5.637910373772879,-23.122294138224145,3.108849028833141,-96.32992550494482,16.539962663017093,-52.86610846158114,7.444323350911882,-33.336065162067605,4.398429825394501,-14.383943910910213,2.194178799785328]},{"theme":"dark","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#D67800","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.29489852310851,19.198605473400605,76.19115652133168,6.500760037097026,60.41041952456072,3.801604173546957,38.795795858188825,2.153958743950203,100.99755572094737,18.94295256384705,75.20084184495119,6.262487955668726,60.349100886792264,3.7945539156195487,34.56886711157111,1.9601948416949677,99.82440786870325,17.93087452582946,67.65471495589931,4.7903727911067815,52.29898260017819,3.0135204434714913,31.0522659160978,1.8184953129572932,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.44622841436909,3.025575236651027,33.364561593196164,1.9098288766402804,101.45737373266948,19.337677744697757,79.14071018974319,7.287203401685083,60.44948115564005,3.8061054053754897,37.96500981666448,2.1136413881018767]},{"theme":"dark","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E3EEFF","#69A2FF","#1975FF","#004FBB","#FFF5F3","#FF877C","#FA0021","#B50014","#FFF7F1","#FF9833","#D87900","#A05800","#B9FFC0","#00CA4A","#00A43B","#007628","#EDEEFF","#9B99FF","#7664FF","#5500E7"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-93.28410718321774,14.535439506623602,-51.011442228336165,6.654206660130301,-32.65955270222981,4.075313081844305,-16.370466566638967,2.297770413224217,-99.44736446714371,15.886233758087014,-55.07262883280597,7.299026812522581,-33.02825162884881,4.121422426501604,-17.569909707299868,2.4103015359642868,-100.24216533025177,16.063919770413232,-58.9183675206033,7.932997007393653,-42.52780974275247,5.391835753982905,-24.79898612463087,3.1522681596337323,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.905085629416114,5.1638402024196015,-22.782862127638978,2.93464773079297,-94.53939096020551,14.80664328764592,-51.95162400089297,6.801199303451522,-32.707141977036414,4.081250761977603,-13.69412377461175,2.0581050526656677]},{"theme":"dark","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E8F1FF","#D5E6FF","#95BEFF","#498FFF","#FFF9F8","#FFDDD8","#FFA59B","#FF5F57","#FFFAF6","#FFDFC5","#FFA95E","#F18700","#C6FFCB","#7DFF91","#00DA51","#00B943","#F0F1FF","#E1E3FF","#B4B6FF","#8982FF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-71.17583391665679,5.008590480308264,-64.08586870464498,4.502412953317652,-40.969901639859465,3.0086620775128243,-18.492326685553408,1.8062558438836862,-77.39176152066469,5.470028106981068,-64.06334225320066,4.50083949888721,-41.06186552000793,3.0141084284175577,-20.58109958398818,1.9067218663179524,-77.74802214310357,5.496966722090798,-64.16882972322054,4.508209630901898,-40.97688137217631,3.00907529195687,-26.93048349706687,2.2267208299060117,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.24499881268402,3.0249663274797034,-25.961518214862377,2.176489332337808,-72.19208007215988,5.082914152992658,-64.24580556994597,4.513590807561596,-40.91738125342747,3.005553523356943,-18.74578210381434,1.818317299241165]},{"theme":"dark","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E5EFFF","#7AADFF","#3D87FF","#005FDF","#FFF6F5","#FF988E","#FF4D48","#D5001A","#FFF8F3","#FFA75A","#EA8300","#BA6700","#BDFFC3","#00D34E","#00B341","#008C31","#EEEFFF","#A5A5FF","#837AFF","#6433FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.52359832003891,10.974551054175622,-50.82334067294272,5.601029591424501,-33.90019185783247,3.6989242542195706,-17.94937633357589,2.2421371448335408,-94.53061684959366,11.962030249162733,-55.11693338695639,6.136943021617957,-35.65662848796355,3.880065704029081,-19.010500723408704,2.3279588656978594,-95.32685818617963,12.095472485308715,-58.832789738116276,6.617312123916825,-43.099834270009964,4.690265818580579,-27.202021451205145,3.0448373808729228,-89.02366089613628,11.055448870118658,-56.507119659393844,6.314879036055816,-41.91393002442956,4.556652813501584,-25.675484951698337,2.9041378624661793,-89.65880574741696,11.158543725868578,-51.69548126800453,5.708204299589472,-34.40239467659594,3.750316223165759,-16.078998268001875,2.0949612264666513]},{"theme":"dark","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00020F","#0045A5","#0060E2","#3F88FF","#140000","#A10011","#D7001B","#FF4F49","#190900","#814600","#A85D00","#EA8300","#000A01","#006320","#00822D","#00B341","#030014","#4A00CC","#6537FF","#847BFF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.15878035868923,12.51550617725489,56.792209524954714,5.308053794065892,44.82231091099692,3.370285744389143,29.010809014184062,2.057710850183084,75.84645644595844,12.343365988484052,55.500674792931,5.036843578382145,43.81499067450534,3.25473208522667,27.348178994525146,1.9655632934834584,74.80511978941351,11.764533211175078,52.793708756474935,4.52413702940673,41.44647230556496,3.0043476386355414,20.422200521634597,1.6411239547419576,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.19495675605003,12.535354496536943,59.24767070343283,5.876074797996776,46.58341111523108,3.5866744685580465,28.54022915013225,2.030988108287992]},{"theme":"dark","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"light-ic","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#000110","#0042E2","#3B78FF","#85AEFF","#150000","#BD0011","#FF001B","#FF998D","#1A0500","#B04A00","#E96400","#FFA174","#000B01","#007C2D","#00A73F","#00D452","#00040B","#00639C","#0089D6","#4DB3FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.76809306682351,20.754602558566376,81.70242596755779,7.298044196933449,63.72862579130605,3.9499301799077116,41.321899652634976,2.211520376313395,105.32470288110206,20.35108757719671,79.14032751842986,6.6207260543602775,64.03544238362204,3.986463961054262,38.10332212672181,2.0602150617562085,104.62285904737456,19.706640245810163,73.93672186564804,5.489431142391079,57.895853978119064,3.3399091341000195,36.26437665881752,1.9807324696579875,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.53601527934143,20.543945015271454,78.36297683006073,6.432254688155311,62.2832355663668,3.784280156272109,42.68471757482252,2.28072562681704]},{"theme":"light-ic","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E8F0FF","#709FFF","#2D6DFF","#0038C6","#FFF4F3","#FF8275","#F30019","#A7000E","#FFF6F1","#FF8C52","#DF5F00","#9D4100","#C2FFC9","#00C84D","#009F3C","#006E27","#DFF0FF","#10A5FF","#0082CB","#005689"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-98.22794037003116,18.339196272629252,-53.8293996799751,8.092203727319841,-34.21813658812841,4.7430333536498335,-15.622162303180753,2.381112763636304,-102.4562582898777,19.486486384763445,-56.97432424245902,8.701923574139988,-34.76332175899368,4.824974728357804,-18.151652148372637,2.6494391947645526,-103.24826961310278,19.70448161329645,-59.10147589327934,9.125072654774526,-40.787696351505694,5.7744786453226835,-22.81212855995232,3.1897383862050708,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.29572232514961,6.0245037012352585,-23.381466511664765,3.259668821930569,-97.12909045511768,18.04563033112125,-52.54162933208091,7.848085216280267,-36.273216344700636,5.055412083856902,-18.54110013749204,2.69234396770907]},{"theme":"light-ic","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00104D","#002488","#0032B2","#040000","#3A0002","#750007","#98000B","#080100","#2E0E00","#602500","#8F3B00","#000200","#001D05","#004114","#006322","#000102","#00192E","#003A5F","#004D7C"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.799792142283025,4.501224406456354,30.04772679653348,3.3069997994324063,23.797727835383583,2.533076638038498,40.23106479600237,5.289901483963394,36.80831529061993,4.503115584720142,27.941932493369514,3.022087143525339,21.190215752118235,2.2723970292907865,40.01441400964806,5.240352767492206,36.83929336929951,4.509994812829007,27.908315541084118,3.0177360720288915,16.77490328531677,1.9034128352995776,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.22977489962569,5.289607582090012,36.84755783504637,4.511831555904809,27.796281383025352,3.0032808914145437,21.058698283639867,2.260143784790762]},{"theme":"light-ic","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#EAF1FF","#81ABFF","#4982FF","#0048F5","#FFF5F4","#FF9589","#FF403B","#CC0013","#FFF7F3","#FF9E6E","#F46900","#BD5000","#C6FFCC","#00D251","#00B043","#008631","#E1F1FF","#45B0FF","#0090E1","#006CA9"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.69551010135778,10.011246151186945,-48.263751486481056,4.97335266465752,-30.44825366943375,3.199530942583609,-12.043546933756078,1.753943193668956,-91.71639880150975,10.602849175762582,-51.72679347557047,5.3568510977778265,-31.21621892784593,3.268795397886287,-14.661523244642579,1.9330507439017046,-92.55409442667087,10.727809933310253,-53.89547122979923,5.603122076267749,-35.95806640240085,3.7112655792375078,-20.04367187639212,2.329915503818876,-87.89039906251095,10.039605852107476,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-86.58838125370931,9.850752189993974,-46.78872683576137,4.81370216625391,-31.383244337404896,3.2839492580871354,-15.736559790538532,2.009284883552937]},{"theme":"light-ic","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000004","#000E48","#002895","#0042E3","#070000","#360001","#730006","#BE0011","#0C0200","#2B0D00","#5F2400","#B04A00","#000400","#001B05","#003F13","#007C2D","#000103","#00172B","#00395C","#00639D"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.74741500641279,5.218664231374835,36.670023859840576,4.5208004701163915,27.672031623266886,3.008810094200282,15.351341296852958,1.8098737923818007,39.57933742278427,5.181007308853048,36.654144364688555,4.517280323585742,27.751464091733354,3.019069960427185,12.671992881770905,1.6349566993987985,39.23716411822194,5.10371254671437,36.601471319299684,4.505617849545331,27.652865692456807,3.006339685224649,7.684471784052528,1.3665581876464832,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.65687576920386,5.198408498908002,36.65150522712613,4.5166954713046765,27.64129393913477,3.0048491045091428,12.054359140378313,1.5979376774972005]},{"theme":"light-ic","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000004","#001864","#002FA8","#0045ED","#080000","#4C0003","#830008","#C60012","#0D0200","#3E1500","#6C2A00","#B84D00","#000401","#002809","#004917","#00822F","#000103","#00233D","#00416A","#0068A4"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.98030914587015,5.909904848562026,38.293478372508915,4.503221171740978,28.94317626341717,3.002107337321695,17.95394033246701,1.9258322867444517,43.782795788734404,5.8597651770218455,38.38866051296957,4.524070782124866,28.984608537472,3.0074014381148784,15.156957289606964,1.7362054736619685,43.437607341847624,5.771384990646562,38.33399448678559,4.512078832150148,28.981628167432756,3.007020263830111,9.892674202630067,1.4441010369850493,43.614187977218386,5.816700203526642,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.88976990866122,5.886966133555759,38.401524303038336,4.526899458852529,29.03631728859714,3.0140233900654434,14.306925700285792,1.683714708917422]},{"theme":"light-ic","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E9F0FF","#74A2FF","#3371FF","#003CD0","#FFF4F3","#FF8679","#F8001A","#AF000F","#FFF6F2","#FF9058","#E36100","#A44400","#C3FFC9","#00CA4E","#00A33D","#007329","#E0F0FF","#21A8FF","#0085D0","#005B90"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.69585202312288,16.62869936569993,-53.53835262595417,7.561646720391368,-33.90853669724825,4.477228400656394,-15.643424374972522,2.314414627530853,-100.79897840003042,17.636700891614694,-56.5097869254783,8.089667628698802,-34.29633291417122,4.530985783372177,-18.10601775891745,2.56107712884258,-101.63443561648342,17.844851007069916,-58.71630179001587,8.4915976550775,-40.34850051574898,5.4087697654311055,-22.889197327723014,3.0820931228283226,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-95.5914257275877,16.361459308701495,-52.26386479821355,7.339891411251783,-35.96637326948896,4.765958094131511,-18.710865986535588,2.6239504003951875]},{"theme":"light-ic","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#000110","#0041DF","#3876FF","#82ACFF","#140000","#BB0011","#FC001B","#FF968A","#1A0500","#AE4900","#E76300","#FF9F70","#000B01","#007B2C","#00A63F","#00D252","#00040B","#00629B","#0088D4","#47B1FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.64252534329776,19.49544279635548,78.08171358923367,6.990900026896573,60.31711036263554,3.7908833425501425,38.14751646036453,2.122399526754989,101.24542779029852,19.156162076239593,75.44574141529571,6.3202869230281635,60.62673294996029,3.82662941345977,34.94277926170305,1.9762445240799382,100.4972913238488,18.51105924752946,70.36281029021609,5.256397052662421,54.37780741212484,3.1906001595018556,32.840214506002376,1.8885138214456323,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.4093478352768,3.0225491335598114,33.31123457222728,1.9076443919396007,101.41044755581565,19.297565623167603,74.60712675826916,6.125340559948453,58.63177587043786,3.604653750954651,39.55009522975671,2.1915837971666887]},{"theme":"light-ic","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E9F1FF","#76A4FF","#3875FF","#003ED7","#FFF4F3","#FF897C","#FC001B","#B50010","#FFF6F2","#FF935D","#E66300","#A94600","#C4FFCA","#00CB4F","#00A53F","#00772B","#E0F0FF","#2BA9FF","#0087D3","#005E95"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-95.34499825068502,14.98175500729861,-52.621417231568934,6.906762415988252,-33.47849770408375,4.178062617945301,-14.893749254597887,2.163552488849563,-99.00844385529109,15.788447906049281,-55.63494162352738,7.390318911682041,-33.467257033822236,4.176644120449875,-17.547592326855693,2.408179342981014,-99.84390107174409,15.974784754119627,-57.911424929350886,7.764836527735112,-39.59013024218227,4.982347876116714,-22.33006111099907,2.8868875311347497,-95.39392806862337,14.992417327878547,-58.26045217149408,7.822951516486187,-41.372488477704024,5.229057297458628,-23.176065198031452,2.976455642863797,-93.8008911828484,14.646846343308852,-51.05892490110174,6.66159703590636,-35.06283530237813,4.380256350040106,-18.07990151348513,2.4590883821528062]},{"theme":"light-ic","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EDF3FF","#D8E6FF","#9ABDFF","#598FFF","#FFF8F7","#FFDDD8","#FFA59A","#FF6156","#FFF9F7","#FFDECE","#FFA77C","#FF7112","#D0FFD4","#7DFF93","#00D955","#00B947","#E5F3FF","#CEE8FF","#79C3FF","#0099EE"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.68698719753608,5.119268434409956,-64.43727527190525,4.526987198998707,-41.01457995095163,3.0113075347043563,-19.296469937711088,1.8446469211142276,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-41.0312066106476,3.012292267380192,-20.969079339398604,1.925648566411713,-77.34727635116013,5.466668081501757,-64.08510797117285,4.502359813071005,-40.89305028080999,3.004113887137535,-23.84545366268376,2.068528569757355,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-40.827825177303865,3.0002560129496776,-26.013778316259767,2.1791858289450103,-71.69069599862902,5.046190228559761,-64.15987367011111,4.5075837056249375,-40.85863800181002,3.0020782433474276,-19.258832894479415,1.8428420367238694]},{"theme":"light-ic","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#EAF1FF","#86AEFF","#5189FF","#0650FF","#FFF6F4","#FF9B8F","#FF5349","#D70015","#FFF7F3","#FFA376","#FA6C00","#C75400","#C8FFCD","#00D553","#00B445","#008D34","#E2F1FF","#50B4FF","#0094E7","#0072B3"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-90.02549742397721,11.218239018758677,-52.117590678997175,5.760385990810306,-35.453068631322715,3.8588722418561026,-17.16315496591595,2.1796295803105563,-94.48648764812391,11.954651961610136,-56.08049309972586,6.260045669338659,-36.68653001054492,3.9880923379680597,-19.42498133620683,2.3619323117143227,-94.88408174929032,12.021194381023134,-58.024655972390946,6.511549338355004,-40.19653971715428,4.366167533936967,-25.014063203920518,2.8441658803090872,-90.45906258458693,11.288985892577246,-57.51342916294683,6.44501257640493,-42.39985031117064,4.611196053798779,-26.099142978345885,2.9428672820648862,-89.03890865199516,11.05791933389335,-51.142776043878534,5.640183677550314,-35.55629387962385,3.86961291577043,-20.57349953874105,2.4573829383449617]},{"theme":"light-ic","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#00010B","#0035BB","#044EFF","#4E86FF","#0F0000","#9F000C","#D60015","#FF4D45","#150400","#903B00","#BB4F00","#F86B00","#000801","#006322","#008230","#00B344","#000307","#005182","#0071B2","#0093E5"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.30385323493446,12.594966222228084,58.55230095328762,5.70794052541694,46.62204712410211,3.5916381056717506,29.005214449343097,2.0573900879913625,76.05323451380515,12.457481581934719,55.93606418979168,5.126251708978212,44.077718465236906,3.284322001440957,27.730078776096413,1.986177453897667,75.43283879959024,12.113874624281086,52.77586207835023,4.520990578468311,41.44187963136357,3.003889327255304,23.868980516769618,1.7916338607346896,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.41072061989075,3.0007825637465393,21.536768417458934,1.687644517337887,76.14729893560808,12.509202438867769,55.739334564053664,5.085602713702602,43.07798550292857,3.173723442083943,28.271676351762963,2.015968674447834]},{"theme":"light-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark-ic","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#D38200","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E2","#8F5BFF","#B49FFF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.36973966240839,20.39226096069594,79.16304803673168,6.626348275559002,63.912632944603466,3.971781216419874,43.318131952880584,2.3140155027501375,105.16667360757414,20.20636839933495,79.43361118518992,6.693805381831162,64.79709340122825,4.079330310903514,38.90470830731088,2.096381256670562,103.76723276112338,18.92212884965907,70.7453629185683,4.926701923645705,54.109273202798526,3.0160152686700097,34.512224892089854,1.9092708836328856,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.58363991444959,3.2220248289870597,37.502971296697396,2.0337403797475795,105.5639998705614,20.569417077144905,82.76169093861434,7.605398027541734,65.05029978451972,4.11090668779679,42.03743585537794,2.2474520733771812]},{"theme":"dark-ic","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#DFEEFF","#4AA0FF","#007AE0","#005198","#FFF4F3","#FF7D79","#EF0030","#A6001E","#FFF7EF","#F99A00","#CA7D00","#915800","#B8FFBE","#00C648","#009F38","#006E24","#F0EDFF","#A98DFF","#894CFF","#6000C7"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.24720863865953,17.81140932471069,-52.09132740819334,7.763491481432673,-34.99617079306692,4.860177125212619,-17.977340963301042,2.6303715448994067,-102.4562582898777,19.486486384763445,-55.7461567674855,8.461541475204797,-34.016004840410574,4.712824540535038,-18.066789785866806,2.6401456993132153,-103.60398268701002,19.802704734429785,-61.68353878496926,9.650187857341214,-44.76775468944945,6.444757556916112,-26.060791619056427,3.599837857825526,-97.27916136850891,18.085610527348415,-59.33082759509665,9.171209294707863,-42.24705692654848,6.016359585841944,-23.350526713793172,3.2558471191579894,-97.94239544297146,18.262727694350296,-53.03355221198167,7.94095381214551,-32.857248309508414,4.541449578831631,-15.041171256747345,2.322063533056465]},{"theme":"dark-ic","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000103","#001834","#00376B","#004A8C","#070000","#390005","#750012","#99001B","#0D0400","#261300","#522F00","#865100","#000300","#001D05","#004112","#006521","#010005","#22004F","#43008F","#5800B7"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.21965913293546,5.287302224624218,36.81767378273908,4.505192880903515,27.950777846769416,3.023233051937069,21.000772856623527,2.25477321245894,40.142120786515875,5.269603471071473,36.88792093057972,4.5208109211231795,27.866909320024153,3.0123854877509646,20.876190217747006,2.243276608438576,39.57947058767665,5.139984882718895,36.89828467875601,4.523118840482662,27.8635036155662,3.011945822214043,13.292640012092916,1.6665035199669143,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.80600977189143,4.502603953746083,29.651273965169533,3.2515347547731954,23.802145579236715,2.533547958530533]},{"theme":"dark-ic","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E1EFFF","#62ACFF","#0087F7","#0065BB","#FFF5F4","#FF928C","#FF3644","#CA0027","#FFF8F1","#FFA533","#DD8900","#AD6A00","#BCFFC2","#00D04C","#00AF3F","#00862E","#F1EFFF","#B29CFF","#956AFF","#7700F5"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-85.70519954500536,9.72347127561709,-46.57313915269262,4.790554592637207,-29.772868283167547,3.1391774083572677,-14.678197720495364,1.9342213705917548,-91.71639880150975,10.602849175762582,-50.907190680193835,5.264997878479995,-30.103011927821978,3.1686137953283313,-14.381529067368087,1.9134497328938256,-92.9101803888625,10.781105347058572,-55.36632323767275,5.772792653136124,-40.27550194757439,4.135743433221995,-23.629833528615176,2.614848030728214,-86.56463892830851,9.847321829129962,-52.75275099572737,5.472775232316544,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.71836703917482,10.014570561299498,-47.715877818006604,4.91379345402092,-29.619990398118716,3.125589171245837,-10.872649839108671,1.6769004806806627]},{"theme":"dark-ic","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000106","#001632","#00366A","#005EAF","#0B0000","#360004","#730011","#BD0023","#100600","#241200","#502E00","#A36300","#000501","#001B04","#004012","#007D2B","#020009","#20004A","#48009A","#6F00E5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.62660370456575,5.191620542906115,36.592103069401674,4.503545882983851,27.695057695096033,3.0117807086970667,12.482735608358766,1.623488823202914,39.46146930028483,5.1544680971312,36.631002505660305,4.512153756975423,27.677944212691237,3.0095726068531357,12.751768680957815,1.6398240151911772,38.729462058672325,4.987982714010286,36.579572970042136,4.500775701626332,27.747483785710887,3.0185550274007946,0,1.2039987886628472,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.63684952401951,5.1939188146581685,36.601297620325695,4.505579426399357,27.609531401873983,3.0007614645527125,16.085712499624492,1.86220122646402]},{"theme":"dark-ic","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000206","#002245","#003E78","#0062B6","#0B0000","#4C0008","#820015","#C50025","#110600","#341C00","#5B3600","#A96700","#000601","#002808","#004916","#00822D","#02000A","#2E0067","#5300AD","#7400EE"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.7603252928922,5.854038847974277,38.29235457140463,4.502975854531618,29.083956028514045,3.020138635078335,15.054852030501879,1.729780743830801,43.694363439742204,5.837205585263101,38.354206470247746,4.516507237210082,29.087182234169767,3.020553277003921,15.230546565278194,1.740856512459411,42.92491991360228,5.63897133999608,38.33365423714702,4.512004338988724,28.93769973409828,3.0014083479801514,0,1.2794758858630608,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.85966524740055,5.879321607935794,38.335090707081115,4.512318847459295,28.952846517354917,3.003342035233188,18.7969313728254,1.9885281036480773]},{"theme":"dark-ic","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E0EEFF","#4FA3FF","#007DE4","#00559F","#FFF4F3","#FF817D","#F40031","#AD001F","#FFF7EF","#FB9C00","#CE7F00","#965B00","#B9FFBF","#00C849","#00A239","#007226","#F0EEFF","#AA90FF","#8C52FF","#6500D0"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-94.7101738760577,16.149472101206463,-51.76844025191021,7.254462182627787,-34.605057818633654,4.573999892517157,-17.84521283217997,2.534242228411278,-100.79897840003042,17.636700891614694,-55.24750846744172,7.863491890193123,-33.54190885081095,4.42668652015819,-17.815136446883052,2.5311583129351276,-101.94670279716274,17.922901715066683,-61.02785966423507,8.921519321161206,-44.34904450398409,6.02788949767693,-25.86724540779468,3.4331609270477186,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.840254848173814,5.636085625741139,-23.122294138224145,3.108849028833141,-96.71860922522349,16.634224259107555,-52.4354046324141,7.369572153296626,-32.71416296796099,4.3135863728502315,-14.766203746026353,2.2302399315399106]},{"theme":"dark-ic","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#CC7E00","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E1","#8F5BFF","#B49FFF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.24417193888262,19.155084079579783,75.03748031320593,6.224334741672483,59.78706522107772,3.730817455350078,39.19256422935483,2.1736266322828217,101.04110588404839,18.980469423093126,75.30804346166417,6.287699296730837,60.6715256777025,3.8318416601446055,34.77914058378512,1.9691960254812915,99.64166503759763,17.774143327140518,66.61979519504256,4.627804134340593,52.265155427331,3.010761189364384,30.386657168564103,1.79343744066295,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.45807219092383,3.026547994099155,33.37740357317164,1.9103555042369325,101.43843214703563,19.321492322017065,78.73738474892319,7.17242701812758,60.92473206099399,3.861502331684902,37.91186813185219,2.111101535692422]},{"theme":"dark-ic","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E0EEFF","#53A4FF","#007FE8","#0058A5","#FFF4F3","#FF8580","#F80032","#B30021","#FFF7F0","#FD9D00","#D18100","#9B5E00","#B9FFC0","#00CA4A","#00A43A","#007628","#F0EEFF","#AC93FF","#8D57FF","#6800D8"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-92.91963933131836,14.457074514504098,-50.53751898728166,6.580638229837842,-33.762466759799054,4.213972880586457,-17.265259153354464,2.3814244063048684,-99.00844385529109,15.788447906049281,-54.621704682580784,7.2261694596451385,-32.708249696736004,4.081389020198301,-17.26088719592284,2.381011454094406,-100.19905346524207,16.054261379481122,-59.919533209261445,8.101702037099825,-43.64240726631874,5.55098822531801,-25.561703474916055,3.236683846808585,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.893113879964474,5.162174617584753,-22.782862127638978,2.93464773079297,-94.92807468048419,14.891026660055605,-51.83505302329671,6.7828987244211,-32.07221654016657,4.002369709649521,-14.066989001346808,2.090531498955086]},{"theme":"dark-ic","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E5F1FF","#D2E7FF","#8AC0FF","#2091FF","#FFF9F8","#FFDDDA","#FFA49E","#FF5C5D","#FFFAF5","#FFE0C0","#FFAA42","#E99000","#C6FFCB","#7DFF91","#00DA50","#00B943","#F3F1FF","#E6E2FF","#C1B2FF","#9D7AFF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-70.80493468751378,4.981574213000164,-64.1741844582375,4.5085838821814495,-40.86541522684489,3.0024791016401666,-17.915271666932362,1.7789289638170434,-77.39176152066469,5.470028106981068,-64.14420274941838,4.506488572747314,-40.82594869447877,3.0001450551263997,-20.102672577261966,1.8834970356793017,-77.70409895709017,5.493642615376888,-64.40976920815079,4.525061719384059,-40.93902681557826,3.00683450902807,-28.2154352545988,2.2940964528512438,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.23137463469248,3.02415799426954,-25.961518214862377,2.176489332337808,-72.5833510184408,5.111647048756453,-64.45521953810703,4.528243512077896,-40.9348520371047,3.006587427640115,-18.521762858381592,1.807654812879132]},{"theme":"dark-ic","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E1EFFF","#68AEFF","#008BFD","#006AC4","#FFF6F5","#FF9791","#FF494F","#D30029","#FFF8F1","#FFA941","#E28C00","#B46E00","#BEFFC3","#00D34D","#00B340","#008C31","#F1EFFF","#B49FFF","#9971FF","#7D09FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.0351868676248,10.895768939711282,-50.01919534416472,5.502978516969682,-33.92042169291382,3.7009882121251625,-19.118481017052428,2.3367853131295435,-94.53061684959366,11.962030249162733,-54.89593536437227,6.108854061876529,-35.13675470036395,3.8260433618990066,-18.695569008603144,2.3023140000481574,-95.24016771148194,12.080915286992608,-59.18458546592085,6.6635751999987605,-44.37309985295556,4.8355935478331284,-28.125674388660414,3.1315039953558483,-89.11957165172129,11.070992163190612,-56.49372778683512,6.313154752438012,-41.901500594929324,4.555261373045625,-25.675484951698337,2.9041378624661793,-90.04835436179425,11.221964232051386,-51.2922333885473,5.658542947646642,-34.21126239038315,3.7307191421666435,-15.254686874750304,2.03178289437105]},{"theme":"dark-ic","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00030D","#004D92","#006CC7","#008BFE","#130001","#9F001C","#D60029","#FF4B50","#190B00","#7B4A00","#A06100","#E38C00","#000A01","#006320","#00822D","#00B341","#040013","#5B00BE","#7E16FF","#9971FF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.08600684668272,12.475516218126787,55.96473752983635,5.13221099097022,43.3369682989835,3.2018590596167122,29.318326289650187,2.0754572365320043,75.88156299830736,12.362776535253154,55.82217638546274,5.102669267083783,43.91931922104849,3.266436635421996,27.87409429449102,1.9940348577564484,74.63415754799017,11.66954886455628,52.68236753448497,4.504554642142053,41.632593181405234,3.023005829909607,19.021862742942993,1.5853635699211195,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.18160530883091,12.528031661692317,58.96992438550627,5.808209180198148,47.36214126331064,3.68860336114861,29.106756402593504,2.0632235490419992]},{"theme":"dark-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]}],"resolveVars":[{"theme":"light","bg":"#FFFFFF","vars":{"--lab-bg-tone-2":"oklch(97.84197% 0.010603 286.202)","--lab-bg-tone-3":"oklch(97.84197% 0.010603 286.202)","--lab-label-primary":"oklch(19.12257% 0.014100 291.556)","--lab-label-secondary":"oklch(56.54196% 0.013721 285.953)","--lab-label-tertiary":"oklch(66.97448% 0.014606 285.999)","--lab-label-quaternary":"oklch(81.39504% 0.013897 286.087)","--lab-border-strong":"oklch(19.12257% 0.014100 291.556)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(19.39430% 0.069681 257.297)","--lab-label-brand-secondary":"oklch(57.33316% 0.205939 257.291)","--lab-label-brand-tertiary":"oklch(67.12978% 0.175419 257.690)","--lab-label-brand-quaternary":"oklch(81.31388% 0.094313 257.967)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(20.16318% 0.082740 29.234)","--lab-label-danger-secondary":"oklch(59.62536% 0.244212 28.655)","--lab-label-danger-tertiary":"oklch(69.12968% 0.198650 28.574)","--lab-label-danger-quaternary":"oklch(82.55135% 0.097676 29.389)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(19.32631% 0.042728 66.944)","--lab-label-warning-secondary":"oklch(57.27465% 0.125021 68.799)","--lab-label-warning-tertiary":"oklch(67.73500% 0.148484 68.161)","--lab-label-warning-quaternary":"oklch(82.04043% 0.139600 68.374)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(67.73500% 0.148484 68.161 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(18.59864% 0.054816 147.311)","--lab-label-success-secondary":"oklch(54.91588% 0.161394 147.418)","--lab-label-success-tertiary":"oklch(64.88263% 0.190555 147.443)","--lab-label-success-quaternary":"oklch(79.02491% 0.232159 147.432)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(64.96409% 0.172888 147.450 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(19.54489% 0.077696 259.509)","--lab-label-info-secondary":"oklch(57.68997% 0.232691 259.838)","--lab-label-info-tertiary":"oklch(67.26428% 0.173598 259.980)","--lab-label-info-quaternary":"oklch(81.49851% 0.092914 259.576)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.5","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.5","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.5","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.5","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(66.97448% 0.014606 285.999)"}},{"theme":"light","bg":"#000000","vars":{"--lab-bg-tone-2":"oklch(7.11623% 0.013707 282.350)","--lab-bg-tone-3":"oklch(7.11623% 0.013707 282.350)","--lab-label-primary":"oklch(98.65156% 0.006606 286.278)","--lab-label-secondary":"oklch(80.14049% 0.013952 286.081)","--lab-label-tertiary":"oklch(69.23260% 0.013029 286.055)","--lab-label-quaternary":"oklch(55.16643% 0.013808 285.938)","--lab-border-strong":"oklch(98.65156% 0.006606 286.278)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-brand-secondary":"oklch(80.06451% 0.101154 257.822)","--lab-label-brand-tertiary":"oklch(69.35433% 0.162198 257.610)","--lab-label-brand-quaternary":"oklch(55.96293% 0.201872 257.393)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.64532% 0.006567 28.833)","--lab-label-danger-secondary":"oklch(81.38408% 0.105319 28.863)","--lab-label-danger-tertiary":"oklch(71.19739% 0.181099 28.350)","--lab-label-danger-quaternary":"oklch(58.10538% 0.238004 28.677)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.72985% 0.009577 72.664)","--lab-label-warning-secondary":"oklch(80.98123% 0.149940 68.750)","--lab-label-warning-tertiary":"oklch(70.11346% 0.152989 68.857)","--lab-label-warning-quaternary":"oklch(55.86543% 0.122417 68.219)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.31403% 0.027558 147.033)","--lab-label-success-secondary":"oklch(77.69544% 0.228159 147.448)","--lab-label-success-tertiary":"oklch(67.10085% 0.197388 147.383)","--lab-label-success-quaternary":"oklch(53.45797% 0.157009 147.442)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-info-secondary":"oklch(80.30515% 0.099317 259.806)","--lab-label-info-tertiary":"oklch(69.49027% 0.160387 260.058)","--lab-label-info-quaternary":"oklch(56.23879% 0.227606 259.853)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.013010286081645773","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.012071078431372548","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.009560345877481427","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.00784313725490196","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(69.23260% 0.013029 286.055)"}},{"theme":"light","bg":"#808080","vars":{"--lab-bg-tone-2":"oklch(58.21536% 0.012099 286.018)","--lab-bg-tone-3":"oklch(58.21536% 0.012099 286.018)","--lab-label-primary":"oklch(13.77177% 0.013406 284.503)","--lab-label-secondary":"oklch(20.89710% 0.013787 291.711)","--lab-label-tertiary":"oklch(33.70211% 0.013945 291.371)","--lab-label-quaternary":"oklch(49.18380% 0.012632 285.926)","--lab-border-strong":"oklch(13.77177% 0.013406 284.503)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.89968% 0.076454 257.725)","--lab-label-brand-tertiary":"oklch(34.17043% 0.122830 257.309)","--lab-label-brand-quaternary":"oklch(49.70427% 0.178878 257.337)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(21.88171% 0.089792 29.234)","--lab-label-danger-tertiary":"oklch(35.54757% 0.145582 28.627)","--lab-label-danger-quaternary":"oklch(51.73054% 0.211869 28.643)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.90753% 0.046395 66.438)","--lab-label-warning-tertiary":"oklch(34.00266% 0.074904 67.452)","--lab-label-warning-quaternary":"oklch(49.79045% 0.108745 68.715)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(91.20808% 0.064865 69.118 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(20.10741% 0.059351 147.255)","--lab-label-success-tertiary":"oklch(32.71649% 0.095649 147.616)","--lab-label-success-quaternary":"oklch(47.52181% 0.139141 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(88.67895% 0.198079 147.416 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(21.26627% 0.087439 260.146)","--lab-label-info-tertiary":"oklch(34.39884% 0.140838 260.069)","--lab-label-info-quaternary":"oklch(50.11242% 0.202893 259.861)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.11057506131405687","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.11023622047244083","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.07169755954418727","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.04724409448818886","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.70211% 0.013945 291.371)"}},{"theme":"light","bg":"#3A3A3C","vars":{"--lab-bg-tone-2":"oklch(36.65084% 0.011912 285.797)","--lab-bg-tone-3":"oklch(36.65084% 0.011912 285.797)","--lab-label-primary":"oklch(98.92134% 0.005280 286.303)","--lab-label-secondary":"oklch(82.53513% 0.012530 281.039)","--lab-label-tertiary":"oklch(72.82755% 0.014297 286.039)","--lab-label-quaternary":"oklch(61.25907% 0.011943 286.042)","--lab-border-strong":"oklch(98.92134% 0.005280 286.303)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-brand-secondary":"oklch(82.44675% 0.088464 257.008)","--lab-label-brand-tertiary":"oklch(73.08383% 0.140495 257.442)","--lab-label-brand-quaternary":"oklch(61.77245% 0.208353 257.413)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(61.75781% 0.208491 257.338 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.86605% 0.005516 31.054)","--lab-label-danger-secondary":"oklch(83.38591% 0.092088 28.223)","--lab-label-danger-tertiary":"oklch(74.63167% 0.153784 28.842)","--lab-label-danger-quaternary":"oklch(64.10554% 0.244701 28.706)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.75683% 0.008520 67.727)","--lab-label-warning-secondary":"oklch(83.08426% 0.131366 68.787)","--lab-label-warning-tertiary":"oklch(73.86147% 0.161425 68.614)","--lab-label-warning-quaternary":"oklch(61.96370% 0.135496 68.532)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.51065% 0.024195 147.300)","--lab-label-success-secondary":"oklch(80.08887% 0.235054 147.469)","--lab-label-success-tertiary":"oklch(70.67977% 0.207519 147.455)","--lab-label-success-quaternary":"oklch(59.24524% 0.173588 147.533)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-info-secondary":"oklch(82.42821% 0.087905 259.666)","--lab-label-info-tertiary":"oklch(73.17409% 0.139071 259.898)","--lab-label-info-quaternary":"oklch(61.97090% 0.205878 259.740)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.06909778454881221","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.060913705583756306","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.040609137055837526","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.028061954965508236","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(72.82755% 0.014297 286.039)"}},{"theme":"light","bg":"#007AFF","vars":{"--lab-bg-tone-2":"oklch(57.87475% 0.012117 286.015)","--lab-bg-tone-3":"oklch(57.87475% 0.012117 286.015)","--lab-label-primary":"oklch(13.41536% 0.016099 291.497)","--lab-label-secondary":"oklch(20.01464% 0.013939 291.638)","--lab-label-tertiary":"oklch(33.31157% 0.013986 291.359)","--lab-label-quaternary":"oklch(48.82850% 0.012656 285.922)","--lab-border-strong":"oklch(13.41536% 0.016099 291.497)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.23588% 0.071518 256.892)","--lab-label-brand-tertiary":"oklch(33.52307% 0.122654 257.729)","--lab-label-brand-quaternary":"oklch(49.33014% 0.177503 257.333)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(20.90400% 0.085780 29.234)","--lab-label-danger-tertiary":"oklch(34.90075% 0.142917 28.593)","--lab-label-danger-quaternary":"oklch(51.33845% 0.210254 28.629)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.32691% 0.044866 67.175)","--lab-label-warning-tertiary":"oklch(33.70499% 0.073308 69.356)","--lab-label-warning-quaternary":"oklch(49.32505% 0.107532 68.994)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(90.50991% 0.070001 68.839 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(19.36939% 0.056254 147.869)","--lab-label-success-tertiary":"oklch(32.05045% 0.093836 147.561)","--lab-label-success-quaternary":"oklch(47.21271% 0.138736 147.423)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(87.96697% 0.214930 147.350 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(20.33878% 0.084139 260.257)","--lab-label-info-tertiary":"oklch(33.98919% 0.139378 260.097)","--lab-label-info-quaternary":"oklch(49.73960% 0.201561 259.877)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1414396647356093","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.1805418331471749","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.09073588245366788","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.06430782839451565","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(33.31157% 0.013986 291.359)"}},{"theme":"light","bg":"#FF3B30","vars":{"--lab-bg-tone-2":"oklch(63.60048% 0.011829 286.059)","--lab-bg-tone-3":"oklch(63.60048% 0.011829 286.059)","--lab-label-primary":"oklch(13.83495% 0.015576 284.180)","--lab-label-secondary":"oklch(25.12484% 0.015017 285.269)","--lab-label-tertiary":"oklch(36.40556% 0.013673 291.446)","--lab-label-quaternary":"oklch(51.30243% 0.012497 285.951)","--lab-border-strong":"oklch(13.83495% 0.015576 284.180)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(14.05977% 0.051776 257.879)","--lab-label-brand-secondary":"oklch(25.33060% 0.091490 257.423)","--lab-label-brand-tertiary":"oklch(36.69778% 0.133197 257.540)","--lab-label-brand-quaternary":"oklch(51.93456% 0.187074 257.359)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.49010% 0.059461 29.234)","--lab-label-danger-secondary":"oklch(26.17120% 0.107130 28.474)","--lab-label-danger-tertiary":"oklch(38.31782% 0.156989 28.748)","--lab-label-danger-quaternary":"oklch(53.87750% 0.220648 28.624)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(14.09840% 0.030367 70.955)","--lab-label-warning-secondary":"oklch(25.16306% 0.055216 68.014)","--lab-label-warning-tertiary":"oklch(36.95716% 0.080989 68.209)","--lab-label-warning-quaternary":"oklch(51.83499% 0.113346 68.533)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(94.55166% 0.039779 69.746 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.47470% 0.038038 148.984)","--lab-label-success-secondary":"oklch(24.13011% 0.070822 147.468)","--lab-label-success-tertiary":"oklch(35.33425% 0.103721 147.463)","--lab-label-success-quaternary":"oklch(49.62199% 0.145290 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(93.08890% 0.111383 147.112 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(14.07469% 0.058967 260.480)","--lab-label-info-secondary":"oklch(25.53067% 0.104920 260.137)","--lab-label-info-tertiary":"oklch(37.14168% 0.149591 259.762)","--lab-label-info-quaternary":"oklch(52.13660% 0.212021 259.943)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1835748792270531","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.4395613333149519","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.1777791862086449","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.1016772651089421","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-badge-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(56.86744% 0.193154 259.910 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(57.10674% 0.215668 258.445 / 1)","--lab-fill-danger":"oklch(59.02521% 0.231590 28.749 / 1)","--lab-icon":"oklch(36.40556% 0.013673 291.446)"}}]} diff --git a/scripts/cargo-workspace.mjs b/scripts/cargo-workspace.mjs new file mode 100644 index 00000000..5f99ceaa --- /dev/null +++ b/scripts/cargo-workspace.mjs @@ -0,0 +1,18 @@ +const WORKSPACE_PACKAGE_HEADER = + /^[ \t]*\[workspace\.package\][ \t]*(?:#.*)?\r?$/mu; +const ANY_TABLE_HEADER = + /^[ \t]*(?:\[[^\[\]\r\n]+\]|\[\[[^\[\]\r\n]+\]\])[ \t]*(?:#.*)?\r?$/mu; +const VERSION_ENTRY = /^[ \t]*version[ \t]*=[ \t]*"([^"\r\n]+)"[ \t]*(?:#.*)?\r?$/mu; + +/** Читает root version, не пересекая границу TOML-таблицы `[workspace.package]`. */ +export function workspaceVersion(cargoSource) { + const header = WORKSPACE_PACKAGE_HEADER.exec(cargoSource); + if (header) { + const remainder = cargoSource.slice(header.index + header[0].length); + const nextTable = remainder.search(ANY_TABLE_HEADER); + const workspacePackage = nextTable < 0 ? remainder : remainder.slice(0, nextTable); + const version = VERSION_ENTRY.exec(workspacePackage)?.[1]; + if (version) return version; + } + throw new Error("workspace core version is absent"); +} diff --git a/scripts/docs-drift.test.mjs b/scripts/docs-drift.test.mjs index 94e26e71..699a9a22 100644 --- a/scripts/docs-drift.test.mjs +++ b/scripts/docs-drift.test.mjs @@ -292,7 +292,9 @@ test('breaking exact-alpha/glow контракт имеет migration и не о 'glow-indeterminate', 'sound-bound-unavailable', 'compositeProfile', - 'diagnosticProfile', + 'layerRecipeProfile', + 'appearanceDiagnosticProfile', + 'selectionDiagnosticProfile', 'resolve_alpha_analog_hex', 'Rollback', ]) { @@ -301,4 +303,63 @@ test('breaking exact-alpha/glow контракт имеет migration и не о assert.match(changelog, /@labpics\/colors 0\.10\.0 \/ Rust 0\.2\.0/); assert.doesNotMatch(readme, /referenceProfile/); assert.doesNotMatch(adr, /referenceProfile/); + assert.doesNotMatch(readme, /\bdiagnosticProfile\b/); + assert.doesNotMatch(adr, /\bdiagnosticProfile\b/); +}); + +test('exact-alpha migration экранирует absolute-value pipes внутри Markdown tables', () => { + const migration = readFileSync( + join(ROOT, 'docs', 'migrations', 'exact-alpha-glow.md'), + 'utf8', + ); + const tableRows = migration.split('\n').filter((line) => line.startsWith('|')); + const malformed = tableRows.filter((line) => /`[^`]*\|ΔJ′\|[^`]*`/.test(line)); + assert.deepEqual(malformed, []); + assert.equal( + tableRows.filter((line) => line.includes('`\\|ΔJ′\\|`')).length, + 3, + 'anti-vacuum: determinate Glow table содержит три |ΔJ′| cells', + ); +}); + +test('legacy muddiness API quarantined as compatibility proxy, not human verdict', () => { + const paths = [ + ['core', 'crates/labcolors-core/src/cleanliness.rs'], + ['wasm', 'crates/labcolors-wasm/src/lib.rs'], + ['ffi', 'crates/labcolors-ffi/src/lib.rs'], + ['conformance docs', 'conformance/README.md'], + ['conformance crate', 'crates/labcolors-conformance/src/lib.rs'], + ['npm docs', 'packages/colors/README.md'], + ['swift docs', 'bindings/swift/README.md'], + ]; + const surfaces = paths.map(([label, path]) => [ + label, + readFileSync(join(ROOT, ...path.split('/')), 'utf8'), + ]); + + for (const [label, source] of surfaces) { + assert.match( + source, + /experimental compatibility proxy/i, + `${label}: legacy API must state its non-observer status`, + ); + } + + const publicText = surfaces.map(([, source]) => source).join('\n'); + for (const forbidden of [ + /Закон Грязи/u, + /Muddiness Law/u, + /0\s*[—-]\s*чистый,\s*1\s*[—-]\s*грязный/u, + /оценка [«"]грязи[»"]/u, + ]) { + assert.doesNotMatch(publicText, forbidden); + } + + const inventory = readFileSync(join(ROOT, 'docs', 'empirical-inventory.md'), 'utf8'); + assert.match(inventory, /M-01[^\n]*Indeterminate provenance/u); + assert.match(inventory, /M-02[^\n]*universal JND Rejected/u); + assert.match(inventory, /M-04[^\n]*Rejected provenance; Indeterminate value/u); + assert.match(inventory, /M-05[^\n]*Rejected provenance; Indeterminate value/u); + assert.match(inventory, /M-12[^\n]*observer claim Rejected/u); + assert.doesNotMatch(inventory, /M-0[45][^\n]*\| cited-measured \|/u); }); diff --git a/scripts/prepare-npm-package.mjs b/scripts/prepare-npm-package.mjs index 8e0f1cde..14d0835e 100644 --- a/scripts/prepare-npm-package.mjs +++ b/scripts/prepare-npm-package.mjs @@ -4,6 +4,8 @@ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises" import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { workspaceVersion } from "./cargo-workspace.mjs"; + const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); export const REPO_ROOT = resolve(SCRIPT_DIR, ".."); export const PACKAGE_DIR = resolve(REPO_ROOT, "packages/colors"); @@ -98,10 +100,7 @@ export async function prepareNpmPackage() { ]); const packageJson = JSON.parse(packageJsonSource); const conformance = JSON.parse(conformanceSource); - const coreVersion = cargoSource.match( - /\[workspace\.package\]([\s\S]*?)\nversion\s*=\s*"([^"]+)"/u, - )?.[2]; - if (!coreVersion) throw new Error("workspace core version is absent"); + const coreVersion = workspaceVersion(cargoSource); const metadata = { schemaVersion: 1, package: { name: packageJson.name, version: packageJson.version }, diff --git a/scripts/verify-package-release.mjs b/scripts/verify-package-release.mjs index b4f4d116..f58ba336 100644 --- a/scripts/verify-package-release.mjs +++ b/scripts/verify-package-release.mjs @@ -13,6 +13,7 @@ import { basename, join, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { isDeepStrictEqual } from "node:util"; +import { workspaceVersion } from "./cargo-workspace.mjs"; import { PACKAGE_DIR, REPO_ROOT, prepareNpmPackage } from "./prepare-npm-package.mjs"; const RELEASE_DIR = resolve(PACKAGE_DIR, ".release"); @@ -47,19 +48,29 @@ function fail(message) { throw new Error(message); } -function command(commandName, args, cwd = REPO_ROOT) { +export const RELEASE_COMMAND_TIMEOUT_MS = 5 * 60 * 1_000; + +export function command( + commandName, + args, + cwd = REPO_ROOT, + { timeoutMs = RELEASE_COMMAND_TIMEOUT_MS } = {}, +) { try { return execFileSync(commandName, args, { cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024, + timeout: timeoutMs, stdio: ["ignore", "pipe", "pipe"], }); } catch (error) { const stderr = error?.stderr?.toString().trim(); const stdout = error?.stdout?.toString().trim(); const detail = [stderr, stdout].filter(Boolean).join("\n"); - fail(`${commandName} ${args.join(" ")} failed${detail ? `:\n${detail}` : ""}`); + const outcome = + error?.code === "ETIMEDOUT" ? `timed out after ${timeoutMs} ms` : "failed"; + fail(`${commandName} ${args.join(" ")} ${outcome}${detail ? `:\n${detail}` : ""}`); } } @@ -82,15 +93,6 @@ async function readJson(path) { } } -function workspaceVersion(cargoSource) { - const workspacePackage = cargoSource.match( - /\[workspace\.package\]([\s\S]*?)(?=\n\[|$)/u, - )?.[1]; - const version = workspacePackage?.match(/^version\s*=\s*"([^"]+)"\s*$/mu)?.[1]; - if (!version) fail("Cargo.toml has no [workspace.package] version"); - return version; -} - function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); } @@ -378,6 +380,16 @@ const config = { decision_profile: "legacy-platform-dependent-v1", }, }, + { + name: "token-a11c", + recipe: { + kind: "material", + source: { kind: "brand" }, + tone_light: 0.72, + tone_dark: 0.28, + floor: "aa-ui", + }, + }, ], }; @@ -402,7 +414,7 @@ assert.match(fingerprint, /^[0-9a-f]{16}$/u); const background = "#000000"; const resolved = engine.resolveTheme(background, "light"); -assert.deepEqual(Object.keys(resolved.roles).sort(), ["token-7f3a", "token-92be"]); +assert.deepEqual(Object.keys(resolved.roles).sort(), ["token-7f3a", "token-92be", "token-a11c"]); const alpha = resolved.roles["token-7f3a"]; assert.equal(alpha.kind, "translucent"); @@ -413,19 +425,43 @@ assert.equal(alpha.alphaCoerced, false); assert.equal(sourceOver(alpha.tintHex, alpha.alpha, background), alpha.compositeHex); assert.match(alpha.css, /^oklch\(.+ \/ 0\.122\)$/u); +const material = resolved.roles["token-a11c"]; +assert.equal(material.kind, "material"); +assert.equal( + material.alphaGuarantee.numericalProfile, + "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1", +); +if (material.alphaGuarantee.kind === "transparent-endpoint-characterized-v1") { + assert.equal(material.alpha, 0); + assert.equal(material.alphaStatus, "satisfied"); + assert.equal(material.guaranteed, true); +} else if (material.alphaGuarantee.kind === "bisection-bracket-characterized-v1") { + assert.equal(material.alpha, material.alphaGuarantee.upperAlpha); + assert.equal(material.alphaStatus, "satisfied"); + assert.equal(material.guaranteed, true); +} else { + assert.equal(material.alphaGuarantee.kind, "opaque-endpoint-characterized-v1"); + assert.equal(material.alpha, 1); + assert.equal(material.alphaStatus, "degraded"); + assert.equal(material.guaranteed, false); +} +assert.ok(Number.isFinite(material.floor)); + const glow = resolved.roles["token-92be"]; assert.equal(glow.kind, "glow"); assert.equal(glow.compositeProfile, "encoded-srgb8-screen-v1"); assert.equal(glow.compositeGuarantee, "bit-exact"); -assert.equal(glow.diagnosticProfile, "cam16-ucs-jprime-li2017-v1"); +assert.equal(glow.layerRecipeProfile, "cam16-jprime-oklab-cusp-v1"); +assert.equal(glow.appearanceDiagnosticProfile, "cam16-ucs-jprime-li2017-v1"); +assert.equal(glow.selectionDiagnosticProfile, "cam16-ucs-jprime-li2017-v1"); assert.equal(glow.decisionProfile, "legacy-platform-dependent-v1"); assert.deepEqual(glow.decisionGuarantee, { kind: "legacy-platform-dependent-v1" }); assert.equal(glow.constraintLayer, "halo"); -assert.ok(glow.targetStatus === "reached" || glow.targetStatus === "unreachable"); +assert.ok(glow.targetStatus === "legacy-reached" || glow.targetStatus === "legacy-unreachable"); assert.ok(Number.isFinite(glow.alpha) && glow.alpha > 0 && glow.alpha <= 1); assert.equal(Number(glow.alphaCss), glow.alpha); assert.equal(glow.achievedDj, glow.haloAchievedDj); -assert.equal(glow.degraded, glow.targetStatus === "unreachable"); +assert.equal(glow.degraded, glow.targetStatus === "legacy-unreachable"); assert.equal(screen(glow.haloHex, glow.alpha, background), glow.haloCompositeHex); assert.equal(screen(glow.coreHex, glow.alpha, background), glow.coreCompositeHex); for (const value of [glow.targetDj, glow.haloAchievedDj, glow.coreAchievedDj]) { @@ -457,6 +493,11 @@ const stableNoop = stableWhite.roles["token-92be"]; assert.equal(stableNoop.kind, "glow"); assert.equal(stableNoop.decisionProfile, "stable-v1"); assert.deepEqual(stableNoop.decisionGuarantee, { kind: "bit-exact" }); +assert.equal(stableNoop.layerRecipeProfile, "cam16-jprime-oklab-cusp-v1"); +assert.equal(stableNoop.appearanceDiagnosticProfile, "cam16-ucs-jprime-li2017-v1"); +assert.equal(stableNoop.selectionDiagnosticProfile, null); +assert.equal(stableNoop.targetStatus, "exact-noop-unreachable"); +assert.equal(stableNoop.degraded, true); assert.equal(stableNoop.haloCompositeHex, "#FFFFFF"); for (const key of [ stableNoop.cssVar, @@ -473,8 +514,13 @@ function typeSmokeSource() { import init, { LabColors, type GlowDecisionGuaranteeV1, + type GlowDeterminateRole, + type GlowDeterminateRoleBase, type GlowRole, + type GlowTargetStatusV1, type LadderPositionV1, + type MaterialRole, + type MaterialRoleBase, type NumericalIndeterminacyV1, type ResolvedTheme, type ThemeConfig, @@ -549,7 +595,7 @@ function glowContract( ): readonly ["indeterminate", "glow-target-or-maximum-v1"] | readonly [ "encoded-srgb8-screen-v1", "halo", - "reached" | "unreachable", + GlowTargetStatusV1, string, number, ] { @@ -565,10 +611,8 @@ function glowContract( ] as const; } -function decisionEvidence(guarantee: GlowDecisionGuaranteeV1): number | string { - return guarantee.kind === "outward-interval-v1" - ? guarantee.upper - guarantee.lower - : guarantee.kind; +function decisionEvidence(guarantee: GlowDecisionGuaranteeV1): string { + return guarantee.kind; } function indeterminacyEvidence(evidence: NumericalIndeterminacyV1): number | string { @@ -577,6 +621,64 @@ function indeterminacyEvidence(evidence: NumericalIndeterminacyV1): number | str : evidence.bounds.kind; } +function determinateGlowEvidence(role: GlowDeterminateRole): string { + if (role.decisionProfile === "stable-v1") { + const status: "exact-noop-unreachable" = role.targetStatus; + const selection: null = role.selectionDiagnosticProfile; + const degraded: true = role.degraded; + void selection; + void degraded; + return status; + } + if (role.targetStatus === "legacy-reached") { + const degraded: false = role.degraded; + void degraded; + return role.targetStatus; + } + const status: "legacy-unreachable" = role.targetStatus; + const degraded: true = role.degraded; + void degraded; + return status; +} + +function materialEvidence(role: MaterialRole): number | boolean { + if (role.alphaStatus === "degraded") { + const alpha: 1 = role.alpha; + const guaranteed: false = role.guaranteed; + void alpha; + return guaranteed; + } + const guaranteed: true = role.guaranteed; + if (role.alphaGuarantee.kind === "bisection-bracket-characterized-v1") { + return role.alphaGuarantee.upperAlpha; + } + return guaranteed; +} + +declare const glowBase: GlowDeterminateRoleBase; +// @ts-expect-error stable profile не может нести legacy status. +const impossibleGlow: GlowDeterminateRole = { + ...glowBase, + decisionProfile: "stable-v1", + decisionGuarantee: { kind: "bit-exact" }, + selectionDiagnosticProfile: null, + targetStatus: "legacy-reached", + degraded: false, +}; + +declare const materialBase: MaterialRoleBase; +// @ts-expect-error compatibility boolean выводится из material status/guarantee. +const impossibleMaterial: MaterialRole = { + ...materialBase, + alpha: 1, + alphaGuarantee: { + kind: "opaque-endpoint-characterized-v1", + numericalProfile: "encoded-srgb-byte-scale-affine-platform-binary64-powf-v1", + }, + alphaStatus: "degraded", + guaranteed: true, +}; + void [ initialise, fingerprint, @@ -586,6 +688,10 @@ void [ glowContract, decisionEvidence, indeterminacyEvidence, + determinateGlowEvidence, + materialEvidence, + impossibleGlow, + impossibleMaterial, ]; `; } @@ -677,6 +783,38 @@ async function verifyCleanConsumer( } } +// Execute the same packed-package runtime smoke under the caller's Node binary. +// CI uses this to prove the public consumer floor independently from the pinned +// release packer. +export async function smokePackedRuntime(tarballPath) { + const tarball = resolve(tarballPath); + const consumer = await mkdtemp(join(tmpdir(), "labcolors-runtime-smoke-")); + try { + await writeFile( + join(consumer, "package.json"), + `${JSON.stringify({ private: true, type: "module" }, null, 2)}\n`, + ); + npm( + [ + "install", + "--offline", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--no-package-lock", + "--save=false", + tarball, + ], + consumer, + ); + const runtimePath = resolve(consumer, "smoke.mjs"); + await writeFile(runtimePath, runtimeSmokeSource()); + command(process.execPath, [runtimePath], consumer); + } finally { + await rm(consumer, { recursive: true, force: true }); + } +} + export async function verifyPackageRelease() { const { sourceSha: source } = await prepareNpmPackage(); @@ -691,8 +829,9 @@ export async function verifyPackageRelease() { const npmVersion = lockedNpmVersion(packageJson); const lockRoot = packageLock.packages?.[""]; if (packageJson.name !== "@labpics/colors") fail(`unexpected npm package name: ${packageJson.name}`); - if (typeof packageJson.engines?.node !== "string" || packageJson.engines.node.length === 0) { - fail("package.json must declare a non-empty engines.node release requirement"); + const consumerNodeFloor = packageJson.engines?.node?.match(/^>=(\d+\.\d+\.\d+)$/u)?.[1]; + if (!consumerNodeFloor) { + fail("package.json engines.node must declare one exact >=X.Y.Z consumer floor"); } if (lockRoot?.name !== packageJson.name || lockRoot?.version !== packageJson.version) { fail("package.json and package-lock.json root identity differ"); @@ -774,8 +913,15 @@ export async function verifyPackageRelease() { byteIdentical: true, }, requirements: { - node: packageJson.engines?.node, - npm: npmVersion, + consumerRuntime: { + node: packageJson.engines.node, + verifiedFloor: consumerNodeFloor, + canonicalGate: "Node 22 consumer floor", + }, + buildToolchain: { + node: process.versions.node, + npm: npmVersion, + }, typescript: { compiler: lockedTypescriptVersion(packageLock), target: "ES2022", @@ -818,8 +964,20 @@ const invokedDirectly = process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url); if (invokedDirectly) { - verifyPackageRelease() + const runtimeSmokeIndex = process.argv.indexOf("--runtime-smoke"); + const action = runtimeSmokeIndex >= 0 + ? (() => { + const tarball = process.argv[runtimeSmokeIndex + 1]; + if (!tarball) fail("--runtime-smoke requires a tarball path"); + return smokePackedRuntime(tarball).then(() => ({ runtimeSmoke: tarball })); + })() + : verifyPackageRelease(); + action .then(async ({ manifest, tarball }) => { + if (runtimeSmokeIndex >= 0) { + console.log(`runtime smoke passed: ${resolve(process.argv[runtimeSmokeIndex + 1])}`); + return; + } await writeGithubOutputs({ manifest, tarball }); console.log(`verified ${relative(REPO_ROOT, tarball).split(sep).join("/")}`); console.log(`wrote ${relative(REPO_ROOT, manifest).split(sep).join("/")}`); From 416accc3e699fce4f02a46aceb8468f721fa3362 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:15:18 +0300 Subject: [PATCH 11/11] fix: close final review findings --- crates/labcolors-core/src/glow.rs | 13 ++++++++----- crates/labcolors-core/src/solve.rs | 2 +- crates/labcolors-core/src/wcag.rs | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/labcolors-core/src/glow.rs b/crates/labcolors-core/src/glow.rs index 8990cccc..a43b5861 100644 --- a/crates/labcolors-core/src/glow.rs +++ b/crates/labcolors-core/src/glow.rs @@ -395,6 +395,12 @@ pub fn screen_layer_over_encoded( ]) } +/// Один канал конечного encoded-sRGB8 screen reference-профиля. +fn screen_channel_over_srgb8(glow: u8, alpha: f64, bg: u8) -> u8 { + (f64::from(bg) + alpha * f64::from(glow) * f64::from(u8::MAX - bg) / f64::from(u8::MAX)).round() + as u8 +} + /// Screen-слой в конечном reference-домене encoded-sRGB8. /// /// Формула вычисляется прямо в шкале байтов и округляется ровно один раз: @@ -407,11 +413,6 @@ pub fn screen_layer_over_encoded( /// # Errors /// /// `Err`, если `alpha` не конечна или лежит вне `[0,1]`. -fn screen_channel_over_srgb8(glow: u8, alpha: f64, bg: u8) -> u8 { - (f64::from(bg) + alpha * f64::from(glow) * f64::from(u8::MAX - bg) / f64::from(u8::MAX)).round() - as u8 -} - pub fn screen_layer_over_srgb8(glow: [u8; 3], alpha: f64, bg: [u8; 3]) -> Result<[u8; 3], String> { if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) { return Err(format!("alpha вне конечного [0,1]: {alpha}")); @@ -928,6 +929,8 @@ pub fn solve_screen_alpha_for_dj( slopes, } = screen_point_inputs(glow_tint_hex, bg_hex)?; if slopes_are_exact_srgb8_noop(slopes) { + // Любая alpha из [0,1] даёт тот же байтовый composite; 0.5 — + // канонический средний представитель, а не измеренная величина. let alpha = 0.5; let alpha_css = crate::css_alpha_value(alpha)?; let composite_srgb8 = bg_bytes; diff --git a/crates/labcolors-core/src/solve.rs b/crates/labcolors-core/src/solve.rs index 396e3002..f89d4af7 100644 --- a/crates/labcolors-core/src/solve.rs +++ b/crates/labcolors-core/src/solve.rs @@ -1320,6 +1320,7 @@ pub(crate) fn bg_luma(rgb: [f64; 3], vc: &ViewingConditions) -> f64 { #[cfg(test)] mod tests { use super::*; + use crate::lpc::lpc_with_vc; #[test] fn internal_invariant_failure_is_not_reported_as_invalid_client_input() { @@ -1327,7 +1328,6 @@ mod tests { assert!(error.to_string().starts_with("internal invariant failure:")); assert!(!error.to_string().starts_with("invalid input:")); } - use crate::lpc::lpc_with_vc; /// D2(a) страж (аудит 2026-07-03): `Unreachable` не несёт never-constructed /// вариантов-заготовок. `UnsupportedBackground` («future inputs», 0 точек diff --git a/crates/labcolors-core/src/wcag.rs b/crates/labcolors-core/src/wcag.rs index 912a74a7..2337472b 100644 --- a/crates/labcolors-core/src/wcag.rs +++ b/crates/labcolors-core/src/wcag.rs @@ -22,7 +22,7 @@ pub(crate) const AA_UI_RATIO: f64 = 3.0; const LEGACY_CHANNEL_SPLIT: f64 = 0.039_28; /// First binary64 value on the power branch of the legacy split. -const LEGACY_CHANNEL_SPLIT_RIGHT: f64 = f64::from_bits(0x3fa4_1c82_16c6_1524); +const LEGACY_CHANNEL_SPLIT_RIGHT: f64 = f64::from_bits(LEGACY_CHANNEL_SPLIT.to_bits() + 1); const RED_WEIGHT: f64 = 0.2126; const GREEN_WEIGHT: f64 = 0.7152;