Files
CLOAK/lib/CloakDebugPage.php
2026-06-14 14:00:24 +08:00

627 lines
29 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* CloakDebugPage — DEBUG 模式调试结果页
*/
class CloakDebugPage
{
/** 判定逻辑所在文件(入口 index.php require 本文件,backtrace 无法可靠取行号) */
const IP_CHECK_FILE = 'ip_check.php';
/** @var array 完整判断流程步骤 */
private static $flow = [];
/** @var array check_result 写入轨迹 */
private static $trail = [];
/** @var array|null */
private static $apiResponse = null;
/** @var array */
private static $flags = [];
/** @var array|null */
private static $exitPoint = null;
/** @var float */
private static $startedAt = 0;
public static function init()
{
self::$flow = [];
self::$trail = [];
self::$apiResponse = null;
self::$flags = [];
self::$exitPoint = null;
self::$startedAt = microtime(true);
}
/**
* 记录流程步骤(请从 ip_check.php 使用 cloak_dbg_step(__LINE__, ...) 调用)
*/
public static function step($title, $status, $detail = '', array $extra = [])
{
$loc = self::callerLoc();
self::stepAt($loc['line'], $title, $status, $detail, $extra);
}
/**
* 记录流程步骤,指定 ip_check.php 行号
*/
public static function stepAt($line, $title, $status, $detail = '', array $extra = [])
{
self::$flow[] = [
'title' => $title,
'status' => $status,
'detail' => $detail,
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'extra' => $extra,
'at' => microtime(true),
];
}
/**
* 记录 check_result 写入(请从 ip_check.php 使用 cloak_dbg_record(__LINE__, ...) 调用)
*/
public static function record($result, $reason, array $extra = [])
{
$loc = self::callerLoc();
self::recordAt($loc['line'], $result, $reason, $extra);
}
/**
* 记录 check_result 写入,指定 ip_check.php 行号
*/
public static function recordAt($line, $result, $reason, array $extra = [])
{
$stage = isset($extra['stage']) ? (string)$extra['stage'] : 'unknown';
$entry = [
'result' => $result,
'reason' => $reason,
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'stage' => $stage,
'extra' => $extra,
'at' => microtime(true),
];
self::$trail[] = $entry;
$decideDetail = $reason !== '' ? $reason : '(理由为空)';
if ($result === 'true' || $result === 'false') {
$decideDetail .= ' → check_result=' . $result;
self::$flags['final'] = $entry;
} elseif ($result === null) {
$decideDetail = ($reason !== '' ? $reason : '(理由为空)') . '(未写入 check_result,流程继续)';
}
self::$flow[] = [
'title' => self::stageTitle($stage),
'status' => ($result === 'true' || $result === 'false') ? 'decide' : 'warn',
'detail' => $decideDetail,
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'extra' => array_merge($extra, ['check_result' => $result]),
'at' => microtime(true),
];
}
public static function setApiResponse(array $response)
{
self::$apiResponse = $response;
}
public static function setFlag($key, $value)
{
self::$flags[$key] = $value;
}
public static function setExitPoint($line, $label, array $extra = [])
{
self::$exitPoint = [
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'label' => $label,
'extra' => $extra,
];
}
public static function renderAndExit(array $ctx)
{
$result = (string)($ctx['check_result'] ?? 'false');
$reason = (string)($ctx['reason'] ?? '');
$final = self::$flags['final'] ?? null;
$isRealPage = ($result === 'true');
$elapsed = isset($ctx['started_at'])
? round((microtime(true) - $ctx['started_at']) * 1000)
: round((microtime(true) - self::$startedAt) * 1000);
$routeInfo = self::buildRouteInfo($ctx, $result);
$diagnosis = self::buildReasonDiagnosis($reason, $result, $ctx);
$flowHtml = self::buildFlowHtml();
$trailHtml = self::buildTrailHtml();
$sessionHtml = self::buildSessionHtml($ctx['session'] ?? []);
$verdictLabel = $isRealPage ? '真实页 (DB_FP)' : '安全页 (DB_ZP)';
$verdictClass = $isRealPage ? 'verdict-pass' : 'verdict-block';
$verdictIcon = $isRealPage ? '✓' : '✕';
$finalLoc = $final ? self::formatLoc($final['file'], $final['line']) : '—';
$finalStage = $final ? htmlspecialchars(self::stageTitle($final['stage'])) : '—';
$finalReason = $final ? htmlspecialchars($final['reason'] !== '' ? $final['reason'] : '(空)') : '—';
$exit = self::$exitPoint;
$exitLoc = $exit ? self::formatLoc($exit['file'], $exit['line']) : self::IP_CHECK_FILE . ':?';
$exitLabel = $exit ? htmlspecialchars($exit['label']) : 'CloakDebugPage::renderAndExit()';
header('Content-Type: text/html; charset=utf-8');
header('X-Robots-Tag: noindex, nofollow');
echo '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">';
echo '<meta name="viewport" content="width=device-width,initial-scale=1">';
echo '<title>Cloak DEBUG — ' . htmlspecialchars($ctx['config_name'] ?? '') . '</title>';
echo self::styles();
echo '</head><body><div class="wrap">';
echo '<header class="hero">';
echo '<div class="hero-badge">DEBUG MODE</div>';
echo '<h1>流量判定调试报告</h1>';
echo '<p class="hero-sub">配置 <code>' . htmlspecialchars($ctx['config_name'] ?? '') . '</code>';
echo ' · 耗时 ' . (int)$elapsed . ' ms · ' . date('Y-m-d H:i:s') . '</p>';
echo '</header>';
// 流程终止位置(醒目)
echo '<section class="card exit-card">';
echo '<h2>流程终止位置</h2>';
echo '<div class="exit-main"><code>' . htmlspecialchars($exitLoc) . '</code></div>';
echo '<p class="exit-desc">DEBUG 在此截停执行(<strong>' . $exitLabel . '</strong>),未继续跳转真实页/安全页。</p>';
echo '<dl class="kv kv-tight">';
echo '<dt>未开启 DEBUG 时下一分支</dt><dd>' . htmlspecialchars($routeInfo['next_branch']) . '</dd>';
echo '<dt>预计跳转目标</dt><dd class="url-break">' . htmlspecialchars($routeInfo['dest'] !== '' ? $routeInfo['dest'] : '—') . '</dd>';
echo '<dt>路由条件</dt><dd><code>' . htmlspecialchars($routeInfo['condition']) . '</code></dd>';
echo '</dl></section>';
echo '<section class="card verdict-card ' . $verdictClass . '">';
echo '<div class="verdict-icon">' . $verdictIcon . '</div>';
echo '<div class="verdict-body">';
echo '<div class="verdict-title">' . htmlspecialchars($verdictLabel) . '</div>';
echo '<div class="verdict-meta">check_result = <code>' . htmlspecialchars($result) . '</code></div>';
echo '</div></section>';
// 判断理由 + 空理由诊断
echo '<section class="card"><h2>判定结论</h2><dl class="kv">';
echo '<dt>当前 $reason</dt><dd>';
if ($reason !== '') {
echo htmlspecialchars($reason);
} else {
echo '<span class="tag tag-warn">为空</span>';
}
echo '</dd>';
echo '<dt>最终写入 check_result</dt><dd><code>' . htmlspecialchars($finalLoc) . '</code>' . $finalStage . '</dd>';
echo '<dt>写入时 $reason</dt><dd>' . $finalReason . '</dd>';
echo '</dl>';
if ($diagnosis !== '') {
echo '<div class="diagnosis-box">' . $diagnosis . '</div>';
}
echo '</section>';
// 完整判断流程
echo '<section class="card"><h2>完整判断流程</h2>';
echo '<p class="hint">按 ip_check.php 实际执行顺序展示;<span class="legend-dot decide"></span> 表示写入 check_result<span class="legend-dot fail"></span> 拦截,<span class="legend-dot skip"></span> 跳过。</p>';
echo $flowHtml !== '' ? $flowHtml : '<p class="hint">暂无流程记录(请确认 CLOAK_DEBUG_MODE 已保存为 ON)。</p>';
echo '</section>';
if ($trailHtml !== '') {
echo '<section class="card"><h2>check_result 写入轨迹</h2>';
echo '<p class="hint">每次对 $_SESSION[\'check_result\'] 的赋值;最后一项为最终判定依据。</p>';
echo $trailHtml . '</section>';
}
if (self::$apiResponse) {
echo '<section class="card"><h2>API 判定接口 (cloak_check_curl)</h2>';
echo self::buildApiHtml(self::$apiResponse);
echo '</section>';
}
echo '<div class="grid-2">';
echo '<section class="card"><h2>Session 状态</h2>';
echo $sessionHtml . '</section>';
echo '<section class="card"><h2>路由预览</h2><dl class="kv kv-tight">';
echo '<dt>SHOW_SITE_MODE</dt><dd><code>' . htmlspecialchars($ctx['mode'] ?? '') . '</code></dd>';
echo '<dt>visit_to_2</dt><dd><code>' . htmlspecialchars($ctx['session']['visit_to_2'] ?? '—') . '</code> <span class="hint-inline">=2 时跳过 API 块</span></dd>';
echo '<dt>visit_to_3</dt><dd><code>' . htmlspecialchars($ctx['session']['visit_to_3'] ?? '—') . '</code></dd>';
echo '</dl></section>';
echo '</div>';
echo '<section class="card"><h2>访客依据信息</h2>';
echo '<table class="data-table"><tbody>' . self::buildVisitorRows($ctx) . '</tbody></table></section>';
echo '<section class="card"><h2>请求参数</h2>';
echo '<table class="data-table"><tbody>' . self::buildQueryRows() . '</tbody></table></section>';
echo '<section class="card"><h2>配置快照</h2>';
echo '<table class="data-table compact"><tbody>' . self::buildConfigRows() . '</tbody></table></section>';
echo '<footer class="foot">DEBUG 模式 — 生产环境请关闭 CLOAK_DEBUG_MODE</footer>';
echo '</div></body></html>';
exit;
}
private static function callerLoc()
{
$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 20);
foreach ($bt as $frame) {
if (empty($frame['file'])) {
continue;
}
if (basename($frame['file']) === self::IP_CHECK_FILE) {
return [
'file' => self::IP_CHECK_FILE,
'line' => (int)($frame['line'] ?? 0),
];
}
}
return [
'file' => self::IP_CHECK_FILE,
'line' => 0,
];
}
private static function formatLoc($file, $line)
{
$file = ($file !== '' && $file !== '—') ? $file : self::IP_CHECK_FILE;
if (strpos($file, 'ip_check') !== false) {
$file = self::IP_CHECK_FILE;
}
return $file . ':' . (int)$line;
}
private static function stageTitle($stage)
{
$map = [
'session_fast_path' => 'Session 快速路径',
'url_args_timeout' => '临时链接参数',
'blacklist' => '黑名单',
'whitelist_ip' => 'IP 白名单',
'whitelist_params' => '链接参数白名单',
'fingerprint_cookie' => 'JS 指纹 Cookie',
'api_cloak' => 'API 远程判定',
'no_referer_or_gcuid' => '无 Referer/gcuid',
'session_partial' => 'Session 已有结果',
'main_block_skipped' => '主判定块跳过',
];
return isset($map[$stage]) ? $map[$stage] : $stage;
}
private static function buildRouteInfo(array $ctx, $result)
{
$mode = $ctx['mode'] ?? '';
$isReal = ($mode !== 'zp') && ((($mode === 'ip_check' && $result !== 'false') || $mode === 'fp'));
$forceRisk = !empty($ctx['force_risk']);
$visit3 = $ctx['session']['visit_to_3'] ?? '';
if (!$isReal) {
return [
'next_branch' => '安全页分支 (ip_check.php ~859)',
'dest' => (string)($ctx['zp_url'] ?? ''),
'condition' => "mode={$mode}, check_result={$result} → false 或 mode=zp",
];
}
if ($forceRisk && $visit3 !== '3') {
return [
'next_branch' => '真实页 + 二次风控 (page_666.php / cloakjs)',
'dest' => self::firstUrl($ctx['fp_urls'] ?? []),
'condition' => 'check_result!=false 且 CLOAK_RISK_NUMBER>0',
];
}
return [
'next_branch' => '真实页 (page_6.php)',
'dest' => self::firstUrl($ctx['fp_urls'] ?? []),
'condition' => "mode={$mode}, check_result={$result}",
];
}
private static function buildReasonDiagnosis($reason, $result, array $ctx)
{
if ($reason !== '') {
return '';
}
$items = [];
$final = self::$flags['final'] ?? null;
if (!$final) {
$items[] = '整个流程<strong>未记录</strong>到任何 <code>check_result</code> 写入点;最终 result 可能来自默认值或未赋值。';
} elseif ($final['reason'] === '') {
$items[] = '最终写入点 <code>' . htmlspecialchars(self::formatLoc($final['file'], $final['line'])) . '</code>' . htmlspecialchars(self::stageTitle($final['stage'])) . ')写入时 <code>$reason</code> 即为空。';
}
if (self::$apiResponse) {
$apiReason = self::$apiResponse['reason'] ?? '';
$apiCalled = !empty(self::$apiResponse['called']);
if ($apiCalled && $apiReason === '') {
$items[] = 'API 接口 <code>cloak_check_curl</code> 已调用,但返回 JSON 中 <code>reason</code> 字段为空(见下方 API 详情)。';
} elseif (!$apiCalled) {
$items[] = 'API 判定<strong>未执行</strong>(可能缺少 ip/Browser/Accept-Language,或主判定块被跳过)。';
}
}
$visit2 = $ctx['session']['visit_to_2'] ?? '';
if ($visit2 === '2' && empty(self::$apiResponse['called'])) {
$items[] = '<code>$_SESSION[\'visit_to_2\']=2</code> 导致主判定块(API 调用)被跳过;若同时 <code>check_result</code> 未重建,<code>$reason</code> 会保持初始空字符串。DEBUG 模式已重置 visit_to_*,若仍出现请检查是否在判定前被重新写入。';
}
if ($result === 'true') {
$items[] = '生产环境中,进入真实页分支后 <code>ip_check.php:818</code> 会将 <code>$logs[\'reason\']</code> 清空(注释:访问仿品时没用跳转理由),日志里可能显示空理由,但 DEBUG 截停前应仍能看到写入时的 reason。';
}
if (empty($items)) {
$items[] = '$reason 在流程开始时初始化为空字符串,且后续没有任何分支对其赋值。';
}
$html = '<h3 class="diag-title">⚠ $reason 为空 — 溯源分析</h3><ul class="diag-list">';
foreach ($items as $item) {
$html .= '<li>' . $item . '</li>';
}
$html .= '</ul>';
return $html;
}
private static function buildFlowHtml()
{
if (empty(self::$flow)) {
return '';
}
$html = '<div class="flow-timeline">';
$final = self::$flags['final'] ?? null;
foreach (self::$flow as $i => $s) {
$isFinal = $final
&& isset($s['extra']['check_result'])
&& ($s['extra']['check_result'] === 'true' || $s['extra']['check_result'] === 'false')
&& $final['line'] === $s['line']
&& $final['stage'] === ($s['extra']['stage'] ?? '');
$cls = 'flow-step status-' . htmlspecialchars($s['status']);
if ($isFinal) {
$cls .= ' flow-final';
}
$html .= '<div class="' . $cls . '">';
$html .= '<div class="flow-marker">' . ($i + 1) . '</div>';
$html .= '<div class="flow-body">';
$html .= '<div class="flow-head">';
$html .= '<span class="flow-title">' . htmlspecialchars($s['title']) . '</span>';
$html .= '<span class="flow-badge badge-' . htmlspecialchars($s['status']) . '">' . htmlspecialchars(self::statusLabel($s['status'])) . '</span>';
$html .= '<code class="flow-loc">' . htmlspecialchars(self::formatLoc($s['file'], $s['line'])) . '</code>';
$html .= '</div>';
if ($s['detail'] !== '') {
$html .= '<div class="flow-detail">' . htmlspecialchars($s['detail']) . '</div>';
}
if (!empty($s['extra'])) {
$show = $s['extra'];
unset($show['stage'], $show['check_result']);
if (!empty($show)) {
$html .= '<pre class="code-block sm">' . htmlspecialchars(json_encode($show, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '</pre>';
}
}
$html .= '</div></div>';
}
$html .= '</div>';
return $html;
}
private static function statusLabel($status)
{
$map = [
'pass' => '通过', 'fail' => '拦截', 'skip' => '跳过',
'warn' => '可疑', 'info' => '信息', 'decide' => '写入结果',
];
return isset($map[$status]) ? $map[$status] : $status;
}
private static function buildTrailHtml()
{
if (empty(self::$trail)) {
return '';
}
$html = '<div class="trail">';
$final = self::$flags['final'] ?? null;
foreach (self::$trail as $e) {
$isFinal = $final && $final['line'] === $e['line'] && $final['stage'] === $e['stage'];
$cls = $isFinal ? ' trail-item final' : ' trail-item';
$res = $e['result'] !== null ? $e['result'] : '—';
$html .= '<div class="' . trim($cls) . '">';
$html .= '<div class="trail-head">';
$html .= '<span class="trail-loc"><code>' . htmlspecialchars(self::formatLoc($e['file'], $e['line'])) . '</code></span>';
$html .= '<span class="trail-stage">' . htmlspecialchars(self::stageTitle($e['stage'])) . '</span>';
if ($e['result'] !== null) {
$html .= '<span class="trail-res">' . htmlspecialchars((string)$res) . '</span>';
}
$html .= '</div>';
$html .= '<div class="trail-reason">' . htmlspecialchars($e['reason'] !== '' ? $e['reason'] : '(理由为空)') . '</div>';
$html .= '</div>';
}
$html .= '</div>';
return $html;
}
private static function buildApiHtml(array $api)
{
$html = '<dl class="kv">';
$html .= '<dt>是否调用</dt><dd>' . (!empty($api['called']) ? '是' : '否') . '</dd>';
if (!empty($api['skip_reason'])) {
$html .= '<dt>未调用原因</dt><dd>' . htmlspecialchars($api['skip_reason']) . '</dd>';
}
$html .= '<dt>API result</dt><dd><code>' . htmlspecialchars((string)($api['result_raw'] ?? $api['check_result'] ?? '—')) . '</code></dd>';
$html .= '<dt>API reason</dt><dd>' . htmlspecialchars((string)($api['reason'] ?? '—')) . '</dd>';
$html .= '<dt>API country</dt><dd>' . htmlspecialchars((string)($api['country'] ?? '—')) . '</dd>';
if (!empty($api['curl_error'])) {
$html .= '<dt>Curl 错误</dt><dd class="tag tag-warn">' . htmlspecialchars($api['curl_error']) . '</dd>';
}
if (!empty($api['raw'])) {
$html .= '<dt>原始响应</dt><dd><pre class="code-block">' . htmlspecialchars(is_string($api['raw']) ? $api['raw'] : json_encode($api['raw'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '</pre></dd>';
}
$html .= '</dl>';
return $html;
}
private static function buildSessionHtml(array $session)
{
if (empty($session)) {
return '<p class="hint">无 Session 快照</p>';
}
$html = '<table class="data-table compact"><tbody>';
foreach ($session as $k => $v) {
$html .= '<tr><th>' . htmlspecialchars((string)$k) . '</th><td><code>' . htmlspecialchars(is_scalar($v) ? (string)$v : json_encode($v, JSON_UNESCAPED_UNICODE)) . '</code></td></tr>';
}
$html .= '</tbody></table>';
return $html;
}
private static function buildVisitorRows(array $ctx)
{
$v = $ctx['visitor'] ?? null;
$rows = [
'IP 地址' => $v ? $v->v_ip : ($ctx['logs']['customers_ip'] ?? ''),
'国家/地区' => $v ? $v->v_country : '',
'浏览器' => $v ? $v->v_Browser : ($ctx['logs']['v_Browser'] ?? ''),
'客户端类型' => $v ? $v->v_Client : ($ctx['logs']['Client'] ?? ''),
'Referer' => $v ? $v->v_referer : ($ctx['logs']['v_referer'] ?? ''),
'当前 URL' => $v ? $v->v_curPageURL : ($ctx['logs']['v_PageURL'] ?? ''),
'Accept-Language' => $v ? $v->accept_language : ($ctx['logs']['accept_language'] ?? ''),
'User-Agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
'visit_md5' => $v ? $v->visit_md5_code : ($ctx['logs']['visit_md5_code'] ?? ''),
'设备型号' => $ctx['device'] ?? '',
'gcuid' => $_SESSION['gcuid'] ?? '',
];
return self::rowsHtml($rows);
}
private static function buildQueryRows()
{
$keys = ['fbclid', 'gclid', 'wbraid', 'gbraid', 'ttclid', 'ad_spm_id'];
$rows = [];
foreach ($keys as $k) {
$rows[$k] = isset($_GET[$k]) ? $_GET[$k] : '(未携带)';
}
return self::rowsHtml($rows);
}
private static function buildConfigRows()
{
$map = [
'SHOW_SITE_MODE_SWITCH' => defined('SHOW_SITE_MODE_SWITCH') ? SHOW_SITE_MODE_SWITCH : '',
'SHOW_SITE_COUNTRY' => defined('SHOW_SITE_COUNTRY') ? SHOW_SITE_COUNTRY : '',
'CLOAK_DEBUG_MODE' => defined('CLOAK_DEBUG_MODE') ? CLOAK_DEBUG_MODE : '',
'CLOAK_RISK_NUMBER' => defined('CLOAK_RISK_NUMBER') ? CLOAK_RISK_NUMBER : '',
'CLOAK_RISK_ENHANCED' => defined('CLOAK_RISK_ENHANCED') ? CLOAK_RISK_ENHANCED : '',
'CLOAK_URL_ARGS_TIMEOUT' => defined('CLOAK_URL_ARGS_TIMEOUT') ? CLOAK_URL_ARGS_TIMEOUT : '',
];
return self::rowsHtml($map);
}
private static function rowsHtml(array $rows)
{
$html = '';
foreach ($rows as $label => $val) {
$html .= '<tr><th>' . htmlspecialchars((string)$label) . '</th><td>' . htmlspecialchars((string)$val) . '</td></tr>';
}
return $html;
}
private static function firstUrl($urls)
{
if (is_array($urls) && !empty($urls)) {
return (string)$urls[0];
}
return is_string($urls) ? $urls : '';
}
private static function styles()
{
return <<<'CSS'
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:linear-gradient(145deg,#0f172a,#1e293b 50%,#0f172a);color:#e2e8f0;min-height:100vh;line-height:1.55}
.wrap{max-width:980px;margin:0 auto;padding:24px 16px 48px}
.hero{text-align:center;margin-bottom:24px}
.hero-badge{display:inline-block;background:#f59e0b;color:#1e293b;font-size:.72rem;font-weight:700;letter-spacing:.08em;padding:4px 10px;border-radius:999px;margin-bottom:10px}
.hero h1{font-size:1.5rem;margin-bottom:6px}
.hero-sub{font-size:.85rem;color:#94a3b8}
.hero-sub code{background:#334155;padding:2px 6px;border-radius:4px}
.card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:20px;margin-bottom:16px}
.card h2{font-size:.92rem;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.04em;margin-bottom:14px}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@media(max-width:720px){.grid-2{grid-template-columns:1fr}}
.exit-card{border-color:#f59e0b;background:linear-gradient(135deg,#422006 0%,#1e293b 40%)}
.exit-main{font-size:1.4rem;font-weight:700;margin-bottom:8px}
.exit-main code{color:#fcd34d}
.exit-desc{font-size:.88rem;color:#cbd5e1;margin-bottom:12px}
.verdict-card{display:flex;align-items:center;gap:20px;padding:22px}
.verdict-pass{border-color:#059669;background:linear-gradient(135deg,#064e3b,#1e293b)}
.verdict-block{border-color:#dc2626;background:linear-gradient(135deg,#7f1d1d,#1e293b)}
.verdict-icon{width:52px;height:52px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:700;flex-shrink:0}
.verdict-pass .verdict-icon{background:#059669;color:#fff}
.verdict-block .verdict-icon{background:#dc2626;color:#fff}
.verdict-title{font-size:1.25rem;font-weight:700}
.verdict-meta{font-size:.85rem;color:#94a3b8;margin-top:4px}
.kv dt{font-size:.76rem;color:#64748b;margin-top:10px}
.kv dt:first-child{margin-top:0}
.kv dd{font-size:.9rem;margin-top:2px;word-break:break-all}
.kv-tight dt{margin-top:6px}
.diagnosis-box{margin-top:14px;padding:14px;background:#422006;border:1px solid #b45309;border-radius:8px;font-size:.85rem}
.diag-title{font-size:.88rem;color:#fcd34d;margin-bottom:8px}
.diag-list{padding-left:18px;color:#fde68a}
.diag-list li{margin-bottom:6px}
.flow-timeline{display:flex;flex-direction:column;gap:0;position:relative;padding-left:8px}
.flow-step{display:flex;gap:14px;padding:12px 0;border-bottom:1px solid #334155;position:relative}
.flow-step:last-child{border-bottom:none}
.flow-step.flow-final .flow-body{border-left:3px solid #f59e0b;padding-left:12px;margin-left:-12px}
.flow-marker{width:28px;height:28px;border-radius:50%;background:#334155;color:#94a3b8;font-size:.75rem;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:2px}
.flow-step.status-decide .flow-marker{background:#b45309;color:#fff}
.flow-step.status-fail .flow-marker{background:#dc2626;color:#fff}
.flow-step.status-pass .flow-marker{background:#059669;color:#fff}
.flow-step.status-skip .flow-marker{background:#475569;color:#cbd5e1}
.flow-body{flex:1;min-width:0}
.flow-head{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:4px}
.flow-title{font-weight:600;font-size:.92rem}
.flow-badge{font-size:.68rem;padding:2px 7px;border-radius:4px;font-weight:600}
.badge-pass{background:#064e3b;color:#6ee7b7}
.badge-fail{background:#7f1d1d;color:#fca5a5}
.badge-skip{background:#334155;color:#94a3b8}
.badge-warn{background:#78350f;color:#fcd34d}
.badge-info{background:#1e3a5f;color:#7dd3fc}
.badge-decide{background:#92400e;color:#fde68a}
.flow-loc{font-size:.72rem;color:#38bdf8}
.flow-detail{font-size:.85rem;color:#cbd5e1}
.legend-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin:0 2px}
.legend-dot.decide{background:#f59e0b}
.legend-dot.fail{background:#dc2626}
.legend-dot.skip{background:#64748b}
.hint{font-size:.8rem;color:#64748b;margin-bottom:12px}
.hint-inline{font-size:.72rem;color:#64748b}
.tag{display:inline-block;font-size:.72rem;padding:2px 8px;border-radius:4px}
.tag-warn{background:#78350f;color:#fcd34d}
.tag-info{background:#1e3a5f;color:#7dd3fc}
.url-break{word-break:break-all;color:#38bdf8;font-size:.85rem}
.data-table{width:100%;border-collapse:collapse;font-size:.84rem}
.data-table th{text-align:left;color:#64748b;font-weight:500;width:130px;padding:7px 10px 7px 0;vertical-align:top;border-bottom:1px solid #334155}
.data-table td{padding:7px 0;word-break:break-all;border-bottom:1px solid #334155}
.data-table.compact th{width:180px}
.data-table tr:last-child th,.data-table tr:last-child td{border-bottom:none}
.code-block{background:#0f172a;border:1px solid #334155;border-radius:6px;padding:10px;font-size:.76rem;overflow-x:auto;margin-top:6px;white-space:pre-wrap}
.code-block.sm{font-size:.7rem;padding:6px 8px}
.trail{display:flex;flex-direction:column;gap:8px}
.trail-item{background:#0f172a;border:1px solid #334155;border-radius:8px;padding:10px 12px}
.trail-item.final{border-color:#f59e0b}
.trail-head{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:4px}
.trail-loc code{font-size:.76rem;color:#38bdf8}
.trail-stage{font-size:.72rem;color:#64748b}
.trail-res{font-size:.72rem;background:#334155;padding:2px 7px;border-radius:4px;font-family:monospace}
.trail-reason{font-size:.85rem;color:#cbd5e1}
.foot{text-align:center;font-size:.76rem;color:#64748b;margin-top:20px}
</style>
CSS;
}
}