From 900b86fa95e50ccee1cc969a8b546a5c0ab400f5 Mon Sep 17 00:00:00 2001 From: Ohualtex <230626673+Ohualtex@users.noreply.github.com> Date: Sun, 7 Jun 2026 18:42:33 +0300 Subject: [PATCH 01/11] =?UTF-8?q?feat(welcome):=20giri=C5=9F=20animasyonla?= =?UTF-8?q?r=C4=B1=20+=20saat-bazl=C4=B1=20tema=20+=20g=C3=BCne=C5=9F/ay?= =?UTF-8?q?=20toprak-arkas=C4=B1=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - İlk yükleme: tema CSS boyamadan önce uygulanır (head inline script) → açılışta gün/gece geçiş animasyonu oynamaz. Saat 06–20 gündüz / 20–06 gece; ilk açılışta sabitlenir (refresh değiştirmez). - Giriş animasyonları (.welcome-intro from-state + transition; RAF + setTimeout fallback): logo üstten, hero aşağıdan (uzatıldı), gündüz güneş parlayarak + bulutlar yandan, gece ay opaklaşarak + yıldızlar üstten inip parlayarak. - Toggle: güneş/ay artık toprağın ARKASINA batıp arkadan doğar (z-index 2 < soil 3, batış translateY 96vh, occlusion ile gizlenir). - Fix: toggle z-index düşünce topbar/hero tıklamayı kapatıyordu → pointer-events none (login butonu auto) ile güneş/ay yeniden tıklanabilir. Co-Authored-By: Claude Opus 4.8 --- frontend/index.html | 19 ++++++- frontend/src/main.js | 21 ++++++-- frontend/src/styles/welcome.css | 87 ++++++++++++++++++++++++--------- 3 files changed, 100 insertions(+), 27 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 0464d9f..d3469ff 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,6 +5,23 @@ SFDAP — Akıllı Tarım Dashboard + + @@ -33,7 +50,7 @@ Site açılışında ilk karşılayan ekran. Sağ üstte tema + "Giriş Yap". Alt-ortada toprağa gömülü Filiz; tıklayınca topraktan fırlar. `_applyAuthGate(null)` bunu gösterir; "Giriş Yap" → #landing. --> -
+
- - + +
+ +
+
+ SFDAP +

Toprağın altında, veriye dönüşen tarım

+

Sensör, hava durumu ve yapay zekâ tahminlerini tek panelde + birleştiren çiftçi-odaklı akıllı tarım karar-destek platformu. Aşağı in, keşfet.

+
+
+ + +
+
+
💧
+
+

Toprak Nemi & Sensörler

+

Nemi 7/24 ölç, susuzluğu erken yakala.

+
+
+
+

Tarlana gömülü sensörler nemi sürekli okur; eşik altına düşünce panelinde + 🥵 Susuz rozeti ve uyarı belirir. Su stresine büyümeden müdahale et.

+
+
+ +
+
+
🚿
+
+

Akıllı Sulama

+

Ne kadar su? Model söylesin.

+
+
+
+

RandomForest modeli; nem, sıcaklık ve bitkiye göre litre cinsinden öneri + verir. “Onayla” dediğinde sulama programına düşer; tamamlandı/iptal ile takip edersin.

+
+
+ +
+
+
🦠
+
+

Hastalık Tanısı

+

Yapraktan teşhis, saniyeler içinde.

+
+
+
+

Yaprak fotoğrafını yükle; model hastalığı, güven skorunu ve şiddeti + döndürür. Sonuç tarla geçmişine işlenir — erken önlem şansı doğar.

+
+
+ +
+
+
🌱
+
+

Gübre Önerisi

+

Eksik besini kg cinsinden gör.

+
+
+
+

Bitki, alan ve mevcut N-P-K değerlerini gir; eksikleri hesaplar + + Türkçe açıklamayla “neden bu kadar” der. 17 bitki türü desteklenir.

+
+
+ +
+
+
🔔
+
+

Akıllı Bildirimler

+

Önemli her şey tek çanda.

+
+
+
+

Düşük nem, açık uyarı, yeni hastalık — hepsi çanda toplanır. “Kontrol et” + tarlaları tarar, tekrar etmeyen (dedup) hatırlatmalar üretir.

+
+
+ + +
+
+

Tarlanı tahmin etmeyi bırak.

+ +
+
+
+
- -
-
- SFDAP -

Toprağın altında, veriye dönüşen tarım

-

Sensör, hava durumu ve yapay zekâ tahminlerini tek panelde - birleştiren çiftçi-odaklı akıllı tarım karar-destek platformu. Aşağı in, keşfet.

-
-
+ +
diff --git a/frontend/src/main.js b/frontend/src/main.js index a14202f..20cc69a 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -266,6 +266,40 @@ async function init() { }, { root: welcome, threshold: 0.4 }); reveals.forEach((el) => io.observe(el)); })(); + // Dolum YAYLI snap (#2): act1 içinde scroll DURUNCA, yeterince çekildiyse başlığa + // YAYLANARAK (overshoot) oturur; az çekildiyse yüzeye geri döner (tersinir). + // Native scroll-snap yay yapamadığı için JS. Özellik sahneleri native snap'te kalır. + (() => { + const welcome = document.getElementById("welcome"); + const act1 = document.querySelector(".welcome-act1"); + if (!welcome || !act1) return; + let timer = null, animating = false, cancelled = false; + // easeOutBack: hedefi biraz aşıp geri oturur = "yay/zıplama" + const easeOutBack = (p) => { const c1 = 1.7, c3 = c1 + 1; return 1 + c3 * (p - 1) ** 3 + c1 * (p - 1) ** 2; }; + const springTo = (target) => { + animating = true; cancelled = false; + const start = welcome.scrollTop, dist = target - start, dur = 480, t0 = performance.now(); + const step = (now) => { + if (cancelled) { animating = false; return; } + const p = Math.min((now - t0) / dur, 1); + welcome.scrollTop = start + dist * easeOutBack(p); + if (p < 1) requestAnimationFrame(step); + else { welcome.scrollTop = target; animating = false; } + }; + requestAnimationFrame(step); + }; + const settle = () => { + if (animating) return; + const travel = Math.max(act1.offsetHeight - welcome.clientHeight, 1); + const st = welcome.scrollTop; + if (st <= 2 || st >= travel) return; // yüzeyde ya da dolum tamam → native/serbest + const target = (st / travel) < 0.22 ? 0 : travel; // tersinir: az çekince→yüzey, yeterli→başlık + if (Math.abs(st - target) > 3) springTo(target); + }; + welcome.addEventListener("scroll", () => { clearTimeout(timer); timer = setTimeout(settle, 130); }, { passive: true }); + welcome.addEventListener("wheel", () => { if (animating) cancelled = true; }, { passive: true }); + welcome.addEventListener("touchstart", () => { if (animating) cancelled = true; }, { passive: true }); + })(); // Tema (light/dark) initTheme(); diff --git a/frontend/src/styles/welcome.css b/frontend/src/styles/welcome.css index 26c1ebb..ae5f483 100644 --- a/frontend/src/styles/welcome.css +++ b/frontend/src/styles/welcome.css @@ -25,7 +25,7 @@ /* 📌 act1: sticky pin'in dolum-fazı boyunca TAKILI kalması için yüksek sarmal. pin 100vh + act1 200vh → pin tam 100vh scroll boyunca takılı (dolum fazı), sonra serbest. */ -.welcome-act1 { position: relative; height: 200vh; flex-shrink: 0; } +.welcome-act1 { position: relative; height: 150vh; flex-shrink: 0; } /* travel=50vh: hafif çekince dolar */ /* 📌 Yüzey sahnesi — STICKY: dolum fazında ekranda TAKILI kalır (slider tutulur); --rise ile toprak grubu düz yukarı çıkıp ekranı doldurur. --rise: JS yazana dek 0 (calc taban). */ .welcome-pin { @@ -71,8 +71,8 @@ } /* Başlık + logo: toprak üstlerine binmeden ÖNCE solup kaybolur (havada asılı kalmasın) */ .welcome-topbar, .welcome-hero { - opacity: calc(1 - var(--rise, 0) * 1.6); /* rise ≈ .625'te 0 */ - transform: translateY(calc(var(--rise, 0) * -40px)); + opacity: calc(1 - var(--rise, 0) * 2); /* rise ≈ .5'te 0 → fill-intro (0.5→) ile çakışmaz */ + transform: translateY(calc(var(--rise, 0) * -44px)); transition: none; /* scroll-linked, animasyon değil */ } @media (max-width: 560px) { /* mobilde höyük 165px → dolgu anchor'ı yükselir (yine tam dolum) */ @@ -363,14 +363,26 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl ════════════════════════════════════════════════════════════════════════ */ .welcome-underground { height: auto; } /* sahneler kendi yüksekliğinde (her biri ~1 ekran) */ -/* Scroll-snap: dolum (act1) serbest kayar; toprak-altı SAHNELERİ yaklaşınca kameraya oturur */ +/* Scroll-snap: özellik sahneleri native proximity ile oturur. Dolum→başlık ise + JS YAYLI snap ile (main.js) — native yay yapamadığı için (#2). */ .welcome { scroll-snap-type: y proximity; } +/* Dolum ÖDÜLÜ intro: dolu toprağın üstünde --rise'a bağlı belirip oturur (#2). + --rise 0.5→0.88 arası fade + yüksel; 0.88+ oturmuş. pin'le yukarı kayıp gider. */ +.welcome-fill-intro { + position: absolute; inset: 0; z-index: 4; pointer-events: none; + display: flex; flex-direction: column; align-items: center; justify-content: center; + text-align: center; padding: 0 30px; + opacity: clamp(0, calc((var(--rise, 0) - 0.5) * 2.6), 1); + transform: translateY(clamp(0px, calc((0.88 - var(--rise, 0)) * 60px), 34px)); +} +.welcome-fill-intro .wu-lead { max-width: 520px; } + /* Tam-ekran sahne: kart + karşı açıklama; zigzag data-side ile yön değişir */ .wu-scene { - min-height: 100vh; scroll-snap-align: center; + min-height: 80vh; scroll-snap-align: center; /* daha kısa → daha az boşluk */ display: flex; align-items: center; justify-content: center; - gap: clamp(26px, 5vw, 70px); padding: 9vh 30px; + gap: clamp(26px, 5vw, 70px); padding: 4vh 30px; max-width: 1080px; margin: 0 auto; } .wu-scene-center { flex-direction: column; text-align: center; } /* intro + cta: tek öğe, ortalı */ @@ -423,7 +435,7 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl /* Yaylı reveal: kart kendi tarafından, açıklama KARŞIDAN, intro/cta aşağıdan girer. Yön sınıfları (.from-l / .from-r / .from-b) main.js'te data-side'a göre eklenir. overshoot easing (1.56) = "yay/zıplama"; in-view = oturmuş. IO çıkışta sıfırlar (re-trigger). */ -.wu-reveal { opacity: 0; transition: opacity .5s ease, transform .75s cubic-bezier(.34, 1.56, .64, 1); } +.wu-reveal { opacity: 0; transition: opacity .32s ease, transform .5s cubic-bezier(.3, 1.5, .5, 1); } /* daha hızlı + punchy yay */ .wu-reveal.from-l { transform: translateX(-72px); } .wu-reveal.from-r { transform: translateX(72px); } .wu-reveal.from-b { transform: translateY(48px); } @@ -439,3 +451,45 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl .wu-scene .wu-card { text-align: left; } .wu-reveal.from-l, .wu-reveal.from-r { transform: translateY(44px); } } + +/* ════════════════════════════════════════════════════════════════════════ + FAZ 3 — TOPRAK-ALTI DEKOR: boş toprağı doldurur + üst/alt geçişi süsler + ════════════════════════════════════════════════════════════════════════ */ +.wu-decor { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; } +.wu-scene { position: relative; z-index: 1; } /* sahneler dekorun ÖNÜNDE */ +/* Doku: ince çakıl/kesek deseni (tüm toprak boyunca, çok hafif) */ +.wu-decor::before { + content: ""; position: absolute; inset: 0; + background-image: radial-gradient(circle, rgba(34, 22, 11, .5) 1px, transparent 1.7px); + background-size: 84px 78px; opacity: .5; +} +.wu-decor > svg, .wu-decor > .wu-pebble { position: absolute; } + +/* Kökler (yüzeyden sarkar → geçişi organik yapar) */ +.wu-root { width: clamp(52px, 6vw, 84px); opacity: .68; } +.wu-root path { animation: wuRootSway 7s ease-in-out infinite; transform-origin: top center; } + +/* Solucan */ +.wu-worm { width: clamp(94px, 11vw, 140px); opacity: .9; transform-origin: center; animation: wuWormWiggle 4.5s ease-in-out infinite; } + +/* Uyuyan köstebek */ +.wu-mole { width: clamp(94px, 11vw, 134px); opacity: .95; } +.wu-zzz { fill: #cbb89f; font-size: 15px; font-weight: 800; animation: wuZzz 3s ease-in-out infinite; } + +/* Filizlenen tohum */ +.wu-seed { width: clamp(19px, 2vw, 27px); opacity: .92; transform-origin: bottom center; animation: wuRootSway 6s ease-in-out infinite; } + +/* Çakıl / tohum */ +.wu-pebble { + width: var(--s, 11px); height: calc(var(--s, 11px) * .82); border-radius: 50%; + background: radial-gradient(circle at 38% 32%, #6b482a, #3a2613 72%); + box-shadow: 0 2px 4px rgba(0, 0, 0, .3), inset 0 1px 1px rgba(255, 255, 255, .12); + opacity: .8; +} + +@keyframes wuRootSway { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(1.6deg); } } +@keyframes wuWormWiggle { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-4px) rotate(-3.5deg); } } +@keyframes wuZzz { 0%, 100% { opacity: .25; } 50% { opacity: .9; } } +@media (prefers-reduced-motion: reduce) { + .wu-root path, .wu-worm, .wu-zzz { animation: none; } +} From 65912e58ba44851bf3d7ec45a86b7cf4540856ec Mon Sep 17 00:00:00 2001 From: Ohualtex <230626673+Ohualtex@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:49:04 +0300 Subject: [PATCH 10/11] =?UTF-8?q?feat(welcome):=20fullpage=20navigasyon=20?= =?UTF-8?q?+=20zengin=20toprak-alt=C4=B1=20dekor=20+=20d=C3=BCzeltmeler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Navigasyon: native scroll yerine FULLPAGE — her kaydırma/swipe/ok = tam 1 sayfa (yüzey→başlık→5 özellik→CTA), easeOutCubic ile smooth. Tek jest=tek sahne (momentum kilidi + garanti açılış → takılmaz). Sağda yan nav-dot'lar (toprakta görünür, tıkla→ o sahneye atla). Kart giriş/çıkışı zigzag yandan + çarpıcı yay (önceki geldiği gibi çıkar). Dekor: 5 farklı kök tasarımı, düzeltilmiş solucan ağzı, mantar/kurtçuk/su damlası, derinlikle taşlaşma (kayalar) + kristal/mineral (derin katman). Gradient nazik kademeli. Düzeltmeler: - Hard-refresh'te yüzey başlığı dolumda toprağın ÖNÜNDE kalıyordu → giriş animasyonu hero'nun çocuklarına taşındı; hero'nun scroll-fade opacity'si artık soldurur - Filiz pop'taki toprak-saçma (dirt) animasyonu kaldırıldı Co-Authored-By: Claude Opus 4.8 --- frontend/index.html | 183 +++++++++++++++++++++++--------- frontend/src/main.js | 132 +++++++++++++++-------- frontend/src/styles/welcome.css | 66 ++++++++---- 3 files changed, 263 insertions(+), 118 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 7d1c678..91d16ab 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -284,38 +284,80 @@

Toprağın altında, veriye dönüşen tarım<
- + diff --git a/frontend/src/main.js b/frontend/src/main.js index 20cc69a..510e662 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -245,60 +245,106 @@ async function init() { }, { passive: true }); update(); })(); - // Toprak-altı sahneleri: kart/açıklama yandan (zigzag), intro/cta aşağıdan YAYLI girer. - // Yön sınıfı data-side'a göre eklenir; IO toggle ile re-trigger (çıkınca sıfırlanır → teker teker). - (() => { - const welcome = document.getElementById("welcome"); - const reveals = document.querySelectorAll(".wu-reveal"); - if (!reveals.length) return; - reveals.forEach((el) => { - const side = el.closest(".wu-scene")?.dataset.side; - if (!side) el.classList.add("from-b"); // intro/cta: aşağıdan - else if (el.classList.contains("wu-card")) el.classList.add(side === "left" ? "from-l" : "from-r"); - else el.classList.add(side === "left" ? "from-r" : "from-l"); // açıklama: KARŞI taraf - }); - if (!welcome || !("IntersectionObserver" in window)) { - reveals.forEach((el) => el.classList.add("in-view")); - return; - } - const io = new IntersectionObserver((entries) => { - entries.forEach((e) => e.target.classList.toggle("in-view", e.isIntersecting)); - }, { root: welcome, threshold: 0.4 }); - reveals.forEach((el) => io.observe(el)); - })(); - // Dolum YAYLI snap (#2): act1 içinde scroll DURUNCA, yeterince çekildiyse başlığa - // YAYLANARAK (overshoot) oturur; az çekildiyse yüzeye geri döner (tersinir). - // Native scroll-snap yay yapamadığı için JS. Özellik sahneleri native snap'te kalır. + // Kart/açıklama giriş YÖNÜ (zigzag, data-side): kart kendi tarafından, açıklama karşıdan. + document.querySelectorAll(".wu-reveal").forEach((el) => { + const side = el.closest(".wu-scene")?.dataset.side; + if (!side) el.classList.add("from-b"); + else if (el.classList.contains("wu-card")) el.classList.add(side === "left" ? "from-l" : "from-r"); + else el.classList.add(side === "left" ? "from-r" : "from-l"); // açıklama: KARŞI taraf + }); + + // FULLPAGE navigasyon: HER kaydırma = 1 sayfa (yüzey → başlık → özellikler → CTA), + // smooth YAY ile kayar. Aktif sahnenin kartı/açıklaması girer; önceki "geldiği gibi" çıkar. (() => { const welcome = document.getElementById("welcome"); const act1 = document.querySelector(".welcome-act1"); if (!welcome || !act1) return; - let timer = null, animating = false, cancelled = false; - // easeOutBack: hedefi biraz aşıp geri oturur = "yay/zıplama" - const easeOutBack = (p) => { const c1 = 1.7, c3 = c1 + 1; return 1 + c3 * (p - 1) ** 3 + c1 * (p - 1) ** 2; }; - const springTo = (target) => { - animating = true; cancelled = false; - const start = welcome.scrollTop, dist = target - start, dur = 480, t0 = performance.now(); + const scenes = [...welcome.querySelectorAll(".wu-scene")]; + let pages = [], page = 0, animating = false, locked = false, idleTimer = null, hardTimer = null; + const unlock = () => { locked = false; clearTimeout(hardTimer); }; + const maybeUnlock = () => { if (!animating) unlock(); }; // tekerlek durunca + animasyon bitince açılır + + const computePages = () => { + const ch = welcome.clientHeight, max = welcome.scrollHeight - ch; + const clamp = (v) => Math.max(0, Math.min(max, Math.round(v))); + const travel = Math.max(act1.offsetHeight - ch, 1); + const sceneTargets = scenes.map((s) => clamp(s.getBoundingClientRect().top + welcome.scrollTop + s.offsetHeight / 2 - ch / 2)); + pages = [0, clamp(travel), ...sceneTargets]; // 0=yüzey, 1=başlık, 2..=özellikler/CTA + }; + const updateReveals = () => { + scenes.forEach((s, i) => { + const active = page === i + 2; // sayfa i+2 → sahne i aktif + s.querySelectorAll(".wu-reveal").forEach((el) => el.classList.toggle("in-view", active)); + }); + }; + // Yan navigasyon noktaları — katmanlar arası HIZLI geçiş (toprağa inince görünür) + const dotsNav = document.createElement("nav"); + dotsNav.className = "wu-dots"; dotsNav.setAttribute("aria-label", "Sayfa navigasyonu"); + let dots = []; + const buildDots = () => { + dotsNav.innerHTML = ""; + dots = pages.map((_, i) => { + const b = document.createElement("button"); + b.type = "button"; b.className = "wu-dot"; b.setAttribute("aria-label", "Sayfa " + (i + 1)); + b.addEventListener("click", () => navTo(i)); + dotsNav.appendChild(b); return b; + }); + }; + const updateDots = () => { + dots.forEach((d, i) => d.classList.toggle("active", i === page)); + dotsNav.classList.toggle("visible", page >= 1); // yüzeyde gizli, toprakta görünür + }; + welcome.appendChild(dotsNav); + const ease = (p) => 1 - Math.pow(1 - p, 3); // easeOutCubic: yumuşak, TAŞMA YOK (sıçrama/jank yok) + const animateTo = (target) => { + animating = true; + const start = welcome.scrollTop, dist = target - start, dur = 520, t0 = performance.now(); const step = (now) => { - if (cancelled) { animating = false; return; } const p = Math.min((now - t0) / dur, 1); - welcome.scrollTop = start + dist * easeOutBack(p); + welcome.scrollTop = start + dist * ease(p); if (p < 1) requestAnimationFrame(step); - else { welcome.scrollTop = target; animating = false; } + else { welcome.scrollTop = target; animating = false; clearTimeout(idleTimer); idleTimer = setTimeout(maybeUnlock, 70); } }; requestAnimationFrame(step); }; - const settle = () => { - if (animating) return; - const travel = Math.max(act1.offsetHeight - welcome.clientHeight, 1); - const st = welcome.scrollTop; - if (st <= 2 || st >= travel) return; // yüzeyde ya da dolum tamam → native/serbest - const target = (st / travel) < 0.22 ? 0 : travel; // tersinir: az çekince→yüzey, yeterli→başlık - if (Math.abs(st - target) > 3) springTo(target); + const navTo = (target) => { + if (locked) return; + target = Math.max(0, Math.min(pages.length - 1, target)); + if (target === page) return; + locked = true; + clearTimeout(hardTimer); hardTimer = setTimeout(unlock, 900); // GARANTİ açılış → sürekli kaydırma takılmaz + page = target; + updateReveals(); // aktif kart girer, öncekiler "geldiği gibi" çıkar (aynı anda) + updateDots(); + animateTo(pages[page]); }; - welcome.addEventListener("scroll", () => { clearTimeout(timer); timer = setTimeout(settle, 130); }, { passive: true }); - welcome.addEventListener("wheel", () => { if (animating) cancelled = true; }, { passive: true }); - welcome.addEventListener("touchstart", () => { if (animating) cancelled = true; }, { passive: true }); + const go = (dir) => navTo(page + dir); + + computePages(); buildDots(); updateReveals(); updateDots(); + window.addEventListener("resize", () => { computePages(); buildDots(); updateDots(); welcome.scrollTop = pages[page]; }); + // TEK JEST = TEK SAHNE: nav sırasında + tekerlek durana dek kilitli (go locked'ı kontrol+set eder). + // HER wheel olayı idle sayacını erteler → tekerlek 90ms DURUNCA + animasyon bitince serbest kalır + // (maybeUnlock iki yoldan da çağrılır → asla takılı kalmaz). + welcome.addEventListener("wheel", (e) => { + e.preventDefault(); + clearTimeout(idleTimer); + idleTimer = setTimeout(maybeUnlock, 90); + go(e.deltaY > 0 ? 1 : -1); + }, { passive: false }); + let touchY = null; + welcome.addEventListener("touchstart", (e) => { touchY = e.touches[0].clientY; }, { passive: true }); + welcome.addEventListener("touchmove", (e) => { e.preventDefault(); }, { passive: false }); + welcome.addEventListener("touchend", (e) => { + if (touchY === null) return; + const dy = touchY - e.changedTouches[0].clientY; + if (Math.abs(dy) > 42) go(dy > 0 ? 1 : -1); + touchY = null; + }, { passive: true }); + window.addEventListener("keydown", (e) => { + if (!welcome.isConnected || getComputedStyle(welcome).display === "none") return; + if (e.key === "ArrowDown" || e.key === "PageDown") { e.preventDefault(); go(1); } + else if (e.key === "ArrowUp" || e.key === "PageUp") { e.preventDefault(); go(-1); } + }); })(); // Tema (light/dark) diff --git a/frontend/src/styles/welcome.css b/frontend/src/styles/welcome.css index ae5f483..0e2d709 100644 --- a/frontend/src/styles/welcome.css +++ b/frontend/src/styles/welcome.css @@ -42,9 +42,10 @@ height: 200vh; position: relative; z-index: 4; /* soil(3)+filiz(2) üstünde → bindirme seam'i + Filiz sızmasını örter */ margin-top: -8px; /* pin altıyla hafif bindirme → sub-pixel boşluk/sızma kapanır (dikişsiz) */ - /* üst = soil SVG dibi (#4e3119) → dikişsiz; aşağı YUMUŞAK koyulaşır (sert orta-bant yok, - dip çok kararmıyor → üst/alt farkı azaldı, daha smooth) */ - background: linear-gradient(180deg, #4e3119 0%, #46291a 30%, #3c2415 60%, #34220f 100%); + /* üst = soil SVG dibi (#4e3119) → dikişsiz; KADEMELİ ve nazik koyulaşır (sert bant yok), + dip hafif cooler/grayer (taşımsı derin toprak hissi). */ + background: linear-gradient(180deg, + #4e3119 0%, #4a2f1a 16%, #432b18 34%, #3a2616 54%, #2f2114 74%, #261c14 100%); } /* ─── SCROLLYTELLING: toprak DÜZ yukarı çıkıp ekranı doldurur (sticky pin) ───── @@ -201,11 +202,11 @@ display: flex; flex-direction: column; align-items: center; text-align: center; padding: 8px 24px 0; max-width: 760px; margin: 0 auto; z-index: 5; pointer-events: none; /* dar ekranda güneş/ay üstüne taşmasın; buton tekrar açılır */ - /* Madde 2: aşağıdan kayarak geliş — biraz daha uzun + biraz daha mesafe + gecikme. - backwards (both DEĞİL): bitince to-state'i TUTMAZ → scroll-fade calc'ı (opacity/ - transform var(--rise)) devralır; aksi halde both, fade'i ezerdi. */ - animation: welcomeFadeUp 1.25s cubic-bezier(.22,1,.36,1) .35s backwards; + /* DİKKAT: giriş animasyonu hero'da DEĞİL (aşağıda çocuklarda). Hero'da olursa animasyon + hero'nun scroll-fade opacity calc'ını EZER → dolumda başlık toprağın önünde kalır (#1 bug). */ } +/* Giriş animasyonu hero'nun ÇOCUKLARINDA → hero'nun opacity calc'ı (scroll-fade) tüm hero'yu soldurur */ +.welcome-title, .welcome-login-btn { animation: welcomeFadeUp 1.25s cubic-bezier(.22, 1, .36, 1) .35s both; } .welcome-title { font-size: clamp(1.9rem, 5.4vw, 3.2rem); font-weight: 800; line-height: 1.1; margin: 0; letter-spacing: -.6px; color: var(--text); } .welcome-title span { background: linear-gradient(120deg, #10b981, #34d399); @@ -320,12 +321,8 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl } .welcome-soil-svg { width: 100%; height: 100%; display: block; } -/* Topraktan fırlayan toz/toprak parçacıkları (pop'ta) */ -.welcome-dirt { - position: absolute; left: 50%; bottom: 150px; width: 9px; height: 9px; border-radius: 50%; - background: #6b4423; opacity: 0; z-index: 5; pointer-events: none; -} -.welcome-stage.popped .welcome-dirt { animation: dirtBurst .85s ease-out forwards; animation-delay: var(--delay, 0s); } +/* Topraktan fırlayan toz parçacıkları KALDIRILDI (#2 — pop'ta dağılan toprak animasyonu istenmiyor) */ +.welcome-dirt { display: none; } @keyframes dirtBurst { 0% { opacity: 0; transform: translate(-50%, 0) scale(.4); } 15% { opacity: 1; } @@ -363,9 +360,7 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl ════════════════════════════════════════════════════════════════════════ */ .welcome-underground { height: auto; } /* sahneler kendi yüksekliğinde (her biri ~1 ekran) */ -/* Scroll-snap: özellik sahneleri native proximity ile oturur. Dolum→başlık ise - JS YAYLI snap ile (main.js) — native yay yapamadığı için (#2). */ -.welcome { scroll-snap-type: y proximity; } +/* Navigasyon FULLPAGE (main.js): her kaydırma = 1 sayfa, smooth yay. Native scroll-snap YOK. */ /* Dolum ÖDÜLÜ intro: dolu toprağın üstünde --rise'a bağlı belirip oturur (#2). --rise 0.5→0.88 arası fade + yüksel; 0.88+ oturmuş. pin'le yukarı kayıp gider. */ @@ -380,7 +375,7 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl /* Tam-ekran sahne: kart + karşı açıklama; zigzag data-side ile yön değişir */ .wu-scene { - min-height: 80vh; scroll-snap-align: center; /* daha kısa → daha az boşluk */ + min-height: 100vh; /* fullpage: her sahne 1 ekran */ display: flex; align-items: center; justify-content: center; gap: clamp(26px, 5vw, 70px); padding: 4vh 30px; max-width: 1080px; margin: 0 auto; @@ -435,10 +430,10 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl /* Yaylı reveal: kart kendi tarafından, açıklama KARŞIDAN, intro/cta aşağıdan girer. Yön sınıfları (.from-l / .from-r / .from-b) main.js'te data-side'a göre eklenir. overshoot easing (1.56) = "yay/zıplama"; in-view = oturmuş. IO çıkışta sıfırlar (re-trigger). */ -.wu-reveal { opacity: 0; transition: opacity .32s ease, transform .5s cubic-bezier(.3, 1.5, .5, 1); } /* daha hızlı + punchy yay */ -.wu-reveal.from-l { transform: translateX(-72px); } -.wu-reveal.from-r { transform: translateX(72px); } -.wu-reveal.from-b { transform: translateY(48px); } +.wu-reveal { opacity: 0; transition: opacity .42s ease, transform .62s cubic-bezier(.3, 1.55, .5, 1); } /* çarpıcı + güçlü yay */ +.wu-reveal.from-l { transform: translateX(-120px) scale(.9); } +.wu-reveal.from-r { transform: translateX(120px) scale(.9); } +.wu-reveal.from-b { transform: translateY(56px) scale(.94); } .wu-reveal.in-view.in-view { opacity: 1; transform: none; } /* çift sınıf → mobil override dahil her zaman kazanır */ @media (prefers-reduced-motion: reduce) { @@ -478,6 +473,17 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl /* Filizlenen tohum */ .wu-seed { width: clamp(19px, 2vw, 27px); opacity: .92; transform-origin: bottom center; animation: wuRootSway 6s ease-in-out infinite; } +/* Mantar / kurtçuk / nem damlası / kaya */ +.wu-mushroom { width: clamp(40px, 4.2vw, 58px); opacity: .94; transform-origin: bottom center; animation: wuRootSway 6.5s ease-in-out infinite; } +.wu-grub { width: clamp(38px, 4vw, 54px); opacity: .9; transform-origin: center; animation: wuWormWiggle 5.5s ease-in-out infinite; } +.wu-drop { width: clamp(13px, 1.6vw, 19px); opacity: .85; transform-origin: top center; animation: wuDrop 3.6s ease-in-out infinite; } +.wu-rock { opacity: .9; } +/* Kristal / mineral pırıltısı (derin katman) */ +.wu-crystal { width: clamp(28px, 3vw, 40px); opacity: .82; animation: wuCrystal 4s ease-in-out infinite; filter: drop-shadow(0 0 6px rgba(160, 130, 220, .35)); } +@keyframes wuCrystal { 0%, 100% { filter: drop-shadow(0 0 5px rgba(160, 130, 220, .3)); } 50% { filter: drop-shadow(0 0 11px rgba(160, 130, 220, .6)); } } +.wu-mineral { width: 6px; height: 6px; border-radius: 50%; background: var(--c, #c9a3f0); box-shadow: 0 0 9px 1px var(--c, #c9a3f0); opacity: .65; animation: wuMineral 3s ease-in-out infinite; } +@keyframes wuMineral { 0%, 100% { opacity: .35; transform: scale(.85); } 50% { opacity: .8; transform: scale(1.12); } } +@keyframes wuDrop { 0%, 100% { transform: translateY(0) scaleY(1); } 55% { transform: translateY(3px) scaleY(1.04); } } /* Çakıl / tohum */ .wu-pebble { @@ -491,5 +497,21 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl @keyframes wuWormWiggle { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-4px) rotate(-3.5deg); } } @keyframes wuZzz { 0%, 100% { opacity: .25; } 50% { opacity: .9; } } @media (prefers-reduced-motion: reduce) { - .wu-root path, .wu-worm, .wu-zzz { animation: none; } + .wu-root path, .wu-worm, .wu-zzz, .wu-seed, .wu-mushroom, .wu-grub, .wu-drop, .wu-crystal, .wu-mineral { animation: none; } +} + +/* ─── YAN NAVİGASYON NOKTALARI — katmanlar arası hızlı geçiş (toprakta görünür) ───── */ +.wu-dots { + position: fixed; right: 22px; top: 50%; transform: translateY(-50%); + display: flex; flex-direction: column; gap: 13px; z-index: 1100; + opacity: 0; pointer-events: none; transition: opacity .45s ease; +} +.wu-dots.visible { opacity: 1; pointer-events: auto; } +.wu-dot { + width: 11px; height: 11px; border-radius: 50%; padding: 0; cursor: pointer; + background: rgba(245, 236, 216, .28); border: 1.5px solid rgba(245, 236, 216, .5); + transition: background .3s ease, transform .3s ease, border-color .3s ease, box-shadow .3s ease; } +.wu-dot:hover { background: rgba(245, 236, 216, .6); transform: scale(1.25); } +.wu-dot.active { background: #8fe388; border-color: #8fe388; transform: scale(1.35); box-shadow: 0 0 10px rgba(143, 227, 136, .55); } +@media (max-width: 560px) { .wu-dots { right: 12px; gap: 11px; } .wu-dot { width: 9px; height: 9px; } } From eb25d14c3ab065a39fc2c78a74420a37c64ac42a Mon Sep 17 00:00:00 2001 From: Ohualtex <230626673+Ohualtex@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:36:06 +0300 Subject: [PATCH 11/11] =?UTF-8?q?feat(welcome):=20kartlar=C4=B1=20b=C3=BCy?= =?UTF-8?q?=C3=BCt=20+=20zenginle=C5=9Ftir=20(mini-g=C3=B6rseller)=20+=20d?= =?UTF-8?q?ekoru=20kartlar=C4=B1n=20arkas=C4=B1ndan=20=C3=A7=C4=B1kar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 5 özellik kartı dikey "ikon+başlık → mini-görsel → alt-bilgi" yapısına büyütüldü (~110px → ~260px) - Her karta özelliğe-özgü mini-görsel (workflow ile paralel tasarlandı, --accent renkli): - 💧 nem: 24-saat sparkline + kuru eşik + %22 - 🚿 sulama: ≈240 L + damla pip + su-dolum çubuğu - 🦠 hastalık: yaprak + leaf_rust + %84 güven halkası - 🌱 gübre: N-P-K çubuk grafik (80/40/50 kg/ha) - 🔔 bildirim: 3-satır liste (kritik/orta/tamam noktaları) - Koyu/derin toprakta kontrast: kart tabanı güçlendirildi (accent %24 + beyaz %7 + koyu taban + parlak kenar) - 7 dekor öğesi kartların/CTA'nın arkasından dikey boşluklara taşındı (kök, mantar, damla, köstebek, kaya, 2 çakıl) - Sonuç: 5 kart + CTA = 0 dekor çakışması (gerçek-dinlenme pozisyonunda ölçülerek doğrulandı) Co-Authored-By: Claude Opus 4.8 --- frontend/index.html | 195 ++++++++++++++++++++++++++------ frontend/src/styles/welcome.css | 175 ++++++++++++++++++++++++++-- 2 files changed, 330 insertions(+), 40 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 91d16ab..dcfe289 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -303,7 +303,7 @@

Toprağın altında, veriye dönüşen tarım< - + @@ -332,7 +332,7 @@

Toprağın altında, veriye dönüşen tarım< - + @@ -346,7 +346,7 @@

Toprağın altında, veriye dönüşen tarım< - + @@ -356,7 +356,7 @@

Toprağın altında, veriye dönüşen tarım< - + @@ -392,7 +392,7 @@

Toprağın altında, veriye dönüşen tarım< - + @@ -413,11 +413,11 @@

Toprağın altında, veriye dönüşen tarım< - + - + @@ -449,11 +449,28 @@

Toprağın altında, veriye dönüşen tarım<
-
💧
-
-

Toprak Nemi & Sensörler

-

Nemi 7/24 ölç, susuzluğu erken yakala.

-
+
+
💧
+

Toprak Nemi & Sensörler

Nemi 7/24 ölç, susuzluğu erken yakala.

+
+
+ + + + + + + + + + +
%22nem
+
kuru eşik
+
+
+ 🥵 Susuz + son 24 sa +

Tarlana gömülü sensörler nemi sürekli okur; eşik altına düşünce panelinde @@ -463,11 +480,45 @@

Toprak Nemi & Sensörler

-
🚿
-
-

Akıllı Sulama

-

Ne kadar su? Model söylesin.

-
+
+
🚿
+

Akıllı Sulama

Ne kadar su? Model söylesin.

+
+
+
+
+ ≈ 240L + önerilen miktar +
+ + + + + + + + + + + + + + + + + + + + + 0 L + ~380 L + +
+
+
+ 🌲 RandomForest + ⚡ tek tıkla programa +

RandomForest modeli; nem, sıcaklık ve bitkiye göre litre cinsinden öneri @@ -477,11 +528,46 @@

Akıllı Sulama

-
🦠
-
-

Hastalık Tanısı

-

Yapraktan teşhis, saniyeler içinde.

-
+
+
🦠
+

Hastalık Tanısı

Yapraktan teşhis, saniyeler içinde.

+
+
+ + + + + + + + + + + + + + teşhis + leaf_rust + + + + + + %84 + güven + + +
+
+ şiddet: orta + yaprak fotoğrafı +

Yaprak fotoğrafını yükle; model hastalığı, güven skorunu ve şiddeti @@ -491,11 +577,34 @@

Hastalık Tanısı

-
🌱
-
-

Gübre Önerisi

-

Eksik besini kg cinsinden gör.

-
+
+
🌱
+

Gübre Önerisi

Eksik besini kg cinsinden gör.

+
+
+ + + + + + 80 + N + + + + 40 + P + + + + 50 + K + +
+
+ 🌾 17 bitki + kg/ha +

Bitki, alan ve mevcut N-P-K değerlerini gir; eksikleri hesaplar + @@ -505,11 +614,33 @@

Gübre Önerisi

-
🔔
-
-

Akıllı Bildirimler

-

Önemli her şey tek çanda.

-
+
+
🔔
+

Akıllı Bildirimler

Önemli her şey tek çanda.

+
+
+
    +
  • + + Tarla A susuz + kritik +
  • +
  • + + Yeni tanı: leaf_rust + orta +
  • +
  • + + Sulama tamamlandı + tamam +
  • +
+
+
+ dedup'lı + 3 yeni +

Düşük nem, açık uyarı, yeni hastalık — hepsi çanda toplanır. “Kontrol et” diff --git a/frontend/src/styles/welcome.css b/frontend/src/styles/welcome.css index 0e2d709..ee4da12 100644 --- a/frontend/src/styles/welcome.css +++ b/frontend/src/styles/welcome.css @@ -403,12 +403,12 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl /* Renkli liquid glass kart (kahverengi toprak üstünde accent-tint buzlu cam) */ .wu-card { position: relative; overflow: hidden; - display: flex; align-items: center; gap: 18px; - padding: 22px 24px 22px 28px; border-radius: 22px; - background: linear-gradient(150deg, color-mix(in srgb, var(--accent) 20%, transparent), rgba(255, 255, 255, .03)); + display: flex; flex-direction: column; gap: 14px; /* dikey: head + mini-görsel + alt-bilgi */ + padding: 22px 24px; border-radius: 22px; + background: linear-gradient(150deg, color-mix(in srgb, var(--accent) 24%, transparent), rgba(255, 255, 255, .07)), rgba(40, 28, 18, .32); backdrop-filter: blur(18px) saturate(1.7); -webkit-backdrop-filter: blur(18px) saturate(1.7); - border: 1px solid color-mix(in srgb, var(--accent) 30%, rgba(255, 255, 255, .14)); + border: 1px solid color-mix(in srgb, var(--accent) 34%, rgba(255, 255, 255, .2)); box-shadow: 0 26px 64px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(255, 255, 255, .26), 0 0 70px -18px var(--accent, #fff); /* karta özgü renkli ışıma */ @@ -417,14 +417,27 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--accent); opacity: .9; } +.wu-card-head { display: flex; align-items: center; gap: 14px; } .wu-card-ic { - flex-shrink: 0; width: 58px; height: 58px; border-radius: 16px; - display: grid; place-items: center; font-size: 1.85rem; + flex-shrink: 0; width: 52px; height: 52px; border-radius: 15px; + display: grid; place-items: center; font-size: 1.7rem; background: color-mix(in srgb, var(--accent) 24%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 48%, transparent); } -.wu-card-body h3 { margin: 0 0 5px; font-size: 1.16rem; font-weight: 700; color: #fff; } -.wu-card-body p { margin: 0; font-size: .92rem; line-height: 1.5; color: rgba(255, 255, 255, .8); } +.wu-card-htx { min-width: 0; } +.wu-card-htx h3 { margin: 0 0 3px; font-size: 1.13rem; font-weight: 700; color: #fff; } +.wu-card-htx p { margin: 0; font-size: .88rem; line-height: 1.45; color: rgba(255, 255, 255, .78); } +/* Mini-görsel alanı + alt-bilgi çipleri (per-card stiller dosya SONUNDA) */ +.wu-card-viz { position: relative; width: 100%; min-height: 84px; } +.wu-card-viz > svg { display: block; width: 100%; } +.wu-card-foot { display: flex; flex-wrap: wrap; gap: 8px; } +.wu-chip { + display: inline-flex; align-items: center; gap: 4px; padding: 4px 11px; border-radius: 999px; + font-size: .72rem; font-weight: 600; line-height: 1.4; color: rgba(255, 255, 255, .9); + background: color-mix(in srgb, var(--accent) 16%, transparent); + border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent); +} +.wu-chip.wu-chip-soft { background: rgba(255, 255, 255, .07); border-color: rgba(255, 255, 255, .14); } .wu-cta { text-align: center; } /* Yaylı reveal: kart kendi tarafından, açıklama KARŞIDAN, intro/cta aşağıdan girer. @@ -515,3 +528,149 @@ html:not([data-theme="light"]) .welcome-intro .wstar-descend { transform: transl .wu-dot:hover { background: rgba(245, 236, 216, .6); transform: scale(1.25); } .wu-dot.active { background: #8fe388; border-color: #8fe388; transform: scale(1.35); box-shadow: 0 0 10px rgba(143, 227, 136, .55); } @media (max-width: 560px) { .wu-dots { right: 12px; gap: 11px; } .wu-dot { width: 9px; height: 9px; } } + +/* ════════════════════════════════════════════════════════════════ + KART MİNİ-GÖRSELLERİ (per-card) — nem sparkline · sulama litre+bar · + hastalık yaprak+güven · gübre NPK · bildirim liste (workflow tasarımı) + ════════════════════════════════════════════════════════════════ */ +/* ── nem ── */ +.wu-card-viz .nem-spark { display: block; width: 100%; height: 100%; overflow: visible; } +.wu-card-viz .nem-area { fill: color-mix(in srgb, var(--accent) 22%, transparent); stroke: none; } +.wu-card-viz .nem-line { fill: none; stroke: var(--accent); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 1px 4px color-mix(in srgb, var(--accent) 60%, transparent)); } +.wu-card-viz .nem-thr { stroke: rgba(255, 255, 255, .42); stroke-width: 1; stroke-dasharray: 4 4; } +.wu-card-viz .nem-dot { fill: #fff; stroke: var(--accent); stroke-width: 2; } +.wu-card-viz .nem-read { position: absolute; top: 4px; right: 6px; display: flex; align-items: baseline; gap: 4px; line-height: 1; } +.wu-card-viz .nem-read b { font-size: 1.18rem; font-weight: 800; color: #fff; letter-spacing: -.3px; } +.wu-card-viz .nem-read span { font-size: .62rem; color: rgba(255, 255, 255, .6); } +.wu-card-viz .nem-thr-tag { position: absolute; left: 4px; top: calc(58 / 84 * 100% - 14px); font-size: .58rem; letter-spacing: .2px; color: rgba(255, 255, 255, .6); text-transform: uppercase; } + +/* ── sulama ── */ +.wu-card-viz .sulama-read { + display: flex; align-items: center; gap: 14px; width: 100%; height: 84px; +} +.wu-card-viz .sulama-num { + flex: 0 0 auto; display: flex; align-items: baseline; flex-wrap: wrap; + gap: 3px 4px; line-height: 1; min-width: 92px; +} +.wu-card-viz .sulama-amt { + font-size: 2.05rem; font-weight: 800; letter-spacing: -.5px; + color: var(--accent); + text-shadow: 0 0 18px color-mix(in srgb, var(--accent) 55%, transparent); +} +.wu-card-viz .sulama-unit { + font-size: 1rem; font-weight: 700; + color: color-mix(in srgb, var(--accent) 78%, #fff); +} +.wu-card-viz .sulama-cap { + flex-basis: 100%; margin-top: 5px; + font-size: .66rem; font-weight: 600; letter-spacing: .3px; + text-transform: uppercase; color: rgba(255,255,255,.6); +} +.wu-card-viz .sulama-svg { flex: 1 1 auto; min-width: 0; height: 84px; } +/* droplet pips */ +.wu-card-viz .sulama-pips .sp.on { fill: var(--accent); } +.wu-card-viz .sulama-pips .sp.half { + fill: color-mix(in srgb, var(--accent) 42%, transparent); + stroke: color-mix(in srgb, var(--accent) 70%, transparent); stroke-width: 1; +} +.wu-card-viz .sulama-pips .sp.off { + fill: rgba(255,255,255,.08); + stroke: rgba(255,255,255,.22); stroke-width: 1; +} +/* water-fill bar */ +.wu-card-viz .sulama-track { fill: rgba(255,255,255,.08); } +.wu-card-viz .sulama-frame { fill: none; stroke: rgba(255,255,255,.16); stroke-width: 1; } +.wu-card-viz .sulama-fill { + fill: color-mix(in srgb, var(--accent) 82%, transparent); +} +.wu-card-viz .sulama-wave { + fill: color-mix(in srgb, var(--accent) 82%, transparent); + animation: sulama-flow 2.6s ease-in-out infinite; +} +@keyframes sulama-flow { + 0%,100% { transform: translateX(0); } + 50% { transform: translateX(-4px); } +} +@media (prefers-reduced-motion: reduce) { + .wu-card-viz .sulama-wave { animation: none; } +} +/* ticks */ +.wu-card-viz .sulama-tick { + fill: rgba(255,255,255,.5); font-size: 9px; font-weight: 600; + font-family: inherit; +} +.wu-card-viz .sulama-tick-r { text-anchor: end; } + +/* ── hastalik ── */ +.wu-card-viz .hastalik-viz { display: block; width: 100%; height: auto; overflow: visible; } +.wu-card-viz .hastalik-leaf { transform-origin: 44px 40px; animation: hastalikLeafSway 5s ease-in-out infinite; } +@keyframes hastalikLeafSway { 0%, 100% { transform: rotate(-2deg); } 50% { transform: rotate(2.5deg); } } +.wu-card-viz .hastalik-lbl-muted { fill: rgba(255,255,255,.6); font-size: 11px; font-weight: 600; letter-spacing: .4px; text-transform: uppercase; font-family: inherit; } +.wu-card-viz .hastalik-lbl-main { fill: #fff; font-size: 18px; font-weight: 800; font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; letter-spacing: -.2px; } +.wu-card-viz .hastalik-arc { animation: hastalikArc 1.1s cubic-bezier(.3, 1, .4, 1) .15s both; } +@keyframes hastalikArc { from { stroke-dashoffset: 163.4; } to { stroke-dashoffset: 26.1; } } +.wu-card-viz .hastalik-pct { fill: #fff; font-size: 16px; font-weight: 800; text-anchor: middle; font-family: inherit; } +.wu-card-viz .hastalik-pct-sub { fill: rgba(255,255,255,.6); font-size: 8px; font-weight: 600; text-anchor: middle; letter-spacing: .6px; text-transform: uppercase; font-family: inherit; } +@media (prefers-reduced-motion: reduce) { + .wu-card-viz .hastalik-leaf, .wu-card-viz .hastalik-arc { animation: none; } + .wu-card-viz .hastalik-arc { stroke-dashoffset: 26.1; } +} + +/* ── gubre ── */ +.wu-card-viz .gubre-npk { display:block; width:100%; height:auto; overflow:visible; } +.wu-card-viz .gubre-base { stroke:rgba(255,255,255,.18); stroke-width:1.5; stroke-linecap:round; } +.wu-card-viz .gubre-track { fill:rgba(255,255,255,.06); } +.wu-card-viz .gubre-bar { stroke:color-mix(in srgb, var(--accent) 55%, transparent); stroke-width:1; } +.wu-card-viz .gubre-n { fill:var(--accent); } +.wu-card-viz .gubre-p { fill:color-mix(in srgb, var(--accent) 45%, transparent); } +.wu-card-viz .gubre-k { fill:color-mix(in srgb, var(--accent) 68%, transparent); } +.wu-card-viz .gubre-val { fill:#fff; font:700 12px/1 system-ui,sans-serif; text-anchor:middle; } +.wu-card-viz .gubre-lab { fill:rgba(255,255,255,.6); font:600 11px/1 system-ui,sans-serif; text-anchor:middle; letter-spacing:.5px; } + +/* ── bildirim ── */ +.wu-card-viz .bildirim-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 6px; + width: 100%; +} +.wu-card-viz .bildirim-row { + display: flex; + align-items: center; + gap: 9px; + padding: 5px 9px; + border-radius: 9px; + background: color-mix(in srgb, var(--accent) 12%, rgba(255, 255, 255, .03)); + border: 1px solid color-mix(in srgb, var(--accent) 18%, transparent); +} +.wu-card-viz .bildirim-dot { + flex: 0 0 auto; + width: 9px; + height: 9px; + border-radius: 50%; + box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 26%, transparent); +} +.wu-card-viz .bildirim-dot--crit { color: #f87171; background: #f87171; } +.wu-card-viz .bildirim-dot--warn { color: #fbbf24; background: #fbbf24; } +.wu-card-viz .bildirim-dot--ok { color: #4ade80; background: #4ade80; } +.wu-card-viz .bildirim-txt { + flex: 1 1 auto; + min-width: 0; + font-size: .82rem; + line-height: 1.2; + color: rgba(255, 255, 255, .88); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.wu-card-viz .bildirim-tag { + flex: 0 0 auto; + font-size: .64rem; + font-weight: 600; + letter-spacing: .02em; + text-transform: uppercase; + color: rgba(255, 255, 255, .55); +}