Compare commits

..

4 Commits

Author SHA1 Message Date
Flatlogic Bot
6a32fa321f OPTEMA AI 2026-06-19 21:10:45 +00:00
Flatlogic Bot
0ab3040f29 OPTEMA AI 2026-06-19 16:04:15 +00:00
Flatlogic Bot
60f601a85b OPTEMA AI 2026-06-19 15:42:30 +00:00
Flatlogic Bot
65e988640d Autosave: 20260619-151237 2026-06-19 15:12:08 +00:00
29 changed files with 3060 additions and 181 deletions

Binary file not shown.

View File

@ -1,3 +1,43 @@
from django.contrib import admin
# Register your models here.
from .models import ActionPlanStep, ProblemCase, RootCause, SolutionOption
class RootCauseInline(admin.TabularInline):
model = RootCause
extra = 0
class SolutionOptionInline(admin.TabularInline):
model = SolutionOption
extra = 0
fields = ("rank", "title", "impact", "efficiency", "speed", "low_risk", "decision_score", "success_rate")
readonly_fields = ("decision_score",)
@admin.register(ProblemCase)
class ProblemCaseAdmin(admin.ModelAdmin):
list_display = ("title", "business_area", "urgency", "priority_score", "financial_impact", "status", "created_at")
list_filter = ("business_area", "status", "urgency")
search_fields = ("title", "description")
inlines = [RootCauseInline, SolutionOptionInline]
@admin.register(SolutionOption)
class SolutionOptionAdmin(admin.ModelAdmin):
list_display = ("title", "problem", "rank", "decision_score", "success_rate")
list_filter = ("rank",)
search_fields = ("title", "problem__title")
@admin.register(ActionPlanStep)
class ActionPlanStepAdmin(admin.ModelAdmin):
list_display = ("solution", "day_index", "title", "is_done")
list_filter = ("is_done",)
search_fields = ("title", "task", "solution__title")
@admin.register(RootCause)
class RootCauseAdmin(admin.ModelAdmin):
list_display = ("factor", "problem", "contribution_score", "parent")
search_fields = ("factor", "why_chain", "problem__title")

75
core/forms.py Normal file
View File

@ -0,0 +1,75 @@
from django import forms
from .models import ProblemCase
class ProblemCaseForm(forms.ModelForm):
class Meta:
model = ProblemCase
fields = ["description", "urgency"]
labels = {
"description": "Masukkan masalah, target, atau hambatan Anda:",
"urgency": "Tingkat Urgensi",
}
widgets = {
"description": forms.Textarea(attrs={
"class": "form-control problem-textarea",
"rows": 9,
"placeholder": "Contoh: BUMN rugi karena biaya tinggi, omzet turun 30%, atau kuliah Jepang/Singapura dengan dana 200 juta.",
}),
"urgency": forms.TextInput(attrs={
"type": "range",
"class": "form-range urgency-slider",
"min": 1,
"max": 5,
"step": 1,
"oninput": "document.getElementById('urgency-output').value=this.value",
}),
}
def clean_description(self):
description = self.cleaned_data["description"].strip()
if len(description) < 20:
raise forms.ValidationError("Tuliskan minimal 20 karakter agar analisis lebih bermakna.")
return description
def clean_urgency(self):
urgency = self.cleaned_data["urgency"]
if urgency < 1 or urgency > 5:
raise forms.ValidationError("Urgensi harus berada pada skala 1 sampai 5.")
return urgency
class WebIntelligenceForm(forms.Form):
query = forms.CharField(
label="Topik atau masalah",
required=False,
max_length=180,
widget=forms.TextInput(attrs={
"class": "form-control",
"placeholder": "Contoh: kerugian BUMN Indonesia",
}),
)
url = forms.URLField(
label="URL sumber data",
max_length=500,
widget=forms.URLInput(attrs={
"class": "form-control",
"placeholder": "https://...",
}),
)
api_key = forms.CharField(
label="OpenAI API Key",
required=False,
widget=forms.PasswordInput(attrs={
"class": "form-control",
"placeholder": "Opsional — isi untuk analisis AI",
"autocomplete": "off",
}, render_value=False),
)
def clean_url(self):
source_url = self.cleaned_data["url"].strip()
if not source_url.lower().startswith(("http://", "https://")):
raise forms.ValidationError("URL harus diawali http:// atau https://.")
return source_url

View File

@ -0,0 +1,82 @@
# Generated by Django 5.2.7 on 2026-06-19 14:38
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ProblemCase',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=160, verbose_name='judul kasus')),
('description', models.TextField(verbose_name='deskripsi masalah')),
('business_area', models.CharField(choices=[('sales', 'Penjualan'), ('operations', 'Operasional'), ('finance', 'Keuangan'), ('marketing', 'Marketing'), ('product', 'Produk/Layanan'), ('people', 'Tim & SDM'), ('other', 'Lainnya')], default='sales', max_length=32, verbose_name='area bisnis')),
('urgency', models.PositiveSmallIntegerField(default=3, verbose_name='urgensi')),
('priority_score', models.PositiveSmallIntegerField(default=0, verbose_name='skor prioritas')),
('financial_impact', models.CharField(default='Sedang', max_length=32, verbose_name='dampak finansial')),
('status', models.CharField(choices=[('draft', 'Draft'), ('analyzed', 'Sudah dianalisis')], default='draft', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Problem Case',
'verbose_name_plural': 'Problem Cases',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='RootCause',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('factor', models.CharField(max_length=120)),
('contribution_score', models.PositiveSmallIntegerField(default=70)),
('why_chain', models.TextField()),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='core.rootcause')),
('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='root_causes', to='core.problemcase')),
],
options={
'ordering': ['-contribution_score', 'factor'],
},
),
migrations.CreateModel(
name='SolutionOption',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=160)),
('impact', models.PositiveSmallIntegerField(default=70)),
('efficiency', models.PositiveSmallIntegerField(default=70)),
('speed', models.PositiveSmallIntegerField(default=70)),
('low_risk', models.PositiveSmallIntegerField(default=70)),
('decision_score', models.DecimalField(decimal_places=2, default=0, max_digits=5)),
('success_rate', models.PositiveSmallIntegerField(default=70)),
('rank', models.PositiveSmallIntegerField(default=1)),
('rationale', models.TextField()),
('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='solutions', to='core.problemcase')),
],
options={
'ordering': ['rank', '-decision_score'],
},
),
migrations.CreateModel(
name='ActionPlanStep',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('day_index', models.PositiveSmallIntegerField()),
('title', models.CharField(max_length=120)),
('task', models.TextField()),
('is_done', models.BooleanField(default=False)),
('solution', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='action_steps', to='core.solutionoption')),
],
options={
'ordering': ['day_index'],
},
),
]

View File

@ -1,3 +1,107 @@
from django.db import models
from django.urls import reverse
# Create your models here.
class ProblemCase(models.Model):
AREA_SALES = "sales"
AREA_OPERATIONS = "operations"
AREA_FINANCE = "finance"
AREA_MARKETING = "marketing"
AREA_PRODUCT = "product"
AREA_PEOPLE = "people"
AREA_OTHER = "other"
BUSINESS_AREA_CHOICES = [
(AREA_SALES, "Penjualan"),
(AREA_OPERATIONS, "Operasional"),
(AREA_FINANCE, "Keuangan"),
(AREA_MARKETING, "Marketing"),
(AREA_PRODUCT, "Produk/Layanan"),
(AREA_PEOPLE, "Tim & SDM"),
(AREA_OTHER, "Lainnya"),
]
STATUS_DRAFT = "draft"
STATUS_ANALYZED = "analyzed"
STATUS_CHOICES = [
(STATUS_DRAFT, "Draft"),
(STATUS_ANALYZED, "Sudah dianalisis"),
]
title = models.CharField("judul kasus", max_length=160)
description = models.TextField("deskripsi masalah")
business_area = models.CharField(
"area bisnis", max_length=32, choices=BUSINESS_AREA_CHOICES, default=AREA_SALES
)
urgency = models.PositiveSmallIntegerField("urgensi", default=3)
priority_score = models.PositiveSmallIntegerField("skor prioritas", default=0)
financial_impact = models.CharField("dampak finansial", max_length=32, default="Sedang")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_DRAFT)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"]
verbose_name = "Problem Case"
verbose_name_plural = "Problem Cases"
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("case_detail", kwargs={"pk": self.pk})
class RootCause(models.Model):
problem = models.ForeignKey(
ProblemCase, related_name="root_causes", on_delete=models.CASCADE
)
parent = models.ForeignKey(
"self", related_name="children", null=True, blank=True, on_delete=models.CASCADE
)
factor = models.CharField(max_length=120)
contribution_score = models.PositiveSmallIntegerField(default=70)
why_chain = models.TextField()
class Meta:
ordering = ["-contribution_score", "factor"]
def __str__(self):
return f"{self.factor} ({self.contribution_score}%)"
class SolutionOption(models.Model):
problem = models.ForeignKey(
ProblemCase, related_name="solutions", on_delete=models.CASCADE
)
title = models.CharField(max_length=160)
impact = models.PositiveSmallIntegerField(default=70)
efficiency = models.PositiveSmallIntegerField(default=70)
speed = models.PositiveSmallIntegerField(default=70)
low_risk = models.PositiveSmallIntegerField(default=70)
decision_score = models.DecimalField(max_digits=5, decimal_places=2, default=0)
success_rate = models.PositiveSmallIntegerField(default=70)
rank = models.PositiveSmallIntegerField(default=1)
rationale = models.TextField()
class Meta:
ordering = ["rank", "-decision_score"]
def __str__(self):
return self.title
class ActionPlanStep(models.Model):
solution = models.ForeignKey(
SolutionOption, related_name="action_steps", on_delete=models.CASCADE
)
day_index = models.PositiveSmallIntegerField()
title = models.CharField(max_length=120)
task = models.TextField()
is_done = models.BooleanField(default=False)
class Meta:
ordering = ["day_index"]
def __str__(self):
return f"Hari {self.day_index}: {self.title}"

View File

@ -1,11 +1,13 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<html lang="id">
<head>
<meta charset="UTF-8">
<title>{% block title %}Knowledge Base{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ page_title|default:"OPTEMA AI" }}{% endblock %}</title>
<meta name="description" content="{{ meta_description|default:project_description|default:'OPTEMA AI mengubah masalah menjadi analisis akar masalah, skor keputusan, dan rencana aksi.' }}">
{% if project_description %}
<meta name="description" content="{{ project_description }}">
<meta property="og:description" content="{{ project_description }}">
<meta property="twitter:description" content="{{ project_description }}">
{% endif %}
@ -13,13 +15,17 @@
<meta property="og:image" content="{{ project_image_url }}">
<meta property="twitter:image" content="{{ project_image_url }}">
{% endif %}
{% load static %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Plus+Jakarta+Sans:wght@600;700;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
{% block head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,18 @@
<nav class="navbar navbar-expand-lg optema-nav" aria-label="Navigasi utama OPTEMA AI">
<div class="container">
<a class="navbar-brand brand-mark" href="{% url 'home' %}">
<span class="brand-icon">O</span> OPTEMA AI
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#optemaNav" aria-controls="optemaNav" aria-expanded="false" aria-label="Buka navigasi">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="optemaNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="{% url 'home' %}#analisis">Mulai Analisis</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'case_list' %}">Daftar Kasus</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'web_intelligence' %}">Web Intelligence</a></li>
<li class="nav-item"><a class="nav-link admin-link" href="/admin/">Admin</a></li>
</ul>
</div>
</div>
</nav>

View File

@ -0,0 +1,174 @@
{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
{% include "core/_nav.html" %}
<main class="page-shell">
<section class="detail-hero">
<div class="container">
{% if messages %}
<div class="mb-4">
{% for message in messages %}
<div class="alert alert-{{ message.tags|default:'info' }}" role="alert">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<div class="row g-4 align-items-center">
<div class="col-lg-8">
<p class="eyebrow mb-2">Analisis Tersimpan · {{ detected_category|default:problem.get_business_area_display }}</p>
<h1 class="page-title">{{ problem.title }}</h1>
<p class="hero-copy mb-0">{{ problem.description }}</p>
</div>
<div class="col-lg-4">
<div class="detail-status-card glass-card">
<div class="output-success mb-3">✅ Analisis Berhasil Disimulasikan!</div>
<p class="mb-0">Case ini tersimpan di database lokal dan bisa dibuka ulang dari Riwayat Case.</p>
</div>
</div>
</div>
</div>
</section>
<section class="container py-5">
<div class="analysis-card mb-4">
<div class="section-kicker">Problem Detection</div>
<h2>1. Problem Detection</h2>
<div class="metric-grid">
<div class="metric-card"><span>Prioritas Skor</span><strong>{{ problem.priority_score }}/100</strong></div>
<div class="metric-card"><span>Dampak Finansial</span><strong>{{ problem.financial_impact }}</strong></div>
</div>
</div>
{% if case_insights %}
<div class="analysis-card mb-4">
<div class="section-kicker">Data & Kalkulasi</div>
<h2>1B. Data yang Terdeteksi dari Pertanyaan</h2>
<p class="formula-note">Bagian ini dibuat dari angka/konteks yang Anda tulis, supaya jawaban tidak generik dan bisa dicek hitungannya.</p>
<div class="metric-grid mb-4">
{% for item in case_insights.detected %}
<div class="metric-card">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<small>{{ item.note }}</small>
</div>
{% endfor %}
</div>
{% if case_insights.calculations %}
<div class="table-responsive mb-4">
<table class="table align-middle solution-table">
<thead><tr><th>Kalkulasi</th><th>Rumus</th><th>Hasil</th></tr></thead>
<tbody>
{% for calc in case_insights.calculations %}
<tr><td><strong>{{ calc.label }}</strong></td><td>{{ calc.formula }}</td><td>{{ calc.result }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if case_insights.comparisons %}
<h3 class="h5 mb-3">Perbandingan Opsi Berdasarkan Data</h3>
<div class="table-responsive mb-4">
<table class="table align-middle solution-table">
<thead><tr><th>Opsi</th><th>Estimasi Total</th><th>Kecocokan Budget</th><th>Catatan</th></tr></thead>
<tbody>
{% for row in case_insights.comparisons %}
<tr><td><strong>{{ row.option }}</strong></td><td>{{ row.estimate }}</td><td>{{ row.budget_fit }}</td><td>{{ row.note }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="formula-note mb-4">{{ case_insights.assumption_note }}</p>
{% endif %}
{% if case_insights.recommendation %}
<div class="alert alert-info mb-3" role="status">{{ case_insights.recommendation }}</div>
{% endif %}
{% if case_insights.missing %}
<div class="alert alert-warning mb-0" role="status"><strong>Data tambahan yang akan membuat jawaban lebih akurat:</strong><ul class="mb-0 mt-2">{% for item in case_insights.missing %}<li>{{ item }}</li>{% endfor %}</ul></div>
{% endif %}
</div>
{% endif %}
<div class="row g-4">
<div class="col-lg-5">
<div class="analysis-card sticky-lg-top">
<div class="section-kicker">Root Cause Analysis</div>
<h2>2. Root Cause (Akar Masalah)</h2>
<p class="formula-note">Akar masalah di bawah diturunkan dari Problem utama dan data terdeteksi pada bagian 1, sehingga bukan daftar generik yang berdiri sendiri.</p>
<div class="cause-list">
{% for cause in problem.root_causes.all %}
<article>
<div class="d-flex justify-content-between gap-3"><strong>{{ cause.factor }}</strong><span>{{ cause.contribution_score }}%</span></div>
<div class="progress" role="progressbar" aria-label="Kontribusi {{ cause.factor }}" aria-valuenow="{{ cause.contribution_score }}" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar score-width-{{ cause.contribution_score }}"></div>
</div>
<p>{{ cause.why_chain }}</p>
</article>
{% endfor %}
</div>
</div>
</div>
<div class="col-lg-7">
<div class="analysis-card mb-4">
<div class="section-kicker">Solution & Decision Scoring</div>
<h2>3. Rekomendasi Solusi & Prediksi Sukses</h2>
<p class="formula-note">Setiap rekomendasi menargetkan akar masalah pada bagian 2. Rumus: Decision Score = Impact×0.4 + Efficiency×0.3 + Speed×0.2 + LowRisk×0.1</p>
<div class="table-responsive">
<table class="table align-middle solution-table">
<thead>
<tr>
<th>Rank</th>
<th>Solusi</th>
<th>Impact</th>
<th>Efficiency</th>
<th>Speed</th>
<th>Low Risk</th>
<th>Sukses Rate</th>
<th>Decision Score</th>
</tr>
</thead>
<tbody>
{% for solution in problem.solutions.all %}
<tr>
<td><span class="rank-badge">#{{ solution.rank }}</span></td>
<td>
<strong>{{ solution.title }}</strong>
<p>{{ solution.rationale }}</p>
</td>
<td>{{ solution.impact }}</td>
<td>{{ solution.efficiency }}</td>
<td>{{ solution.speed }}</td>
<td>{{ solution.low_risk }}</td>
<td>{{ solution.success_rate }}%</td>
<td><strong>{{ solution.decision_score|floatformat:1 }}</strong></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if top_solution %}
<div class="analysis-card action-card">
<div class="section-kicker">Action Plan</div>
<h2>4. Action Plan 5 Hari: {{ top_solution.title }}</h2>
<div class="timeline">
{% for step in top_solution.action_steps.all %}
<article>
<span>Hari {{ step.day_index }}</span>
<div>
<h3>{{ step.title }}</h3>
<p>{{ step.task }}</p>
</div>
</article>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
<div class="mt-5 d-flex gap-3 flex-wrap">
<a href="{% url 'home' %}#analisis" class="btn btn-optema">Analisis Kasus Baru</a>
<a href="{% url 'case_list' %}" class="btn btn-outline-optema">Kembali ke Riwayat Case</a>
</div>
</section>
</main>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
{% include "core/_nav.html" %}
<main class="page-shell">
<section class="container py-5">
<div class="d-flex justify-content-between align-items-end mb-4 flex-wrap gap-3">
<div>
<p class="eyebrow mb-2">Case Library</p>
<h1 class="page-title">Daftar Kasus OPTEMA</h1>
<p class="section-copy mb-0">Histori masalah yang sudah diproses menjadi keputusan dan action plan.</p>
</div>
<a href="{% url 'home' %}#analisis" class="btn btn-optema">Tambah Kasus Baru</a>
</div>
<div class="row g-4">
{% for case in cases %}
<div class="col-md-6 col-xl-4">
<a class="case-card h-100" href="{{ case.get_absolute_url }}">
<span class="case-area">{{ case.get_business_area_display }} · Urgensi {{ case.urgency }}/5</span>
<h2>{{ case.title }}</h2>
<p>{{ case.description|truncatechars:130 }}</p>
<div class="case-meta"><span>Prioritas {{ case.priority_score }}/100</span><span>{{ case.created_at|date:"d M Y" }}</span></div>
</a>
</div>
{% empty %}
<div class="col-12">
<div class="empty-state">
<h2>Case library masih kosong.</h2>
<p>Buat analisis pertama untuk mulai membangun histori keputusan.</p>
<a href="{% url 'home' %}#analisis" class="btn btn-optema">Mulai Analisis</a>
</div>
</div>
{% endfor %}
</div>
</section>
</main>
{% endblock %}

View File

@ -1,145 +1,201 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ project_name }}{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
}
</style>
{% endblock %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
{% include "core/_nav.html" %}
<main>
<div class="card">
<h1>Analyzing your requirements and generating your app…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
<section class="hero-section mvp-hero position-relative overflow-hidden">
<div class="shape shape-one" aria-hidden="true"></div>
<div class="shape shape-two" aria-hidden="true"></div>
<div class="container position-relative">
<div class="row align-items-center g-5 py-5">
<div class="col-lg-7">
<p class="eyebrow mb-3">Mesin Pemecah Masalah Universal & Decision Intelligence Platform</p>
<h1 class="display-heading mb-4">💡 OPTEMA AI</h1>
<h2 class="hero-subtitle mb-3">Optimal, Effective, Efficient Management Assistant</h2>
<p class="hero-copy mb-4">MVP single-user tanpa login: masukkan masalah pemerintah/publik, bisnis, keuangan, karier, logistik, teknologi, atau pendidikan; atur urgensi, lalu dapatkan simulasi Problem Detection, Root Cause, Decision Scoring, dan Action Plan yang tersimpan otomatis.</p>
<div class="d-flex flex-wrap gap-3">
<a href="#analisis" class="btn btn-optema btn-lg">Jalankan Analisis AI</a>
<a href="{% url 'case_list' %}" class="btn btn-outline-optema btn-lg">Lihat Riwayat Case</a>
</div>
<div class="trust-strip mt-4" aria-label="Modul MVP">
<span>Problem Detection</span>
<span>Root Cause</span>
<span>Decision Score</span>
</div>
</div>
<div class="col-lg-5">
<div class="decision-panel glass-card">
<div class="panel-topline d-flex justify-content-between align-items-center mb-4">
<span class="status-dot"><i></i> Simulasi MVP</span>
<span class="score-pill">Prioritas 95/100</span>
</div>
<div class="mini-chart mb-4" aria-label="Preview root cause score">
<div class="bar-95"><span>Keterbatasan Dana</span><b>96%</b></div>
<div class="bar-88"><span>Target 3 Tahun</span><b>92%</b></div>
<div class="bar-85"><span>Biaya Hidup</span><b>88%</b></div>
<div class="bar-70"><span>Bahasa & Admission</span><b>76%</b></div>
</div>
<div class="solution-preview">
<p class="small-label">Solusi Teratas</p>
<h2>Prioritaskan Jepang + Beasiswa</h2>
<p>Decision Score 83.8 · Sukses Rate 84%</p>
</div>
</div>
</div>
</div>
</div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
<p class="runtime">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p>
</div>
</section>
<section class="section-pad" id="analisis">
<div class="container">
<div class="section-kicker">MVP Workspace</div>
<h2 class="section-title mb-4">Input kiri, output analisis kanan — seperti prototype Streamlit.</h2>
{% if messages %}
<div class="mb-4">
{% for message in messages %}
<div class="alert alert-{{ message.tags|default:'info' }}" role="alert">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<div class="row g-4 align-items-start mvp-workspace">
<div class="col-lg-4">
<div class="form-shell input-panel sticky-lg-top">
<div class="panel-heading">
<span class="panel-icon">📥</span>
<div>
<h2>Input Masalah</h2>
<p>Masukkan masalah, target, atau hambatan Anda. Contoh: BUMN rugi karena biaya tinggi, omzet toko turun, atau kuliah Singapura/Jepang dengan budget terbatas.</p>
</div>
</div>
<form method="post" novalidate>
{% csrf_token %}
{{ form.non_field_errors }}
<div class="mb-4">
<label class="form-label" for="{{ form.description.id_for_label }}">{{ form.description.label }}</label>
{{ form.description }}
{% for error in form.description.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center gap-3">
<label class="form-label mb-0" for="{{ form.urgency.id_for_label }}">{{ form.urgency.label }}</label>
<output class="urgency-value" id="urgency-output">{{ form.urgency.value|default:3 }}</output>
</div>
{{ form.urgency }}
<div class="range-labels"><span>1</span><span>3</span><span>5</span></div>
{% for error in form.urgency.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
<button type="submit" class="btn btn-optema btn-lg w-100">Jalankan Analisis AI</button>
<p class="form-note mb-0 mt-3">Hasil akan disimpan otomatis ke Riwayat Case.</p>
</form>
</div>
</div>
<div class="col-lg-8">
<div class="analysis-card output-panel">
<div class="output-success mb-4">✅ Analisis Berhasil Disimulasikan!</div>
<section class="output-section">
<h2>1. Problem Detection</h2>
<div class="metric-grid">
<div class="metric-card"><span>Prioritas Skor</span><strong>95/100</strong></div>
<div class="metric-card"><span>Dampak / Constraint</span><strong>Budget ketat</strong></div>
</div>
</section>
<section class="output-section">
<h2>2. Root Cause (Akar Masalah)</h2>
<div class="cause-list compact-cause-list">
<article>
<div class="d-flex justify-content-between gap-3"><strong>Keterbatasan Dana</strong><span>96%</span></div>
<div class="progress"><div class="progress-bar score-width-95"></div></div>
</article>
<article>
<div class="d-flex justify-content-between gap-3"><strong>Target Lulus 3 Tahun</strong><span>92%</span></div>
<div class="progress"><div class="progress-bar score-width-88"></div></div>
</article>
<article>
<div class="d-flex justify-content-between gap-3"><strong>Biaya Hidup Negara</strong><span>88%</span></div>
<div class="progress"><div class="progress-bar score-width-85"></div></div>
</article>
<article>
<div class="d-flex justify-content-between gap-3"><strong>Bahasa & Admission</strong><span>76%</span></div>
<div class="progress"><div class="progress-bar score-width-70"></div></div>
</article>
</div>
</section>
<section class="output-section mb-0">
<h2>3. Rekomendasi Solusi & Prediksi Sukses</h2>
<p class="formula-note">Decision Score = Impact×0.4 + Efficiency×0.3 + Speed×0.2 + LowRisk×0.1</p>
<div class="table-responsive">
<table class="table align-middle solution-table preview-table">
<thead>
<tr>
<th>Solusi</th>
<th>Impact</th>
<th>Efficiency</th>
<th>Speed</th>
<th>Low Risk</th>
<th>Sukses Rate</th>
<th>Decision Score</th>
</tr>
</thead>
<tbody>
<tr><td><strong>Prioritaskan Jepang + Beasiswa/Part-time Legal</strong></td><td>88</td><td>86</td><td>72</td><td>78</td><td>84%</td><td><strong>83.8</strong></td></tr>
<tr><td><strong>Pathway Hemat: Lokal lalu Transfer</strong></td><td>76</td><td>88</td><td>68</td><td>86</td><td>80%</td><td><strong>78.4</strong></td></tr>
<tr><td><strong>Singapura Jika Scholarship Besar</strong></td><td>82</td><td>58</td><td>86</td><td>55</td><td>66%</td><td><strong>72.9</strong></td></tr>
</tbody>
</table>
</div>
</section>
</div>
</div>
</div>
</div>
</section>
<section class="section-pad pt-0">
<div class="container">
<div class="d-flex justify-content-between align-items-end mb-4 gap-3 flex-wrap">
<div>
<div class="section-kicker">Histori Keputusan</div>
<h2 class="section-title mb-0">Kasus terbaru</h2>
</div>
<a href="{% url 'case_list' %}" class="link-optema">Lihat semua kasus →</a>
</div>
<div class="row g-4">
{% for case in recent_cases %}
<div class="col-md-6 col-xl-3">
<a class="case-card h-100" href="{{ case.get_absolute_url }}">
<span class="case-area">{{ case.get_business_area_display }}</span>
<h3>{{ case.title }}</h3>
<p>{{ case.description|truncatechars:88 }}</p>
<div class="case-meta"><span>Prioritas {{ case.priority_score }}/100</span><span>{{ case.financial_impact }}</span></div>
</a>
</div>
{% empty %}
<div class="col-12">
<div class="empty-state">
<h3>Belum ada kasus tersimpan.</h3>
<p>Gunakan form di atas untuk membuat analisis OPTEMA AI pertama Anda.</p>
</div>
</div>
{% endfor %}
</div>
</div>
</section>
</main>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
<footer class="site-footer">
<div class="container d-flex flex-wrap justify-content-between gap-2">
<span>OPTEMA AI — Optimal, Effective, Efficient Management Assistant</span>
<span>Decision Score = I×0.40 + E×0.30 + S×0.20 + LR×0.10</span>
</div>
</footer>
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,142 @@
{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
{% include "core/_nav.html" %}
<main class="page-shell">
<section class="hero-section position-relative overflow-hidden">
<div class="shape shape-one" aria-hidden="true"></div>
<div class="shape shape-two" aria-hidden="true"></div>
<div class="container position-relative py-5">
<div class="row align-items-center g-5">
<div class="col-lg-7">
<p class="eyebrow mb-3">OPTEMA AI Web Intelligence</p>
<h1 class="display-heading mb-4">⚡ Analisis Masalah Berbasis Data Internet</h1>
<p class="hero-copy mb-4">Masukkan topik dan URL sumber publik. OPTEMA akan mengambil konten halaman, menampilkan data yang dibaca, lalu menyusun Problem Detection, Root Cause, Solusi, Risiko, Action Plan, dan KPI jika OpenAI API Key tersedia.</p>
<div class="trust-strip mt-4" aria-label="Output Web Intelligence">
<span>Scrape URL</span>
<span>Problem Detection</span>
<span>KPI</span>
</div>
</div>
<div class="col-lg-5">
<div class="decision-panel glass-card">
<div class="panel-topline d-flex justify-content-between align-items-center mb-4">
<span class="status-dot"><i></i> Live URL Analysis</span>
<span class="score-pill">6 Output</span>
</div>
<div class="solution-preview">
<p class="small-label">Format Analisis</p>
<h2>Internet Data → Insight → Action</h2>
<p class="mb-0">Cocok untuk artikel berita, laporan, pengumuman, atau sumber data publik berbasis HTML.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section-pad pt-4">
<div class="container">
{% if messages %}
<div class="mb-4">
{% for message in messages %}
<div class="alert alert-{{ message.tags|default:'info' }}" role="alert">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<div class="row g-4 align-items-start">
<div class="col-lg-4">
<div class="form-shell input-panel sticky-lg-top">
<div class="panel-heading">
<span class="panel-icon">🌐</span>
<div>
<h2>Input Sumber Data</h2>
<p>API key tidak disimpan. Jika dikosongkan, halaman tetap menampilkan hasil scraping data internet.</p>
</div>
</div>
<form method="post" novalidate>
{% csrf_token %}
{{ form.non_field_errors }}
<div class="mb-4">
<label class="form-label" for="{{ form.query.id_for_label }}">{{ form.query.label }}</label>
{{ form.query }}
{% for error in form.query.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
<div class="mb-4">
<label class="form-label" for="{{ form.url.id_for_label }}">{{ form.url.label }}</label>
{{ form.url }}
{% for error in form.url.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
<div class="mb-4">
<label class="form-label" for="{{ form.api_key.id_for_label }}">{{ form.api_key.label }}</label>
{{ form.api_key }}
<div class="form-text">Gunakan API key pribadi Anda hanya untuk sesi analisis ini.</div>
{% for error in form.api_key.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
<button type="submit" class="btn btn-optema btn-lg w-100">Analisis</button>
<p class="form-note mb-0 mt-3">Sumber lokal seperti localhost diblokir untuk keamanan.</p>
</form>
</div>
</div>
<div class="col-lg-8">
{% if source_data %}
<div class="analysis-card output-panel mb-4">
<div class="output-success mb-4">✅ Data Internet Berhasil Dibaca</div>
<section class="output-section">
<h2>Data Internet</h2>
<div class="metric-grid mb-4">
<div class="metric-card"><span>Domain</span><strong>{{ source_data.domain }}</strong></div>
<div class="metric-card"><span>Paragraf</span><strong>{{ source_data.paragraph_count }}</strong></div>
</div>
<p class="small-label mb-2">Judul</p>
<div class="alert alert-light border" role="status">{{ source_data.title }}</div>
<label class="form-label" for="content-preview">Konten terbaca</label>
<textarea id="content-preview" class="form-control problem-textarea" rows="12" readonly>{{ source_data.content_preview }}</textarea>
<p class="form-note mt-3 mb-0">Preview dibatasi 5.000 karakter; analisis AI memakai maksimal 10.000 karakter pertama.</p>
</section>
</div>
{% else %}
<div class="empty-state mb-4">
<h2>Belum ada data internet.</h2>
<p>Masukkan URL sumber data publik untuk mulai membaca konten halaman.</p>
</div>
{% endif %}
{% if ai_result %}
<div class="analysis-card output-panel">
<div class="output-success mb-4">🤖 OPTEMA AI</div>
<section class="output-section mb-0">
<h2>Hasil Analisis</h2>
<div class="alert alert-light border mb-0">{{ ai_result|linebreaksbr }}</div>
</section>
</div>
{% elif analysis_error %}
<div class="analysis-card output-panel">
<section class="output-section mb-0">
<h2>Status Analisis AI</h2>
<div class="alert alert-warning mb-0" role="alert">{{ analysis_error }}</div>
</section>
</div>
{% endif %}
</div>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="container d-flex flex-wrap justify-content-between gap-2">
<span>OPTEMA AI — Web Intelligence</span>
<span>Problem Detection · Root Cause · Solusi · Risiko · Action Plan · KPI</span>
</div>
</footer>
{% endblock %}

View File

@ -1,7 +1,10 @@
from django.urls import path
from .views import home
from .views import case_detail, case_list, home, web_intelligence
urlpatterns = [
path("", home, name="home"),
path("cases/", case_list, name="case_list"),
path("web-intelligence/", web_intelligence, name="web_intelligence"),
path("cases/<int:pk>/", case_detail, name="case_detail"),
]

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
Django==5.2.7
mysqlclient==2.2.7
python-dotenv==1.1.1
requests==2.32.3
beautifulsoup4==4.12.3

View File

@ -1,4 +1,429 @@
/* Custom styles for the application */
body {
font-family: system-ui, -apple-system, sans-serif;
/* OPTEMA AI custom brand system */
:root {
--optema-ink: #14213d;
--optema-muted: #627084;
--optema-primary: #0f8b8d;
--optema-primary-dark: #0b6768;
--optema-secondary: #f6ae2d;
--optema-accent: #f26419;
--optema-mint: #dff8f5;
--optema-sand: #fff6e4;
--optema-cloud: #f7fafc;
--optema-white: #ffffff;
--optema-line: #dce8ee;
--optema-shadow: 0 24px 70px rgba(20, 33, 61, 0.14);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
color: var(--optema-ink);
background:
radial-gradient(circle at 8% 4%, rgba(15, 139, 141, 0.14), transparent 28rem),
radial-gradient(circle at 95% 8%, rgba(246, 174, 45, 0.16), transparent 24rem),
var(--optema-cloud);
}
h1, h2, h3, .navbar-brand {
font-family: 'Plus Jakarta Sans', 'Inter', sans-serif;
letter-spacing: -0.04em;
}
a { color: inherit; }
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible {
outline: 3px solid rgba(246, 174, 45, 0.65) !important;
outline-offset: 3px;
}
.optema-nav {
position: sticky;
top: 0;
z-index: 20;
background: rgba(247, 250, 252, 0.82);
backdrop-filter: blur(18px);
border-bottom: 1px solid rgba(220, 232, 238, 0.82);
}
.brand-mark { font-weight: 800; color: var(--optema-ink); }
.brand-icon {
display: inline-grid;
place-items: center;
width: 2.15rem; height: 2.15rem;
margin-right: .45rem;
border-radius: 14px;
color: white;
background: linear-gradient(135deg, var(--optema-primary), var(--optema-accent));
box-shadow: 0 12px 24px rgba(15, 139, 141, 0.24);
}
.nav-link { font-weight: 700; color: var(--optema-muted); }
.nav-link:hover, .nav-link:focus { color: var(--optema-primary); }
.admin-link {
border: 1px solid var(--optema-line);
border-radius: 999px;
padding-inline: 1rem !important;
background: rgba(255,255,255,.7);
}
.hero-section {
min-height: 78vh;
display: flex;
align-items: center;
background:
linear-gradient(145deg, rgba(223,248,245,.88), rgba(255,246,228,.72) 55%, rgba(255,255,255,.9)),
var(--optema-cloud);
}
.shape {
position: absolute; border-radius: 999px; filter: blur(.2px); opacity: .82;
}
.shape-one {
width: 15rem; height: 15rem; right: 6%; top: 12%;
background: linear-gradient(135deg, rgba(15,139,141,.2), rgba(246,174,45,.38));
}
.shape-two {
width: 9rem; height: 9rem; left: 4%; bottom: 8%;
background: linear-gradient(135deg, rgba(242,100,25,.22), rgba(15,139,141,.2));
}
.eyebrow, .section-kicker {
color: var(--optema-primary-dark);
text-transform: uppercase;
letter-spacing: .12em;
font-size: .78rem;
font-weight: 800;
}
.display-heading {
font-size: clamp(2.7rem, 6vw, 5.9rem);
line-height: .95;
font-weight: 800;
}
.hero-copy, .section-copy {
color: var(--optema-muted);
font-size: clamp(1rem, 1vw + .76rem, 1.2rem);
line-height: 1.75;
}
.btn-optema {
border: 0;
color: #fff;
font-weight: 800;
border-radius: 999px;
padding: .9rem 1.35rem;
background: linear-gradient(135deg, var(--optema-primary), var(--optema-primary-dark));
box-shadow: 0 14px 35px rgba(15, 139, 141, .26);
}
.btn-optema:hover, .btn-optema:focus { color:#fff; transform: translateY(-1px); box-shadow: 0 18px 44px rgba(15,139,141,.32); }
.btn-outline-optema {
border: 1px solid rgba(15,139,141,.3);
color: var(--optema-primary-dark);
background: rgba(255,255,255,.68);
font-weight: 800;
border-radius: 999px;
padding: .9rem 1.35rem;
}
.btn-outline-optema:hover, .btn-outline-optema:focus { background: var(--optema-mint); color: var(--optema-primary-dark); }
.trust-strip { display: flex; flex-wrap: wrap; gap: .65rem; }
.trust-strip span, .score-pill, .status-dot {
display: inline-flex; align-items: center; gap: .45rem;
border: 1px solid rgba(15,139,141,.16);
background: rgba(255,255,255,.72);
border-radius: 999px;
padding: .55rem .8rem;
color: var(--optema-muted);
font-weight: 800;
font-size: .86rem;
}
.status-dot i { width: .55rem; height: .55rem; border-radius: 999px; background: #23c483; box-shadow: 0 0 0 6px rgba(35,196,131,.15); }
.glass-card, .form-shell, .analysis-card, .empty-state, .case-card, .metric-card {
border: 1px solid rgba(220, 232, 238, .9);
background: rgba(255,255,255,.82);
backdrop-filter: blur(20px);
box-shadow: var(--optema-shadow);
border-radius: 32px;
}
.decision-panel { padding: clamp(1.35rem, 3vw, 2rem); }
.mini-chart { display: grid; gap: 1rem; }
.mini-chart div {
--bar: 70%;
position: relative; overflow: hidden;
display: flex; justify-content: space-between;
padding: .9rem 1rem;
border-radius: 18px;
background: #eef6f7;
font-weight: 800;
}
.mini-chart div::before {
content: ''; position: absolute; inset: 0 auto 0 0; width: var(--bar);
background: linear-gradient(90deg, rgba(15,139,141,.24), rgba(246,174,45,.28));
}
.mini-chart span, .mini-chart b { position: relative; z-index: 1; }
.bar-95 { --bar: 95% !important; } .bar-88 { --bar: 88% !important; } .bar-82 { --bar: 82% !important; }
.solution-preview {
padding: 1.2rem; border-radius: 24px;
background: linear-gradient(135deg, var(--optema-ink), #183b56); color: white;
}
.solution-preview h2 { font-size: 1.35rem; }
.solution-preview p { color: rgba(255,255,255,.74); margin-bottom: 0; }
.small-label { color: var(--optema-secondary) !important; text-transform: uppercase; font-weight: 800; letter-spacing: .12em; font-size: .75rem; }
.section-pad { padding: clamp(4rem, 8vw, 7rem) 0; }
.section-title, .page-title { font-size: clamp(2rem, 3.5vw, 3.6rem); font-weight: 800; line-height: 1.02; }
.form-shell { padding: clamp(1.25rem, 3vw, 2rem); }
.form-control, .form-select { border: 1px solid var(--optema-line); border-radius: 18px; padding: .9rem 1rem; }
.form-control:focus, .form-select:focus { border-color: var(--optema-primary); box-shadow: 0 0 0 .22rem rgba(15,139,141,.13); }
.form-label { font-weight: 800; color: var(--optema-ink); }
.form-text { color: var(--optema-muted); }
.feature-list { display: grid; gap: .85rem; margin-top: 1.5rem; }
.feature-list div { display: flex; gap: .8rem; align-items: center; color: var(--optema-muted); }
.feature-list strong {
display:grid; place-items:center; width:2rem; height:2rem; border-radius:12px;
background: var(--optema-sand); color: var(--optema-accent);
}
.case-card {
display: flex; flex-direction: column; gap: .9rem;
padding: 1.35rem; text-decoration: none; transition: .2s ease;
}
.case-card:hover, .case-card:focus { transform: translateY(-4px); border-color: rgba(15,139,141,.35); color: var(--optema-ink); }
.case-card h2, .case-card h3 { font-size: 1.15rem; font-weight: 800; margin: 0; }
.case-card p { color: var(--optema-muted); margin: 0; line-height: 1.65; }
.case-area { color: var(--optema-primary-dark); font-size: .78rem; font-weight: 900; text-transform: uppercase; letter-spacing: .1em; }
.case-meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: .7rem; color: var(--optema-muted); font-weight: 800; font-size: .86rem; margin-top: auto; }
.empty-state { padding: 2rem; text-align: center; }
.empty-state p { color: var(--optema-muted); }
.link-optema { color: var(--optema-primary-dark); font-weight: 900; text-decoration: none; }
.link-optema:hover { color: var(--optema-accent); }
.site-footer {
padding: 1.5rem 0; color: var(--optema-muted);
border-top: 1px solid var(--optema-line); background: rgba(255,255,255,.68);
}
.page-shell { min-height: 100vh; background: linear-gradient(180deg, rgba(223,248,245,.55), transparent 24rem); }
.detail-hero { padding: clamp(3rem, 6vw, 5rem) 0; background: linear-gradient(135deg, rgba(223,248,245,.9), rgba(255,246,228,.85)); }
.metric-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 1rem; }
.metric-card { padding: 1.25rem; }
.metric-card span { color: var(--optema-muted); font-weight: 800; display:block; }
.metric-card strong { font-family:'Plus Jakarta Sans'; font-size: 1.75rem; }
.analysis-card { padding: clamp(1.2rem, 2.6vw, 2rem); }
.analysis-card h2 { font-weight: 800; margin-bottom: 1.2rem; }
.cause-list { display: grid; gap: 1.2rem; }
.cause-list article p { color: var(--optema-muted); margin: .6rem 0 0; line-height: 1.6; }
.progress { height: .65rem; background: #e6f2f3; border-radius: 999px; margin-top: .55rem; }
.progress-bar { background: linear-gradient(90deg, var(--optema-primary), var(--optema-secondary)); }
.solution-table { --bs-table-bg: transparent; }
.solution-table th { color: var(--optema-muted); text-transform: uppercase; letter-spacing: .08em; font-size: .78rem; }
.solution-table td { padding: 1rem .75rem; }
.solution-table p { color: var(--optema-muted); margin: .25rem 0; }
.solution-table small { color: var(--optema-primary-dark); font-weight: 800; }
.rank-badge { display: inline-flex; border-radius: 999px; background: var(--optema-sand); color: var(--optema-accent); padding: .35rem .6rem; font-weight: 900; }
.action-card { background: linear-gradient(145deg, rgba(255,255,255,.92), rgba(223,248,245,.76)); }
.timeline { display: grid; gap: 1rem; }
.timeline article { display: grid; grid-template-columns: 5rem 1fr; gap: 1rem; align-items: start; }
.timeline article > span {
display: inline-flex; justify-content:center; padding: .45rem .65rem; border-radius: 999px;
background: var(--optema-ink); color: white; font-weight: 900; font-size: .82rem;
}
.timeline h3 { font-size: 1.05rem; font-weight: 800; margin-bottom: .2rem; }
.timeline p { color: var(--optema-muted); margin: 0; line-height: 1.6; }
.score-width-0 { width: 0%; }
.score-width-1 { width: 1%; }
.score-width-2 { width: 2%; }
.score-width-3 { width: 3%; }
.score-width-4 { width: 4%; }
.score-width-5 { width: 5%; }
.score-width-6 { width: 6%; }
.score-width-7 { width: 7%; }
.score-width-8 { width: 8%; }
.score-width-9 { width: 9%; }
.score-width-10 { width: 10%; }
.score-width-11 { width: 11%; }
.score-width-12 { width: 12%; }
.score-width-13 { width: 13%; }
.score-width-14 { width: 14%; }
.score-width-15 { width: 15%; }
.score-width-16 { width: 16%; }
.score-width-17 { width: 17%; }
.score-width-18 { width: 18%; }
.score-width-19 { width: 19%; }
.score-width-20 { width: 20%; }
.score-width-21 { width: 21%; }
.score-width-22 { width: 22%; }
.score-width-23 { width: 23%; }
.score-width-24 { width: 24%; }
.score-width-25 { width: 25%; }
.score-width-26 { width: 26%; }
.score-width-27 { width: 27%; }
.score-width-28 { width: 28%; }
.score-width-29 { width: 29%; }
.score-width-30 { width: 30%; }
.score-width-31 { width: 31%; }
.score-width-32 { width: 32%; }
.score-width-33 { width: 33%; }
.score-width-34 { width: 34%; }
.score-width-35 { width: 35%; }
.score-width-36 { width: 36%; }
.score-width-37 { width: 37%; }
.score-width-38 { width: 38%; }
.score-width-39 { width: 39%; }
.score-width-40 { width: 40%; }
.score-width-41 { width: 41%; }
.score-width-42 { width: 42%; }
.score-width-43 { width: 43%; }
.score-width-44 { width: 44%; }
.score-width-45 { width: 45%; }
.score-width-46 { width: 46%; }
.score-width-47 { width: 47%; }
.score-width-48 { width: 48%; }
.score-width-49 { width: 49%; }
.score-width-50 { width: 50%; }
.score-width-51 { width: 51%; }
.score-width-52 { width: 52%; }
.score-width-53 { width: 53%; }
.score-width-54 { width: 54%; }
.score-width-55 { width: 55%; }
.score-width-56 { width: 56%; }
.score-width-57 { width: 57%; }
.score-width-58 { width: 58%; }
.score-width-59 { width: 59%; }
.score-width-60 { width: 60%; }
.score-width-61 { width: 61%; }
.score-width-62 { width: 62%; }
.score-width-63 { width: 63%; }
.score-width-64 { width: 64%; }
.score-width-65 { width: 65%; }
.score-width-66 { width: 66%; }
.score-width-67 { width: 67%; }
.score-width-68 { width: 68%; }
.score-width-69 { width: 69%; }
.score-width-70 { width: 70%; }
.score-width-71 { width: 71%; }
.score-width-72 { width: 72%; }
.score-width-73 { width: 73%; }
.score-width-74 { width: 74%; }
.score-width-75 { width: 75%; }
.score-width-76 { width: 76%; }
.score-width-77 { width: 77%; }
.score-width-78 { width: 78%; }
.score-width-79 { width: 79%; }
.score-width-80 { width: 80%; }
.score-width-81 { width: 81%; }
.score-width-82 { width: 82%; }
.score-width-83 { width: 83%; }
.score-width-84 { width: 84%; }
.score-width-85 { width: 85%; }
.score-width-86 { width: 86%; }
.score-width-87 { width: 87%; }
.score-width-88 { width: 88%; }
.score-width-89 { width: 89%; }
.score-width-90 { width: 90%; }
.score-width-91 { width: 91%; }
.score-width-92 { width: 92%; }
.score-width-93 { width: 93%; }
.score-width-94 { width: 94%; }
.score-width-95 { width: 95%; }
.score-width-96 { width: 96%; }
.score-width-97 { width: 97%; }
.score-width-98 { width: 98%; }
.score-width-99 { width: 99%; }
.score-width-100 { width: 100%; }
@media (max-width: 767px) {
.metric-grid { grid-template-columns: 1fr; }
.timeline article { grid-template-columns: 1fr; }
.hero-section { min-height: auto; }
}
/* Streamlit-inspired MVP workspace */
.hero-subtitle {
font-family: 'Plus Jakarta Sans', 'Inter', sans-serif;
font-size: clamp(1.25rem, 1.2vw + 1rem, 2rem);
font-weight: 800;
color: var(--optema-primary-dark);
letter-spacing: -0.03em;
}
.mvp-hero .display-heading { letter-spacing: -0.07em; }
.mvp-workspace .sticky-lg-top { top: 6rem; }
.panel-heading {
display: flex;
gap: 1rem;
align-items: flex-start;
margin-bottom: 1.4rem;
}
.panel-heading h2 {
font-size: 1.45rem;
margin: 0 0 .25rem;
font-weight: 900;
}
.panel-heading p,
.form-note,
.formula-note,
.detail-status-card p {
color: var(--optema-muted);
line-height: 1.65;
}
.panel-icon {
display: inline-grid;
place-items: center;
width: 3rem;
height: 3rem;
border-radius: 18px;
background: linear-gradient(135deg, var(--optema-mint), var(--optema-sand));
box-shadow: 0 16px 28px rgba(15, 139, 141, .12);
font-size: 1.35rem;
}
.problem-textarea {
min-height: 15rem;
resize: vertical;
border-radius: 22px;
border-color: rgba(15,139,141,.18);
background: rgba(255,255,255,.82);
}
.problem-textarea:focus {
border-color: var(--optema-primary);
box-shadow: 0 0 0 .25rem rgba(15,139,141,.12);
}
.urgency-slider { accent-color: var(--optema-primary); }
.urgency-value {
min-width: 2.4rem;
text-align: center;
padding: .35rem .7rem;
border-radius: 999px;
color: #fff;
background: var(--optema-ink);
font-weight: 900;
}
.range-labels {
display: flex;
justify-content: space-between;
color: var(--optema-muted);
font-size: .82rem;
font-weight: 800;
margin-top: -.3rem;
}
.output-panel { overflow: hidden; }
.output-success {
display: inline-flex;
align-items: center;
gap: .5rem;
color: #087447;
background: rgba(35, 196, 131, .12);
border: 1px solid rgba(35, 196, 131, .22);
border-radius: 999px;
padding: .7rem 1rem;
font-weight: 900;
}
.output-section {
padding: 1.25rem 0;
border-top: 1px solid var(--optema-line);
}
.output-section:first-of-type { border-top: 0; padding-top: 0; }
.output-section h2 { font-size: clamp(1.25rem, 1vw + 1rem, 1.8rem); }
.compact-cause-list { gap: .85rem; }
.compact-cause-list article p { display: none; }
.preview-table th,
.preview-table td { white-space: nowrap; }
.detail-status-card { padding: 1.35rem; }
.bar-85 { --bar: 85% !important; }
.bar-70 { --bar: 70% !important; }
@media (max-width: 991px) {
.mvp-workspace .sticky-lg-top { position: static; }
}

View File

@ -1,21 +1,429 @@
/* OPTEMA AI custom brand system */
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
--optema-ink: #14213d;
--optema-muted: #627084;
--optema-primary: #0f8b8d;
--optema-primary-dark: #0b6768;
--optema-secondary: #f6ae2d;
--optema-accent: #f26419;
--optema-mint: #dff8f5;
--optema-sand: #fff6e4;
--optema-cloud: #f7fafc;
--optema-white: #ffffff;
--optema-line: #dce8ee;
--optema-shadow: 0 24px 70px rgba(20, 33, 61, 0.14);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
margin: 0;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
color: var(--optema-ink);
background:
radial-gradient(circle at 8% 4%, rgba(15, 139, 141, 0.14), transparent 28rem),
radial-gradient(circle at 95% 8%, rgba(246, 174, 45, 0.16), transparent 24rem),
var(--optema-cloud);
}
h1, h2, h3, .navbar-brand {
font-family: 'Plus Jakarta Sans', 'Inter', sans-serif;
letter-spacing: -0.04em;
}
a { color: inherit; }
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible {
outline: 3px solid rgba(246, 174, 45, 0.65) !important;
outline-offset: 3px;
}
.optema-nav {
position: sticky;
top: 0;
z-index: 20;
background: rgba(247, 250, 252, 0.82);
backdrop-filter: blur(18px);
border-bottom: 1px solid rgba(220, 232, 238, 0.82);
}
.brand-mark { font-weight: 800; color: var(--optema-ink); }
.brand-icon {
display: inline-grid;
place-items: center;
width: 2.15rem; height: 2.15rem;
margin-right: .45rem;
border-radius: 14px;
color: white;
background: linear-gradient(135deg, var(--optema-primary), var(--optema-accent));
box-shadow: 0 12px 24px rgba(15, 139, 141, 0.24);
}
.nav-link { font-weight: 700; color: var(--optema-muted); }
.nav-link:hover, .nav-link:focus { color: var(--optema-primary); }
.admin-link {
border: 1px solid var(--optema-line);
border-radius: 999px;
padding-inline: 1rem !important;
background: rgba(255,255,255,.7);
}
.hero-section {
min-height: 78vh;
display: flex;
align-items: center;
background:
linear-gradient(145deg, rgba(223,248,245,.88), rgba(255,246,228,.72) 55%, rgba(255,255,255,.9)),
var(--optema-cloud);
}
.shape {
position: absolute; border-radius: 999px; filter: blur(.2px); opacity: .82;
}
.shape-one {
width: 15rem; height: 15rem; right: 6%; top: 12%;
background: linear-gradient(135deg, rgba(15,139,141,.2), rgba(246,174,45,.38));
}
.shape-two {
width: 9rem; height: 9rem; left: 4%; bottom: 8%;
background: linear-gradient(135deg, rgba(242,100,25,.22), rgba(15,139,141,.2));
}
.eyebrow, .section-kicker {
color: var(--optema-primary-dark);
text-transform: uppercase;
letter-spacing: .12em;
font-size: .78rem;
font-weight: 800;
}
.display-heading {
font-size: clamp(2.7rem, 6vw, 5.9rem);
line-height: .95;
font-weight: 800;
}
.hero-copy, .section-copy {
color: var(--optema-muted);
font-size: clamp(1rem, 1vw + .76rem, 1.2rem);
line-height: 1.75;
}
.btn-optema {
border: 0;
color: #fff;
font-weight: 800;
border-radius: 999px;
padding: .9rem 1.35rem;
background: linear-gradient(135deg, var(--optema-primary), var(--optema-primary-dark));
box-shadow: 0 14px 35px rgba(15, 139, 141, .26);
}
.btn-optema:hover, .btn-optema:focus { color:#fff; transform: translateY(-1px); box-shadow: 0 18px 44px rgba(15,139,141,.32); }
.btn-outline-optema {
border: 1px solid rgba(15,139,141,.3);
color: var(--optema-primary-dark);
background: rgba(255,255,255,.68);
font-weight: 800;
border-radius: 999px;
padding: .9rem 1.35rem;
}
.btn-outline-optema:hover, .btn-outline-optema:focus { background: var(--optema-mint); color: var(--optema-primary-dark); }
.trust-strip { display: flex; flex-wrap: wrap; gap: .65rem; }
.trust-strip span, .score-pill, .status-dot {
display: inline-flex; align-items: center; gap: .45rem;
border: 1px solid rgba(15,139,141,.16);
background: rgba(255,255,255,.72);
border-radius: 999px;
padding: .55rem .8rem;
color: var(--optema-muted);
font-weight: 800;
font-size: .86rem;
}
.status-dot i { width: .55rem; height: .55rem; border-radius: 999px; background: #23c483; box-shadow: 0 0 0 6px rgba(35,196,131,.15); }
.glass-card, .form-shell, .analysis-card, .empty-state, .case-card, .metric-card {
border: 1px solid rgba(220, 232, 238, .9);
background: rgba(255,255,255,.82);
backdrop-filter: blur(20px);
box-shadow: var(--optema-shadow);
border-radius: 32px;
}
.decision-panel { padding: clamp(1.35rem, 3vw, 2rem); }
.mini-chart { display: grid; gap: 1rem; }
.mini-chart div {
--bar: 70%;
position: relative; overflow: hidden;
display: flex; justify-content: space-between;
padding: .9rem 1rem;
border-radius: 18px;
background: #eef6f7;
font-weight: 800;
}
.mini-chart div::before {
content: ''; position: absolute; inset: 0 auto 0 0; width: var(--bar);
background: linear-gradient(90deg, rgba(15,139,141,.24), rgba(246,174,45,.28));
}
.mini-chart span, .mini-chart b { position: relative; z-index: 1; }
.bar-95 { --bar: 95% !important; } .bar-88 { --bar: 88% !important; } .bar-82 { --bar: 82% !important; }
.solution-preview {
padding: 1.2rem; border-radius: 24px;
background: linear-gradient(135deg, var(--optema-ink), #183b56); color: white;
}
.solution-preview h2 { font-size: 1.35rem; }
.solution-preview p { color: rgba(255,255,255,.74); margin-bottom: 0; }
.small-label { color: var(--optema-secondary) !important; text-transform: uppercase; font-weight: 800; letter-spacing: .12em; font-size: .75rem; }
.section-pad { padding: clamp(4rem, 8vw, 7rem) 0; }
.section-title, .page-title { font-size: clamp(2rem, 3.5vw, 3.6rem); font-weight: 800; line-height: 1.02; }
.form-shell { padding: clamp(1.25rem, 3vw, 2rem); }
.form-control, .form-select { border: 1px solid var(--optema-line); border-radius: 18px; padding: .9rem 1rem; }
.form-control:focus, .form-select:focus { border-color: var(--optema-primary); box-shadow: 0 0 0 .22rem rgba(15,139,141,.13); }
.form-label { font-weight: 800; color: var(--optema-ink); }
.form-text { color: var(--optema-muted); }
.feature-list { display: grid; gap: .85rem; margin-top: 1.5rem; }
.feature-list div { display: flex; gap: .8rem; align-items: center; color: var(--optema-muted); }
.feature-list strong {
display:grid; place-items:center; width:2rem; height:2rem; border-radius:12px;
background: var(--optema-sand); color: var(--optema-accent);
}
.case-card {
display: flex; flex-direction: column; gap: .9rem;
padding: 1.35rem; text-decoration: none; transition: .2s ease;
}
.case-card:hover, .case-card:focus { transform: translateY(-4px); border-color: rgba(15,139,141,.35); color: var(--optema-ink); }
.case-card h2, .case-card h3 { font-size: 1.15rem; font-weight: 800; margin: 0; }
.case-card p { color: var(--optema-muted); margin: 0; line-height: 1.65; }
.case-area { color: var(--optema-primary-dark); font-size: .78rem; font-weight: 900; text-transform: uppercase; letter-spacing: .1em; }
.case-meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: .7rem; color: var(--optema-muted); font-weight: 800; font-size: .86rem; margin-top: auto; }
.empty-state { padding: 2rem; text-align: center; }
.empty-state p { color: var(--optema-muted); }
.link-optema { color: var(--optema-primary-dark); font-weight: 900; text-decoration: none; }
.link-optema:hover { color: var(--optema-accent); }
.site-footer {
padding: 1.5rem 0; color: var(--optema-muted);
border-top: 1px solid var(--optema-line); background: rgba(255,255,255,.68);
}
.page-shell { min-height: 100vh; background: linear-gradient(180deg, rgba(223,248,245,.55), transparent 24rem); }
.detail-hero { padding: clamp(3rem, 6vw, 5rem) 0; background: linear-gradient(135deg, rgba(223,248,245,.9), rgba(255,246,228,.85)); }
.metric-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 1rem; }
.metric-card { padding: 1.25rem; }
.metric-card span { color: var(--optema-muted); font-weight: 800; display:block; }
.metric-card strong { font-family:'Plus Jakarta Sans'; font-size: 1.75rem; }
.analysis-card { padding: clamp(1.2rem, 2.6vw, 2rem); }
.analysis-card h2 { font-weight: 800; margin-bottom: 1.2rem; }
.cause-list { display: grid; gap: 1.2rem; }
.cause-list article p { color: var(--optema-muted); margin: .6rem 0 0; line-height: 1.6; }
.progress { height: .65rem; background: #e6f2f3; border-radius: 999px; margin-top: .55rem; }
.progress-bar { background: linear-gradient(90deg, var(--optema-primary), var(--optema-secondary)); }
.solution-table { --bs-table-bg: transparent; }
.solution-table th { color: var(--optema-muted); text-transform: uppercase; letter-spacing: .08em; font-size: .78rem; }
.solution-table td { padding: 1rem .75rem; }
.solution-table p { color: var(--optema-muted); margin: .25rem 0; }
.solution-table small { color: var(--optema-primary-dark); font-weight: 800; }
.rank-badge { display: inline-flex; border-radius: 999px; background: var(--optema-sand); color: var(--optema-accent); padding: .35rem .6rem; font-weight: 900; }
.action-card { background: linear-gradient(145deg, rgba(255,255,255,.92), rgba(223,248,245,.76)); }
.timeline { display: grid; gap: 1rem; }
.timeline article { display: grid; grid-template-columns: 5rem 1fr; gap: 1rem; align-items: start; }
.timeline article > span {
display: inline-flex; justify-content:center; padding: .45rem .65rem; border-radius: 999px;
background: var(--optema-ink); color: white; font-weight: 900; font-size: .82rem;
}
.timeline h3 { font-size: 1.05rem; font-weight: 800; margin-bottom: .2rem; }
.timeline p { color: var(--optema-muted); margin: 0; line-height: 1.6; }
.score-width-0 { width: 0%; }
.score-width-1 { width: 1%; }
.score-width-2 { width: 2%; }
.score-width-3 { width: 3%; }
.score-width-4 { width: 4%; }
.score-width-5 { width: 5%; }
.score-width-6 { width: 6%; }
.score-width-7 { width: 7%; }
.score-width-8 { width: 8%; }
.score-width-9 { width: 9%; }
.score-width-10 { width: 10%; }
.score-width-11 { width: 11%; }
.score-width-12 { width: 12%; }
.score-width-13 { width: 13%; }
.score-width-14 { width: 14%; }
.score-width-15 { width: 15%; }
.score-width-16 { width: 16%; }
.score-width-17 { width: 17%; }
.score-width-18 { width: 18%; }
.score-width-19 { width: 19%; }
.score-width-20 { width: 20%; }
.score-width-21 { width: 21%; }
.score-width-22 { width: 22%; }
.score-width-23 { width: 23%; }
.score-width-24 { width: 24%; }
.score-width-25 { width: 25%; }
.score-width-26 { width: 26%; }
.score-width-27 { width: 27%; }
.score-width-28 { width: 28%; }
.score-width-29 { width: 29%; }
.score-width-30 { width: 30%; }
.score-width-31 { width: 31%; }
.score-width-32 { width: 32%; }
.score-width-33 { width: 33%; }
.score-width-34 { width: 34%; }
.score-width-35 { width: 35%; }
.score-width-36 { width: 36%; }
.score-width-37 { width: 37%; }
.score-width-38 { width: 38%; }
.score-width-39 { width: 39%; }
.score-width-40 { width: 40%; }
.score-width-41 { width: 41%; }
.score-width-42 { width: 42%; }
.score-width-43 { width: 43%; }
.score-width-44 { width: 44%; }
.score-width-45 { width: 45%; }
.score-width-46 { width: 46%; }
.score-width-47 { width: 47%; }
.score-width-48 { width: 48%; }
.score-width-49 { width: 49%; }
.score-width-50 { width: 50%; }
.score-width-51 { width: 51%; }
.score-width-52 { width: 52%; }
.score-width-53 { width: 53%; }
.score-width-54 { width: 54%; }
.score-width-55 { width: 55%; }
.score-width-56 { width: 56%; }
.score-width-57 { width: 57%; }
.score-width-58 { width: 58%; }
.score-width-59 { width: 59%; }
.score-width-60 { width: 60%; }
.score-width-61 { width: 61%; }
.score-width-62 { width: 62%; }
.score-width-63 { width: 63%; }
.score-width-64 { width: 64%; }
.score-width-65 { width: 65%; }
.score-width-66 { width: 66%; }
.score-width-67 { width: 67%; }
.score-width-68 { width: 68%; }
.score-width-69 { width: 69%; }
.score-width-70 { width: 70%; }
.score-width-71 { width: 71%; }
.score-width-72 { width: 72%; }
.score-width-73 { width: 73%; }
.score-width-74 { width: 74%; }
.score-width-75 { width: 75%; }
.score-width-76 { width: 76%; }
.score-width-77 { width: 77%; }
.score-width-78 { width: 78%; }
.score-width-79 { width: 79%; }
.score-width-80 { width: 80%; }
.score-width-81 { width: 81%; }
.score-width-82 { width: 82%; }
.score-width-83 { width: 83%; }
.score-width-84 { width: 84%; }
.score-width-85 { width: 85%; }
.score-width-86 { width: 86%; }
.score-width-87 { width: 87%; }
.score-width-88 { width: 88%; }
.score-width-89 { width: 89%; }
.score-width-90 { width: 90%; }
.score-width-91 { width: 91%; }
.score-width-92 { width: 92%; }
.score-width-93 { width: 93%; }
.score-width-94 { width: 94%; }
.score-width-95 { width: 95%; }
.score-width-96 { width: 96%; }
.score-width-97 { width: 97%; }
.score-width-98 { width: 98%; }
.score-width-99 { width: 99%; }
.score-width-100 { width: 100%; }
@media (max-width: 767px) {
.metric-grid { grid-template-columns: 1fr; }
.timeline article { grid-template-columns: 1fr; }
.hero-section { min-height: auto; }
}
/* Streamlit-inspired MVP workspace */
.hero-subtitle {
font-family: 'Plus Jakarta Sans', 'Inter', sans-serif;
font-size: clamp(1.25rem, 1.2vw + 1rem, 2rem);
font-weight: 800;
color: var(--optema-primary-dark);
letter-spacing: -0.03em;
}
.mvp-hero .display-heading { letter-spacing: -0.07em; }
.mvp-workspace .sticky-lg-top { top: 6rem; }
.panel-heading {
display: flex;
gap: 1rem;
align-items: flex-start;
margin-bottom: 1.4rem;
}
.panel-heading h2 {
font-size: 1.45rem;
margin: 0 0 .25rem;
font-weight: 900;
}
.panel-heading p,
.form-note,
.formula-note,
.detail-status-card p {
color: var(--optema-muted);
line-height: 1.65;
}
.panel-icon {
display: inline-grid;
place-items: center;
width: 3rem;
height: 3rem;
border-radius: 18px;
background: linear-gradient(135deg, var(--optema-mint), var(--optema-sand));
box-shadow: 0 16px 28px rgba(15, 139, 141, .12);
font-size: 1.35rem;
}
.problem-textarea {
min-height: 15rem;
resize: vertical;
border-radius: 22px;
border-color: rgba(15,139,141,.18);
background: rgba(255,255,255,.82);
}
.problem-textarea:focus {
border-color: var(--optema-primary);
box-shadow: 0 0 0 .25rem rgba(15,139,141,.12);
}
.urgency-slider { accent-color: var(--optema-primary); }
.urgency-value {
min-width: 2.4rem;
text-align: center;
padding: .35rem .7rem;
border-radius: 999px;
color: #fff;
background: var(--optema-ink);
font-weight: 900;
}
.range-labels {
display: flex;
justify-content: space-between;
color: var(--optema-muted);
font-size: .82rem;
font-weight: 800;
margin-top: -.3rem;
}
.output-panel { overflow: hidden; }
.output-success {
display: inline-flex;
align-items: center;
gap: .5rem;
color: #087447;
background: rgba(35, 196, 131, .12);
border: 1px solid rgba(35, 196, 131, .22);
border-radius: 999px;
padding: .7rem 1rem;
font-weight: 900;
}
.output-section {
padding: 1.25rem 0;
border-top: 1px solid var(--optema-line);
}
.output-section:first-of-type { border-top: 0; padding-top: 0; }
.output-section h2 { font-size: clamp(1.25rem, 1vw + 1rem, 1.8rem); }
.compact-cause-list { gap: .85rem; }
.compact-cause-list article p { display: none; }
.preview-table th,
.preview-table td { white-space: nowrap; }
.detail-status-card { padding: 1.35rem; }
.bar-85 { --bar: 85% !important; }
.bar-70 { --bar: 70% !important; }
@media (max-width: 991px) {
.mvp-workspace .sticky-lg-top { position: static; }
}