From 79bd32f46e1f33bda5b569d9d1ad24147fa1052f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=85=BE=E8=AE=AFWorkBuddy?= Date: Thu, 20 Aug 2026 16:40:22 +0800 Subject: [PATCH] =?UTF-8?q?B-229:=20=E7=99=BB=E5=BD=95=E9=A1=B5=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=A1=8C=E9=9D=A2=E5=8F=8C=E5=8C=BA=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E5=B9=B6=E6=94=B6=E7=B4=A7=E4=B8=89=E6=A1=A3=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把居中手机卡改回横向双区,端口选择复用分段控件节奏,避免再叠加一套登录样式。 Co-authored-by: Cursor --- tests/test_login_frontend.py | 75 ++++++++++++ tests/test_login_layout.py | 222 +++++++++++++++++++++++++++++++++++ web/index.html | 27 +++-- web/styles.css | 101 ++++++++-------- 4 files changed, 364 insertions(+), 61 deletions(-) create mode 100644 tests/test_login_frontend.py create mode 100644 tests/test_login_layout.py diff --git a/tests/test_login_frontend.py b/tests/test_login_frontend.py new file mode 100644 index 0000000..2ea5f1f --- /dev/null +++ b/tests/test_login_frontend.py @@ -0,0 +1,75 @@ +"""Static contracts for the B-229 login desktop layout.""" + +from __future__ import annotations + +from pathlib import Path +import re +import unittest + +ROOT = Path(__file__).resolve().parents[1] +WEB = ROOT / "web" + + +class LoginFrontendContractTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.html = (WEB / "index.html").read_text(encoding="utf-8") + cls.css = (WEB / "styles.css").read_text(encoding="utf-8") + cls.js = (WEB / "app.js").read_text(encoding="utf-8") + + def test_dual_zone_markup_and_auth_controls(self) -> None: + self.assertIn('class="entry-context"', self.html) + self.assertIn('class="entry-form-wrap"', self.html) + self.assertIn('id="loginForm"', self.html) + self.assertIn('id="togglePassword"', self.html) + self.assertIn('id="changePassword"', self.html) + self.assertIn('id="loginError"', self.html) + self.assertIn('name="username"', self.html) + self.assertIn('name="password"', self.html) + self.assertIn('name="new_password"', self.html) + self.assertIn('name="confirm_password"', self.html) + self.assertIn('value="admin"', self.html) + self.assertIn('value="company"', self.html) + self.assertIn("总账管理端", self.html) + self.assertIn("公司业务端", self.html) + self.assertIn("服务状态", self.html) + self.assertIn("当前账期", self.html) + self.assertNotIn("entry-card", self.html) + self.assertNotIn('value="group-admin"', self.html) + self.assertNotIn('value="demo123456"', self.html) + + def test_login_css_lives_in_one_section(self) -> None: + self.assertIn("minmax(460px, 520px)", self.css) + self.assertIn("width: min(440px, 100%)", self.css) + self.assertIn("@media (max-width: 1023px)", self.css) + self.assertIn("@media (max-width: 720px)", self.css) + self.assertIn("minmax(400px, 440px)", self.css) + self.assertNotIn(".entry-card", self.css) + self.assertNotIn("0 0 60px rgba(55, 235, 137, 0.06)", self.css) + login_block = self.css.split("/* Login */", 1)[1] + after_login = login_block.split("/* ---- B-44", 1)[0] if "/* ---- B-44" in login_block else login_block + self.assertEqual(1, after_login.count(".entry-shell { width: 100%;")) + self.assertEqual(3, len(re.findall(r"\.entry-shell \{", after_login))) + later = self.css.split("/* ---- B-44 intercompany balances ---- */", 1)[-1] + self.assertNotIn(".entry-shell", later) + self.assertNotIn(".entry-form {", later) + self.assertNotIn(".role-switch {", later) + + def test_role_switch_reuses_segmented_rhythm(self) -> None: + self.assertIn(".role-switch { display: flex; gap: 4px; padding: 4px;", self.css) + self.assertIn("min-height: 36px", self.css) + self.assertIn('role="radiogroup"', self.html) + self.assertIn("class=\"sr-only\"", self.html) + + def test_auth_javascript_untouched(self) -> None: + self.assertIn("function initEntry()", self.js) + self.assertIn('fetch("/api/login"', self.js) + self.assertIn('fetch("/api/password/change"', self.js) + self.assertIn("must_change_password", self.js) + self.assertIn("#togglePassword", self.js) + self.assertIn("进入总账管理端", self.js) + self.assertIn("进入公司业务端", self.js) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_login_layout.py b/tests/test_login_layout.py new file mode 100644 index 0000000..17a5944 --- /dev/null +++ b/tests/test_login_layout.py @@ -0,0 +1,222 @@ +"""Chromium layout regression for the B-229 login page. + +Measures dual-zone geometry, form width, overflow and overlap at the +accepted viewports. Screenshots are written next to other visual evidence. +""" + +from __future__ import annotations + +import base64 +import json +import subprocess +import sys +import tempfile +import threading +import time +import unittest +from http.server import ThreadingHTTPServer +from pathlib import Path +from urllib.request import urlopen + +ROOT = Path(__file__).resolve().parents[1] +if str(Path(__file__).resolve().parent) not in sys.path: + sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from test_b44_layout import QuietHandler, _Cdp, chrome_bin # noqa: E402 + +SCREEN_DIR = ROOT / "screenshots" +LOGIN_URL = "/web/index.html" +MEASURE = r""" +(() => { + const box = (el) => { + if (!el) return null; + const r = el.getBoundingClientRect(); + return { + left: r.left, right: r.right, top: r.top, bottom: r.bottom, + width: r.width, height: r.height + }; + }; + const overlapArea = (a, b) => { + if (!a || !b) return 0; + const dx = Math.min(a.right, b.right) - Math.max(a.left, b.left); + const dy = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top); + if (dx <= 0 || dy <= 0) return 0; + return Math.round(dx * dy); + }; + const context = document.querySelector(".entry-context"); + const wrap = document.querySelector(".entry-form-wrap"); + const form = document.querySelector(".entry-form"); + const role = document.querySelector(".role-switch"); + const spans = [...document.querySelectorAll(".role-switch span")]; + const brand = document.querySelector("#product-name"); + const status = document.querySelector(".entry-status"); + const statement = document.querySelector(".entry-statement h1"); + const ctx = box(context); + const wr = box(wrap); + const fm = box(form); + const sp = spans.map(box); + const clipped = (el) => Boolean(el && el.scrollWidth > el.clientWidth + 1); + return { + viewport: window.innerWidth, + viewportH: window.innerHeight, + grid: getComputedStyle(document.querySelector(".entry-shell")).gridTemplateColumns, + contextBox: ctx, + wrapBox: wr, + formBox: fm, + formWidth: fm ? Math.round(fm.width) : 0, + roleHeight: role ? Math.round(role.getBoundingClientRect().height) : 0, + roleHorizontal: sp.length === 2 && sp[0].right <= sp[1].left + 2, + dualZone: Boolean(ctx && wr && ctx.right <= wr.left + 2 && Math.abs(ctx.top - wr.top) < 48), + stacked: Boolean(ctx && wr && ctx.bottom <= wr.top + 8), + overlap: overlapArea(ctx, fm), + pageOverflowX: document.documentElement.scrollWidth - window.innerWidth, + formBottom: fm ? fm.bottom : 0, + formFitsViewport: Boolean(fm && fm.top >= -1 && fm.bottom <= window.innerHeight + 2), + brandClipped: clipped(brand), + statementClipped: clipped(statement), + statusText: (status?.textContent || "").replace(/\s+/g, " ").trim(), + periodText: (document.querySelector(".entry-facts")?.textContent || "").replace(/\s+/g, " ").trim(), + hasToggle: Boolean(document.querySelector("#togglePassword")), + hasError: Boolean(document.querySelector("#loginError")), + changeHidden: document.querySelector("#changePassword")?.hidden === true, + }; +})() +""" + +VIEWPORTS = ( + (1440, 900, "desktop", "b229-login-1440.png"), + (1366, 768, "desktop", "b229-login-1366.png"), + (1024, 768, "desktop", "b229-login-1024.png"), + (768, 1024, "compact", "b229-login-768.png"), + (375, 812, "mobile", "b229-login-375.png"), +) + + +class LoginLayoutTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.chrome = chrome_bin() + cls.httpd = ThreadingHTTPServer(("127.0.0.1", 0), QuietHandler) + cls.http_thread = threading.Thread(target=cls.httpd.serve_forever, daemon=True) + cls.http_thread.start() + cls.port = cls.httpd.server_address[1] + cls.tmp = tempfile.TemporaryDirectory(prefix="b229-login-", ignore_cleanup_errors=True) + cls.proc = subprocess.Popen( + [ + str(cls.chrome), + "--headless=new", + "--disable-gpu", + "--no-first-run", + "--disable-extensions", + "--hide-scrollbars", + "--remote-debugging-port=0", + f"--user-data-dir={cls.tmp.name}", + "about:blank", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + port_file = Path(cls.tmp.name) / "DevToolsActivePort" + deadline = time.time() + 15 + listing = None + while time.time() < deadline: + if port_file.exists() and port_file.stat().st_size: + text = port_file.read_text(encoding="utf-8").strip().splitlines() + if text: + cls.debug_port = int(text[0]) + try: + listing = json.loads( + urlopen(f"http://127.0.0.1:{cls.debug_port}/json/list", timeout=5).read() + ) + except Exception: + listing = None + if listing and any(item.get("type") == "page" for item in listing): + break + time.sleep(0.05) + else: + raise RuntimeError("Chrome DevTools port not ready") + page = next(item for item in listing if item.get("type") == "page") + cls.cdp = _Cdp(page["webSocketDebuggerUrl"]) + cls.cdp.call("Runtime.enable") + cls.cdp.call("Page.enable") + SCREEN_DIR.mkdir(exist_ok=True) + + @classmethod + def tearDownClass(cls) -> None: + if getattr(cls, "cdp", None): + cls.cdp.close() + if getattr(cls, "proc", None): + cls.proc.terminate() + try: + cls.proc.wait(timeout=5) + except Exception: + cls.proc.kill() + cls.proc.wait(timeout=5) + time.sleep(0.2) + if getattr(cls, "httpd", None): + cls.httpd.shutdown() + cls.httpd.server_close() + if getattr(cls, "tmp", None): + cls.tmp.cleanup() + + def _open(self, width: int, height: int) -> dict: + self.cdp.call( + "Emulation.setDeviceMetricsOverride", + { + "width": width, + "height": height, + "deviceScaleFactor": 1, + "mobile": width <= 720, + }, + ) + self.cdp.call("Page.navigate", {"url": f"http://127.0.0.1:{self.port}{LOGIN_URL}"}) + time.sleep(0.4) + deadline = time.time() + 8 + last = None + while time.time() < deadline: + result = self.cdp.call("Runtime.evaluate", {"expression": MEASURE, "returnByValue": True}) + last = result.get("result", {}).get("value") + if last and last.get("formWidth"): + return last + time.sleep(0.1) + raise TimeoutError(f"login metrics not ready: {last}") + + def _shot(self, name: str) -> None: + raw = self.cdp.call("Page.captureScreenshot", {"format": "png", "captureBeyondViewport": False}) + (SCREEN_DIR / name).write_bytes(base64.b64decode(raw["data"])) + + def test_login_viewports(self) -> None: + for width, height, mode, shot in VIEWPORTS: + with self.subTest(width=width, height=height, mode=mode): + metrics = self._open(width, height) + self._shot(shot) + self.assertEqual(width, metrics["viewport"], metrics) + self.assertEqual(0, metrics["overlap"], metrics) + self.assertLessEqual(metrics["pageOverflowX"], 1, metrics) + self.assertFalse(metrics["brandClipped"], metrics) + self.assertFalse(metrics["statementClipped"], metrics) + self.assertTrue(metrics["roleHorizontal"], metrics) + self.assertLessEqual(metrics["roleHeight"], 52, metrics) + self.assertIn("运行中", metrics["statusText"], metrics) + self.assertIn("2026 年 7 月", metrics["periodText"], metrics) + self.assertTrue(metrics["hasToggle"], metrics) + self.assertTrue(metrics["hasError"], metrics) + self.assertTrue(metrics["changeHidden"], metrics) + if mode == "desktop": + self.assertTrue(metrics["dualZone"], metrics) + self.assertFalse(metrics["stacked"], metrics) + self.assertGreaterEqual(metrics["formWidth"], 420, metrics) + self.assertLessEqual(metrics["formWidth"], 460, metrics) + self.assertTrue(metrics["formFitsViewport"], metrics) + elif mode == "compact": + self.assertTrue(metrics["dualZone"], metrics) + self.assertFalse(metrics["stacked"], metrics) + self.assertGreaterEqual(metrics["formWidth"], 360, metrics) + self.assertLessEqual(metrics["formWidth"], 440, metrics) + else: + self.assertTrue(metrics["stacked"], metrics) + self.assertFalse(metrics["dualZone"], metrics) + + +if __name__ == "__main__": + unittest.main() diff --git a/web/index.html b/web/index.html index 26c68b7..5eda606 100644 --- a/web/index.html +++ b/web/index.html @@ -11,19 +11,31 @@
-
+
河南金牛实业集团集团资金往来管理系统
+

服务状态运行中

+
+

一笔往来,追溯到双方银行凭证。

+

选择与账号一致的工作端口后进入对应工作台。总账管理端查看集团全貌,公司业务端只处理本公司账务。

+
+
+
全局起算日
2026.01.01
+
当前账期
2026 年 7 月
+
覆盖银行
已对接 6 家
+
+
+

登录

请选择与账号一致的工作端口

- - + +
@@ -36,11 +48,6 @@
-
-
全局起算日
2026.01.01
-
当前账期
2026 年 7 月
-
覆盖银行
已对接 6 家
-
diff --git a/web/styles.css b/web/styles.css index 4e9408a..f58af2c 100644 --- a/web/styles.css +++ b/web/styles.css @@ -435,32 +435,59 @@ main { width: min(1560px, 100%); margin: 0 auto; padding: 12px 32px 58px; } /* Login */ .entry-page { overflow-x: hidden; } -.entry-shell { min-height: 100vh; display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(430px, 0.85fr); } -.entry-context { position: relative; min-height: 100vh; display: flex; flex-direction: column; justify-content: space-between; padding: clamp(32px, 5vw, 76px); border-right: 1px solid var(--color-line); background: #080808; } -.entry-context::after { content: ""; position: absolute; inset: 12% 8% 12% auto; width: 1px; background: rgba(55, 235, 137, 0.32); box-shadow: 0 0 38px rgba(55, 235, 137, 0.45); } -.entry-brand { display: flex; align-items: center; gap: 12px; } -.entry-brand > span:last-child { display: flex; flex-direction: column; } +.entry-shell { width: 100%; max-width: none; min-height: 100vh; display: grid; grid-template-columns: minmax(0, 1fr) minmax(460px, 520px); margin: 0; padding: 0; } +.entry-context { position: relative; min-width: 0; min-height: 100vh; display: flex; flex-direction: column; justify-content: space-between; padding: clamp(28px, 4vw, 56px) clamp(28px, 4vw, 64px); border-right: 1px solid var(--color-line); background: var(--color-bg-soft); } +.entry-brand { display: flex; align-items: center; gap: 12px; min-width: 0; } +.entry-brand > span:last-child { display: flex; flex-direction: column; min-width: 0; } +.entry-brand strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .entry-brand small { color: var(--color-ink-muted); } -.entry-statement { max-width: 680px; margin: 80px 0; } -.entry-statement h1 { max-width: 640px; font-size: clamp(38px, 5vw, 70px); line-height: 1.12; letter-spacing: 0; } -.entry-statement p { margin-top: 20px; color: var(--color-ink-muted); } -.entry-facts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; max-width: 670px; } -.entry-facts div { padding: 15px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: var(--color-surface-muted); } -.entry-facts dt { color: var(--color-ink-muted); } -.entry-facts dd { margin-top: 4px; color: var(--color-primary); } -.entry-form-wrap { min-height: 100vh; display: grid; place-items: center; padding: 40px; background: var(--color-bg); } -.entry-form { width: min(420px, 100%); display: grid; gap: 18px; padding: 28px; border: 1px solid var(--color-line); border-radius: var(--radius-xl); background: var(--color-surface); box-shadow: var(--shadow-panel); backdrop-filter: blur(24px); } -.entry-form header h2 { font-size: 25px; } +.entry-status { display: flex; align-items: center; gap: 8px; margin: 14px 0 0; color: var(--color-ink-soft); font-size: 12px; } +.entry-status span { color: var(--color-ink-muted); } +.entry-status b { color: var(--color-ink); font-weight: 700; } +.entry-status b::before { content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 6px; border-radius: 50%; background: var(--color-primary); vertical-align: 1px; } +.entry-statement { max-width: 640px; margin: 48px 0 auto; } +.entry-statement h1 { max-width: 620px; font-size: clamp(30px, 3.4vw, 48px); line-height: 1.18; letter-spacing: 0; } +.entry-statement p { margin-top: 12px; color: var(--color-ink-muted); max-width: 520px; } +.entry-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; max-width: 640px; } +.entry-facts div { min-width: 0; padding: 12px; border: 1px solid var(--color-line); border-radius: var(--radius-sm); background: var(--color-surface-muted); } +.entry-facts dt { color: var(--color-ink-muted); font-size: 11px; } +.entry-facts dd { margin-top: 4px; color: var(--color-primary); overflow-wrap: anywhere; } +.entry-form-wrap { min-width: 0; min-height: 100vh; display: grid; place-items: center; padding: 28px 24px; background: var(--color-bg); } +.entry-form { width: min(440px, 100%); display: grid; gap: 11px; padding: 22px 22px 20px; border: 1px solid var(--color-line); border-radius: var(--radius-lg); background: var(--color-surface); box-shadow: var(--shadow-low); backdrop-filter: blur(18px); } +.entry-form header h2 { font-size: 20px; } +.entry-form header p { margin-top: 4px; font-size: 12px; } .entry-form header p, .entry-note { color: var(--color-ink-muted); } -.role-switch { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; } -.role-switch label { cursor: pointer; } -.role-switch input { position: absolute; opacity: 0; } -.role-switch span { min-height: 83px; display: grid; grid-template-columns: 30px 1fr; align-content: center; gap: 1px 9px; padding: 12px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: rgba(0, 0, 0, 0.14); transition: border-color var(--duration-fast), background var(--duration-fast), transform var(--duration-fast); } -.role-switch svg { grid-row: 1 / 3; align-self: center; color: var(--color-ink-muted); } -.role-switch small { color: var(--color-ink-muted); } -.role-switch input:checked + span { border-color: rgba(55, 235, 137, 0.4); background: var(--color-primary-wash); transform: translateY(-1px); } +.entry-form .field { gap: 4px; } +.role-switch { display: flex; gap: 4px; padding: 4px; border: 1px solid var(--color-line); border-radius: 13px; background: rgba(0, 0, 0, 0.18); } +.role-switch label { position: relative; flex: 1; min-width: 0; cursor: pointer; } +.role-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; } +.role-switch span { min-height: 36px; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 0 8px; border: 0; border-radius: 9px; background: transparent; color: var(--color-ink-muted); transition: color var(--duration-fast), background var(--duration-fast); } +.role-switch svg { width: 15px; height: 15px; flex: 0 0 auto; } +.role-switch b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; } +.role-switch input:checked + span { background: var(--color-primary-wash); color: var(--color-ink); } .role-switch input:checked + span svg { color: var(--color-primary); } -.entry-note { font-size: 10px; } +.role-switch input:focus-visible + span { outline: 2px solid var(--color-primary-strong); outline-offset: 2px; } +.entry-note { font-size: 12px; } + +@media (max-width: 1023px) { + .entry-shell { grid-template-columns: minmax(0, 1fr) minmax(400px, 440px); } + .entry-context { padding: 24px 20px; } + .entry-statement { margin: 28px 0 auto; } + .entry-statement h1 { font-size: clamp(24px, 3.6vw, 34px); } + .entry-facts { grid-template-columns: 1fr; max-width: none; } + .entry-form-wrap { padding: 20px 16px; } + .entry-form { width: min(400px, 100%); padding: 18px; gap: 10px; } +} + +@media (max-width: 720px) { + .entry-shell { grid-template-columns: 1fr; } + .entry-context { min-height: auto; padding: 22px 16px 16px; border-right: 0; border-bottom: 1px solid var(--color-line); } + .entry-statement { margin: 18px 0 14px; } + .entry-statement h1 { font-size: 24px; } + .entry-facts { grid-template-columns: 1fr; } + .entry-form-wrap { min-height: auto; padding: 16px 16px 32px; align-content: start; } + .entry-form { width: 100%; padding: 16px; } +} @media (max-width: 1180px) { .app-shell { grid-template-columns: 88px minmax(0, 1fr); } @@ -497,9 +524,6 @@ main { width: min(1560px, 100%); margin: 0 auto; padding: 12px 32px 58px; } .subject-strip { grid-template-columns: repeat(3, 1fr); } .work-progress { grid-template-columns: 1fr 24px 1fr; row-gap: 16px; } .work-progress i:nth-of-type(2) { display: none; } - .entry-shell { grid-template-columns: 1fr; } - .entry-context { min-height: 58vh; border-right: 0; border-bottom: 1px solid var(--color-line); } - .entry-form-wrap { min-height: auto; } } @media (max-width: 720px) { @@ -573,12 +597,6 @@ main { width: min(1560px, 100%); margin: 0 auto; padding: 12px 32px 58px; } .form-grid { grid-template-columns: 1fr; } .toast-region { right: 12px; bottom: 12px; left: 12px; } .toast { min-width: 0; max-width: none; } - .entry-context { min-height: auto; padding: 30px 22px; } - .entry-statement { margin: 54px 0 42px; } - .entry-statement h1 { font-size: 39px; } - .entry-facts { grid-template-columns: 1fr; } - .entry-form-wrap { padding: 32px 16px 48px; } - .entry-form { padding: 22px; } } @media (max-width: 460px) { @@ -586,7 +604,6 @@ main { width: min(1560px, 100%); margin: 0 auto; padding: 12px 32px 58px; } .metric-copy { min-height: 52px; } .metric-label { font-size: 11px; } .metric-value { font-size: 23px; } - .role-switch { grid-template-columns: 1fr; } } @media (max-width: 1180px) { @@ -718,24 +735,6 @@ body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-event .table-scroll { padding: 2px 14px 8px; } .table-summary { margin: 0 14px; } -/* ===== 登录页重做:居中集团登录卡 ===== */ -.entry-shell { display: grid; grid-template-columns: 1fr; place-items: center; min-height: 100vh; padding: 48px 20px; } -.entry-card { position: relative; width: min(470px, 100%); overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.13); border-radius: var(--radius-xl); background: linear-gradient(165deg, rgba(36, 36, 36, 0.9) 0%, rgba(16, 16, 16, 0.92) 60%, rgba(10, 10, 10, 0.94) 100%); box-shadow: var(--shadow-panel), 0 0 60px rgba(55, 235, 137, 0.06); backdrop-filter: blur(28px) saturate(125%); } -.entry-card::before { content: ""; position: absolute; inset: 0; background: radial-gradient(150% 110% at 50% 118%, rgba(255, 255, 255, 0.07), transparent 55%), linear-gradient(165deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.03) 42%, transparent 70%); pointer-events: none; } -.entry-card::after { content: ""; position: absolute; top: 0; left: 18%; right: 18%; height: 2px; background: linear-gradient(90deg, transparent, rgba(55, 235, 137, 0.65), transparent); } -.entry-brand { flex-direction: column; justify-content: center; gap: 15px; min-height: 0; padding: 36px 28px 24px; border-bottom: 1px solid var(--color-line); text-align: center; } -.entry-brand .brand-mark { width: 54px; height: 54px; border-radius: 17px; font-size: 26px; } -.entry-brand > span:last-child { align-items: center; } -.entry-brand strong { font-size: 21px; letter-spacing: 0.03em; } -.entry-brand small { margin-top: 4px; font-size: 12px; letter-spacing: 0.08em; } -.entry-form { width: 100%; gap: 16px; padding: 24px 28px 10px; border: 0; background: transparent; box-shadow: none; backdrop-filter: none; } -.entry-form header { text-align: center; } -.entry-form header h2 { font-size: 22px; } -.entry-form header p { margin-top: 4px; color: var(--color-ink-muted); font-size: 12px; } -.entry-facts { max-width: none; gap: 10px; padding: 16px 28px 28px; } -.entry-facts div { padding: 12px; text-align: center; } -.entry-facts dd { margin-top: 2px; font-size: 12px; } - @media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } }