Commit 2b18c83e authored by MD. SHAHIDUL ISLAM's avatar MD. SHAHIDUL ISLAM

feat: redesign analysis UI with a modern terminal-inspired light theme and updated dashboard layout

parent 8c2c0d86
@if (predictionMode) {
<app-timesfm-prediction />
} @else {
<main class="analysis-page" [class.bangla-copy]="language() === 'bn'">
<!-- Top Navigation Bar -->
<header class="analysis-toolbar">
<a class="brand" href="#top" aria-label="DSE AI Analysis home">
<span class="brand-dot"></span>
<span>DSE <strong class="brand-accent">AI TERMINAL</strong></span>
<main id="top" class="research-terminal" [class.bangla-copy]="language() === 'bn'">
<header class="topbar">
<a class="brand" href="#top" aria-label="DSE research home">
<span class="brand-mark">D</span>
<span class="brand-copy">
<strong>DOHA RESEARCH</strong>
<small>DSE · MULTI-AGENT DOSSIER</small>
</span>
</a>
<div class="analysis-controls">
<div class="selector-wrapper">
<div class="topbar-actions">
<label class="stock-selector">
<span>{{ language() === 'bn' ? 'কোম্পানি' : 'Company' }}</span>
<select
[(ngModel)]="selectedSymbol"
[disabled]="loadingStocks()"
(change)="selectStock()"
aria-label="Stock Select"
aria-label="Choose a DSE company"
>
@for (stock of stocks(); track stock.symbol) {
<option [value]="stock.symbol">
{{ stock.symbol }} · {{ stock.name }}
</option>
<option [value]="stock.symbol">{{ stock.symbol }} · {{ stock.name }}</option>
}
</select>
</div>
</label>
<button
class="run-btn"
class="primary-action"
type="button"
[disabled]="loadingAnalysis()"
(click)="runAnalysis(false)"
>
<span class="btn-pulse" *ngIf="loadingAnalysis()"></span>
{{ language() === 'bn' ? 'বিশ্লেষণ চালান' : 'Run analysis' }}
@if (loadingAnalysis()) { <span class="button-spinner"></span> }
<span>{{ language() === 'bn' ? 'নতুন বিশ্লেষণ' : 'Run new analysis' }}</span>
</button>
<div class="language-switch" role="group" aria-label="Report language">
......@@ -42,449 +43,389 @@
</div>
</header>
<!-- Loading State -->
@if (loadingAnalysis() && analysis()) {
<div class="progress-notice" aria-live="polite">
<span class="progress-pulse"></span>
<strong>{{ language() === 'bn' ? 'নতুন প্রতিবেদন তৈরি হচ্ছে' : 'Building a fresh report' }}</strong>
<span>{{ jobMessage() || (language() === 'bn' ? 'এজেন্টরা প্রমাণ পর্যালোচনা করছে…' : 'Agents are reviewing the evidence…') }}</span>
</div>
}
@if (loadingAnalysis() && !analysis()) {
<section class="loading-state" aria-live="polite">
<div class="loading-spinner"></div>
<h2>{{ language() === 'bn' ? 'এআই এজেন্ট তথ্য বিশ্লেষণ করছে...' : 'AI agents are synthesizing market evidence...' }}</h2>
<p>{{ jobMessage() || (language() === 'bn' ? 'ডিএসই তথ্য লোড হচ্ছে…' : 'Loading DSE evidence…') }}</p>
<div class="skeleton-grid">
<div class="skeleton-card short"></div>
<div class="skeleton-card tall"></div>
<div class="skeleton-card medium"></div>
<div class="loading-orbit"><i></i><span>D</span></div>
<p class="eyebrow">MULTI-AGENT RESEARCH IN PROGRESS</p>
<h1>{{ language() === 'bn' ? 'প্রমাণ থেকে বিনিয়োগ সিদ্ধান্ত তৈরি হচ্ছে' : 'Turning evidence into an investment decision' }}</h1>
<p>{{ jobMessage() || (language() === 'bn' ? 'ডিএসই তথ্য লোড হচ্ছে…' : 'Loading verified DSE evidence…') }}</p>
<div class="loading-steps" aria-hidden="true">
<span class="active">Market</span><i></i><span>Fundamentals</span><i></i><span>Debate</span><i></i><span>Risk</span><i></i><span>Decision</span>
</div>
</section>
}
<!-- Error / Empty State -->
@if (errorMessage() && !analysis()) {
<section class="empty-state" aria-live="polite">
<div class="alert-icon">⚠️</div>
<h1>{{ language() === 'bn' ? 'বিশ্লেষণ প্রস্তুত নয়' : 'Analysis not ready' }}</h1>
<span class="state-icon">!</span>
<p class="eyebrow">REPORT NOT AVAILABLE</p>
<h1>{{ language() === 'bn' ? 'এই কোম্পানির বিশ্লেষণ এখনও প্রস্তুত নয়' : 'No completed dossier for this company yet' }}</h1>
<p>{{ errorMessage() }}</p>
<button class="run-btn" type="button" (click)="runAnalysis(false)">
{{ language() === 'bn' ? 'এখন বিশ্লেষণ চালান' : 'Run analysis now' }}
<button class="primary-action" type="button" (click)="runAnalysis(false)">
{{ language() === 'bn' ? 'এখন বিশ্লেষণ চালান' : 'Run the full analysis' }}
</button>
</section>
}
<!-- Main Analysis Report Dashboard -->
@if (analysis(); as report) {
<div class="dashboard-grid">
<!-- LEFT PANEL: Market Stats & AI Score -->
<section class="panel-left">
<div class="stock-header-card">
<span class="eyebrow-accent">{{ report.sector }}</span>
<h1 class="company-title">{{ report.company_name }}</h1>
<div class="ticker-tags">
<span class="tag symbol">{{ report.symbol }}</span>
@if (report.ai_research) {
<span class="tag ai">AI-GROUNDED</span>
}
<section class="report-hero" [class]="decisionTone(report)">
<div class="hero-main">
<div class="hero-meta">
<span class="sector-chip">{{ report.sector || 'DSE Equity' }}</span>
<span class="date-chip">{{ report.analysis_date | date: 'mediumDate' }}</span>
@if (report.ai_research) { <span class="verified-chip"><i></i> AI GROUNDED</span> }
</div>
<p class="eyebrow">INVESTMENT RESEARCH DOSSIER</p>
<h1>{{ report.company_name }}</h1>
<div class="symbol-line">
<strong>{{ report.symbol }}</strong>
<span>Dhaka Stock Exchange</span>
<span>Generated {{ report.generated_at | date: 'medium' }}</span>
</div>
<div class="decision-block">
<div class="decision-copy">
<span class="decision-kicker">{{ language() === 'bn' ? 'চূড়ান্ত অবস্থান' : 'FINAL POSITION' }}</span>
<strong>{{ finalDecision(report) }}</strong>
<small>{{ hasAgentEvidence(report) ? 'Portfolio Manager · final multi-agent authority' : report.ai_research ? copy(report.ai_research.trader_report.action) : copy(report.score_label) }}</small>
</div>
<span class="decision-mark" aria-hidden="true">
{{ decisionTone(report) === 'positive' ? '✓' : decisionTone(report) === 'negative' ? '×' : '!' }}
</span>
</div>
</div>
<!-- Real-Time Metrics & Statistics -->
<div class="metrics-panel">
<h3 class="panel-subtitle">{{ language() === 'bn' ? 'বাজার পরিসংখ্যান' : 'Market Statistics' }}</h3>
<div class="metric-row-main">
<div class="price-box">
<span class="stat-label">{{ language() === 'bn' ? 'সর্বশেষ দাম' : 'Latest Price' }}</span>
<span class="price-value">{{ formatTaka(report.market.latest_price) }}</span>
<aside class="hero-score-card">
<div class="score-chart">
<canvas #scoreCanvas aria-label="Fundamental score"></canvas>
<div class="score-center">
<strong>{{ report.fundamental_score }}</strong>
<span>/100</span>
</div>
@if (report.market.change_percent !== null) {
<div class="change-box" [class.negative]="report.market.change_percent < 0" [class.positive]="report.market.change_percent > 0">
<span class="change-value">
{{ report.market.change_percent > 0 ? '▲' : '▼' }}
{{ Math.abs(report.market.change_percent) | number: '1.1-2' }}%
</span>
<span class="stat-label">{{ language() === 'bn' ? 'পরিবর্তন' : 'Daily Change' }}</span>
</div>
}
<div class="score-copy">
<span>{{ report.ai_research ? (language() === 'bn' ? 'এআই মৌলিক স্কোর' : 'AI fundamental score') : (language() === 'bn' ? 'মৌলিক স্কোর' : 'Fundamental score') }}</span>
<strong>{{ copy(report.score_label) }}</strong>
<small>{{ report.ai_research ? (report.ai_research.score_confidence | titlecase) + ' confidence' : 'Deterministic DSE evidence' }}</small>
</div>
</aside>
</section>
<div class="stats-table">
<div class="stat-cell">
<span class="cell-label">{{ language() === 'bn' ? 'পূর্ববর্তী ক্লোজ' : 'Prev Close' }}</span>
<span class="cell-value">{{ formatTaka(report.market.previous_close) }}</span>
</div>
<div class="stat-cell">
<span class="cell-label">{{ language() === 'bn' ? '৫২ সপ্তাহের সর্বনিম্ন' : '52W Low' }}</span>
<span class="cell-value">{{ formatTaka(report.market.fifty_two_week_low) }}</span>
<section class="market-ribbon" aria-label="Market snapshot">
<div class="price-cell">
<span>{{ language() === 'bn' ? 'সর্বশেষ মূল্য' : 'Latest price' }}</span>
<strong>{{ formatTaka(report.market.latest_price) }}</strong>
@if (report.market.change_percent !== null) {
<small [class.positive-text]="report.market.change_percent > 0" [class.negative-text]="report.market.change_percent < 0">
{{ report.market.change_percent > 0 ? '▲' : report.market.change_percent < 0 ? '▼' : '•' }}
{{ signedPercent(report.market.change_percent) }} today
</small>
}
</div>
<div class="stat-cell">
<span class="cell-label">{{ language() === 'bn' ? '৫২ সপ্তাহের সর্বোচ্চ' : '52W High' }}</span>
<span class="cell-value">{{ formatTaka(report.market.fifty_two_week_high) }}</span>
<div>
<span>{{ language() === 'bn' ? 'পূর্ববর্তী ক্লোজ' : 'Previous close' }}</span>
<strong>{{ formatTaka(report.market.previous_close) }}</strong>
</div>
<div class="stat-cell full">
<span class="cell-label">{{ language() === 'bn' ? 'বিশ্লেষণের তারিখ' : 'As of Date' }}</span>
<span class="cell-value date">{{ report.analysis_date | date: 'mediumDate' }}</span>
<div>
<span>52W low</span>
<strong>{{ formatTaka(report.market.fifty_two_week_low) }}</strong>
</div>
<div class="range-cell">
<div><span>52-week position</span><strong>{{ rangePosition(report) | number: '1.0-0' }}%</strong></div>
<div class="mini-range"><i [style.left.%]="rangePosition(report)"></i></div>
<small><span>Low</span><span>High</span></small>
</div>
<div>
<span>52W high</span>
<strong>{{ formatTaka(report.market.fifty_two_week_high) }}</strong>
</div>
</section>
<!-- Score Ring Widget -->
<div class="score-widget">
<h3 class="panel-subtitle">{{ report.ai_research ? (language() === 'bn' ? 'এআই মৌলিক স্কোর' : 'AI Fundamental Score') : (language() === 'bn' ? 'মৌলিক স্কোর' : 'Fundamental Score') }}</h3>
<div class="chart-container">
<div class="canvas-wrapper">
<canvas #scoreCanvas aria-label="Fundamental score chart"></canvas>
</div>
<div class="score-center-value">
<span class="score-num">{{ report.fundamental_score }}</span>
<span class="score-max">/100</span>
<nav class="section-nav" aria-label="Report sections">
<button type="button" (click)="jumpTo('decision-summary')">01 · Decision</button>
<button type="button" (click)="jumpTo('valuation')">02 · Valuation</button>
<button type="button" (click)="jumpTo('factor-scorecard')">03 · Factors</button>
<button type="button" (click)="jumpTo('research-dossier')">04 · Full dossier</button>
<span>{{ reportDocumentCount() }} agent documents</span>
</nav>
<section id="decision-summary" class="decision-layout section-anchor">
<article class="investment-case surface-card">
<header class="section-heading compact">
<div>
<p class="eyebrow">01 · DECISION AT A GLANCE</p>
<h2>{{ copy(report.headline) }}</h2>
</div>
<span class="status-badge" [class]="decisionTone(report)">
<i>{{ decisionTone(report) === 'positive' ? '✓' : decisionTone(report) === 'negative' ? '×' : '!' }}</i>
{{ finalDecision(report) }}
</span>
</header>
<p class="lead-copy">{{ copy(report.in_depth_snippet) }}</p>
<div class="takeaway-list">
@for (takeaway of report.takeaways; track takeaway.en; let index = $index) {
<div class="takeaway-item">
<span>{{ (index + 1).toString().padStart(2, '0') }}</span>
<p>{{ copy(takeaway) }}</p>
</div>
<div class="score-verdict-tag">{{ copy(report.score_label) }}</div>
}
</div>
</section>
<!-- CENTER PANEL: Our Take & Valuation Matrix -->
<section class="panel-center">
<!-- Editorial Take -->
<div class="editorial-card">
<div class="editorial-header">
<span class="editorial-label">{{ language() === 'bn' ? 'আমাদের মত' : 'OUR TAKE' }}</span>
<span class="score-pill">{{ copy(report.score_label) }}</span>
@if (report.ai_research; as ai) {
<div class="ai-rationale">
<div class="ai-rationale-head">
<span class="ai-badge">AI SYNTHESIS</span>
<small>{{ ai.provider }} · {{ ai.model }}</small>
</div>
<p>{{ copy(ai.score_rationale) }}</p>
<footer>
<span>Evidence confidence</span>
<strong>{{ ai.score_confidence | uppercase }}</strong>
</footer>
</div>
}
</article>
<h2 class="editorial-headline">“{{ copy(report.headline) }}”</h2>
<ul class="bullet-highlights">
@for (takeaway of report.takeaways; track takeaway.en) {
<li>
<span class="bullet-marker"></span>
<p>{{ copy(takeaway) }}</p>
</li>
<aside class="decision-rail">
@if (report.ai_research; as ai) {
<article class="rail-card thesis">
<span class="rail-icon positive"></span>
<div><small>INVESTMENT THESIS</small><p>{{ copy(ai.trader_report.investment_thesis) }}</p></div>
</article>
<article class="rail-card entry">
<span class="rail-icon caution"></span>
<div><small>ENTRY STRATEGY</small><p>{{ copy(ai.trader_report.entry_strategy) }}</p></div>
</article>
<article class="rail-card risk">
<span class="rail-icon negative"></span>
<div><small>RISK CONTROLS</small><p>{{ copy(ai.trader_report.risk_controls) }}</p></div>
</article>
<article class="rail-card horizon">
<span class="rail-icon neutral"></span>
<div><small>TIME HORIZON</small><p>{{ copy(ai.trader_report.time_horizon) }}</p></div>
</article>
} @else {
<article class="rail-card neutral-card">
<span class="rail-icon neutral">i</span>
<div><small>REPORT MODE</small><p>This result uses deterministic DSE calculations. The AI trader synthesis is not available for this saved report.</p></div>
</article>
}
</ul>
</aside>
</section>
@if (report.ai_research; as ai) {
<div class="ai-rationale-box">
<div class="rationale-header">
<span class="ai-chip">AI Synthesis</span>
<span class="confidence-chip">{{ language() === 'bn' ? 'আস্থা' : 'Confidence' }}: {{ ai.score_confidence | uppercase }}</span>
</div>
<p class="rationale-text">{{ copy(ai.score_rationale) }}</p>
<div class="provenance-footer">
Model: {{ ai.provider }} / {{ ai.model }}
</div>
</div>
<section class="trigger-grid">
<article class="trigger-card positive">
<header><span>+</span><div><p class="eyebrow">WHAT COULD WORK</p><h3>Potential catalysts</h3></div></header>
<ul>
@for (item of ai.trader_report.catalysts; track item.en) { <li>{{ copy(item) }}</li> }
</ul>
</article>
<article class="trigger-card negative">
<header><span>×</span><div><p class="eyebrow">WHEN TO REASSESS</p><h3>Invalidation conditions</h3></div></header>
<ul>
@for (item of ai.trader_report.invalidation_conditions; track item.en) { <li>{{ copy(item) }}</li> }
</ul>
</article>
</section>
}
</div>
<!-- Valuation Card -->
<div class="valuation-matrix-card">
<div class="valuation-header">
<section id="valuation" class="valuation-section section-anchor">
<header class="section-heading">
<div>
<span class="eyebrow-accent">{{ language() === 'bn' ? 'আজকের মূল্যায়ন' : 'VALUATION TODAY' }}</span>
<div class="valuation-prices">
<div class="price-stat">
<span class="stat-lbl">{{ language() === 'bn' ? 'বর্তমান মূল্য' : 'Current Price' }}</span>
<span class="price-val">{{ formatTaka(report.valuation.current_price) }}</span>
</div>
<div class="price-stat">
<span class="stat-lbl">{{ language() === 'bn' ? 'প্রাক্কলিত ন্যায্য মূল্য' : 'Rough Estimate' }}</span>
<span class="price-val estimate">{{ formatTaka(report.valuation.rough_estimate) }}</span>
</div>
</div>
<p class="eyebrow">02 · PRICE VS VALUE</p>
<h2>{{ language() === 'bn' ? 'দামের পেছনে মূল্য কতটা শক্ত?' : 'What does the price imply?' }}</h2>
<p>{{ copy(report.valuation.summary) }}</p>
</div>
<span class="valuation-verdict" [class]="report.valuation.verdict">{{ copy(report.valuation.verdict_label) }}</span>
</header>
<span class="verdict-badge" [class]="report.valuation.verdict">
{{ copy(report.valuation.verdict_label) }}
</span>
</div>
<!-- Fair Range Visual Track -->
@if (report.valuation.fair_range_low !== null && report.valuation.fair_range_high !== null) {
<div class="valuation-slider-container">
<span class="slider-lbl">{{ language() === 'bn' ? 'ন্যায্য মূল্যের সীমা' : 'Fair Valuation Range' }}</span>
<div class="custom-slider-track">
<div class="fair-zone-bar"
[style.left.%]="15"
[style.right.%]="15"></div>
<div class="current-price-indicator"
[style.left.%]="50">
<span class="tooltip">{{ language() === 'bn' ? 'বর্তমান' : 'Current' }}</span>
<div class="valuation-board surface-card">
<div class="valuation-numbers">
<div><span>CURRENT PRICE</span><strong>{{ formatTaka(report.valuation.current_price) }}</strong></div>
<div><span>ROUGH FAIR VALUE</span><strong>{{ formatTaka(report.valuation.rough_estimate) }}</strong></div>
<div>
<span>ESTIMATE GAP</span>
<strong [class.positive-text]="(fairValueGap(report) ?? 0) > 0" [class.negative-text]="(fairValueGap(report) ?? 0) < 0">
{{ fairValueGap(report) === null ? '—' : signedPercent(fairValueGap(report)) }}
</strong>
</div>
<div><span>CONFIDENCE</span><strong>{{ report.valuation.confidence | titlecase }}</strong></div>
</div>
<div class="slider-labels">
<span>{{ formatTaka(report.valuation.fair_range_low) }}</span>
<strong class="label-mid">{{ language() === 'bn' ? 'ন্যায্য মূল্য সীমা' : 'Fair Range' }}</strong>
<span>{{ formatTaka(report.valuation.fair_range_high) }}</span>
@if (report.valuation.fair_range_low !== null && report.valuation.fair_range_high !== null) {
<div class="valuation-range">
<div class="range-label-row"><span>Relative value map</span><small>Not a price target</small></div>
<div class="value-track">
<span class="fair-zone" [style.left.%]="valuationScale(report).low" [style.width.%]="valuationScale(report).high - valuationScale(report).low"></span>
<i class="current-marker" [style.left.%]="valuationScale(report).current"><b>Current</b></i>
</div>
<div class="value-track-labels">
<span [style.left.%]="valuationScale(report).low">{{ formatTaka(report.valuation.fair_range_low) }}</span>
<span [style.left.%]="valuationScale(report).high">{{ formatTaka(report.valuation.fair_range_high) }}</span>
</div>
}
<p class="valuation-desc">{{ copy(report.valuation.summary) }}</p>
@if (report.ai_research) {
<div class="weight-disclaimer">
💡 {{ language() === 'bn'
? 'এআই প্রতিটি হিসাব-পদ্ধতির নির্ভরযোগ্যতার ওজন নির্ধারণ করেছে। ন্যায্য সীমা আনুমানিক মূল্যের ৮০%–১২০%।'
: 'AI weights methods based on reliability. Fair range is calculated at 80%–120% of estimated value.' }}
</div>
}
<!-- Method list -->
<div class="methods-grid">
<div class="valuation-methods">
@for (method of report.valuation.methods; track method.key) {
<div class="method-card" [class.disabled]="!method.available">
<span class="method-dot" [class.active]="method.available"></span>
<div class="method-details">
<span class="method-name">{{ copy(method.label) }}</span>
<span class="method-value">{{ formatTaka(method.value) }}</span>
</div>
</div>
<article [class.unavailable]="!method.available">
<span [class.available]="method.available">{{ method.available ? '✓' : '—' }}</span>
<div><small>{{ copy(method.label) }}</small><strong>{{ formatTaka(method.value) }}</strong></div>
</article>
}
</div>
<p class="method-note">These are educational estimates calculated from available DSE evidence. They are not guaranteed values or instructions to trade.</p>
</div>
</section>
<!-- RIGHT PANEL: Score Factors -->
<section class="panel-right">
<h3 class="right-panel-title">{{ language() === 'bn' ? 'মৌলিক বিষয়সমূহ' : 'Fundamental Factors' }}</h3>
<section id="factor-scorecard" class="factor-section section-anchor">
<header class="section-heading">
<div>
<p class="eyebrow">03 · EVIDENCE SCORECARD</p>
<h2>{{ language() === 'bn' ? 'কোন বিষয় শক্ত, কোনটিতে সতর্কতা?' : 'Where the evidence is strong—and where it is not' }}</h2>
<p>Green supports the case, amber requires monitoring, and red deserves investigation before capital is committed.</p>
</div>
<div class="status-legend" aria-label="Signal legend">
<span class="positive"><i></i> Positive</span>
<span class="caution"><i>!</i> Watch</span>
<span class="negative"><i>×</i> Concern</span>
</div>
</header>
<div class="factors-stack">
<div class="factor-grid">
@for (factor of report.factors; track factor.key) {
<article class="factor-dashboard-card" [class]="factor.status">
<header class="factor-hdr">
<div class="factor-title-box">
<h4 class="factor-title">{{ copy(factor.title) }}</h4>
<span class="factor-subtitle">{{ copy(factor.subtitle) }}</span>
</div>
<span class="factor-badge" [class]="factor.status">
{{ factorStatus(factor.status) }}
</span>
<article class="factor-card" [class]="factor.status">
<header>
<div class="factor-mark"><span>{{ factor.status === 'positive' ? '✓' : factor.status === 'negative' ? '×' : factor.status === 'caution' ? '!' : '•' }}</span></div>
<div><p>{{ copy(factor.subtitle) }}</p><h3>{{ copy(factor.title) }}</h3></div>
<strong>{{ factorAverage(report, factor.key) | number: '1.0-1' }}<small>/10</small></strong>
</header>
<p class="factor-desc-snippet">{{ copy(factor.explanation) }}</p>
<!-- Inner metrics list -->
<div class="factor-metrics-list">
<p class="factor-explanation">{{ copy(factor.explanation) }}</p>
<div class="factor-metrics">
@for (metric of factor.metrics; track metric.key) {
<div class="metric-row-ui">
<span class="metric-lbl">{{ copy(metric.label) }}</span>
<div class="metric-bar-wrapper">
<div class="progress-bar-track">
<div class="progress-bar-fill"
[class.positive]="metric.score >= 7"
[class.caution]="metric.score >= 4 && metric.score < 7"
[class.negative]="metric.score < 4"
[style.width.%]="metric.score * 10"></div>
</div>
<span class="metric-val">
<strong>{{ metric.display_value }}</strong>
<div class="factor-metric">
<div><span>{{ copy(metric.label) }}</span><strong>{{ metric.display_value }}</strong></div>
<div class="metric-track"><i [class]="metricStatus(metric.score)" [style.width.%]="metric.score * 10"></i></div>
<small>{{ metric.score | number: '1.0-1' }}/10</small>
</span>
</div>
</div>
}
</div>
<footer><span class="status-badge" [class]="factor.status"><i>{{ factor.status === 'positive' ? '✓' : factor.status === 'negative' ? '×' : '!' }}</i>{{ factorStatus(factor.status) }}</span></footer>
</article>
}
</div>
</section>
</div>
<!-- FULL REPORT CONSOLE (Tabbed Layout) -->
<section class="reports-console-section">
<div class="console-header">
<span class="console-title">{{ language() === 'bn' ? 'গবেষণা ও এজেন্ট তথ্য' : 'RESEARCH & AGENT CONSOLE' }}</span>
<div class="console-tabs" role="tablist">
<button
type="button"
role="tab"
[class.active]="activeReportTab() === 'overview'"
(click)="activeReportTab.set('overview')">
{{ language() === 'bn' ? 'গভীর বিশ্লেষণ' : 'In-depth Report' }}
</button>
@if (report.ai_research) {
<button
type="button"
role="tab"
[class.active]="activeReportTab() === 'trader'"
(click)="activeReportTab.set('trader')">
{{ language() === 'bn' ? 'ট্রেডার ভিউ' : 'AI Trader View' }}
</button>
}
@if (hasAgentEvidence(report)) {
<button
type="button"
role="tab"
[class.active]="activeReportTab() === 'agents'"
(click)="activeReportTab.set('agents')">
{{ language() === 'bn' ? 'এজেন্ট প্রমাণ' : 'Agent Evidence' }}
</button>
<button
type="button"
role="tab"
[class.active]="activeReportTab() === 'raw'"
(click)="activeReportTab.set('raw')">
{{ language() === 'bn' ? 'স্টেট লগ' : 'Technical Log' }}
</button>
}
</div>
</div>
<div class="console-body">
<!-- TAB 1: In-depth Analysis Report -->
@if (activeReportTab() === 'overview') {
<div class="tab-pane-fade">
<div class="in-depth-header">
<h2>{{ copy(report.in_depth_title) }}</h2>
<p class="snippet-highlight">{{ copy(report.in_depth_snippet) }}</p>
</div>
<div class="report-sections-grid">
<section class="summary-reports">
<header class="section-heading compact">
<div><p class="eyebrow">DETERMINISTIC SUMMARY</p><h2>{{ copy(report.in_depth_title) }}</h2></div>
</header>
<div class="summary-report-grid">
@for (section of report.report_sections; track section.key) {
<article class="report-section-card">
<h3 class="section-title">{{ copy(section.title) }}</h3>
<p class="section-summary">{{ copy(section.summary) }}</p>
<article [class]="getSectionStatus(section.key)">
<header><span>{{ getSectionStatus(section.key) === 'positive' ? '✓' : getSectionStatus(section.key) === 'negative' ? '×' : getSectionStatus(section.key) === 'caution' ? '!' : '•' }}</span><h3>{{ copy(section.title) }}</h3></header>
<p>{{ copy(section.summary) }}</p>
@if (section.bullets.length) {
<ul class="section-bullets">
@for (bullet of section.bullets; track bullet.en) {
<li>{{ copy(bullet) }}</li>
}
</ul>
<ul>@for (bullet of section.bullets; track bullet.en) { <li>{{ copy(bullet) }}</li> }</ul>
}
</article>
}
</div>
</div>
}
<!-- TAB 2: AI Trader View -->
@if (activeReportTab() === 'trader' && report.ai_research; as ai) {
<div class="tab-pane-fade trader-view-pane">
<div class="trader-hero-row">
<div class="action-recommendation">
<span class="sub">{{ language() === 'bn' ? 'সুপারিশকৃত অ্যাকশন' : 'RECOMMENDED ACTION' }}</span>
<h2>{{ copy(ai.trader_report.action) }}</h2>
</div>
<div class="rating-box" [class]="ai.trader_report.rating.toLowerCase()">
<span class="sub">{{ language() === 'bn' ? 'রেটিং' : 'TRADER RATING' }}</span>
<span class="rating-value">{{ ai.trader_report.rating }}</span>
</div>
</div>
<div class="trader-exec-summary">
<h3>{{ language() === 'bn' ? 'এক্সিকিউটিভ সামারি' : 'Executive Summary' }}</h3>
<p>{{ copy(ai.trader_report.executive_summary) }}</p>
</div>
<div class="trader-strategy-grid">
<article class="strat-card">
<h4>🎯 {{ language() === 'bn' ? 'বিনিয়োগ যুক্তি' : 'Investment Thesis' }}</h4>
<p>{{ copy(ai.trader_report.investment_thesis) }}</p>
</article>
<article class="strat-card">
<h4>📥 {{ language() === 'bn' ? 'এন্ট্রি কৌশল' : 'Entry Strategy' }}</h4>
<p>{{ copy(ai.trader_report.entry_strategy) }}</p>
</article>
<article class="strat-card">
<h4>🛡️ {{ language() === 'bn' ? 'ঝুঁকি নিয়ন্ত্রণ' : 'Risk Controls' }}</h4>
<p>{{ copy(ai.trader_report.risk_controls) }}</p>
</article>
</section>
<article class="strat-card">
<h4>⏳ {{ language() === 'bn' ? 'সময়সীমা' : 'Time Horizon' }}</h4>
<p>{{ copy(ai.trader_report.time_horizon) }}</p>
</article>
<section id="research-dossier" class="dossier-section section-anchor">
<header class="dossier-heading">
<div>
<p class="eyebrow">04 · COMPLETE MULTI-AGENT RECORD</p>
<h2>Every report. One readable dossier.</h2>
<p>The CLI’s full analyst, research, trading, risk, and portfolio output is preserved below. Open any document to read its headings, tables, lists, and conclusions in a clean research format.</p>
</div>
<div class="dossier-count"><strong>{{ reportDocumentCount() }}</strong><span>documents available</span></div>
</header>
<div class="trader-lists-row">
<div class="list-column">
<h4>⚡ {{ language() === 'bn' ? 'সম্ভায্য অনুঘটক' : 'Potential Catalysts' }}</h4>
<ul class="console-bullets">
@for (item of ai.trader_report.catalysts; track item.en) {
<li>{{ copy(item) }}</li>
@if (reportGroups().length) {
<div class="dossier-layout">
<aside class="dossier-index">
<p>REPORT INDEX</p>
@for (group of reportGroups(); track group.key; let index = $index) {
<button type="button" (click)="jumpTo(group.key)"><span>{{ (index + 1).toString().padStart(2, '0') }}</span>{{ group.title }}<small>{{ group.documents.length }}</small></button>
}
</ul>
</div>
<button type="button" class="technical-link" (click)="jumpTo('source-ledger')"><span></span>Sources & technical log</button>
</aside>
<div class="dossier-content">
@for (group of reportGroups(); track group.key) {
<section [id]="group.key" class="report-group section-anchor">
<header>
<p class="eyebrow">{{ group.kicker }}</p>
<h3>{{ group.title }}</h3>
<p>{{ group.description }}</p>
</header>
<div class="list-column">
<h4>🚫 {{ language() === 'bn' ? 'যে অবস্থায় মত বদলাবে' : 'Invalidation Conditions' }}</h4>
<ul class="console-bullets">
@for (item of ai.trader_report.invalidation_conditions; track item.en) {
<li>{{ copy(item) }}</li>
<div class="document-stack" [class.debate-documents]="group.key === 'research-debate'">
@for (document of group.documents; track document.key) {
<details class="report-document" [class]="document.tone" [open]="document.open">
<summary>
<span class="document-avatar">{{ document.icon }}</span>
<span class="document-title"><small>{{ document.role }}</small><strong>{{ document.title }}</strong></span>
<span class="document-tone" [class]="document.tone">
{{ document.tone === 'positive' ? '✓' : document.tone === 'negative' ? '×' : document.tone === 'caution' ? '!' : '•' }}
</span>
<span class="document-toggle"><i></i></span>
</summary>
<article class="markdown-report" [innerHTML]="document.html"></article>
</details>
}
</ul>
</div>
</section>
}
</div>
<div class="data-quality-box">
<strong>{{ language() === 'bn' ? 'তথ্যের মান:' : 'Data Quality:' }}</strong> {{ copy(ai.data_quality) }}
</div>
} @else {
<div class="no-dossier">
<span>i</span>
<div><h3>No multi-agent state was attached to this saved analysis.</h3><p>Run a fresh analysis from the main CLI to populate the complete analyst, debate, risk, and portfolio dossier.</p></div>
</div>
}
</section>
<!-- TAB 3: Agent Evidence -->
@if (activeReportTab() === 'agents') {
<div class="tab-pane-fade agents-pane">
<p class="agents-notice">{{ language() === 'bn' ? 'নিচের অংশগুলো মাল্টি-এজেন্ট রান থেকে অপরিবর্তিত রাখা হয়েছে।' : 'These analyst reports are compiled directly by the independent trading subagents.' }}</p>
<div class="evidence-accordion">
@if (report.agent_reports.final_trade_decision) {
<details class="evidence-details" open>
<summary class="evidence-summary">⚡ {{ language() === 'bn' ? 'চূড়ান্ত ট্রেড সিদ্ধান্ত' : 'Final Trade Decision' }}</summary>
<pre class="raw-output">{{ report.agent_reports.final_trade_decision }}</pre>
</details>
}
@if (report.agent_reports.fundamentals_report) {
<details class="evidence-details">
<summary class="evidence-summary">📊 {{ language() === 'bn' ? 'মৌলিক বিশ্লেষক' : 'Fundamentals Analyst' }}</summary>
<pre class="raw-output">{{ report.agent_reports.fundamentals_report }}</pre>
</details>
}
@if (report.agent_reports.market_report) {
<details class="evidence-details">
<summary class="evidence-summary">📈 {{ language() === 'bn' ? 'বাজার বিশ্লেষক' : 'Market Analyst' }}</summary>
<pre class="raw-output">{{ report.agent_reports.market_report }}</pre>
</details>
}
@if (report.agent_reports.news_report) {
<details class="evidence-details">
<summary class="evidence-summary">📰 {{ language() === 'bn' ? 'সংবাদ বিশ্লেষক' : 'News Analyst' }}</summary>
<pre class="raw-output">{{ report.agent_reports.news_report }}</pre>
</details>
<section id="source-ledger" class="source-section section-anchor">
<header class="section-heading compact">
<div><p class="eyebrow">PROVENANCE & AUDIT</p><h2>Know where every layer came from.</h2></div>
</header>
<div class="source-grid">
@for (source of report.sources; track source.name) {
<article>
<span>{{ source.source_type === 'dse_api' ? 'DSE' : source.source_type === 'agent_state' ? 'AI' : source.source_type === 'calculation' ? 'ƒx' : 'LLM' }}</span>
<div><small>{{ source.source_type.replace('_', ' ') }}</small><h3>{{ source.name }}</h3><p>{{ source.detail }}</p></div>
</article>
}
</div>
</div>
}
<!-- TAB 4: Technical JSON state log -->
@if (activeReportTab() === 'raw') {
<div class="tab-pane-fade raw-state-pane">
<pre class="json-state">{{ rawState(report) }}</pre>
</div>
@if (hasAgentEvidence(report)) {
<details class="technical-state">
<summary>Developer view · complete JSON state log <span>Open technical record</span></summary>
<pre>{{ rawState(report) }}</pre>
</details>
}
</div>
</section>
<!-- Footer -->
<footer class="analysis-footer">
<strong>DSE AI TERMINAL</strong>
<span>{{ copy(report.disclaimer) }}</span>
<footer class="report-footer">
<div class="footer-brand"><span>D</span><strong>DOHA RESEARCH</strong></div>
<div class="disclaimer"><strong>Important</strong><p>{{ copy(report.disclaimer) }}</p></div>
<a href="#top">Back to top ↑</a>
</footer>
}
</main>
</main>
}
:host {
--ink: #17211b;
--ink-soft: #4f5c54;
--muted: #758078;
--canvas: #f4f3ee;
--surface: #ffffff;
--surface-soft: #f8f8f4;
--line: #dfe3dc;
--line-strong: #c8cec6;
--green: #147d57;
--green-soft: #e8f4ed;
--green-line: #a9cfba;
--amber: #b66a17;
--amber-soft: #fff3df;
--amber-line: #e7c18f;
--red: #bd4141;
--red-soft: #fbeaea;
--red-line: #e4afaf;
--blue: #225b77;
--blue-soft: #eaf2f6;
--shadow-sm: 0 1px 2px rgba(26, 37, 30, 0.05);
--shadow-md: 0 12px 34px rgba(26, 37, 30, 0.08);
display: block;
color: #f3f4f6;
background: #090d16;
}
.analysis-page {
--bg: #090d16;
--bg-panel: #111827;
--bg-panel-hover: #1f2937;
--border: #1f2937;
--border-focus: #374151;
--text: #f3f4f6;
--text-muted: #9ca3af;
--blue: #0ea5e9;
--blue-glow: rgba(14, 165, 233, 0.15);
--green: #10b981;
--green-glow: rgba(16, 185, 129, 0.15);
--red: #ef4444;
--red-glow: rgba(239, 68, 68, 0.15);
--amber: #f59e0b;
--amber-glow: rgba(245, 158, 11, 0.15);
.research-terminal {
min-height: 100vh;
padding: 1.5rem clamp(1rem, 4vw, 3rem) 4rem;
background:
radial-gradient(circle at 0% 0%, rgba(14, 165, 233, 0.08), transparent 45rem),
radial-gradient(circle at 100% 100%, rgba(16, 185, 129, 0.05), transparent 45rem),
var(--bg);
font-family: 'Inter', sans-serif;
padding: 0 2rem 3rem;
color: var(--ink);
background: var(--canvas);
}
.bangla-copy {
line-height: 1.7;
.company-title {
font-family: 'Noto Sans Bengali', sans-serif;
}
.editorial-headline {
font-family: 'Noto Sans Bengali', sans-serif;
line-height: 1.5;
}
}
.bangla-copy { font-family: 'Noto Sans Bengali', 'Inter', sans-serif; }
/* Header & Controls */
.analysis-toolbar {
.topbar {
position: sticky;
z-index: 40;
top: 0;
max-width: 1500px;
min-height: 5rem;
margin: 0 auto;
padding: 0.85rem 0;
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1560px;
margin: 0 auto 2.5rem;
padding-bottom: 1.25rem;
border-bottom: 1px solid var(--border);
gap: 1.5rem;
background: rgba(244, 243, 238, 0.94);
border-bottom: 1px solid var(--line);
backdrop-filter: blur(18px);
}
.brand {
display: flex;
display: inline-flex;
align-items: center;
gap: 0.75rem;
color: var(--text);
font-size: 0.85rem;
font-weight: 700;
letter-spacing: 0.18em;
color: inherit;
text-decoration: none;
}
.brand-dot {
width: 0.65rem;
height: 0.65rem;
.brand-mark,
.footer-brand span {
width: 2.1rem;
height: 2.1rem;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--blue);
box-shadow: 0 0 10px var(--blue);
background: var(--green);
color: white;
font-family: 'Plus Jakarta Sans', sans-serif;
font-size: 0.82rem;
font-weight: 800;
}
.brand-accent {
color: var(--blue);
}
.brand-copy { display: flex; flex-direction: column; gap: 0.12rem; }
.brand-copy strong { font-size: 0.78rem; letter-spacing: 0.16em; }
.brand-copy small { color: var(--muted); font-size: 0.58rem; letter-spacing: 0.1em; }
.analysis-controls {
.topbar-actions { display: flex; align-items: center; gap: 0.65rem; }
.stock-selector {
min-width: 19rem;
display: flex;
align-items: center;
gap: 1rem;
gap: 0.65rem;
padding: 0.42rem 0.55rem 0.42rem 0.8rem;
background: var(--surface);
border: 1px solid var(--line-strong);
border-radius: 0.65rem;
box-shadow: var(--shadow-sm);
}
.selector-wrapper {
position: relative;
min-width: 15rem;
select {
width: 100%;
min-height: 2.5rem;
padding: 0.5rem 2rem 0.5rem 1rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
color: var(--text);
background: var(--bg-panel);
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
appearance: none;
transition: all 0.2s ease;
&:hover {
border-color: var(--border-focus);
background: var(--bg-panel-hover);
}
.stock-selector span { color: var(--muted); font-size: 0.62rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
.stock-selector select { min-width: 0; flex: 1; border: 0; color: var(--ink); background: transparent; font-size: 0.8rem; font-weight: 700; cursor: pointer; }
&:focus {
outline: none;
border-color: var(--blue);
box-shadow: 0 0 0 3px var(--blue-glow);
}
}
&::after {
content: "▼";
position: absolute;
right: 0.85rem;
top: 50%;
transform: translateY(-50%) scale(0.75);
color: var(--text-muted);
pointer-events: none;
}
}
.run-btn {
.primary-action {
min-height: 2.75rem;
padding: 0.7rem 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.5rem;
padding: 0.5rem 1.5rem;
border: 1px solid var(--blue);
border-radius: 0.5rem;
color: #fff;
background: var(--blue);
font-size: 0.9rem;
font-weight: 600;
gap: 0.5rem;
border: 1px solid #0f6948;
border-radius: 0.65rem;
background: var(--green);
color: white;
font-size: 0.76rem;
font-weight: 750;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
&:hover:not(:disabled) {
background: #0284c7;
border-color: #0284c7;
box-shadow: 0 0 12px rgba(14, 165, 233, 0.4);
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
box-shadow: 0 5px 14px rgba(20, 125, 87, 0.18);
transition: 160ms ease;
}
.btn-pulse {
width: 0.75rem;
height: 0.75rem;
margin-right: 0.5rem;
border: 2px solid transparent;
border-top-color: #fff;
.primary-action:hover:not(:disabled) { transform: translateY(-1px); background: #106c4a; }
.primary-action:disabled { cursor: wait; opacity: 0.7; }
.button-spinner {
width: 0.85rem;
height: 0.85rem;
border: 2px solid rgba(255,255,255,.35);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.language-switch {
padding: 0.22rem;
display: flex;
padding: 0.2rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
background: var(--bg-panel);
border: 1px solid var(--line-strong);
border-radius: 0.65rem;
background: var(--surface);
}
button {
min-height: 2.1rem;
padding: 0.25rem 0.85rem;
.language-switch button {
min-width: 2.25rem;
padding: 0.5rem 0.55rem;
border: 0;
border-radius: 0.35rem;
color: var(--text-muted);
border-radius: 0.45rem;
background: transparent;
color: var(--muted);
font-size: 0.66rem;
font-weight: 800;
cursor: pointer;
font-size: 0.78rem;
font-weight: 700;
transition: all 0.2s ease;
&:hover {
color: var(--text);
}
&.active {
color: #fff;
background: var(--blue);
}
}
}
/* Loading & Skeletons */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 25rem;
text-align: center;
max-width: 1560px;
margin: auto;
h2 {
margin: 1.5rem 0 0.5rem;
font-size: 1.25rem;
color: var(--text);
}
p {
color: var(--text-muted);
font-size: 0.9rem;
margin-bottom: 2.5rem;
}
}
.loading-spinner {
width: 2.5rem;
height: 2.5rem;
border: 3px solid rgba(14, 165, 233, 0.1);
border-top-color: var(--blue);
border-radius: 50%;
animation: spin 1s cubic-bezier(0.55, 0.085, 0.68, 0.53) infinite;
}
.language-switch button.active { background: var(--ink); color: white; }
.skeleton-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
gap: 1.5rem;
width: 100%;
}
.skeleton-card {
border-radius: 0.75rem;
background: linear-gradient(90deg, #111827 25%, #1f2937 50%, #111827 75%);
background-size: 200% 100%;
animation: skeleton-pulse 1.6s infinite;
&.short { height: 6rem; }
&.medium { height: 12rem; }
&.tall { height: 20rem; }
}
@keyframes skeleton-pulse {
to { background-position: -200% 0; }
}
/* Empty & Error State */
.empty-state {
.progress-notice {
position: relative;
z-index: 45;
max-width: 1500px;
margin: 0.55rem auto 0;
padding: 0.72rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 28rem;
text-align: center;
max-width: 600px;
margin: 4rem auto;
padding: 3rem;
border: 1px dashed var(--border);
border-radius: 1rem;
background: var(--bg-panel);
.alert-icon {
font-size: 2.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 0.75rem;
}
p {
color: var(--text-muted);
font-size: 0.95rem;
margin-bottom: 2rem;
line-height: 1.5;
}
}
/* Dashboard Core Layout Grid */
.dashboard-grid {
display: grid;
grid-template-columns: 20rem 1fr 21rem;
gap: 1.5rem;
max-width: 1560px;
margin: 0 auto 2.5rem;
align-items: start;
}
/* Panels Common */
.panel-left, .panel-center, .panel-right {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Card Styling */
.stock-header-card,
.metrics-panel,
.score-widget,
.editorial-card,
.valuation-matrix-card,
.factor-dashboard-card {
border: 1px solid var(--border);
border-radius: 0.75rem;
background: var(--bg-panel);
padding: 1.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
&:hover {
border-color: var(--border-focus);
}
}
/* LEFT PANEL COMPONENT STYLING */
.stock-header-card {
display: flex;
flex-direction: column;
}
.eyebrow-accent {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.12em;
gap: 0.65rem;
background: var(--blue-soft);
border: 1px solid #bcd0db;
border-radius: 0.65rem;
color: var(--blue);
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.company-title {
font-size: 1.5rem;
font-weight: 800;
line-height: 1.15;
letter-spacing: -0.025em;
color: var(--text);
margin: 0 0 1rem;
}
.ticker-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
font-size: 0.72rem;
font-weight: 700;
padding: 0.25rem 0.65rem;
border-radius: 0.25rem;
letter-spacing: 0.05em;
&.symbol {
background: #1e293b;
color: #cbd5e1;
border: 1px solid #334155;
}
&.ai {
background: var(--green-glow);
color: var(--green);
border: 1px solid rgba(16, 185, 129, 0.3);
}
}
/* Market Metrics Panel */
.panel-subtitle {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.12em;
color: var(--text-muted);
text-transform: uppercase;
margin: 0 0 1.25rem;
border-bottom: 1px solid var(--border);
padding-bottom: 0.5rem;
}
.metric-row-main {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 1.5rem;
}
.price-box {
display: flex;
flex-direction: column;
}
.stat-label {
font-size: 0.7rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.price-value {
font-size: 1.85rem;
font-weight: 800;
color: var(--text);
line-height: 1.1;
margin-top: 0.2rem;
}
.change-box {
display: flex;
flex-direction: column;
align-items: flex-end;
&.positive {
.change-value { color: var(--green); }
}
&.negative {
.change-value { color: var(--red); }
}
}
.change-value {
font-size: 1.25rem;
font-weight: 700;
line-height: 1.1;
margin-bottom: 0.2rem;
}
.stats-table {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.85rem;
}
.stat-cell {
display: flex;
flex-direction: column;
padding: 0.5rem 0.75rem;
border-radius: 0.35rem;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.02);
&.full {
grid-column: span 2;
}
}
.cell-label {
font-size: 0.65rem;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 0.15rem;
}
.cell-value {
font-size: 0.9rem;
font-weight: 700;
color: var(--text);
&.date {
font-size: 0.8rem;
color: var(--blue);
}
}
/* Score Ring Widget */
.score-widget {
display: flex;
flex-direction: column;
align-items: center;
}
.chart-container {
position: relative;
width: 11rem;
height: 11rem;
margin: 1rem 0;
}
.progress-notice strong { font-size: 0.72rem; }
.progress-notice > span:last-child { color: #557080; }
.progress-pulse { width: 0.55rem; height: 0.55rem; border-radius: 50%; background: var(--blue); box-shadow: 0 0 0 0 rgba(34,91,119,.4); animation: pulse 1.5s infinite; }
.canvas-wrapper {
position: absolute;
inset: 0;
}
.score-center-value {
position: absolute;
inset: 0;
.loading-state,
.empty-state {
max-width: 920px;
min-height: calc(100vh - 6rem);
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
}
.score-num {
font-size: 2.75rem;
font-weight: 800;
color: var(--blue);
line-height: 1;
}
.score-max {
font-size: 0.75rem;
font-weight: 700;
color: var(--text-muted);
letter-spacing: 0.05em;
margin-top: 0.15rem;
}
.score-verdict-tag {
font-size: 0.85rem;
font-weight: 700;
color: var(--blue);
text-transform: uppercase;
letter-spacing: 0.1em;
background: var(--blue-glow);
padding: 0.35rem 0.85rem;
border-radius: 0.25rem;
border: 1px solid rgba(14, 165, 233, 0.25);
margin-top: 0.5rem;
text-align: center;
width: 100%;
}
/* CENTER PANEL STYLING */
/* Editorial Card (Our Take) */
.editorial-card {
display: flex;
flex-direction: column;
}
.editorial-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.25rem;
border-bottom: 1px solid var(--border);
padding-bottom: 0.75rem;
}
.editorial-label {
font-size: 0.75rem;
font-weight: 800;
color: var(--text-muted);
letter-spacing: 0.15em;
}
.score-pill {
font-size: 0.65rem;
font-weight: 800;
color: var(--green);
background: var(--green-glow);
border: 1px solid rgba(16, 185, 129, 0.2);
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
text-transform: uppercase;
}
.editorial-headline {
font-size: clamp(1.25rem, 2.2vw, 1.6rem);
font-weight: 700;
line-height: 1.35;
color: var(--text);
margin: 0 0 1.25rem;
letter-spacing: -0.015em;
}
.bullet-highlights {
list-style: none;
padding: 0;
margin: 0 0 1.5rem;
display: flex;
flex-direction: column;
gap: 0.85rem;
li {
display: flex;
gap: 0.75rem;
align-items: flex-start;
}
}
.bullet-marker {
width: 0.4rem;
height: 0.4rem;
border-radius: 50%;
background: var(--blue);
box-shadow: 0 0 6px var(--blue);
flex-shrink: 0;
margin-top: 0.5rem;
}
.bullet-highlights p {
margin: 0;
font-size: 0.95rem;
line-height: 1.5;
color: #d1d5db;
}
.ai-rationale-box {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 0.5rem;
}
.rationale-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.65rem;
}
.ai-chip {
font-size: 0.65rem;
font-weight: 700;
color: var(--blue);
background: var(--blue-glow);
padding: 0.15rem 0.45rem;
border-radius: 0.2rem;
text-transform: uppercase;
}
.confidence-chip {
font-size: 0.65rem;
font-weight: 600;
color: var(--text-muted);
}
.rationale-text {
font-size: 0.85rem;
line-height: 1.55;
color: var(--text-muted);
margin: 0 0 0.65rem;
}
.provenance-footer {
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.3);
text-align: right;
}
/* Valuation Matrix Card */
.valuation-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.valuation-prices {
display: flex;
gap: 2.5rem;
margin-top: 0.5rem;
}
.price-stat {
display: flex;
flex-direction: column;
}
.stat-lbl {
font-size: 0.65rem;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 0.2rem;
}
.price-val {
font-size: 1.6rem;
font-weight: 800;
color: var(--text);
line-height: 1.1;
&.estimate {
color: var(--green);
}
}
.verdict-badge {
font-size: 0.72rem;
font-weight: 800;
padding: 0.35rem 0.75rem;
border-radius: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.05em;
&.looks_cheap {
background: var(--green-glow);
color: var(--green);
border: 1px solid rgba(16, 185, 129, 0.2);
}
&.fair {
background: var(--amber-glow);
color: var(--amber);
border: 1px solid rgba(245, 158, 11, 0.2);
}
&.looks_expensive {
background: var(--red-glow);
color: var(--red);
border: 1px solid rgba(239, 68, 68, 0.2);
}
&.insufficient_data {
background: #1e293b;
color: #94a3b8;
border: 1px solid #334155;
}
}
/* Custom Valuation Slider Track */
.valuation-slider-container {
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.015);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1.25rem;
margin-bottom: 1.25rem;
}
.slider-lbl {
font-size: 0.7rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1.25rem;
}
.custom-slider-track {
height: 0.5rem;
background: #1e293b;
border-radius: 1rem;
position: relative;
margin: 1.25rem 0.5rem;
}
.fair-zone-bar {
position: absolute;
top: 0;
bottom: 0;
background: var(--green);
border-radius: 1rem;
opacity: 0.35;
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
}
.current-price-indicator {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 1rem;
height: 1rem;
border-radius: 50%;
background: #fff;
border: 3px solid var(--blue);
box-shadow: 0 0 8px var(--blue);
cursor: default;
.tooltip {
position: absolute;
bottom: 135%;
left: 50%;
transform: translateX(-50%);
background: var(--blue);
color: #fff;
font-size: 0.6rem;
font-weight: 700;
padding: 0.15rem 0.35rem;
border-radius: 0.2rem;
text-transform: uppercase;
white-space: nowrap;
opacity: 0.9;
&::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 4px;
border-style: solid;
border-color: var(--blue) transparent transparent transparent;
}
}
}
.slider-labels {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.25rem;
font-size: 0.8rem;
color: var(--text-muted);
.label-mid {
font-size: 0.7rem;
text-transform: uppercase;
color: var(--text-muted);
font-weight: 600;
}
span {
font-weight: 700;
color: var(--text);
}
}
.loading-state h1,
.empty-state h1 { max-width: 760px; margin: 0.6rem 0 0.9rem; font-family: 'Plus Jakarta Sans', sans-serif; font-size: clamp(2rem, 5vw, 4rem); line-height: 1.05; letter-spacing: -0.055em; }
.loading-state > p:last-of-type,
.empty-state > p:last-of-type { max-width: 640px; margin: 0 0 1.5rem; color: var(--ink-soft); line-height: 1.6; }
.valuation-desc {
font-size: 0.9rem;
line-height: 1.55;
color: #cbd5e1;
margin: 0 0 1rem;
}
.loading-orbit { position: relative; width: 5.5rem; height: 5.5rem; display: grid; place-items: center; margin-bottom: 1.5rem; }
.loading-orbit i { position: absolute; inset: 0; border: 2px solid var(--line); border-top-color: var(--green); border-radius: 50%; animation: spin 1.2s linear infinite; }
.loading-orbit span { width: 3.4rem; height: 3.4rem; display: grid; place-items: center; border-radius: 50%; background: var(--green); color: white; font-weight: 800; }
.weight-disclaimer {
font-size: 0.75rem;
color: var(--text-muted);
background: rgba(255, 255, 255, 0.02);
border-left: 3px solid var(--blue);
padding: 0.5rem 0.75rem;
border-radius: 0.25rem;
margin-bottom: 1.5rem;
}
.loading-steps { display: flex; align-items: center; gap: 0.7rem; margin-top: 1.6rem; color: var(--muted); font-size: 0.64rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
.loading-steps i { width: 2.25rem; height: 1px; background: var(--line-strong); }
.loading-steps span.active { color: var(--green); }
.state-icon { width: 4rem; height: 4rem; display: grid; place-items: center; margin-bottom: 1rem; border-radius: 50%; color: var(--red); background: var(--red-soft); border: 1px solid var(--red-line); font-size: 1.6rem; font-weight: 800; }
.methods-grid {
.report-hero {
max-width: 1500px;
min-height: 29rem;
margin: 2rem auto 0;
padding: clamp(2rem, 5vw, 4.5rem);
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.method-card {
display: flex;
grid-template-columns: minmax(0, 1fr) 22rem;
gap: 4rem;
align-items: center;
gap: 0.65rem;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.015);
border: 1px solid var(--border);
border-radius: 0.5rem;
transition: all 0.2s ease;
&:hover:not(.disabled) {
border-color: var(--border-focus);
background: var(--bg-panel-hover);
}
&.disabled {
opacity: 0.35;
background: transparent;
}
}
.method-dot {
width: 0.45rem;
height: 0.45rem;
border-radius: 50%;
background: #475569;
flex-shrink: 0;
&.active {
background: var(--green);
box-shadow: 0 0 6px var(--green);
}
}
.method-details {
display: flex;
flex-direction: column;
min-width: 0;
}
.method-name {
font-size: 0.72rem;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.method-value {
font-size: 0.85rem;
font-weight: 700;
color: var(--text);
margin-top: 0.1rem;
}
/* RIGHT PANEL STYLING: Factors Stack */
.right-panel-title {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.12em;
color: var(--text-muted);
text-transform: uppercase;
margin: 0;
border-bottom: 1px solid var(--border);
padding-bottom: 0.5rem;
}
.factors-stack {
display: flex;
flex-direction: column;
gap: 1rem;
}
.factor-dashboard-card {
display: flex;
flex-direction: column;
border-left: 3px solid #475569;
&.positive { border-left-color: var(--green); }
&.caution { border-left-color: var(--amber); }
&.negative { border-left-color: var(--red); }
&.neutral { border-left-color: #64748b; }
}
.factor-hdr {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 0.5rem;
}
.factor-title-box {
display: flex;
flex-direction: column;
min-width: 0;
}
.factor-title {
font-size: 0.95rem;
font-weight: 700;
color: var(--text);
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.factor-subtitle {
font-size: 0.7rem;
color: var(--text-muted);
margin-top: 0.1rem;
}
.factor-badge {
font-size: 0.65rem;
font-weight: 700;
padding: 0.15rem 0.45rem;
border-radius: 0.2rem;
text-transform: uppercase;
white-space: nowrap;
&.positive { background: var(--green-glow); color: var(--green); }
&.caution { background: var(--amber-glow); color: var(--amber); }
&.negative { background: var(--red-glow); color: var(--red); }
&.neutral { background: rgba(100, 116, 139, 0.15); color: #94a3b8; }
}
.factor-desc-snippet {
font-size: 0.82rem;
line-height: 1.45;
color: var(--text-muted);
background: rgba(255, 255, 255, 0.015);
border-radius: 0.35rem;
padding: 0.65rem 0.75rem;
margin: 0.75rem 0 1rem;
}
.factor-metrics-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.metric-row-ui {
display: flex;
flex-direction: column;
}
.metric-lbl {
font-size: 0.72rem;
color: #94a3b8;
margin-bottom: 0.35rem;
}
.metric-bar-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
}
.progress-bar-track {
height: 0.35rem;
background: #1e293b;
border-radius: 1rem;
flex: 1;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 1rem;
background: #94a3b8;
&.positive { background: var(--green); }
&.caution { background: var(--amber); }
&.negative { background: var(--red); }
}
.metric-val {
font-size: 0.8rem;
display: flex;
align-items: baseline;
gap: 0.2rem;
min-width: 3.5rem;
justify-content: flex-end;
strong {
color: var(--text);
}
small {
font-size: 0.6rem;
color: var(--text-muted);
}
}
/* REPORTS & CONSOLE TABS SECTION */
.reports-console-section {
max-width: 1560px;
border: 1px solid var(--line-strong);
border-top: 5px solid var(--green);
border-radius: 1.2rem 1.2rem 0.25rem 0.25rem;
background: var(--surface);
box-shadow: var(--shadow-md);
}
.report-hero.caution { border-top-color: var(--amber); }
.report-hero.negative { border-top-color: var(--red); }
.report-hero.neutral { border-top-color: #64748b; }
.hero-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 0.55rem; margin-bottom: 2.5rem; }
.sector-chip,
.date-chip,
.verified-chip { padding: 0.38rem 0.65rem; border-radius: 999px; color: var(--ink-soft); background: var(--surface-soft); border: 1px solid var(--line); font-size: 0.62rem; font-weight: 750; letter-spacing: 0.05em; text-transform: uppercase; }
.verified-chip { color: var(--green); background: var(--green-soft); border-color: var(--green-line); }
.verified-chip i { font-style: normal; }
.eyebrow { margin: 0 0 0.55rem; color: var(--green); font-size: 0.66rem; font-weight: 800; letter-spacing: 0.16em; line-height: 1.4; text-transform: uppercase; }
.report-hero h1 { max-width: 900px; margin: 0; font-family: 'Plus Jakarta Sans', sans-serif; font-size: clamp(2.5rem, 5.6vw, 5.6rem); line-height: 0.98; letter-spacing: -0.065em; }
.symbol-line { display: flex; align-items: center; flex-wrap: wrap; gap: 0.8rem; margin-top: 1.15rem; color: var(--muted); font-size: 0.7rem; }
.symbol-line strong { padding: 0.28rem 0.5rem; color: white; background: var(--ink); border-radius: 0.35rem; font-size: 0.7rem; letter-spacing: 0.06em; }
.symbol-line span + span { padding-left: 0.8rem; border-left: 1px solid var(--line-strong); }
.decision-block { max-width: 780px; margin-top: 2.4rem; padding: 1.1rem 1.2rem; display: flex; justify-content: space-between; align-items: center; border: 1px solid var(--green-line); border-left: 5px solid var(--green); border-radius: 0.75rem; background: var(--green-soft); }
.report-hero.caution .decision-block { background: var(--amber-soft); border-color: var(--amber-line); border-left-color: var(--amber); }
.report-hero.negative .decision-block { background: var(--red-soft); border-color: var(--red-line); border-left-color: var(--red); }
.decision-copy { display: grid; grid-template-columns: auto 1fr; align-items: baseline; column-gap: 0.85rem; }
.decision-kicker { grid-column: 1/-1; margin-bottom: 0.18rem; color: var(--muted); font-size: 0.56rem; font-weight: 800; letter-spacing: 0.13em; }
.decision-copy strong { color: var(--green); font-family: 'Plus Jakarta Sans', sans-serif; font-size: 1.55rem; text-transform: uppercase; }
.report-hero.caution .decision-copy strong { color: var(--amber); }
.report-hero.negative .decision-copy strong { color: var(--red); }
.decision-copy small { color: var(--ink-soft); font-size: 0.78rem; }
.decision-mark { width: 2.4rem; height: 2.4rem; display: grid; place-items: center; border-radius: 50%; color: white; background: var(--green); font-size: 1.2rem; font-weight: 800; }
.report-hero.caution .decision-mark { background: var(--amber); }
.report-hero.negative .decision-mark { background: var(--red); }
.hero-score-card { align-self: stretch; padding: 2rem; display: flex; flex-direction: column; align-items: center; justify-content: center; border-left: 1px solid var(--line); }
.score-chart { position: relative; width: 12rem; height: 12rem; }
.score-center { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; pointer-events: none; }
.score-center strong { font-family: 'Plus Jakarta Sans', sans-serif; font-size: 2.7rem; line-height: 0.9; letter-spacing: -0.06em; }
.score-center span { margin-top: 0.35rem; color: var(--muted); font-size: 0.72rem; }
.score-copy { display: flex; flex-direction: column; align-items: center; margin-top: 1.25rem; text-align: center; }
.score-copy > span { color: var(--muted); font-size: 0.62rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; }
.score-copy strong { margin-top: 0.28rem; font-size: 1.1rem; }
.score-copy small { margin-top: 0.25rem; color: var(--muted); font-size: 0.68rem; }
.market-ribbon {
max-width: 1500px;
margin: 0 auto;
border: 1px solid var(--border);
border-radius: 0.75rem;
background: var(--bg-panel);
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.console-header {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.01);
border-bottom: 1px solid var(--border);
padding: 0.25rem 1.25rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.console-title {
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 0.15em;
color: var(--blue);
}
.console-tabs {
display: flex;
gap: 0.25rem;
button {
background: transparent;
border: 0;
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 600;
padding: 1rem 1.25rem;
cursor: pointer;
position: relative;
transition: all 0.2s ease;
&:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.02);
}
&.active {
color: var(--blue);
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--blue);
box-shadow: 0 0 6px var(--blue);
}
}
}
}
.console-body {
padding: 1.5rem;
}
/* Animations for Tab changes */
.tab-pane-fade {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(2px); }
to { opacity: 1; transform: translateY(0); }
}
/* In-depth Analysis tab content */
.in-depth-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
h2 {
font-size: 1.25rem;
font-weight: 700;
margin: 0 0 0.5rem;
}
.snippet-highlight {
font-size: 0.92rem;
line-height: 1.55;
color: var(--text-muted);
margin: 0;
}
}
.report-sections-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
gap: 1.25rem;
}
.report-section-card {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.015);
border: 1px solid var(--border);
border-radius: 0.5rem;
.section-title {
font-size: 1rem;
font-weight: 700;
margin: 0 0 0.75rem;
color: var(--blue);
}
.section-summary {
font-size: 0.85rem;
line-height: 1.5;
color: #cbd5e1;
margin: 0 0 1rem;
}
}
.section-bullets {
list-style: none;
padding-left: 0;
margin: 0;
grid-template-columns: 1.15fr repeat(2, .75fr) 1.4fr .75fr;
border: 1px solid var(--line-strong);
border-top: 0;
background: var(--surface);
box-shadow: var(--shadow-sm);
}
.market-ribbon > div { min-height: 6.7rem; padding: 1.3rem 1.5rem; display: flex; flex-direction: column; justify-content: center; border-right: 1px solid var(--line); }
.market-ribbon > div:last-child { border-right: 0; }
.market-ribbon span { color: var(--muted); font-size: 0.58rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; }
.market-ribbon strong { margin-top: 0.35rem; font-family: 'Plus Jakarta Sans', sans-serif; font-size: 1.25rem; }
.market-ribbon .price-cell strong { font-size: 1.75rem; letter-spacing: -0.04em; }
.market-ribbon small { margin-top: 0.22rem; color: var(--muted); font-size: 0.65rem; }
.positive-text { color: var(--green) !important; }
.negative-text { color: var(--red) !important; }
.range-cell > div:first-child { display: flex; justify-content: space-between; align-items: center; }
.range-cell > div:first-child strong { margin: 0; font-size: 0.74rem; }
.mini-range { position: relative; height: 0.35rem; margin: 0.75rem 0 0.4rem; border-radius: 1rem; background: #e3e6e0; }
.mini-range::before { content: ''; position: absolute; left: 25%; right: 25%; inset-block: 0; background: var(--green-soft); }
.mini-range i { position: absolute; top: 50%; width: 0.78rem; height: 0.78rem; border: 2px solid white; border-radius: 50%; background: var(--ink); box-shadow: 0 0 0 1px var(--ink); transform: translate(-50%, -50%); }
.range-cell > small { display: flex; justify-content: space-between; }
.range-cell > small span { font-size: 0.5rem; }
.section-nav {
position: sticky;
z-index: 30;
top: 5rem;
max-width: 1500px;
margin: 1.2rem auto 0;
padding: 0.55rem;
display: flex;
flex-direction: column;
gap: 0.55rem;
li {
font-size: 0.82rem;
line-height: 1.5;
color: var(--text-muted);
position: relative;
padding-left: 0.85rem;
&::before {
content: "▪";
position: absolute;
left: 0;
color: var(--blue);
top: -0.05rem;
}
}
}
/* AI Trader View tab content */
.trader-view-pane {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.trader-hero-row {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.015);
border: 1px solid var(--border);
padding: 1.25rem;
border-radius: 0.5rem;
}
.action-recommendation {
h2 {
font-size: 1.5rem;
font-weight: 800;
margin: 0.25rem 0 0;
color: var(--blue);
}
}
.rating-box {
display: flex;
flex-direction: column;
align-items: flex-end;
.rating-value {
font-size: 1.5rem;
font-weight: 800;
margin-top: 0.25rem;
text-transform: uppercase;
}
&.buy { .rating-value { color: var(--green); } }
&.overweight { .rating-value { color: var(--green); } }
&.hold { .rating-value { color: var(--amber); } }
&.underweight { .rating-value { color: var(--red); } }
&.sell { .rating-value { color: var(--red); } }
}
.sub {
font-size: 0.65rem;
font-weight: 700;
color: var(--text-muted);
letter-spacing: 0.08em;
}
.trader-exec-summary {
h3 {
font-size: 0.95rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
margin: 0 0 0.5rem;
}
p {
font-size: 0.95rem;
line-height: 1.6;
color: #e2e8f0;
margin: 0;
}
}
.trader-strategy-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
gap: 1rem;
gap: 0.35rem;
border: 1px solid var(--line);
border-radius: 0.75rem;
background: rgba(255,255,255,.92);
box-shadow: var(--shadow-sm);
backdrop-filter: blur(16px);
}
.strat-card {
padding: 1rem;
background: rgba(255, 255, 255, 0.015);
border: 1px solid var(--border);
border-radius: 0.5rem;
.section-nav button { padding: 0.65rem 0.8rem; border: 0; border-radius: 0.48rem; color: var(--ink-soft); background: transparent; font-size: 0.65rem; font-weight: 750; cursor: pointer; }
.section-nav button:hover { color: var(--green); background: var(--green-soft); }
.section-nav > span { margin-left: auto; padding-right: 0.55rem; color: var(--muted); font-size: 0.62rem; font-weight: 700; }
.section-anchor { scroll-margin-top: 9rem; }
h4 {
font-size: 0.85rem;
font-weight: 700;
margin: 0 0 0.65rem;
color: var(--text);
}
.surface-card { background: var(--surface); border: 1px solid var(--line); border-radius: 1rem; box-shadow: var(--shadow-sm); }
p {
font-size: 0.8rem;
line-height: 1.5;
color: var(--text-muted);
margin: 0;
}
}
.trader-lists-row {
.decision-layout {
max-width: 1500px;
margin: 4rem auto 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
grid-template-columns: minmax(0, 1.6fr) minmax(19rem, .8fr);
gap: 1.25rem;
}
.list-column {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.015);
border: 1px solid var(--border);
border-radius: 0.5rem;
h4 {
font-size: 0.85rem;
font-weight: 700;
margin: 0 0 1rem;
}
}
.console-bullets {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.65rem;
li {
font-size: 0.82rem;
line-height: 1.45;
color: var(--text-muted);
position: relative;
padding-left: 1rem;
&::before {
content: "•";
position: absolute;
left: 0;
color: var(--blue);
}
}
}
.data-quality-box {
font-size: 0.8rem;
color: var(--text-muted);
border-top: 1px solid var(--border);
padding-top: 1rem;
}
/* Agent Evidence Tab Content */
.agents-notice {
font-size: 0.85rem;
color: var(--text-muted);
margin-top: 0;
margin-bottom: 1.25rem;
}
.evidence-accordion {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.evidence-details {
border: 1px solid var(--border);
border-radius: 0.5rem;
background: rgba(255, 255, 255, 0.01);
overflow: hidden;
&[open] {
.evidence-summary {
border-bottom: 1px solid var(--border);
background: rgba(255, 255, 255, 0.02);
}
}
}
.evidence-summary {
padding: 1rem;
font-size: 0.85rem;
font-weight: 700;
color: var(--text);
cursor: pointer;
user-select: none;
transition: background 0.2s ease;
&:hover {
background: rgba(255, 255, 255, 0.015);
}
}
.raw-output {
margin: 0;
padding: 1.25rem;
font-family: 'Inter', sans-serif;
font-size: 0.82rem;
line-height: 1.6;
color: var(--text-muted);
white-space: pre-wrap;
word-break: break-word;
background: rgba(0, 0, 0, 0.15);
max-height: 25rem;
overflow: auto;
}
/* Raw State Log */
.raw-state-pane {
background: rgba(0, 0, 0, 0.15);
border-radius: 0.5rem;
border: 1px solid var(--border);
padding: 1rem;
}
.json-state {
margin: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.75rem;
color: #38bdf8;
white-space: pre;
overflow: auto;
max-height: 32rem;
}
/* Footer styling */
.analysis-footer {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1560px;
margin: 2.5rem auto 0;
padding-top: 1.25rem;
border-top: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.75rem;
gap: 1.5rem;
strong {
color: var(--text);
letter-spacing: 0.12em;
}
span {
max-width: 60%;
text-align: right;
line-height: 1.4;
}
}
/* RESPONSIVE BREAKPOINTS (FINTECH GRIDS) */
@media (max-width: 1280px) {
.dashboard-grid {
grid-template-columns: 20rem 1fr;
}
.panel-right {
grid-column: span 2;
.factors-stack {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
gap: 1rem;
}
}
.investment-case { padding: clamp(1.5rem, 3vw, 2.5rem); }
.section-heading { max-width: 1500px; margin: 0 auto 1.6rem; display: flex; align-items: flex-end; justify-content: space-between; gap: 2rem; }
.section-heading.compact { margin-bottom: 1.2rem; align-items: flex-start; }
.section-heading h2 { max-width: 880px; margin: 0; font-family: 'Plus Jakarta Sans', sans-serif; font-size: clamp(1.7rem, 3.2vw, 2.7rem); line-height: 1.08; letter-spacing: -0.045em; }
.section-heading > div > p:last-child:not(.eyebrow) { max-width: 760px; margin: 0.65rem 0 0; color: var(--ink-soft); font-size: 0.86rem; line-height: 1.6; }
.lead-copy { max-width: 920px; margin: 0 0 1.5rem; color: var(--ink-soft); font-size: 0.93rem; line-height: 1.7; }
.status-badge { width: fit-content; display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.4rem 0.65rem; border-radius: 999px; color: #566259; background: #eef0ec; border: 1px solid #d6dad4; font-size: 0.62rem; font-weight: 800; white-space: nowrap; text-transform: uppercase; }
.status-badge i { width: 1rem; height: 1rem; display: grid; place-items: center; border-radius: 50%; color: white; background: #64748b; font-style: normal; font-size: 0.65rem; }
.status-badge.positive { color: var(--green); background: var(--green-soft); border-color: var(--green-line); }
.status-badge.positive i { background: var(--green); }
.status-badge.caution { color: var(--amber); background: var(--amber-soft); border-color: var(--amber-line); }
.status-badge.caution i { background: var(--amber); }
.status-badge.negative { color: var(--red); background: var(--red-soft); border-color: var(--red-line); }
.status-badge.negative i { background: var(--red); }
.takeaway-list { display: grid; grid-template-columns: repeat(3, 1fr); border-block: 1px solid var(--line); }
.takeaway-item { padding: 1.2rem 1.1rem; display: grid; grid-template-columns: auto 1fr; gap: 0.65rem; border-right: 1px solid var(--line); }
.takeaway-item:first-child { padding-left: 0; }
.takeaway-item:last-child { border-right: 0; }
.takeaway-item > span { color: var(--green); font-family: 'Plus Jakarta Sans', sans-serif; font-size: 0.62rem; font-weight: 800; }
.takeaway-item p { margin: 0; color: var(--ink-soft); font-size: 0.78rem; line-height: 1.55; }
.ai-rationale { margin-top: 1.5rem; padding: 1.25rem; border: 1px solid #bad0db; border-left: 4px solid var(--blue); border-radius: 0.7rem; background: var(--blue-soft); }
.ai-rationale-head { display: flex; justify-content: space-between; gap: 1rem; }
.ai-rationale-head small { color: #6d7d85; font-size: 0.62rem; }
.ai-badge { color: var(--blue); font-size: 0.6rem; font-weight: 850; letter-spacing: 0.1em; }
.ai-rationale > p { margin: 0.7rem 0; color: #34596b; font-size: 0.83rem; line-height: 1.62; }
.ai-rationale footer { padding-top: 0.65rem; display: flex; justify-content: space-between; border-top: 1px solid #cbdde5; color: #66808c; font-size: 0.6rem; }
.ai-rationale footer strong { color: var(--blue); }
.decision-rail { display: flex; flex-direction: column; gap: 0.75rem; }
.rail-card { min-height: 7.2rem; padding: 1rem; display: grid; grid-template-columns: auto 1fr; gap: 0.85rem; background: var(--surface); border: 1px solid var(--line); border-radius: 0.8rem; box-shadow: var(--shadow-sm); }
.rail-icon { width: 2rem; height: 2rem; display: grid; place-items: center; border-radius: 0.5rem; font-weight: 850; }
.rail-icon.positive { color: var(--green); background: var(--green-soft); }
.rail-icon.caution { color: var(--amber); background: var(--amber-soft); }
.rail-icon.negative { color: var(--red); background: var(--red-soft); }
.rail-icon.neutral { color: var(--blue); background: var(--blue-soft); }
.rail-card small { color: var(--muted); font-size: 0.55rem; font-weight: 800; letter-spacing: 0.09em; }
.rail-card p { margin: 0.4rem 0 0; color: var(--ink-soft); font-size: 0.73rem; line-height: 1.48; }
.trigger-grid { max-width: 1500px; margin: 1.25rem auto 0; display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
.trigger-card { padding: 1.4rem; border-radius: 0.9rem; border: 1px solid var(--line); background: var(--surface); }
.trigger-card.positive { border-color: var(--green-line); background: #fbfefc; }
.trigger-card.negative { border-color: var(--red-line); background: #fffcfc; }
.trigger-card header { display: flex; gap: 0.8rem; align-items: center; }
.trigger-card header > span { width: 2.4rem; height: 2.4rem; display: grid; place-items: center; border-radius: 50%; font-size: 1.1rem; font-weight: 800; }
.trigger-card.positive header > span { color: var(--green); background: var(--green-soft); }
.trigger-card.negative header > span { color: var(--red); background: var(--red-soft); }
.trigger-card h3 { margin: 0; font-size: 1rem; }
.trigger-card .eyebrow { margin-bottom: 0.1rem; font-size: 0.52rem; }
.trigger-card.negative .eyebrow { color: var(--red); }
.trigger-card ul { margin: 1rem 0 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0.65rem; list-style: none; }
.trigger-card li { position: relative; padding-left: 1rem; color: var(--ink-soft); font-size: 0.74rem; line-height: 1.5; }
.trigger-card li::before { content: '•'; position: absolute; left: 0; color: var(--green); }
.trigger-card.negative li::before { color: var(--red); }
.valuation-section,
.factor-section,
.summary-reports,
.dossier-section,
.source-section { max-width: 1500px; margin: 5rem auto 0; }
.valuation-verdict { padding: 0.55rem 0.8rem; border: 1px solid var(--line); border-radius: 0.55rem; background: var(--surface); color: var(--ink-soft); font-size: 0.66rem; font-weight: 850; text-transform: uppercase; }
.valuation-verdict.looks_cheap { color: var(--green); background: var(--green-soft); border-color: var(--green-line); }
.valuation-verdict.fair { color: var(--amber); background: var(--amber-soft); border-color: var(--amber-line); }
.valuation-verdict.looks_expensive { color: var(--red); background: var(--red-soft); border-color: var(--red-line); }
.valuation-board { padding: clamp(1.5rem, 3vw, 2.4rem); }
.valuation-numbers { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); border-radius: 0.75rem; overflow: hidden; }
.valuation-numbers > div { padding: 1.2rem; display: flex; flex-direction: column; border-right: 1px solid var(--line); background: var(--surface-soft); }
.valuation-numbers > div:last-child { border-right: 0; }
.valuation-numbers span { color: var(--muted); font-size: 0.55rem; font-weight: 800; letter-spacing: .08em; }
.valuation-numbers strong { margin-top: .35rem; font-family: 'Plus Jakarta Sans', sans-serif; font-size: 1.45rem; }
.valuation-range { margin: 3rem 1rem 2.4rem; }
.range-label-row { display: flex; justify-content: space-between; color: var(--ink-soft); font-size: 0.68rem; font-weight: 750; }
.range-label-row small { color: var(--muted); font-weight: 500; }
.value-track { position: relative; height: .55rem; margin-top: 2.1rem; border-radius: 999px; background: #e4e7e1; }
.fair-zone { position: absolute; inset-block: 0; border-radius: 999px; background: var(--green); opacity: .35; }
.fair-zone::after { content: 'FAIR VALUE ZONE'; position: absolute; left: 50%; bottom: 1.1rem; color: var(--green); font-size: .52rem; font-weight: 850; letter-spacing: .08em; white-space: nowrap; transform: translateX(-50%); }
.current-marker { position: absolute; top: 50%; width: 1rem; height: 1rem; border: 3px solid white; border-radius: 50%; background: var(--ink); box-shadow: 0 0 0 1px var(--ink); transform: translate(-50%, -50%); }
.current-marker b { position: absolute; bottom: 1.3rem; left: 50%; padding: .2rem .35rem; border-radius: .25rem; background: var(--ink); color: white; font-size: .5rem; font-style: normal; white-space: nowrap; transform: translateX(-50%); }
.value-track-labels { position: relative; height: 1.8rem; margin-top: .45rem; }
.value-track-labels span { position: absolute; color: var(--ink-soft); font-size: .62rem; font-weight: 700; transform: translateX(-50%); }
.valuation-methods { display: grid; grid-template-columns: repeat(4, 1fr); gap: .75rem; }
.valuation-methods article { min-height: 5.2rem; padding: .85rem; display: grid; grid-template-columns: auto 1fr; gap: .65rem; align-items: center; border: 1px solid var(--line); border-radius: .65rem; background: var(--surface-soft); }
.valuation-methods article.unavailable { opacity: .48; }
.valuation-methods article > span { width: 1.4rem; height: 1.4rem; display: grid; place-items: center; border-radius: 50%; background: #e2e5df; color: var(--muted); font-size: .65rem; font-weight: 800; }
.valuation-methods article > span.available { color: white; background: var(--green); }
.valuation-methods div { min-width: 0; display: flex; flex-direction: column; }
.valuation-methods small { color: var(--muted); font-size: .58rem; line-height: 1.35; }
.valuation-methods strong { margin-top: .3rem; font-size: .9rem; }
.method-note { margin: 1rem 0 0; color: var(--muted); font-size: .65rem; line-height: 1.45; }
.status-legend { display: flex; flex-wrap: wrap; gap: .45rem; }
.status-legend span { padding: .35rem .55rem; border-radius: 999px; font-size: .58rem; font-weight: 750; }
.status-legend i { margin-right: .15rem; font-style: normal; }
.status-legend .positive { color: var(--green); background: var(--green-soft); }
.status-legend .caution { color: var(--amber); background: var(--amber-soft); }
.status-legend .negative { color: var(--red); background: var(--red-soft); }
.factor-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
.factor-card { --tone: #64748b; --tone-soft: #eef0f2; padding: 1.35rem; border: 1px solid var(--line); border-top: 4px solid var(--tone); border-radius: .85rem; background: var(--surface); box-shadow: var(--shadow-sm); }
.factor-card.positive { --tone: var(--green); --tone-soft: var(--green-soft); }
.factor-card.caution { --tone: var(--amber); --tone-soft: var(--amber-soft); }
.factor-card.negative { --tone: var(--red); --tone-soft: var(--red-soft); }
.factor-card > header { display: grid; grid-template-columns: auto 1fr auto; gap: .8rem; align-items: center; }
.factor-mark { width: 2.4rem; height: 2.4rem; display: grid; place-items: center; border-radius: .55rem; color: var(--tone); background: var(--tone-soft); font-weight: 850; }
.factor-card header p { margin: 0 0 .14rem; color: var(--muted); font-size: .56rem; font-weight: 750; letter-spacing: .04em; text-transform: uppercase; }
.factor-card h3 { margin: 0; font-size: 1rem; }
.factor-card header > strong { color: var(--tone); font-family: 'Plus Jakarta Sans', sans-serif; font-size: 1.5rem; }
.factor-card header > strong small { color: var(--muted); font-size: .6rem; }
.factor-explanation { min-height: 2.6rem; margin: 1rem 0; color: var(--ink-soft); font-size: .76rem; line-height: 1.55; }
.factor-metrics { display: flex; flex-direction: column; border-top: 1px solid var(--line); }
.factor-metric { padding: .75rem 0; display: grid; grid-template-columns: minmax(9rem, 1.2fr) 1fr 2.6rem; align-items: center; gap: .75rem; border-bottom: 1px solid var(--line); }
.factor-metric > div:first-child { display: flex; justify-content: space-between; gap: .6rem; }
.factor-metric span { color: var(--muted); font-size: .64rem; }
.factor-metric strong { font-size: .68rem; }
.metric-track { height: .33rem; overflow: hidden; border-radius: 999px; background: #e5e8e2; }
.metric-track i { display: block; height: 100%; border-radius: inherit; background: #64748b; }
.metric-track i.positive { background: var(--green); }
.metric-track i.caution { background: var(--amber); }
.metric-track i.negative { background: var(--red); }
.factor-metric > small { color: var(--muted); font-size: .58rem; text-align: right; }
.factor-card > footer { padding-top: .9rem; display: flex; justify-content: flex-end; }
.summary-reports { padding: clamp(1.5rem, 3vw, 2.5rem); border: 1px solid var(--line); border-radius: 1rem; background: var(--surface); }
.summary-report-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: .85rem; }
.summary-report-grid article { --tone: #64748b; padding: 1rem; border: 1px solid var(--line); border-left: 4px solid var(--tone); border-radius: .65rem; background: var(--surface-soft); }
.summary-report-grid article.positive { --tone: var(--green); }
.summary-report-grid article.caution { --tone: var(--amber); }
.summary-report-grid article.negative { --tone: var(--red); }
.summary-report-grid header { display: flex; gap: .55rem; align-items: center; }
.summary-report-grid header span { width: 1.35rem; height: 1.35rem; display: grid; place-items: center; border-radius: 50%; color: white; background: var(--tone); font-size: .62rem; font-weight: 850; }
.summary-report-grid h3 { margin: 0; font-size: .86rem; }
.summary-report-grid p { margin: .7rem 0 0; color: var(--ink-soft); font-size: .7rem; line-height: 1.5; }
.summary-report-grid ul { margin: .75rem 0 0; padding-left: 1rem; color: var(--muted); font-size: .66rem; line-height: 1.5; }
.dossier-section { padding-top: 4rem; border-top: 1px solid var(--line-strong); }
.dossier-heading { display: flex; justify-content: space-between; align-items: flex-end; gap: 2rem; }
.dossier-heading h2 { margin: 0; font-family: 'Plus Jakarta Sans', sans-serif; font-size: clamp(2rem, 4vw, 3.6rem); letter-spacing: -.055em; line-height: 1; }
.dossier-heading > div > p:last-child { max-width: 780px; margin: .8rem 0 0; color: var(--ink-soft); font-size: .86rem; line-height: 1.65; }
.dossier-count { padding: .8rem 1rem; display: flex; align-items: baseline; gap: .5rem; border: 1px solid var(--line); border-radius: .65rem; background: var(--surface); white-space: nowrap; }
.dossier-count strong { color: var(--green); font-family: 'Plus Jakarta Sans', sans-serif; font-size: 1.8rem; }
.dossier-count span { color: var(--muted); font-size: .62rem; }
.dossier-layout { margin-top: 2rem; display: grid; grid-template-columns: 15rem minmax(0, 1fr); gap: 1.2rem; align-items: start; }
.dossier-index { position: sticky; top: 9.5rem; padding: .75rem; border: 1px solid var(--line); border-radius: .75rem; background: var(--surface); box-shadow: var(--shadow-sm); }
.dossier-index > p { margin: .35rem .5rem .65rem; color: var(--muted); font-size: .55rem; font-weight: 850; letter-spacing: .12em; }
.dossier-index button { width: 100%; padding: .7rem .5rem; display: grid; grid-template-columns: 1.6rem 1fr auto; align-items: center; gap: .35rem; border: 0; border-bottom: 1px solid var(--line); background: transparent; color: var(--ink-soft); font-size: .67rem; text-align: left; cursor: pointer; }
.dossier-index button:hover { color: var(--green); background: var(--green-soft); }
.dossier-index button > span { color: var(--muted); font-size: .52rem; font-weight: 800; }
.dossier-index button small { min-width: 1.2rem; padding: .16rem; border-radius: 999px; background: #ecefea; color: var(--muted); font-size: .5rem; text-align: center; }
.dossier-index .technical-link { margin-top: .4rem; border: 0; color: var(--muted); }
.dossier-content { min-width: 0; }
.report-group { margin-bottom: 1.2rem; padding: 1.4rem; border: 1px solid var(--line); border-radius: .85rem; background: var(--surface); box-shadow: var(--shadow-sm); }
.report-group > header { padding-bottom: 1rem; border-bottom: 1px solid var(--line); }
.report-group > header h3 { margin: 0; font-family: 'Plus Jakarta Sans', sans-serif; font-size: 1.35rem; letter-spacing: -.025em; }
.report-group > header > p:last-child { margin: .42rem 0 0; color: var(--muted); font-size: .72rem; line-height: 1.5; }
.document-stack { margin-top: 1rem; display: grid; gap: .7rem; }
.report-document { --tone: #64748b; --tone-soft: #eef0f2; overflow: hidden; border: 1px solid var(--line); border-left: 4px solid var(--tone); border-radius: .65rem; background: var(--surface); }
.report-document.positive { --tone: var(--green); --tone-soft: var(--green-soft); }
.report-document.caution { --tone: var(--amber); --tone-soft: var(--amber-soft); }
.report-document.negative { --tone: var(--red); --tone-soft: var(--red-soft); }
.report-document[open] { box-shadow: 0 8px 24px rgba(26,37,30,.07); }
.report-document summary { min-height: 4.6rem; padding: .75rem 1rem; display: grid; grid-template-columns: auto 1fr auto auto; align-items: center; gap: .8rem; cursor: pointer; list-style: none; }
.report-document summary::-webkit-details-marker { display: none; }
.report-document summary:hover { background: var(--surface-soft); }
.document-avatar { width: 2.4rem; height: 2.4rem; display: grid; place-items: center; border-radius: .55rem; color: var(--tone); background: var(--tone-soft); font-size: .65rem; font-weight: 850; }
.document-title { min-width: 0; display: flex; flex-direction: column; gap: .18rem; }
.document-title small { color: var(--muted); font-size: .55rem; font-weight: 750; letter-spacing: .07em; text-transform: uppercase; }
.document-title strong { overflow: hidden; font-size: .82rem; text-overflow: ellipsis; white-space: nowrap; }
.document-tone { width: 1.55rem; height: 1.55rem; display: grid; place-items: center; border-radius: 50%; color: var(--tone); background: var(--tone-soft); font-size: .7rem; font-weight: 850; }
.document-toggle { width: 1.4rem; height: 1.4rem; display: grid; place-items: center; }
.document-toggle i { position: relative; width: .65rem; height: 1px; background: var(--ink-soft); }
.document-toggle i::after { content: ''; position: absolute; inset: 0; background: inherit; transform: rotate(90deg); transition: 150ms ease; }
.report-document[open] .document-toggle i::after { transform: rotate(0); }
.markdown-report { padding: clamp(1.2rem, 3vw, 2.4rem); border-top: 1px solid var(--line); background: #fff; }
:host ::ng-deep .markdown-report {
color: #2b3730;
font-family: 'Noto Sans Bengali', 'Inter', sans-serif;
font-size: .82rem;
line-height: 1.72;
}
:host ::ng-deep .markdown-report h2,
:host ::ng-deep .markdown-report h3,
:host ::ng-deep .markdown-report h4,
:host ::ng-deep .markdown-report h5 { margin: 1.8rem 0 .7rem; color: var(--ink); font-family: 'Plus Jakarta Sans', 'Noto Sans Bengali', sans-serif; line-height: 1.25; letter-spacing: -.02em; }
:host ::ng-deep .markdown-report h2:first-child,
:host ::ng-deep .markdown-report h3:first-child { margin-top: 0; }
:host ::ng-deep .markdown-report h2 { padding-bottom: .55rem; border-bottom: 1px solid var(--line); font-size: 1.35rem; }
:host ::ng-deep .markdown-report h3 { font-size: 1.12rem; }
:host ::ng-deep .markdown-report h4 { font-size: .98rem; }
:host ::ng-deep .markdown-report h5 { color: var(--ink-soft); font-size: .88rem; }
:host ::ng-deep .markdown-report p { margin: .75rem 0; }
:host ::ng-deep .markdown-report strong { color: var(--ink); font-weight: 750; }
:host ::ng-deep .markdown-report em { color: var(--ink-soft); }
:host ::ng-deep .markdown-report ul,
:host ::ng-deep .markdown-report ol { margin: .8rem 0; padding-left: 1.35rem; }
:host ::ng-deep .markdown-report li { margin: .34rem 0; padding-left: .2rem; }
:host ::ng-deep .markdown-report li::marker { color: var(--tone); font-weight: 800; }
:host ::ng-deep .markdown-report blockquote { margin: 1rem 0; padding: .9rem 1rem; border-left: 4px solid var(--tone); border-radius: 0 .45rem .45rem 0; background: var(--tone-soft); color: var(--ink-soft); }
:host ::ng-deep .markdown-report code { padding: .12rem .28rem; border: 1px solid var(--line); border-radius: .25rem; background: var(--surface-soft); color: #8a3e3e; font-family: ui-monospace, monospace; font-size: .76rem; }
:host ::ng-deep .markdown-report pre { padding: 1rem; overflow: auto; border-radius: .5rem; background: #18221c; color: #d8e9df; }
:host ::ng-deep .markdown-report pre code { padding: 0; border: 0; background: transparent; color: inherit; }
:host ::ng-deep .markdown-report hr { margin: 1.5rem 0; border: 0; border-top: 1px solid var(--line); }
:host ::ng-deep .report-table-wrap { margin: 1rem 0; overflow-x: auto; border: 1px solid var(--line); border-radius: .55rem; }
:host ::ng-deep .markdown-report table { width: 100%; border-collapse: collapse; background: white; font-size: .74rem; }
:host ::ng-deep .markdown-report th { padding: .7rem .75rem; background: #eef1ec; color: var(--ink); font-size: .61rem; font-weight: 800; text-align: left; letter-spacing: .04em; text-transform: uppercase; }
:host ::ng-deep .markdown-report td { padding: .68rem .75rem; border-top: 1px solid var(--line); vertical-align: top; }
:host ::ng-deep .markdown-report tr:nth-child(even) td { background: #fafbf8; }
.no-dossier { margin-top: 1.5rem; padding: 1.5rem; display: flex; gap: 1rem; border: 1px solid var(--amber-line); border-radius: .8rem; background: var(--amber-soft); }
.no-dossier > span { width: 2rem; height: 2rem; display: grid; place-items: center; border-radius: 50%; color: white; background: var(--amber); font-weight: 800; }
.no-dossier h3 { margin: 0; font-size: .9rem; }
.no-dossier p { margin: .35rem 0 0; color: var(--ink-soft); font-size: .72rem; }
.source-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: .75rem; }
.source-grid article { padding: 1rem; display: grid; grid-template-columns: auto 1fr; gap: .75rem; border: 1px solid var(--line); border-radius: .65rem; background: var(--surface); }
.source-grid article > span { width: 2.25rem; height: 2.25rem; display: grid; place-items: center; border-radius: .5rem; color: var(--blue); background: var(--blue-soft); font-size: .58rem; font-weight: 850; }
.source-grid small { color: var(--muted); font-size: .52rem; font-weight: 750; text-transform: uppercase; }
.source-grid h3 { margin: .16rem 0 .35rem; font-size: .78rem; }
.source-grid p { margin: 0; color: var(--ink-soft); font-size: .65rem; line-height: 1.5; }
.technical-state { margin-top: .9rem; overflow: hidden; border: 1px solid var(--line); border-radius: .65rem; background: var(--surface); }
.technical-state summary { padding: .9rem 1rem; display: flex; justify-content: space-between; color: var(--ink-soft); font-size: .68rem; font-weight: 700; cursor: pointer; }
.technical-state summary span { color: var(--green); }
.technical-state pre { max-height: 34rem; margin: 0; padding: 1.2rem; overflow: auto; border-top: 1px solid var(--line); background: #17211b; color: #cde2d5; font-family: ui-monospace, monospace; font-size: .65rem; line-height: 1.55; }
.report-footer { max-width: 1500px; margin: 4rem auto 0; padding: 1.5rem 0; display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 2rem; border-top: 1px solid var(--line-strong); }
.footer-brand { display: flex; align-items: center; gap: .7rem; white-space: nowrap; }
.footer-brand span { width: 1.8rem; height: 1.8rem; font-size: .65rem; }
.footer-brand strong { font-size: .67rem; letter-spacing: .12em; }
.disclaimer { display: flex; align-items: baseline; justify-content: center; gap: .7rem; }
.disclaimer strong { color: var(--red); font-size: .62rem; text-transform: uppercase; }
.disclaimer p { max-width: 760px; margin: 0; color: var(--muted); font-size: .62rem; line-height: 1.5; }
.report-footer a { color: var(--green); font-size: .65rem; font-weight: 750; text-decoration: none; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulse { 70% { box-shadow: 0 0 0 .55rem rgba(34,91,119,0); } 100% { box-shadow: 0 0 0 0 rgba(34,91,119,0); } }
@media (max-width: 1180px) {
.report-hero { grid-template-columns: minmax(0, 1fr) 17rem; gap: 2rem; }
.hero-score-card { padding-inline: 1rem; }
.market-ribbon { grid-template-columns: repeat(3, 1fr); }
.market-ribbon > div:nth-child(3) { border-right: 0; }
.market-ribbon > div:nth-child(n+4) { border-top: 1px solid var(--line); }
.market-ribbon .range-cell { grid-column: span 2; }
.decision-layout { grid-template-columns: 1fr; }
.decision-rail { display: grid; grid-template-columns: repeat(2, 1fr); }
.valuation-methods { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 900px) {
.analysis-toolbar {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.analysis-controls {
justify-content: space-between;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
.panel-left, .panel-center, .panel-right {
grid-column: span 1;
}
.panel-right .factors-stack {
grid-template-columns: 1fr;
}
.valuation-header {
flex-direction: column;
gap: 1rem;
}
.methods-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.analysis-controls {
flex-direction: column;
align-items: stretch;
}
.selector-wrapper {
min-width: unset;
}
.language-switch {
justify-content: center;
}
.console-tabs {
flex-direction: column;
width: 100%;
button {
text-align: left;
padding: 0.75rem 1rem;
&.active::after {
left: 0;
top: 0;
bottom: 0;
width: 2px;
height: auto;
}
}
}
.report-sections-grid {
grid-template-columns: 1fr;
}
.trader-hero-row {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.rating-box {
align-items: flex-start;
}
.analysis-footer {
flex-direction: column;
text-align: center;
gap: 1rem;
span {
max-width: 100%;
text-align: center;
}
}
.research-terminal { padding-inline: 1rem; }
.topbar { position: relative; align-items: flex-start; }
.topbar-actions { flex-wrap: wrap; justify-content: flex-end; }
.stock-selector { min-width: min(19rem, 100%); }
.report-hero { min-height: 0; padding: 2rem; grid-template-columns: 1fr; }
.hero-score-card { padding: 1.5rem 0 0; flex-direction: row; gap: 1.5rem; border-left: 0; border-top: 1px solid var(--line); }
.score-chart { width: 8rem; height: 8rem; }
.score-center strong { font-size: 2rem; }
.score-copy { margin-top: 0; align-items: flex-start; text-align: left; }
.section-nav { top: .5rem; overflow-x: auto; }
.section-nav button { white-space: nowrap; }
.section-nav > span { display: none; }
.takeaway-list { grid-template-columns: 1fr; }
.takeaway-item { padding: .9rem 0; border-right: 0; border-bottom: 1px solid var(--line); }
.takeaway-item:last-child { border-bottom: 0; }
.factor-grid { grid-template-columns: 1fr; }
.dossier-layout { grid-template-columns: 1fr; }
.dossier-index { position: static; display: grid; grid-template-columns: repeat(2, 1fr); }
.dossier-index > p { grid-column: 1/-1; }
.dossier-index button { border-right: 1px solid var(--line); }
}
@media (max-width: 680px) {
.research-terminal { padding-inline: .65rem; }
.topbar { flex-direction: column; }
.topbar-actions { width: 100%; justify-content: stretch; }
.stock-selector { width: 100%; min-width: 0; }
.primary-action { flex: 1; }
.report-hero { margin-top: 1rem; padding: 1.35rem; }
.hero-meta { margin-bottom: 1.5rem; }
.report-hero h1 { font-size: clamp(2.3rem, 13vw, 4rem); }
.symbol-line span:last-child { width: 100%; padding-left: 0; border-left: 0; }
.decision-block { align-items: flex-start; }
.decision-copy { grid-template-columns: 1fr; }
.decision-copy strong { font-size: 1.25rem; }
.decision-copy small { margin-top: .28rem; }
.hero-score-card { justify-content: center; }
.market-ribbon { grid-template-columns: repeat(2, 1fr); }
.market-ribbon > div { min-height: 5.5rem; padding: 1rem; border-top: 1px solid var(--line); }
.market-ribbon > div:nth-child(even) { border-right: 0; }
.market-ribbon .range-cell { grid-column: span 2; }
.market-ribbon > div:last-child { grid-column: span 2; }
.section-nav { justify-content: flex-start; }
.section-heading,
.dossier-heading { align-items: flex-start; flex-direction: column; gap: 1rem; }
.decision-layout { margin-top: 3rem; }
.decision-rail { grid-template-columns: 1fr; }
.trigger-grid { grid-template-columns: 1fr; }
.trigger-card ul { grid-template-columns: 1fr; }
.valuation-section,
.factor-section,
.summary-reports,
.dossier-section,
.source-section { margin-top: 3.5rem; }
.valuation-numbers { grid-template-columns: repeat(2, 1fr); }
.valuation-numbers > div:nth-child(2) { border-right: 0; }
.valuation-numbers > div:nth-child(n+3) { border-top: 1px solid var(--line); }
.valuation-methods { grid-template-columns: 1fr; }
.factor-metric { grid-template-columns: 1fr 2.5rem; }
.metric-track { grid-column: 1/-1; grid-row: 2; }
.factor-metric > small { grid-column: 2; grid-row: 1; }
.dossier-index { grid-template-columns: 1fr; }
.dossier-index button { border-right: 0; }
.report-group { padding: .85rem; }
.report-document summary { grid-template-columns: auto 1fr auto; }
.document-tone { display: none; }
.markdown-report { padding: 1rem; }
.report-footer { grid-template-columns: 1fr; text-align: left; }
.disclaimer { align-items: flex-start; flex-direction: column; }
}
......@@ -4,21 +4,18 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
ElementRef,
OnDestroy,
OnInit,
ViewChild,
signal,
ViewChild,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import {
ArcElement,
Chart,
DoughnutController,
Tooltip,
} from 'chart.js';
import { ArcElement, Chart, DoughnutController, Tooltip } from 'chart.js';
import { Subscription, switchMap, takeWhile, timer } from 'rxjs';
import {
AnalysisJob,
AnalysisLanguage,
......@@ -32,6 +29,27 @@ import { TimesFmPredictionComponent } from './timesfm-prediction.component';
Chart.register(DoughnutController, ArcElement, Tooltip);
type DecisionTone = FactorStatus;
interface ReportDocument {
key: string;
title: string;
role: string;
icon: string;
content: string;
html: string;
tone: DecisionTone;
open: boolean;
}
interface ReportGroup {
key: string;
kicker: string;
title: string;
description: string;
documents: ReportDocument[];
}
@Component({
selector: 'app-root',
standalone: true,
......@@ -47,14 +65,23 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
readonly stocks = signal<StockOption[]>([]);
readonly analysis = signal<StockAnalysis | null>(null);
readonly language = signal<AnalysisLanguage>('en');
readonly loadingStocks = signal<boolean>(true);
readonly loadingAnalysis = signal<boolean>(false);
readonly jobMessage = signal<string>('');
readonly errorMessage = signal<string>('');
readonly fullReportOpen = signal<boolean>(false);
readonly activeReportTab = signal<'overview' | 'trader' | 'agents' | 'raw'>('overview');
readonly loadingStocks = signal(true);
readonly loadingAnalysis = signal(false);
readonly jobMessage = signal('');
readonly errorMessage = signal('');
readonly subscriptions: Subscription[] = [];
readonly reportGroups = computed<ReportGroup[]>(() => {
const report = this.analysis();
return report ? this.buildReportGroups(report) : [];
});
readonly reportDocumentCount = computed(() =>
this.reportGroups().reduce(
(total, group) => total + group.documents.length,
0,
),
);
selectedSymbol = 'GP';
private scoreCanvas: ElementRef<HTMLCanvasElement> | null = null;
private scoreChart: Chart<'doughnut'> | null = null;
......@@ -73,7 +100,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
ngOnInit(): void {
if (this.predictionMode) return;
this.titleService.setTitle('DSE AI Stock Analysis');
this.titleService.setTitle('DSE Investment Research Dossier');
const query = new URLSearchParams(window.location.search);
const requestedSymbol = query.get('symbol')?.trim().toUpperCase();
const requestedDate = query.get('date')?.trim();
......@@ -105,7 +132,6 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
}
selectStock(): void {
this.fullReportOpen.set(false);
this.loadLatest(this.selectedSymbol);
}
......@@ -115,7 +141,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
this.jobMessage.set(
this.language() === 'bn'
? 'বিশ্লেষণ সারিতে যোগ করা হচ্ছে…'
: 'Queueing analysis…',
: 'Queueing the multi-agent analysis…',
);
this.subscriptions.push(
this.stockAnalysisService.createAnalysis(this.selectedSymbol, force).subscribe({
......@@ -125,17 +151,6 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
);
}
toggleFullReport(): void {
this.fullReportOpen.update((open) => !open);
if (!this.fullReportOpen()) return;
setTimeout(() => {
document.getElementById('full-analysis')?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
});
}
factorStatus(status: FactorStatus): string {
const labels: Record<FactorStatus, BilingualText> = {
positive: { en: 'Positive', bn: 'ইতিবাচক' },
......@@ -146,28 +161,382 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
return this.copy(labels[status]);
}
getSectionStatus(sectionKey: string): FactorStatus {
const report = this.analysis();
if (!report) return 'neutral';
const mapping: Record<string, string> = {
earnings: 'profitability',
financial_health: 'financial_health',
business: 'business_quality',
valuation: 'valuation',
dividend: 'dividend',
};
return (
report.factors.find(
(factor) => factor.key === (mapping[sectionKey] || sectionKey),
)?.status ?? 'neutral'
);
}
metricStatus(score: number): FactorStatus {
if (score >= 7) return 'positive';
if (score < 4) return 'negative';
return 'caution';
}
factorAverage(report: StockAnalysis, key: string): number {
const metrics = report.factors.find((factor) => factor.key === key)?.metrics ?? [];
if (!metrics.length) return 0;
return metrics.reduce((total, metric) => total + metric.score, 0) / metrics.length;
}
scoreTone(score: number): DecisionTone {
if (score >= 70) return 'positive';
if (score >= 45) return 'caution';
return 'negative';
}
finalDecision(report: StockAnalysis): string {
const source =
report.agent_reports.final_trade_decision ||
this.stateString(report, 'final_trade_decision');
return (
this.extractDecision(source) ??
report.ai_research?.trader_report.rating ??
'Review'
);
}
decisionTone(report: StockAnalysis): DecisionTone {
const decision = this.finalDecision(report).toLowerCase();
if (/buy|overweight|accumulate/.test(decision)) return 'positive';
if (/sell|underweight|avoid/.test(decision)) return 'negative';
if (/hold|watch|review/.test(decision)) return 'caution';
return 'neutral';
}
formatTaka(value: number | null): string {
return value === null
? '—'
: `৳${value.toLocaleString('en-BD', { maximumFractionDigits: 1 })}`;
}
signedPercent(value: number | null): string {
if (value === null) return '—';
return `${value > 0 ? '+' : ''}${value.toFixed(2)}%`;
}
fairValueGap(report: StockAnalysis): number | null {
const estimate = report.valuation.rough_estimate;
const price = report.valuation.current_price;
if (estimate === null || price <= 0) return null;
return ((estimate / price) - 1) * 100;
}
rangePosition(report: StockAnalysis): number {
const low = report.market.fifty_two_week_low;
const high = report.market.fifty_two_week_high;
if (low === null || high === null || high <= low) return 50;
return this.clamp(((report.market.latest_price - low) / (high - low)) * 100, 0, 100);
}
valuationScale(report: StockAnalysis): {
current: number;
low: number;
high: number;
} {
const current = report.valuation.current_price;
const fairLow = report.valuation.fair_range_low ?? current;
const fairHigh = report.valuation.fair_range_high ?? current;
const scaleLow = Math.min(current, fairLow) * 0.92;
const scaleHigh = Math.max(current, fairHigh) * 1.08;
const spread = scaleHigh - scaleLow || 1;
const position = (value: number) =>
this.clamp(((value - scaleLow) / spread) * 100, 2, 98);
return {
current: position(current),
low: position(fairLow),
high: position(fairHigh),
};
}
hasAgentEvidence(report: StockAnalysis): boolean {
return this.buildReportGroups(report).some((group) => group.documents.length > 0);
}
rawState(report: StockAnalysis): string {
return JSON.stringify(report.agent_reports.raw_state, null, 2);
}
hasAgentEvidence(report: StockAnalysis): boolean {
const evidence = report.agent_reports;
return Boolean(
evidence.market_report ||
evidence.news_report ||
evidence.fundamentals_report ||
evidence.investment_plan ||
evidence.final_trade_decision ||
Object.keys(evidence.raw_state).length,
jumpTo(id: string): void {
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
renderMarkdown(markdown: string): string {
const escaped = this.escapeHtml(markdown || '').replace(/\r\n?/g, '\n');
const lines = escaped.split('\n');
const output: string[] = [];
let index = 0;
while (index < lines.length) {
const line = lines[index];
if (!line.trim()) {
index += 1;
continue;
}
if (/^```/.test(line.trim())) {
const code: string[] = [];
index += 1;
while (index < lines.length && !/^```/.test(lines[index].trim())) {
code.push(lines[index]);
index += 1;
}
index += 1;
output.push(`<pre><code>${code.join('\n')}</code></pre>`);
continue;
}
const heading = line.match(/^(#{1,6})\s+(.+)$/);
if (heading) {
const level = Math.min(5, heading[1].length + 1);
output.push(`<h${level}>${this.inlineMarkdown(heading[2])}</h${level}>`);
index += 1;
continue;
}
if (/^\s*([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
output.push('<hr>');
index += 1;
continue;
}
if (
line.includes('|') &&
index + 1 < lines.length &&
/^\s*\|?\s*:?-{3,}/.test(lines[index + 1])
) {
const headers = this.tableCells(line);
index += 2;
const rows: string[][] = [];
while (index < lines.length && lines[index].includes('|') && lines[index].trim()) {
rows.push(this.tableCells(lines[index]));
index += 1;
}
output.push(
`<div class="report-table-wrap"><table><thead><tr>${headers
.map((cell) => `<th>${this.inlineMarkdown(cell)}</th>`)
.join('')}</tr></thead><tbody>${rows
.map(
(row) =>
`<tr>${row
.map((cell) => `<td>${this.inlineMarkdown(cell)}</td>`)
.join('')}</tr>`,
)
.join('')}</tbody></table></div>`,
);
continue;
}
if (/^\s*>\s?/.test(line)) {
const quote: string[] = [];
while (index < lines.length && /^\s*>\s?/.test(lines[index])) {
quote.push(lines[index].replace(/^\s*>\s?/, ''));
index += 1;
}
output.push(`<blockquote>${quote.map((item) => this.inlineMarkdown(item)).join('<br>')}</blockquote>`);
continue;
}
const unordered = /^\s*[-*+]\s+/.test(line);
const ordered = /^\s*\d+[.)]\s+/.test(line);
if (unordered || ordered) {
const tag = ordered ? 'ol' : 'ul';
const items: string[] = [];
const pattern = ordered ? /^\s*\d+[.)]\s+/ : /^\s*[-*+]\s+/;
while (index < lines.length && pattern.test(lines[index])) {
items.push(lines[index].replace(pattern, ''));
index += 1;
}
output.push(
`<${tag}>${items
.map((item) => `<li>${this.inlineMarkdown(item)}</li>`)
.join('')}</${tag}>`,
);
continue;
}
const paragraph: string[] = [line.trim()];
index += 1;
while (
index < lines.length &&
lines[index].trim() &&
!this.isMarkdownBoundary(lines, index)
) {
paragraph.push(lines[index].trim());
index += 1;
}
output.push(`<p>${paragraph.map((item) => this.inlineMarkdown(item)).join('<br>')}</p>`);
}
return output.join('');
}
private buildReportGroups(report: StockAnalysis): ReportGroup[] {
const state = this.asRecord(report.agent_reports.raw_state);
const research = this.asRecord(state['investment_debate_state']);
const risk = this.asRecord(state['risk_debate_state']);
const direct = report.agent_reports;
const document = (
key: string,
title: string,
role: string,
icon: string,
content: unknown,
tone: DecisionTone,
open = false,
): ReportDocument | null => {
const text = typeof content === 'string' ? content.trim() : '';
return text
? {
key,
title,
role,
icon,
content: text,
html: this.renderMarkdown(text),
tone,
open,
}
: null;
};
const compact = (items: Array<ReportDocument | null>) =>
items.filter((item): item is ReportDocument => item !== null);
const groups: ReportGroup[] = [
{
key: 'analyst-desk',
kicker: 'I · EVIDENCE COLLECTION',
title: 'Analyst desk',
description: 'Independent market, sentiment, news, and company-fundamentals reviews.',
documents: compact([
document('market', 'Market & technical report', 'Market Analyst', 'M', direct.market_report || state['market_report'], 'neutral'),
document('sentiment', 'Market sentiment report', 'Sentiment Analyst', 'S', state['sentiment_report'], 'caution'),
document('news', 'News & disclosure report', 'News Analyst', 'N', direct.news_report || state['news_report'], 'caution'),
document('fundamentals', 'Fundamental report', 'Fundamentals Analyst', 'F', direct.fundamentals_report || state['fundamentals_report'], 'positive'),
]),
},
{
key: 'research-debate',
kicker: 'II · ADVERSARIAL RESEARCH',
title: 'Bull vs bear debate',
description: 'The upside case and downside case are kept separate before the research manager decides.',
documents: compact([
document('bull', 'Bull case', 'Bull Researcher', 'B+', research['bull_history'], 'positive'),
document('bear', 'Bear case', 'Bear Researcher', 'B−', research['bear_history'], 'negative'),
document('research-manager', 'Research manager decision', 'Research Manager', 'RM', research['judge_decision'] || direct.investment_plan || state['investment_plan'], 'caution', true),
]),
},
{
key: 'trade-plan',
kicker: 'III · EXECUTION PLAN',
title: 'Trader plan',
description: 'The research conclusion translated into an entry, sizing, timing, and monitoring plan.',
documents: compact([
document('trader', 'Transaction proposal', 'Trader', 'T', state['trader_investment_plan'] || state['trader_investment_decision'], 'caution', true),
]),
},
{
key: 'risk-committee',
kicker: 'IV–V · RISK & FINAL AUTHORITY',
title: 'Risk committee and portfolio decision',
description: 'Aggressive, conservative, and balanced risk views followed by the final portfolio-manager decision.',
documents: compact([
document('aggressive', 'Upside-oriented risk view', 'Aggressive Analyst', 'A', risk['aggressive_history'], 'positive'),
document('conservative', 'Capital-preservation view', 'Conservative Analyst', 'C', risk['conservative_history'], 'negative'),
document('neutral', 'Balanced risk view', 'Neutral Analyst', 'N', risk['neutral_history'], 'caution'),
document('portfolio', 'Final portfolio decision', 'Portfolio Manager', 'PM', risk['judge_decision'] || direct.final_trade_decision || state['final_trade_decision'], this.decisionTone(report), true),
]),
},
];
return groups.filter((group) => group.documents.length > 0);
}
private extractDecision(content: string): string | null {
if (!content) return null;
const strongPatterns = [
/(?:\*\*)?final\s+(?:transaction\s+)?(?:proposal|decision|recommendation|rating)(?:\*\*)?\s*[:\-]\s*(?:\*\*)?\s*(BUY|OVERWEIGHT|HOLD|UNDERWEIGHT|SELL|ACCUMULATE|AVOID)/gi,
/(?:\*\*)?(?:rating|recommendation|decision)(?:\*\*)?\s*[:\-]\s*(?:\*\*)?\s*(BUY|OVERWEIGHT|HOLD|UNDERWEIGHT|SELL|ACCUMULATE|AVOID)/gi,
];
for (const pattern of strongPatterns) {
const matches = [...content.matchAll(pattern)];
if (matches.length) return this.titleCase(matches[matches.length - 1][1]);
}
const fallback = [...content.matchAll(/\b(BUY|OVERWEIGHT|HOLD|UNDERWEIGHT|SELL|ACCUMULATE|AVOID)\b/gi)];
return fallback.length ? this.titleCase(fallback[fallback.length - 1][1]) : null;
}
private titleCase(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
}
private stateString(report: StockAnalysis, key: string): string {
const value = this.asRecord(report.agent_reports.raw_state)[key];
return typeof value === 'string' ? value : '';
}
private asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
private clamp(value: number, low: number, high: number): number {
return Math.max(low, Math.min(high, value));
}
private tableCells(line: string): string[] {
return line
.trim()
.replace(/^\|/, '')
.replace(/\|$/, '')
.split('|')
.map((cell) => cell.trim());
}
private isMarkdownBoundary(lines: string[], index: number): boolean {
const line = lines[index];
return (
/^(#{1,6})\s+/.test(line) ||
/^```/.test(line.trim()) ||
/^\s*>\s?/.test(line) ||
/^\s*[-*+]\s+/.test(line) ||
/^\s*\d+[.)]\s+/.test(line) ||
/^\s*([-*_])(?:\s*\1){2,}\s*$/.test(line) ||
(line.includes('|') &&
index + 1 < lines.length &&
/^\s*\|?\s*:?-{3,}/.test(lines[index + 1]))
);
}
private inlineMarkdown(value: string): string {
return value
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/__([^_]+)__/g, '<strong>$1</strong>')
.replace(/(^|[^*])\*([^*]+)\*/g, '$1<em>$2</em>');
}
private escapeHtml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
private loadStocks(): void {
this.loadingStocks.set(true);
this.subscriptions.push(
......@@ -233,8 +602,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
}
this.cdr.markForCheck();
},
error: () =>
this.handleError('The analysis job status could not be checked.'),
error: () => this.handleError('The analysis job status could not be checked.'),
}),
);
}
......@@ -257,6 +625,12 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
private renderScoreChart(): void {
const report = this.analysis();
if (!this.scoreCanvas || !report) return;
const colors: Record<DecisionTone, string> = {
positive: '#147d57',
caution: '#bf741f',
negative: '#c24949',
neutral: '#64748b',
};
this.scoreChart?.destroy();
this.scoreChart = new Chart(this.scoreCanvas.nativeElement, {
type: 'doughnut',
......@@ -265,7 +639,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
datasets: [
{
data: [report.fundamental_score, 100 - report.fundamental_score],
backgroundColor: ['#0ea5e9', '#1e293b'],
backgroundColor: [colors[this.scoreTone(report.fundamental_score)], '#e7e9e4'],
borderWidth: 0,
hoverOffset: 0,
},
......@@ -274,7 +648,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '78%',
cutout: '80%',
animation: { duration: 450 },
plugins: {
legend: { display: false },
......
......@@ -101,6 +101,12 @@ export interface AIResearch {
trader_report: AITraderReport;
}
export interface EvidenceSource {
name: string;
source_type: 'dse_api' | 'agent_state' | 'calculation' | 'ai_analysis';
detail: string;
}
export interface StockAnalysis {
schema_version: '1.0';
analysis_id: string;
......@@ -121,6 +127,7 @@ export interface StockAnalysis {
report_sections: ReportSection[];
agent_reports: AgentReports;
ai_research?: AIResearch | null;
sources: EvidenceSource[];
disclaimer: BilingualText;
}
......
:host {
display: block;
color: #e5e7eb;
background: #090d14;
color: #17201c;
background: #f3f0e8;
}
.prediction-page {
--panel: #111827;
--panel-soft: #151d2b;
--line: #263244;
--muted: #94a3b8;
--amber: #f59e0b;
--violet: #a78bfa;
--cyan: #22d3ee;
--panel: #fbfaf5;
--panel-soft: #eef1ea;
--line: #d7d7cb;
--muted: #667068;
--amber: #b96715;
--violet: #7c3aed;
--cyan: #0b7458;
min-height: 100vh;
padding: 1.25rem clamp(1rem, 3vw, 3rem) 3rem;
background:
radial-gradient(circle at 10% -10%, rgb(34 211 238 / 10%), transparent 28rem),
radial-gradient(circle at 100% 5%, rgb(167 139 250 / 9%), transparent 32rem),
#090d14;
radial-gradient(circle at 10% -10%, rgba(11, 116, 88, 0.04), transparent 28rem),
radial-gradient(circle at 100% 5%, rgba(124, 58, 237, 0.03), transparent 32rem),
#f3f0e8;
}
.prediction-toolbar,
......@@ -55,7 +55,7 @@
display: inline-flex;
align-items: center;
gap: 0.7rem;
color: #e5e7eb;
color: #17201c;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.16em;
......@@ -68,10 +68,10 @@
width: 2rem;
height: 2rem;
place-items: center;
border: 1px solid #155e75;
border-radius: 0.55rem;
color: var(--cyan);
background: #0c2630;
border-radius: 50%;
color: #fff;
background: var(--cyan);
font-weight: 900;
font-size: 1rem;
}
......@@ -80,7 +80,7 @@
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.8rem;
border: 1px solid #374151;
border: 1px solid var(--line);
border-radius: 2rem;
color: var(--muted);
font-size: 0.78rem;
......@@ -92,10 +92,10 @@
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: #64748b;
background: #8a918b;
}
.live-pill.connected { color: #6ee7b7; border-color: #065f46; }
.live-pill.connected .status-dot { background: #34d399; box-shadow: 0 0 0 0.25rem rgb(52 211 153 / 12%); }
.live-pill.connected { color: #0b7458; border-color: #91ae9c; background: #edf6ef; }
.live-pill.connected .status-dot { background: #0b7458; box-shadow: 0 0 0 0.25rem rgba(11, 116, 88, 0.12); }
.hero {
justify-content: space-between;
......@@ -105,7 +105,7 @@
.eyebrow {
margin: 0 0 0.65rem;
color: #7dd3fc;
color: var(--cyan);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.18em;
......@@ -115,31 +115,34 @@
.symbol-line { gap: 0.75rem; flex-wrap: wrap; }
.symbol-line h1 {
margin: 0 0.5rem 0 0;
color: #f8fafc;
color: #17201c;
font-family: Georgia, serif;
font-size: clamp(3.2rem, 7vw, 6.8rem);
font-weight: 500;
line-height: 0.9;
letter-spacing: -0.065em;
}
.symbol-line span {
padding: 0.35rem 0.75rem;
border: 1px solid #334155;
border: 1px solid var(--line);
border-radius: 2rem;
color: #cbd5e1;
background: #111827;
color: #17201c;
background: var(--panel);
font-size: 0.82rem;
font-weight: 600;
}
.hero-copy { max-width: 48rem; color: var(--muted); line-height: 1.65; }
.accuracy-card {
flex: 0 0 min(100%, 20rem);
padding: 1.3rem 1.5rem;
border: 1px solid #3b475b;
border: 1px solid var(--line);
border-radius: 1.2rem;
background: linear-gradient(145deg, #172033, #101622);
box-shadow: 0 1.5rem 4rem rgb(0 0 0 / 28%);
background: var(--panel);
box-shadow: 0 0.6rem 2rem rgba(42, 56, 47, 0.04);
}
.accuracy-card > span { color: var(--muted); font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.1em; }
.accuracy-card strong { display: block; margin: 0.4rem 0; color: #f8fafc; font-size: 4rem; line-height: 1; }
.accuracy-card strong { display: block; margin: 0.4rem 0; color: #17201c; font-size: 4rem; line-height: 1; }
.accuracy-card small { color: var(--cyan); font-size: 1.4rem; }
.accuracy-card p { margin: 0; color: var(--muted); font-size: 0.8rem; }
......@@ -158,10 +161,10 @@
}
.metric-grid span, .metric-grid small { display: block; color: var(--muted); }
.metric-grid span { font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.08em; }
.metric-grid strong { display: block; overflow: hidden; margin: 0.4rem 0 0.2rem; color: #f8fafc; font-size: clamp(1.2rem, 2vw, 1.75rem); text-overflow: ellipsis; }
.metric-grid strong { display: block; overflow: hidden; margin: 0.4rem 0 0.2rem; color: #17201c; font-size: clamp(1.2rem, 2vw, 1.75rem); text-overflow: ellipsis; font-weight: 800; }
.metric-grid small { font-size: 0.75rem; }
.metric-grid article.negative strong { color: #fb7185; }
.metric-grid .live-metric { border-color: #155e75; background: #0d202a; }
.metric-grid article.negative strong { color: var(--red); }
.metric-grid .live-metric { border-color: #91ae9c; background: #edf6ef; }
.metric-grid .live-metric strong { color: var(--cyan); }
.chart-card,
......@@ -170,33 +173,34 @@
.table-card {
border: 1px solid var(--line);
border-radius: 1.1rem;
background: rgb(17 24 39 / 88%);
background: var(--panel);
box-shadow: 0 0.6rem 2rem rgba(42, 56, 47, 0.04);
}
.chart-card { overflow: hidden; margin-bottom: 1rem; }
.chart-card > header { justify-content: space-between; gap: 1.5rem; padding: 1.1rem 1.25rem; border-bottom: 1px solid var(--line); }
.chart-card h2, .detail-grid h2, .table-card h2 { margin: 0; color: #f8fafc; font-size: 1.15rem; }
.chart-card h2, .detail-grid h2, .table-card h2 { margin: 0; color: #17201c; font-family: Georgia, serif; font-size: 1.15rem; font-weight: 600; }
.legend { justify-content: flex-end; gap: 1rem; flex-wrap: wrap; color: var(--muted); font-size: 0.76rem; }
.legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
.legend i { width: 1.25rem; height: 0.16rem; border-radius: 1rem; background: #34d399; }
.legend i { width: 1.25rem; height: 0.16rem; border-radius: 1rem; background: var(--cyan); }
.legend .backtest { background: var(--amber); }
.legend .actual-line { background: #34d399; }
.legend .actual-line { background: var(--cyan); }
.legend .future { background: var(--violet); }
.legend .live { background: var(--cyan); }
.legend .interval {
height: 0;
border-top: 2px dashed rgb(245 158 11 / 65%);
border-top: 2px dashed rgba(185, 103, 21, 0.65);
border-radius: 0;
background: transparent;
}
.legend .future-interval { border-color: rgb(167 139 250 / 70%); }
.legend .future-interval { border-color: rgba(124, 58, 237, 0.7); }
.chart-controls { display: grid; justify-items: end; gap: 0.7rem; }
.chart-mode-switch {
display: inline-grid;
grid-template-columns: repeat(2, 1fr);
padding: 0.2rem;
border: 1px solid #334155;
border: 1px solid var(--line);
border-radius: 0.7rem;
background: #0b1220;
background: #f0f2eb;
}
.chart-mode-switch button,
.interval-toggle {
......@@ -213,9 +217,8 @@
border-radius: 0.5rem;
}
.chart-mode-switch button.active {
color: #f8fafc;
background: #253146;
box-shadow: 0 0 0 1px rgb(148 163 184 / 12%);
color: #fff;
background: var(--cyan);
}
.chart-mode-switch button:focus-visible,
.interval-toggle:focus-visible {
......@@ -231,20 +234,20 @@
padding: 0.65rem 1.25rem;
border-bottom: 1px solid var(--line);
color: var(--muted);
background: #0d1420;
background: #eef1ea;
font-size: 0.76rem;
}
.chart-context-note p { margin: 0; line-height: 1.5; }
.interval-toggle {
flex: 0 0 auto;
padding: 0.45rem 0.7rem;
border: 1px solid #475569;
border: 1px solid var(--line);
border-radius: 0.55rem;
}
.interval-toggle.active {
border-color: #7c3aed;
color: #ddd6fe;
background: rgb(124 58 237 / 14%);
border-color: var(--cyan);
color: #fff;
background: var(--cyan);
}
.prediction-chart { width: 100%; min-height: 520px; }
.chart-card > footer {
......@@ -256,22 +259,22 @@
color: var(--muted);
font-size: 0.76rem;
}
.chart-card > footer strong { color: #dbe5f3; font-weight: 600; }
.chart-card > footer strong { color: #17201c; font-weight: 600; }
.detail-grid { display: grid; grid-template-columns: 1.35fr 1fr; gap: 1rem; margin-bottom: 1rem; }
.live-card, .model-card { padding: 1.3rem; }
.live-comparison { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; margin-top: 1.2rem; }
.live-comparison div { padding: 0.9rem; border: 1px solid #164e63; border-radius: 0.7rem; background: #0c2029; }
.live-comparison div { padding: 0.9rem; border: 1px solid #91ae9c; border-radius: 0.7rem; background: #edf6ef; }
.live-comparison span { display: block; color: var(--muted); font-size: 0.75rem; }
.live-comparison strong { display: block; margin-top: 0.35rem; color: var(--cyan); font-size: 1.35rem; }
.live-card > p:last-child, .waiting-live { color: var(--muted); font-size: 0.82rem; }
.waiting-live { display: flex; align-items: center; gap: 0.8rem; min-height: 5.8rem; }
.pulse-ring { width: 0.75rem; height: 0.75rem; flex: 0 0 auto; border-radius: 50%; background: var(--cyan); box-shadow: 0 0 0 0.45rem rgb(34 211 238 / 10%); }
code { color: #bae6fd; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.pulse-ring { width: 0.75rem; height: 0.75rem; flex: 0 0 auto; border-radius: 50%; background: var(--cyan); box-shadow: 0 0 0 0.45rem rgba(11, 116, 88, 0.12); }
code { color: #0b7458; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.model-card dl { margin: 1rem 0 0; }
.model-card dl div { display: grid; grid-template-columns: 8rem 1fr; gap: 1rem; padding: 0.65rem 0; border-top: 1px solid var(--line); }
.model-card dt { color: var(--muted); }
.model-card dd { overflow-wrap: anywhere; margin: 0; color: #e5e7eb; text-align: right; }
.model-card dd { overflow-wrap: anywhere; margin: 0; color: #17201c; text-align: right; font-weight: 600; }
.table-card { overflow: hidden; margin-bottom: 1rem; }
.table-card > header { justify-content: space-between; gap: 2rem; padding: 1.2rem 1.3rem; }
......@@ -280,15 +283,15 @@ code { color: #bae6fd; font-family: ui-monospace, SFMono-Regular, Menlo, monospa
table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
th, td { padding: 0.8rem 1.3rem; border-top: 1px solid var(--line); text-align: right; white-space: nowrap; }
th:first-child, td:first-child { text-align: left; }
th { color: var(--muted); background: #0d1420; font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; }
td { color: #dbe5f3; font-size: 0.82rem; }
th { color: var(--muted); background: #f0f2eb; font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; }
td { color: #17201c; font-size: 0.82rem; }
.prediction-footer { justify-content: space-between; gap: 2rem; padding: 1rem 0; color: var(--muted); font-size: 0.75rem; }
.prediction-footer span { max-width: 60rem; }
.loading-state, .empty-state { display: grid; min-height: 60vh; place-content: center; text-align: center; }
.loading-state h1, .empty-state h1 { margin-bottom: 0.5rem; color: #f8fafc; }
.loading-state h1, .empty-state h1 { margin-bottom: 0.5rem; color: #17201c; font-family: Georgia, serif; }
.loading-state p, .empty-state p { color: var(--muted); }
.loader { width: 2rem; height: 2rem; margin: 0 auto 1rem; border: 2px solid #334155; border-top-color: var(--cyan); border-radius: 50%; animation: spin 0.8s linear infinite; }
.loader { width: 2rem; height: 2rem; margin: 0 auto 1rem; border: 2px solid var(--line); border-top-color: var(--cyan); border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 1050px) {
......
......@@ -6,15 +6,15 @@
html {
scroll-behavior: smooth;
background-color: #05070c;
background-color: #f3f0e8;
}
html,
body {
min-height: 100%;
margin: 0;
background: #090d16;
color: #f3f4f6;
background: #f3f0e8;
color: #17201c;
overflow-x: hidden;
}
......@@ -31,7 +31,7 @@ textarea {
button,
select,
summary {
outline-color: #0ea5e9;
outline-color: #0b7458;
}
/* Custom Scrollbar */
......@@ -41,14 +41,14 @@ summary {
}
::-webkit-scrollbar-track {
background: #0b0f19;
background: #f3f0e8;
}
::-webkit-scrollbar-thumb {
background: #1f2937;
background: #d7d7cb;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #374151;
background: #b8b8aa;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment