@danhachuel/pandanews 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  Todas as versões publicadas são registradas aqui antes do publish (regra em AGENTS.md). A etiqueta visual usa PATCH de dois dígitos (ex.: 0.1.00); o registry npm normaliza para a versão canônica (ex.: 0.1.0).
4
4
 
5
+ ## 0.4.00 (2026-10-30)
6
+
7
+ - **Supabase opcional (Fase 6)**: detecção automática de credenciais (`SUPABASE_URL`/`SUPABASE_SERVICE_ROLE_KEY` no ambiente ou em Configurações); teste de conexão real (`POST /configuracoes/testar-supabase`); schema SQL pronto para o SQL Editor (`GET /configuracoes/schema-supabase`); migração dos dados locais SQLite → Supabase (`POST /configuracoes/migrar-supabase`) com relatório por tabela. O app continua 100% local e funcional sem Supabase
8
+ - **Meta diária** (Gradiente de Meta): card "Meta de hoje" no Dashboard com "faltam X notícias para bater a meta", barra de progresso e campo configurável em Configurações → Agendamento (`meta_diaria`)
9
+ - **Breadcrumbs** em todas as telas internas (Lei de Jakob): Dashboard / Blogs, Dashboard / Notícias, etc.
10
+ - **Configurações reformulada**: seção Banco de dados com status do backend ativo (SQLite local), detecção de credenciais, teste, schema SQL e migração — sem travas de pré-registro
11
+ - Revisão das 7 leis de usabilidade: Fitts (CTAs ≥40px), Hick (menu 7 itens, wizard 3 passos, expanders), Zeigarnik (checklist + stepper + meta), Jakob (breadcrumbs, ações destrutivas isoladas), Gradiente de Meta (meta diária, gráfico 7 dias), von Restorff (CTA verde, badge âmbar de rascunho), Miller (blocos ≤7, datas pt-BR)
12
+
5
13
  ## 0.3.01 (2026-10-25)
6
14
 
7
15
  - **Páginas legais e de verificação** em `legal/` (padrão AutoPost-Lunga, adaptado ao PandaNews): `index.html` (apresentação do app, funcionalidades e provedores de API), `terms.html` (Termos de Uso em pt-BR) e `privacy.html` (Política de Privacidade em conformidade com a LGPD — Lei 13.709/2018)
package/README.md CHANGED
@@ -34,12 +34,14 @@ npx --yes @danhachuel/pandanews install
34
34
 
35
35
  O runtime fica em `~/.pandanews` (sobrescreva com a variável de ambiente `PANDANEWS_HOME`) — **upgrades de versão não reinstalam** o que já existe: o instalador detecta Python, dependências e dados já presentes e só instala o que falta.
36
36
 
37
- ## Status (versão 0.3.00)
37
+ ## Status (versão 0.4.00)
38
38
 
39
39
  - **Pipeline completo**: coleta RSS → redação via LLM (fallback NVIDIA → Gemini) → imagem fotojornalística via Gemini Image → rascunho para revisão
40
40
  - **Scheduler ativo**: um job por blog ativo (frequência configurável), toggle global em Automação e execução imediata (⚡ Gerar notícia agora)
41
- - UI completa: Dashboard, Blogs, Notícias (gerar/regenerar/aprovar), Imagens (galeria + geração manual), Automação, Configurações, Logs
42
- - Próximo (Fase 6): detecção automática de Supabase, migração SQLite → Supabase e revisão das 7 leis de UX
41
+ - **Supabase opcional**: detecção automática de credenciais, teste de conexão, schema SQL e migração SQLite → Supabase — o app segue 100% local por padrão
42
+ - **Meta diária** no Dashboard ("faltam X notícias") e breadcrumbs em todas as telas internas
43
+ - Páginas legais/de verificação em `legal/` (LGPD), meta diária configurável e revisão das 7 leis de usabilidade concluídas
44
+ - Produto alinhado ao plano mestre: Fases 0 a 6 concluídas
43
45
 
44
46
  - `npx @danhachuel/pandanews` instala tudo junto (Python portátil + dependências + banco local) e sobe o backend em 127.0.0.1:8000 com página em 127.0.0.1:4030 — se a porta estiver ocupada, usa a seguinte automaticamente (4031, 4032, ...)
45
47
  - A UI React completa (Dashboard, Blogs, Notícias, Imagens, Automação, Configurações) e o pipeline de notícias chegam nas próximas versões (Fases 1 a 3 do plano — ver `CHANGELOG.md`)
@@ -31,7 +31,7 @@ async def lifespan(_: FastAPI):
31
31
  scheduler.parar()
32
32
 
33
33
 
34
- app = FastAPI(title="PandaNews API", version="0.3.1", lifespan=lifespan)
34
+ app = FastAPI(title="PandaNews API", version="0.4.0", lifespan=lifespan)
35
35
 
36
36
  app.add_middleware(
37
37
  CORSMiddleware,
@@ -37,9 +37,40 @@ def obter_configuracoes(db: Session = Depends(get_db)) -> dict:
37
37
  "supabase_service_role_key": _mask(get_config(db, "supabase_service_role_key")),
38
38
  "scheduler_ativo": get_config(db, "scheduler_ativo", "true") == "true",
39
39
  "intervalo_padrao_horas": int(get_config(db, "intervalo_padrao_horas", "6") or 6),
40
+ "meta_diaria": int(get_config(db, "meta_diaria", "3") or 3),
41
+ "supabase_detectado": bool(SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY)
42
+ or bool(get_config(db, "supabase_url") and get_config(db, "supabase_service_role_key")),
40
43
  }
41
44
 
42
45
 
46
+ @router.post("/testar-supabase")
47
+ def testar_supabase() -> dict:
48
+ from backend.core import supabase_db
49
+
50
+ try:
51
+ cred = supabase_db.obter_credenciais()
52
+ return {**supabase_db.testar_conexao(), "credencial_em": cred["origem"]}
53
+ except Exception as exc: # noqa: BLE001
54
+ return {"ok": False, "detail": str(exc)[:400]}
55
+
56
+
57
+ @router.get("/schema-supabase")
58
+ def schema_supabase() -> dict:
59
+ from backend.core import supabase_db
60
+
61
+ return {"sql": supabase_db.SCHEMA_SQL}
62
+
63
+
64
+ @router.post("/migrar-supabase")
65
+ def migrar_supabase() -> dict:
66
+ from backend.core import supabase_db
67
+
68
+ try:
69
+ return supabase_db.migrar_do_sqlite()
70
+ except Exception as exc: # noqa: BLE001
71
+ return {"ok": False, "detail": str(exc)[:400]}
72
+
73
+
43
74
  @router.put("")
44
75
  def salvar_configuracoes(payload: ConfiguracoesPatch, db: Session = Depends(get_db)) -> dict:
45
76
  data = payload.model_dump(exclude_none=True)
@@ -45,6 +45,9 @@ def estatisticas(db: Session = Depends(get_db)) -> dict:
45
45
  }
46
46
  onboarding["concluidos"] = sum(1 for v in onboarding.values() if v)
47
47
 
48
+ meta = int(get_config(db, "meta_diaria", "3") or 3)
49
+ faltam_meta = max(0, meta - noticias_hoje)
50
+
48
51
  return {
49
52
  "blogs_total": blogs_total,
50
53
  "blogs_ativos": blogs_ativos,
@@ -53,6 +56,8 @@ def estatisticas(db: Session = Depends(get_db)) -> dict:
53
56
  "noticias_pendentes": noticias_pendentes,
54
57
  "erros_total": erros_total,
55
58
  "scheduler_ativo": get_config(db, "scheduler_ativo", "true") == "true",
59
+ "meta_diaria": meta,
60
+ "faltam_meta": faltam_meta,
56
61
  "geracao_7dias": geracao,
57
62
  "ultimos_erros": ultimos_erros,
58
63
  "onboarding": onboarding,
@@ -51,6 +51,7 @@ class ConfiguracoesPatch(BaseModel):
51
51
  db_backend: Optional[str] = None
52
52
  scheduler_ativo: Optional[bool] = None
53
53
  intervalo_padrao_horas: Optional[int] = Field(default=None, ge=1, le=168)
54
+ meta_diaria: Optional[int] = Field(default=None, ge=0, le=100)
54
55
 
55
56
 
56
57
  class LlmTestIn(BaseModel):
@@ -0,0 +1,194 @@
1
+ """Backend opcional Supabase (Fase 6).
2
+
3
+ Detecção automática de credenciais em SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY
4
+ (ambiente — verificação de plataformas) ou na tabela configuracoes (chaves
5
+ supabase_url / supabase_service_role_key). Oferece teste de conexão, schema SQL
6
+ para o painel do Supabase e migração dos dados locais (SQLite → Supabase).
7
+ Serviços opcionais nunca bloqueiam o funcionamento local do app.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from sqlalchemy import select
12
+
13
+ TABELAS_ESPERADAS = ("blogs", "noticias", "configuracoes", "logs")
14
+
15
+ SCHEMA_SQL = """-- PandaNews — schema para Supabase (execute no SQL Editor do painel)
16
+ create table if not exists public.blogs (
17
+ id bigint primary key generated always as identity,
18
+ nome text not null,
19
+ tema text not null default 'geral',
20
+ regiao text not null default 'Brasil',
21
+ fontes_rss text not null default '[]',
22
+ estilo_editorial text not null default '',
23
+ frequencia_horas integer not null default 6,
24
+ status text not null default 'ativo',
25
+ created_at timestamptz not null default now()
26
+ );
27
+
28
+ create table if not exists public.noticias (
29
+ id bigint primary key generated always as identity,
30
+ blog_id bigint not null references public.blogs(id) on delete cascade,
31
+ titulo text not null default '',
32
+ corpo text not null default '',
33
+ resumo text not null default '',
34
+ imagem_url text not null default '',
35
+ status text not null default 'rascunho',
36
+ llm_provider text not null default '',
37
+ fonte_url text not null default '',
38
+ fonte_titulo text not null default '',
39
+ created_at timestamptz not null default now()
40
+ );
41
+
42
+ create table if not exists public.configuracoes (
43
+ chave text primary key,
44
+ valor text not null default ''
45
+ );
46
+
47
+ create table if not exists public.logs (
48
+ id bigint primary key generated always as identity,
49
+ blog_id bigint references public.blogs(id) on delete cascade,
50
+ etapa text not null default '',
51
+ status text not null default '',
52
+ mensagem text not null default '',
53
+ created_at timestamptz not null default now()
54
+ );
55
+ """
56
+
57
+
58
+ def obter_credenciais() -> dict:
59
+ """Credenciais do Supabase: ambiente tem prioridade; senão, configuracoes."""
60
+ import os
61
+
62
+ url = os.environ.get("SUPABASE_URL", "")
63
+ key = os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "")
64
+ if url and key:
65
+ return {"url": url, "key": key, "origem": "ambiente"}
66
+ from backend.core.database import Configuracao, SessionLocal
67
+
68
+ db = SessionLocal()
69
+ try:
70
+ row_url = db.get(Configuracao, "supabase_url")
71
+ row_key = db.get(Configuracao, "supabase_service_role_key")
72
+ if row_url and row_url.valor and row_key and row_key.valor:
73
+ return {"url": row_url.valor, "key": row_key.valor, "origem": "configuracoes"}
74
+ finally:
75
+ db.close()
76
+ return {"url": "", "key": "", "origem": ""}
77
+
78
+
79
+ def _client():
80
+ from supabase import create_client
81
+
82
+ cred = obter_credenciais()
83
+ if not (cred["url"] and cred["key"]):
84
+ raise RuntimeError(
85
+ "Credenciais do Supabase não encontradas. Defina SUPABASE_URL e SUPABASE_SERVICE_ROLE_KEY "
86
+ "no ambiente ou informe-as em Configurações → Banco de dados."
87
+ )
88
+ return create_client(cred["url"], cred["key"])
89
+
90
+
91
+ def _tabela_existe(client, tabela: str) -> bool:
92
+ try:
93
+ client.table(tabela).select("*").limit(1).execute()
94
+ return True
95
+ except Exception as exc: # noqa: BLE001
96
+ msg = str(exc).lower()
97
+ if "relation" in msg or "does not exist" in msg or "pgrst205" in msg:
98
+ return False
99
+ raise
100
+
101
+
102
+ def testar_conexao() -> dict:
103
+ """Testa a conexão e a existência do schema (sem criar nada sozinho)."""
104
+ client = _client()
105
+ faltando = [t for t in TABELAS_ESPERADAS if not _tabela_existe(client, t)]
106
+ if faltando:
107
+ return {
108
+ "ok": False,
109
+ "detail": f"Conexão ok, mas faltam as tabelas: {', '.join(faltando)}. Execute o schema SQL no painel do Supabase (botão 'Ver schema SQL').",
110
+ "faltando": faltando,
111
+ }
112
+ return {"ok": True, "detail": "Conexão ok e schema encontrado."}
113
+
114
+
115
+ def migrar_do_sqlite() -> dict:
116
+ """Copia blogs, notícias, configurações e logs do SQLite para o Supabase."""
117
+ from backend.core.database import Blog, Configuracao, Log, Noticia, SessionLocal
118
+
119
+ client = _client()
120
+ faltando = [t for t in TABELAS_ESPERADAS if not _tabela_existe(client, t)]
121
+ if faltando:
122
+ raise RuntimeError(
123
+ f"Faltam as tabelas no Supabase: {', '.join(faltando)} — execute o schema SQL antes de migrar."
124
+ )
125
+
126
+ db = SessionLocal()
127
+ migrados: dict[str, int] = {}
128
+ try:
129
+ blogs = db.scalars(select(Blog)).all()
130
+ if blogs:
131
+ client.table("blogs").upsert(
132
+ [
133
+ {
134
+ "id": b.id,
135
+ "nome": b.nome,
136
+ "tema": b.tema,
137
+ "regiao": b.regiao,
138
+ "fontes_rss": b.fontes_rss,
139
+ "estilo_editorial": b.estilo_editorial,
140
+ "frequencia_horas": b.frequencia_horas,
141
+ "status": b.status,
142
+ "created_at": b.created_at.isoformat() if b.created_at else None,
143
+ }
144
+ for b in blogs
145
+ ]
146
+ ).execute()
147
+ migrados["blogs"] = len(blogs)
148
+
149
+ noticias = db.scalars(select(Noticia)).all()
150
+ if noticias:
151
+ client.table("noticias").upsert(
152
+ [
153
+ {
154
+ "id": n.id,
155
+ "blog_id": n.blog_id,
156
+ "titulo": n.titulo,
157
+ "corpo": n.corpo,
158
+ "resumo": n.resumo,
159
+ "imagem_url": n.imagem_url,
160
+ "status": n.status,
161
+ "llm_provider": n.llm_provider,
162
+ "fonte_url": n.fonte_url or "",
163
+ "fonte_titulo": n.fonte_titulo or "",
164
+ "created_at": n.created_at.isoformat() if n.created_at else None,
165
+ }
166
+ for n in noticias
167
+ ]
168
+ ).execute()
169
+ migrados["noticias"] = len(noticias)
170
+
171
+ confs = db.scalars(select(Configuracao)).all()
172
+ if confs:
173
+ client.table("configuracoes").upsert([{"chave": c.chave, "valor": c.valor} for c in confs]).execute()
174
+ migrados["configuracoes"] = len(confs)
175
+
176
+ logs = db.scalars(select(Log).order_by(Log.created_at.desc()).limit(200)).all()
177
+ if logs:
178
+ client.table("logs").upsert(
179
+ [
180
+ {
181
+ "id": l.id,
182
+ "blog_id": l.blog_id,
183
+ "etapa": l.etapa,
184
+ "status": l.status,
185
+ "mensagem": l.mensagem,
186
+ "created_at": l.created_at.isoformat() if l.created_at else None,
187
+ }
188
+ for l in logs
189
+ ]
190
+ ).execute()
191
+ migrados["logs"] = len(logs)
192
+ finally:
193
+ db.close()
194
+ return {"ok": True, "migrados": migrados}
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-1{left:.25rem}.left-11{left:2.75rem}.left-9{left:2.25rem}.top-1{top:.25rem}.z-50{z-index:50}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-40{height:10rem}.h-44{height:11rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-auto{height:auto}.h-full{height:100%}.max-h-48{max-height:12rem}.max-h-56{max-height:14rem}.max-h-\[85vh\]{max-height:85vh}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-16{width:4rem}.w-20{width:5rem}.w-40{width:10rem}.w-44{width:11rem}.w-6{width:1.5rem}.w-60{width:15rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-400\/40{border-color:#fbbf2466}.border-panda\/40{border-color:#22c55e66}.border-red-400\/40{border-color:#f8717166}.border-slate-100{--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity, 1))}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))}.border-slate-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1))}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-black\/60{background-color:#0009}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-night{--tw-bg-opacity: 1;background-color:rgb(11 18 32 / var(--tw-bg-opacity, 1))}.bg-panda{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-panda\/10{background-color:#22c55e1a}.bg-panda\/15{background-color:#22c55e26}.bg-panda\/30{background-color:#22c55e4d}.bg-panda\/70{background-color:#22c55eb3}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-sky-100{--tw-bg-opacity: 1;background-color:rgb(224 242 254 / var(--tw-bg-opacity, 1))}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.bg-slate-300{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.object-cover{-o-object-fit:cover;object-fit:cover}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-2{padding-bottom:.5rem}.pr-4{padding-right:1rem}.pt-2{padding-top:.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-emerald-300{--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-panda{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-panda-dark{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-sky-700{--tw-text-opacity: 1;color:rgb(3 105 161 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-slate-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.text-slate-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.line-through{text-decoration-line:line-through}.placeholder-slate-400::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}.placeholder-slate-400::placeholder{--tw-placeholder-opacity: 1;color:rgb(148 163 184 / var(--tw-placeholder-opacity, 1))}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}body{--tw-bg-opacity: 1;background-color:rgb(11 18 32 / var(--tw-bg-opacity, 1));font-family:Inter,system-ui,sans-serif;--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}html:not(.dark) body{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.last\:border-0:last-child{border-width:0px}.hover\:border-slate-400:hover{--tw-border-opacity: 1;border-color:rgb(148 163 184 / var(--tw-border-opacity, 1))}.hover\:bg-panda-dark:hover{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-100:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.hover\:text-slate-800:hover{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-panda:focus{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.dark\:border-slate-700:is(.dark *){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.dark\:border-slate-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity, 1))}.dark\:bg-amber-500\/15:is(.dark *){background-color:#f59e0b26}.dark\:bg-card:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(16 26 44 / var(--tw-bg-opacity, 1))}.dark\:bg-emerald-500\/15:is(.dark *){background-color:#10b98126}.dark\:bg-night:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(11 18 32 / var(--tw-bg-opacity, 1))}.dark\:bg-red-500\/15:is(.dark *){background-color:#ef444426}.dark\:bg-sky-500\/15:is(.dark *){background-color:#0ea5e926}.dark\:bg-slate-700:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-700\/60:is(.dark *){background-color:#33415599}.dark\:bg-slate-800:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.dark\:text-amber-400:is(.dark *){--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.dark\:text-emerald-400:is(.dark *){--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.dark\:text-panda:is(.dark *){--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.dark\:text-red-400:is(.dark *){--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.dark\:text-sky-400:is(.dark *){--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.dark\:text-slate-200:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.dark\:text-slate-300:is(.dark *){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.dark\:text-slate-400:is(.dark *){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.dark\:text-slate-500:is(.dark *){--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.dark\:text-slate-600:is(.dark *){--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.dark\:placeholder-slate-500:is(.dark *)::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(100 116 139 / var(--tw-placeholder-opacity, 1))}.dark\:placeholder-slate-500:is(.dark *)::placeholder{--tw-placeholder-opacity: 1;color:rgb(100 116 139 / var(--tw-placeholder-opacity, 1))}.dark\:hover\:border-slate-500:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity, 1))}.dark\:hover\:bg-slate-800\/60:hover:is(.dark *){background-color:#1e293b99}.dark\:hover\:text-slate-200:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}@media (min-width: 640px){.sm\:inline{display:inline}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 768px){.md\:flex{display:flex}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}